Skip to content

A shared clipboard between the development host and ToyOS under QEMU, through QEMU's vdagent - #894

Merged
Japabu merged 7 commits into
mainfrom
wt/toyos-clipboard
Oct 13, 2026
Merged

Japabu merged 7 commits into
mainfrom
wt/toyos-clipboard

Conversation

@Japabu

@Japabu Japabu commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

A shared clipboard between the development host and ToyOS under QEMU: copy on the host and paste in ToyOS, and the other way. The guest end is /system/bin/vdagent, the agent of QEMU's qemu-vdagent chardev. It talks to that chardev over a virtio-serial function of its own, and it talks to a new clipboard port of the compositor. The owner gave his go. The design was accepted on this pull request before the build. Every statement about what QEMU 11.1.1 does is behaviour measured black-box, and the logs are on ToyOSOrg/evidence pr-894. Nothing in the tree cites an implementation's source.

What changed, and why

The launch (build/vdagent.rs, build/qemu.rs, tests/common/qemu.rs). Every virtio machine gets -chardev qemu-vdagent,id=vdagent0,clipboard=on,mouse=off. It also gets a virtio-serial-pci-non-transitional with max_ports=2, with virtserialport nr=1 name=com.redhat.spice.0 on it.

  • These arguments come from one function in the toyos_build library. That function reads the codec crate's declaration (PORT_NAME, MAX_PORTS, PORT_ID), and both launchers call it.
  • The function is created just before the console's. So it enumerates first (00:06.0 before the console's 00:07.0 on q35), and every virtio guest boot runs the kernel's console rule.
  • cargo run now passes -display cocoa on macOS and -display gtk,clipboard=on on other hosts, one choice per host. The harness keeps -display none.
  • QEMU is asked for qemu-vdagent (-machine none -chardev help) before anything launches. A non-macOS cargo run also asks for gtk (-machine none -display help). The machine is named because qemu-system-aarch64 has no default one and lists nothing without it. A missing one is refused by name ("built without spice-protocol", "no GTK display; Debian ships it as qemu-system-gui"). Nothing falls back.

The kernel: two 1af4:1043 functions are two devices. Both rules are pure decisions in toyos_pci (kernel/pci/src/choose.rs), with host tests.

  • (a) The console drives the first virtio console function that does not offer VIRTIO_CONSOLE_F_MULTIPORT (toyos_pci::console). The single-port driver can drive nothing else: a multiport device names its console ports on control queues (virtio 1.2 §5.3.6.2).
    • The feature read is the first half of VirtioDevice::init, split out as VirtioDevice::offer. That half is §3.1.1's reset, ACKNOWLEDGE, DRIVER and the read. init is now offer followed by Offered::accept.
    • It runs before bus mastering, because enable_bus_master is what records a function as the kernel's.
    • A declined function is reset again, named in the log (offers MULTIPORT … reset and left to a claim) and left to a claim.
  • (b) A claim's vendor:device resolves among the functions the kernel does not drive (toyos_pci::claimed). One such match is the claim's, two are Ambiguous, and driven matches alone are KernelDriven. usbd_drives_the_spare's KernelDriven control keeps its line and stays green.

The codec, toyos-vdagent (lib/vdagent). This is an input boundary of its own: pure, no_std, and tested on the host. Every word it decodes comes from the host.

  • It holds the channel's declaration, virtio-serial's control messages (§5.3.6.2), and the vdagent stream (spice/vd_agent.h at 499cc832). The stream is decoded as a stream, so a header may straddle two receive buffers.
  • Only a chunk over VD_AGENT_MAX_DATA_SIZE ends the decode. Everything else is refused by name and the next message still decodes: another port is skipped; another protocol, a message declared past 4 + MAX_TEXT, an unknown type or a size its type's layout does not have is refused at its header, and its data counted off unread. An admitted message carries its type as an enum, so the decode of a whole one cannot refuse.
  • A kept message's buffer doubles as bytes arrive but never past its declared size: each reserve covers at least the chunk it takes, so Vec's own doubling never decides the capacity.
  • LARGEST_TRANSMIT is computed by the encoder: capabilities plus a 2 MiB push, which comes to 2,105,448 bytes.

The agent, /system/bin/vdagent (system/vdagent). It drives the function over toyos-pci-claim and toyos-virtio, in netstack's pattern.

  • It negotiates MULTIPORT and refuses a max_nr_ports that is not 2. It builds the six queues before DRIVER_OK, all on the one MSI-X entry.
  • It keeps 8 control buffers posted, and posts each again before its event is handled.
  • It runs §5.3.6.2's handshake, and answers the host's PORT_OPEN(0/1) with its own.
  • It ends by name on: another port added, a console port, its port removed, an unframed stream, a used ring it does not believe, or a transmit not taken back within 5 s (policy).
  • Every decision is made in the nested pure crate toyos-vdagent-bridge:
    • It pushes each guest copy as GRAB [serial] with its CLIPBOARD, or with a RELEASE for a copy of nothing, in one transmit. Once QEMU holds the text, no host paste waits on the guest.
    • It answers no host REQUEST.
    • It follows QEMU's measured grab serials: a host grab is kept if and only if s ≥ last, and each session starts at 0.
    • It withdraws an owed REQUEST on a guest copy, and an unsent push on a kept host grab.
    • It keeps one set to the compositor in flight (toyos_vdagent_bridge::sets) until the compositor answers it, with only the newest host text behind it, and drops that one when a guest copy is delivered: its text was taken before the copy.
  • The agent logs lengths and serials, never a clipboard byte.
  • Where no function is handed to it (none on the T14; none handed over on aarch64 cargo run, below) it says so in one line and exits 0.

The compositor and sdk/window. A third port, clipboard, with one connection, the watcher:

  • The watcher waits for its first frame (MSG_CLIPBOARD_WATCH) in a slot of its own (toyos_desktop::Port, pending_admits). A second watcher is refused by name.
  • One notification is outstanding at a time: a client's copy is sent once the watcher has asked, in the paste's two encodings, empty included.
  • What the watcher asked and was not sent lives in the compositor's Session for its whole life (Watch), so a copy made before the agent watches is sent at its first ask. A watcher that leaves takes its ask and its open region with it, and leaves the unsent copy to the next one (Watch::leave).
  • A watcher's inline set lands only while its ask is outstanding. A set through a region lands only if the ask was outstanding at its MSG_COPY_BEGIN and no copy was delivered since, judged at MSG_COPY_COMMIT. So a host text that crossed a guest copy never lands over it, whether the delivery came before or after the region opened.
  • Every watcher set, inline or committed, is answered with MSG_CLIPBOARD_SET_ANSWERED (compositor → watcher, bare, 14), whether or not it landed. window::watcher::Watcher holds one set in flight until that answer and panics on a second (the caller's bound), so a host copying at any rate cannot fill the compositor's pipe and end the agent.
  • A watcher's set is never sent back to the watcher.
  • The decisions are one pure type, toyos_desktop::clipboard::Watch.
  • The poller ceiling moves MAX_WINDOW_SLOTS from 218 to 216.

Authority. [programs.vdagent] holds devices = ["pci:1af4:1043"] and receives = ["clipboard"], and runs in [boot] start. The compositor's row serves clipboard, in images/system.toml and in the two test configs whose compositor rows serve prompt (metalcase, panelcase). HELD_ALONE gains (clipboard, vdagent), and the port's name and holder are declared in toyos_manifest::clipboard. exempt.owns sits in system/vdagent/Cargo.toml's metadata, where the tree keeps it.

kernel/src/device.rs (the fence widened by this one file): ClaimError::Ambiguous and KernelDriven say what rule (b) made true, more than one function the kernel does not drive and every matching function driven.

Issues. Four. The first three are held by the orchestrator:

  • issues/a-cargo-run-boot-claims-every-pcidev-slot.md: sound, net, disk and now the agent take all four of pcidev::MAX_FUNCTIONS. Exit: a cargo run boot leaves a slot free.
  • issues/the-shared-clipboard-is-unmeasured-on-linux-and-windows-hosts.md. Exit: a copy and a paste each way, measured on a Linux host under GTK and on a Windows host.
  • issues/a-declined-console-function-keeps-a-kernel-mapping-of-its-firmware-bar.md: VirtioDevice::offer maps the declined function's capability windows through map_mmio, which has no unmap, and pcidev later moves that BAR. Virtio 1.2 §4.1.4.9's configuration access capability is named as the way to read with nothing mapped. Exit: a declined function leaves no kernel mapping and no driven window.
  • issues/an-aarch64-cargo-run-boot-hands-over-no-pci-claim.md, owned by the arm64 track: cargo run's virt under HVF has no ITS, and with one by hand every claim's BAR placement still fails (below). It records the agent's aarch64 claim and handshake as unbooted. Exit: a cargo run -- --arch aarch64 boot shows diskserver serving and vdagent: port 1 open.

Fix round 3, finding by finding

Against the round-3 review (#894 (comment)).

finding what was done its test
NOTE, issues/an-aarch64-cargo-run-boot-hands-over-no-pci-claim.md:18-21 is false of the log rewritten to what the log shows, by its lines: each function answers at its firmware BAR (0x0f0107ff, 0x79000000, 0x30bf8024, 0x00000103, :327, :386, :433, :470); every moved BAR reads 0xffffffff except at 0x4010000000, the ECAM base (:212) inside a window firmware declared (:225), where the NVMe reads 0x00081b36 and the other three 0x00000000 — (prose); every one of the 32 placement lines was read

Fix round 2, finding by finding

Against the round-2 review (#894 (comment)) and its rulings (the newest comment).

finding what was done its test
BLOCKER, the capacity bound is false at QEMU's chunking stream.rs: the reserve's floor is data.len() + take, a total, where it was take, an increment a_kept_message_buffers_no_more_than_it_declares now feeds a 2,048-byte text through host() as well as MAX_TEXT, asserting the capacity is at most the declared size. With 3a60123b6's line it is 101, "4016 bytes held for 2052 declared"; with the fix 0; the log names that red arm m24, the same patch as m25 below (capacity-arms.log:11, :25, :35). At the head, m25 (that line restored) reds it, 101
BLOCKER, MSG_CLIPBOARD_SET_ANSWERED accepted by name in the rulings; no code changed —
B5, the aarch64 boot measured by hand with an ITS, below; it still does not reach port 1 open, so the agent's aarch64 claim and handshake are recorded unbooted in the new issue, and the two lines shown in round 1 stand —
NOTE, the defect B5's boot found issues/an-aarch64-cargo-run-boot-hands-over-no-pci-claim.md —
NOTE, the body's sentence about the track corrected below: the track records only that HVF's GIC refuses an ITS and that a virt with one is its=on,kernel-irqchip=off; it does not record that cargo run's boot hands over no claim —

The aarch64 boot with an ITS (aarch64-its-boot.log, the argv aarch64-its-boot.argv.log). The image is cargo run -- --arch aarch64 --build-only at 24880cdc5 (build 24880cdc5… clean, :350); 60f08b937 adds only the issue. cargo run -- --arch aarch64 was run with a qemu-system-aarch64 first on PATH (aarch64-its-boot.shim.sh) that logs cargo run's own argv and execs QEMU 11.1.1 with only -machine changed to virt,gic-version=3,its=on,iommu=smmuv3,kernel-irqchip=off. The tree was not changed. Exit 0, QEMU ended by me over QMP once the agent had exited (:492); QEMU's stderr is empty.

  • The ITS is there and armed (:174, :203), and each claim's DeviceID is mapped to its slot's LPI.
  • The first two lines stand: the agent's function declined (:281), and the console on 00:08.0 (:282-287).
  • port 1 open does not appear. Every claim fails a step later than without the ITS: pcidev: PCI 00:07.0 NOT HANDED OVER — its BAR was moved onto 8 64-bit address(es) inside the windows firmware declared and the function answered at none of them (:487), and the same for 00:02.0, 00:06.0 and 00:05.0 (:343, :418, :450). The agent exits on "no virtio-serial function was handed to this program" (:491).
  • The kernel's xHCI driver gets no LPI and initialises no controller (:272), so the compositor again panics on "the manifest gives this program the keyboard" (:379).
  • Neither the BAR placement nor cargo run's machine is in this branch's fence. Both are the new issue's, owned by the arm64 track.

Fix round 1, finding by finding

Against the build review (#894 (comment)) and the rulings (#894 (comment)).

finding what was done its test, red on 519b43215
B1, a region set crossing a delivery lands Watch records at begin whether the ask was outstanding, and a delivery marks an open region stale; the agent drops the set queued behind the one in flight when it reads a delivery (Sets::delivered) a_region_opened_after_a_delivery_refuses_the_commit_after_the_next_ask, the review's sequence: 101 on 519b43215's clipboard.rs (base-red/run.log:2). a_delivered_guest_copy_drops_the_set_behind is new API; m23 reds it
B2, a copy before the watcher is lost Watch is a Session field; client_copy records every copy; Watch::leave at reap a_copy_made_while_nobody_watched_is_sent_at_the_next_ask is new API (leave), so it cannot exist at 519b43215; m18 (a leave that forgets the unsent copy) and m19 (one that keeps the ask) red it. The session glue itself has no host test: the pure claim it rests on, a copy before any ask is sent at the first ask, is a_client_set_is_sent_once_asked, unchanged
B3, B4 the_launch_spells_the_codecs_declaration and a_message_past_the_buffer_is_the_callers_bug deleted —
B5, the aarch64 cargo run boot Booted. It first refused to launch at 95d5161f: qemu-system-aarch64 -chardev help exits 1, "No machine specified" (log:286). The listings now name -machine none, measured to list both on QEMU 11.1.1 for both architectures. At the head the boot shows the first two lines and not the third; see below —
N, dead unsent = false gone: set reads the ask alone and takes &self —
N, refusals after buffering every refusal at the header, data counted off; Body::Keep carries a typed Kind and no protocol a_refusal_is_named_at_its_header_before_its_data_arrives: 101 on 519b43215's stream.rs (base-red/run.log:3)
N, buffer doubling to 4 MiB growth doubles, capped at the declared size a_kept_message_buffers_no_more_than_it_declares: 101 on 519b43215's stream.rs (base-red/run.log:4)
N, inline sets unbounded one set in flight, answered by MSG_CLIPBOARD_SET_ANSWERED a_set_waits_for_the_answer_to_the_one_in_flight, only_the_newest_set_waits: new API; m21, m22 red them
N, map_mmio window filed, held by the orchestrator (above) —
N, slot issue owner status: assigned, held by the orchestrator —
N, ClaimError docs fixed in kernel/src/device.rs —
N, T14 readings and logs below, and both logs on pr-894 —
N, EXIT= lines added later every EXIT= line in this round's logs is appended by the command that ran it —

Also found by B5's boot: the agent said "no virtio-serial function on this machine" while the supervisor said it was there and not handed over. It now says none was handed to it.

Checks

Round 2's logs are at evidence e626cced, from head 60f08b937, except capacity-arms.log, which ran with the fix uncommitted at no recorded head; both arms are held at the head by mutations/run.log (m25, 101) and host-ci.log. Round 1's are at b1283478, from head 3a60123b6, which merges origin/main at 1b080a3f1; its mutations and base-red runs are from 95d5161f, and the one commit after it touches build/vdagent.rs and the agent's exit line, which none of them reaches.

gate exit log
round 3: cargo run -- --ci host at 7cf25b31f 0, 76 steps green host-ci.log:9035-9036
round 2: cargo run -- --ci host at 60f08b937 0, 76 steps green host-ci.log:9051-9052
round 2: cargo test --test toyos-build -- the_opening_exchange_with_qemus_agent_completes, the guest row the fix reaches 0, PASS guest-clipboard.log:12, :18
round 2: the capacity test's two arms fix 0; 3a60123b6's line 101 capacity-arms.log
round 2: the stream mutations at 60f08b937, m01, m24 (rebased onto the fixed line) and m25 script 0; each builds (0) and reds its named test (101) mutations/run.log
round 2: the aarch64 boot with an ITS, by hand 0, QEMU quit over QMP; port 1 open absent aarch64-its-boot.log
round 1: cargo run -- --ci host 0, 76 steps green host-ci.log:9046-9047
round 1: cargo test, the whole guest suite 0, 53 of 53 guest-suite.log:971, :1025
the smoke row, in that suite PASS (7 s) guest-suite.log:778
cargo run -- --arch aarch64 at 95d5161f 1, refused before launch aarch64-cargo-run-at-95d5161f.log:286-289
cargo run -- --arch aarch64 at the head 0; QEMU stopped by me once the agent had exited aarch64-cargo-run.log
13 mutations of this round script 0; each builds (0) and reds its named test (101) mutations/run.log
3 new tests on 519b43215's file script 0; each builds (0) and is red (101) base-red/run.log
negative control (round 1): the smoke row on base 858da4d42, agent absent 1, STALLED negative-control-base-858da4d.log:12
round 1's 17 mutations, at 519b43215 each red (101) mutations/run.log

The aarch64 boot without an ITS, round 1 (aarch64-cargo-run.log). Of the three lines the review named:

  • the agent's function declined: virtio-console: PCI 00:07.0 offers MULTIPORT … reset and left to a claim (:279);
  • the console on the next function: virtio-console: found at PCI 00:08.0 and initialized (:280-285);
  • the agent's port 1 open does not appear. pcidev: PCI 00:07.0 NOT HANDED OVER — its interrupts would not be remapped on this machine (:397): cargo run's virt under HVF has no ITS (ITS: the MADT names none), so no claim is handed over, soundserver's, netstack's and diskserver's among them, none of which this branch changes. The arm64 track (issues/toyos-runs-on-arm64.md) records only that HVF's own GIC refuses an ITS, "so a virt with one is its=on,kernel-irqchip=off"; it does not record that cargo run's machine has none, which is now issues/an-aarch64-cargo-run-boot-hands-over-no-pci-claim.md. The same boot shows the compositor ending on "the manifest gives this program the keyboard": no USB function is handed over either. Round 2's boot with an ITS is above.

Mutations of this round, each a checked patch, built, its named test run, and restored, the tree clean after: the size bound lifted (m01); a set sent back (m11); a delivery without an ask (m12); a stale inline set landing (m13); a region judged at its begin alone (m14) and at its commit alone, 519b43215's rule (m17); a leave that forgets the unsent copy (m18), keeps the ask (m19) or keeps the region (m20); a set sent while one is in flight (m21); the oldest set kept behind it (m22); a delivery that keeps the set behind (m23); growth past the declared size (m24). Round 2 rebases m24 onto the fixed line and adds m25, the reserve short of the chunk (3a60123b6's line). Their patches are beside the log, with the script that wrote them.

Independent oracles.

  • Two external specifications: virtio 1.2 (cs01) and spice-protocol's vd_agent.h at 499cc832. The encoders are tested byte for byte against the header's packed layouts and §5.3.6.2.
  • QEMU 11.1.1's own vdagent and virtio-serial, live in the smoke row: the device maker's implementation, run as a black box.
  • The black-box probe logs on pr-894 (qemu-vdagent-blackbox.log, -requests.log, -interleave.log). They back every QEMU fact the bridge's tests encode.

Why the new guest test needs QEMU. the_opening_exchange_with_qemus_agent_completes boots tests/images/clipboardcase.toml on HeadlessVirtioGpu, the one virtio machine with a framebuffer for the compositor.

  • No type and no host test reaches the other end of the handshake and of the request, which is QEMU's own chardev and device.
  • The device exists only under QEMU, so no metal row reaches it either.
  • Its one claim: the opening exchange completes, in order.

What it costs. The smoke boot is 7 s (guest-suite.log:778). Round 1 measured the suite at 89.7 s on the base and 96.6 s with the row, one sample each; this round's run took 44.5 s for 53 tests, on a warm cache. These are measurements, not bounds. In the image, bin/vdagent and one process start in every images/system.toml boot, the T14's included.

The T14, the orchestrator's run of 519b43215 (judge-rows.log, kernel.log, masked):

  • The booted build is 519b43215 clean (kernel.log:322). spawn_cost and pci_inventory PASS, 2 of 2 (judge-rows.log:14-22).
  • 24 functions (kernel.log:57-81). None carries 1af4:1059, 1af4:1041, 1b36:0010 or 1af4:1043, and no vendor:device is on two functions, so rule (b) changes no claim on that machine and rule (a) finds no console function.
  • spawn_cost: toybox median 1111 µs, min 927 µs, over 50 (kernel.log:531).
  • No new T14 run is owed. Neither fix round changes kernel code (only device.rs's doc comments), and neither row reaches the compositor's clipboard, the window SDK's watcher or the agent.

Net lines. git diff --shortstat origin/main...HEAD: 45 files, +3,474 −154. Round 1 put production at 2,456 (with the test lines inside modified files), tests at 867, issues and Cargo.lock at 102. Round 1 alone: 14 files, +359 −143. Round 2 alone (git diff --shortstat 3a60123b6..HEAD): 2 files, +51 −8, one production line changed, the rest the test and the issue. Round 3 alone: 1 file, +8 −2, the issue's prose.

Dependencies. No external crate is added. The new crates are toyos-vdagent (a boundary), toyos-vdagent-bridge (the program's pure decisions) and the vdagent program. toyos-build takes toyos-vdagent so the launch reads the agent's declaration.

Staged evidence: the owner's copy and paste on the Mac

Nothing waits on this. It is the cocoa peer, which no probe could reach.

  1. cargo run on the Mac. QEMU must list qemu-vdagent (qemu-system-x86_64 -chardev help), and this development host's 11.1.1 does. The console prints vdagent: port 1 open, the host announces capabilities … and then vdagent: the host's clipboard, N bytes, where N is the length of whatever the Mac's clipboard held at boot.
  2. Host to guest: copy a line in a macOS app with ⌘C. The console prints vdagent: the host grabs the clipboard, serial S, 1 type(s) and vdagent: the host's clipboard, N bytes. Open the Editor in ToyOS and paste with GUI+V. If cocoa takes ⌘V for QEMU's own menu, use whatever key reaches the guest as GUI. The line appears.
  3. Guest to host: select text in the ToyOS Editor and copy it with GUI+C. The console prints vdagent: a guest copy of N bytes. Paste with ⌘V in a macOS app, and the text appears.

Record each console line and what appeared, and attach them here.

What I am unsure of

Outside the kernel fence, accepted by name in the rulings:

  • tests/common/iommu.rs: iommu_virtio_platform counts the agent's function among those the kernel does not drive (- 2 became - 3).
  • tests/images/metalcase.toml and panelcase.toml: their compositor rows serve clipboard, as they serve prompt.

The agent on aarch64 is not booted past its claim. No claim is handed over on cargo run's virt under HVF, nor with an ITS by hand, so the agent's handshake on aarch64 rests on the same code that passes on q35; issues/an-aarch64-cargo-run-boot-hands-over-no-pci-claim.md records it unbooted.

B2's glue has no test that reaches it. Session now owns Watch and records every copy; no host test drives Session, and the one guest row does not copy before the agent watches.

A new frame type, accepted by name in round 2's rulings. MSG_CLIPBOARD_SET_ANSWERED is how the ruling's "one in flight" is bounded for inline sets, which had no answer: the alternative, every set through a region, cannot carry an empty set (copy_fits refuses 0 bytes).

Two spellings of the clipboard's bound. toyos_vdagent::MAX_TEXT is a second spelling of window::MAX_CLIPBOARD_BYTES, asserted equal at compile time in the agent, as the compositor asserts MAX_SNAPSHOT_BYTES == MAX_FRAME_LEN. sdk/window is guest-built and the codec is host-tested, so the codec cannot read it.

offer turns on memory decode before the reset. enable_bus_master used to do both at once.

"A session that would pass u32::MAX ends by name." This is my reading, because a host serial reaches the bound: the session stops flowing (SerialsExhausted) until the next announcement, and the program does not end.

Unmeasured: the cocoa and GTK peers. The steps above cover the Mac, and the issue covers the rest.

🤖 Generated with Claude Code

https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy

@Japabu

Japabu commented Oct 12, 2026

Copy link
Copy Markdown
Collaborator Author

Design review, round 1, head 0b12adca4.

Net lines (git diff --shortstat origin/main...0b12adca4): none, one empty commit. Production 0, tests 0.

Evidence: qemu-shape.log at ToyOSOrg/evidence 57668f44, whose commit message names head 0b12adca4. Both QEMU runs of the shape end with EXIT=139, and the cause is under the crash question below. QEMU was read at v11.1.1, spice-vdagent at e940b009 and spice-protocol at 499cc832.

The brief's questions

  • The two kernel changes. This shape needs both, and no design without them is better. The only shape that needs neither puts the agent's function on transitional 1af4:1003, which the console never matches and a claim names alone. That puts a legacy interface on the machine to avoid a kernel decision. A multiport console is the kernel addition that §2 rightly refuses. Rule (a) draws the spec's line: a function without MULTIPORT has one port, and that port is its console; a multiport function names its console port by control message (hw/char/virtio-serial-bus.c:403-405). Two defects remain: no test reaches (a) (BLOCKER 5), and (b) is not limited to virtio (NOTE 4).

  • The clipboard port. It is the least authority the feature allows. Any design that answers a host paste needs every guest copy. The port carries no window and no input, and the agent holds no compositor connector. HELD_ALONE keeps the port to one row. How the port delivers copies is not least (BLOCKER 4).

  • Answering every request. It is necessary. Cocoa (ui/cocoa.m:1804-1810) and GTK (ui/gtk-clipboard.c:51-57) wait with no bound, and an empty answer ends both waits (ui/clipboard.c:249-253). The roast's "a killed agent closes the port" holds, through the bus-master clear rather than a reset:

    • tear_down clears bus mastering first (kernel/src/pcidev/mod.rs:1454);
    • QEMU drops DRIVER_OK when it is cleared (hw/virtio/virtio-pci.c:811-815);
    • virtio-serial then closes the guest end (hw/char/virtio-serial-bus.c:622-623, :585-600, hw/char/virtio-console.c:117-118);
    • the vdagent unregisters (ui/vdagent.c:874-884).

    Answering is not sufficient: see BLOCKER 3.

  • The crash. This configuration does not cause it. QEMU 11.1.1's info virtio-status dereferences NULL on every virtio device (NOTE 1). cargo run never sends that command, so the crash gives no reason to call the launch change unsafe.

  • QEMU builds without qemu-vdagent. See NOTE 2.

BLOCKER

  1. PR body §3, the bridge table's "host capabilities" row against its "host CLIPBOARD" row — the answer to the opening REQUEST is refused, so the host's text from before boot never lands — Why: no grab precedes that REQUEST, so the answer arrives while the owner is None, and the table's own rule sets the guest clipboard "only when the owner is Host". Fix: the opening REQUEST makes the owner Host, as a host grab would. If the owner is already Guest, because a copy was made between WATCH and the port's open, the agent grabs instead. Say whether WATCH delivers the clipboard the compositor already holds. Name the unit test of each transition.

  2. PR body §3 wire, "capabilities (CLIPBOARD_BY_DEMAND alone)", and §"Linux's guest", "departs from it in one place" — when a host copy and a guest copy cross, both clipboards end up empty — Why: this is a race, and a departure from the state of the art (A design follows what its hardware's manufacturers or the published state of the art back, and a bench verdict never blocks it #885) that the body neither follows nor cites.

    • spice-vdagent announces CLIPBOARD_GRAB_SERIAL (src/vdagentd/vdagentd.c:160) and orders crossing grabs by it (:290-305, :715-727). QEMU honours it by discarding a lower-ordered guest grab (ui/vdagent.c:518-530).
    • Without it, each side believes the other owns. The guest, holding Host, requests, and QEMU answers empty because the vdagent owns (ui/vdagent.c:569-582). A host paste then asks the guest, which answers with no data.

    Fix: announce CLIPBOARD_GRAB_SERIAL and keep vdagentd's serial rule. That includes QEMU's serial reset, which closes and reopens the host end (ui/vdagent.c:455-466), so the serial module must handle a host PORT_OPEN(0). Otherwise, show that the crossing cannot happen.

  3. PR body, Roast, "The agent's rule of answering every request is the only defence" — that defence fails whenever the guest is not running — Why: QEMU's peers wait on the UI thread for a guest that cannot run, so a host paste of guest-owned text freezes QEMU's window, and the paste gets nothing:

    • cargo run --debug parks a triple-faulted guest with -action shutdown=pause (build/qemu.rs:286), and the window stays frozen for the rest of the session;
    • a panicked kernel holds its panel for 60 s before it resets (lib/tco/src/lib.rs:137), and the window is frozen that long;
    • a gdb stop on the always-on -s (build/qemu.rs:269) freezes the window until the debugger continues.

    Fix: weigh sending the text right after each GRAB. QEMU stores a CLIPBOARD that its owner sends unrequested (ui/vdagent.c:585-605, ui/clipboard.c:232-257), and after that qemu_clipboard_request returns at once, so neither loop waits. Weigh it against the protocol's by-demand reading. The design already relies on QEMU's implementation for the opening REQUEST. Choose one, and record whatever stays in issues/ with an owner and an exit.

  4. PR body §4, "A refused send drops the watcher by name", and the roast's "the bridge is gone for the boot" — any program can end the bridge for the boot — Why: every program holding compositor can do it by copying in a loop, and every installed package holds it (images/system.toml:37). Until the drop, each copy past 4096 bytes pins a fresh region of up to 2 MiB in the pipe. The roast found this compromise and neither removes nor records it. Fix: keep one notification outstanding. The compositor keeps only a changed flag, and it sends the latest non-bridge clipboard when the watcher asks for the next one. Then the pipe never fills, and the server doctrine's drop never fires on a live watcher.

  5. PR body §2(a) and §9, "A console chosen wrong is a boot with no console" — no test can fail on rule (a), and none covers rule (b)'s refusals — Why:

    • The launch creates the console's function before the agent's, so the console enumerates first (qemu-shape.log:47-60: the console at 00:01.0, the agent's at 00:02.0). The existing first match (kernel/src/drivers/virtio_console.rs:199) already takes it.
    • The mutation "drop rule (a) and keep devices.iter().find(|d| d.is_id(VIRTIO_VENDOR, VIRTIO_CONSOLE_DEVICE))" therefore stays green on every boot.
    • Rule (b) has three outcomes: two undriven matches give Ambiguous, all driven gives KernelDriven, and one undriven among driven matches is chosen. Only the smoke test, through its success path, would fail on a missing (b).

    Fix: write (b)'s resolution as a pure decision beside toyos_pci::slot (kernel/pci/src/slot.rs), host-tested on all three outcomes. Write (a)'s choice over the functions' feature words the same way, or make the launch create the agent's function first so that every boot runs rule (a).

NOTE

  1. PR body, "What I am unsure of", last bullet — "which step faults is not known" — Why: QEMU's source answers it, and the evidence has no clean run of the shape.

    • QEMU 11.1.1's qmp_x_query_virtio_status never sets backend_features (hw/virtio/virtio-qmp.c:697-768); v10.0.0 set it at :749-750.
    • hmp_virtio_status prints "Backend features:" and then reads features->transports through that NULL (hw/virtio/virtio-hmp-cmds.c:154-155, :54).
    • The last line before both EXIT=139 results is that header (qemu-shape.log:109-110, :182-183). It faults the same way on the console function as on the agent's.

    Still, no run of the shape exits 0. The build round's evidence should carry the same command line with only info pci and quit, exiting 0.

  2. PR body, "What I am unsure of", CI and Windows — qemu-vdagent is unmeasured on both — Why:

    • CI. Debian builds QEMU with spice on amd64 (debian/control:93-94 of qemu 1:11.1.2+ds-1, one revision past the pin). vdagent.c compiles into the main binary whenever spice-protocol is found (ui/meson.build:68). So CI very likely has it, but nobody has measured it. "The image moves" cannot add a compile option to the Debian 11.1.1 build that build/ci/qemu-version pins, so measure in the pinned image now: that decides whether the harness half of the design exists at all.
    • Windows. Nothing falls back by design, so a Windows QEMU built without spice-protocol stops cargo run at launch. Cite the Windows build's spice protocol support line (meson.build:4915) or measure it. If the Windows build lacks it, file that gap.
  3. PR body §1, -display gtk,clipboard=on — the body cites the GTK clipboard's off default but not why it is off — Why:

    • Upstream shipped GTK's clipboard as "EXPERIMENTAL, MAY HANG" through 11.0 (meson_options.txt:270-271 at v11.0.0), and 11.1 still defaults it off (qapi/ui.json:1322-1323).
    • No Linux or Windows host has run it.
    • A Linux QEMU without the GTK module (Debian packages it in qemu-system-gui) now stops cargo run at launch.

    Name which hang it is, and that BLOCKER 3's choice closes it. Have the build's prerequisite check name the spice-protocol and GTK requirements before a launch can fail.

  4. PR body §8, "rules 2(a) and 2(b) find no virtio function there and change nothing" — this is false for rule (b) — Why: (b) changes how every claim resolves on every machine, not only virtio claims. The T14 run that the build round asks for should carry its inventory, showing that no vendor:device named in a devices list is carried by two functions.

  5. kernel/src/pcidev/mod.rs:146 — MAX_FUNCTIONS is 4, and the agent takes the last free slot on every cargo run boot — Why: a cargo run virtio boot already claims three functions (images/system.toml:85, :95, :235). QEMU adds PM and FLR only on an Express port (hw/virtio/virtio-pci.c:2197-2198, :2260-2307), so the kernel finds no reset for a root-bus virtio function. An agent that ended therefore keeps its slot as residue (kernel/pci/src/slot.rs:4-9). The body says none of this.

  6. PR body §9, "one smoke assertion, on a boot the guest suite already runs" — the smoke test names no boot — Why: the shared boot starts no compositor (tests/images/testcases.toml:3), and the agent opens the port only after it holds the compositor (§3, "The loop"). Name a boot that runs the compositor, and add the row to that boot's config.

  7. PR body §3, wire, and serial's control-message decoding — both decode words from outside the guest's trust — Why: Fit gives an input boundary a pure crate of its own whoever uses it, as toyos-acpi and toyos-elf are. Here both are modules of system/vdagent. That crate is also where max_ports = 2 and com.redhat.spice.0 can be declared once and read by both the launch and the agent. Today each is two declarations.

  8. sdk/window/src/lib.rs:458-471 — the bridge's set reuses clipboard_set's exchange, which blocks — Why: past 4096 bytes the exchange blocks in recv_header with no bound, inside the agent's single loop. A stalled compositor then stalls every host request the agent owes, which contradicts §3's "There is no flat wait". Route the set through the Poller, or name its bound.

  9. PR body §7, "no secret is newly exposed" — the eager fetch exposes host secrets to the guest — Why: every host copy, including a password manager's, moves into guest memory whether or not it is pasted there, and every cargo run boot pulls in the host's current clipboard. State that neither the agent nor the compositor ever logs a clipboard byte.

  10. PR body §2, "which the brief does not name" — the build round would fall outside its fence — Why: the comment that accepts this design must widen the fence by name, to kernel/src/drivers/virtio_console.rs, kernel/src/pcidev/mod.rs and kernel/pci/.

DESIGN NEEDS CHANGES: let the opening REQUEST's answer land (owner Host, or GRAB if the guest already copied); announce CLIPBOARD_GRAB_SERIAL with vdagentd's serial rule and QEMU's close-and-reopen, or show the crossing cannot happen; answer the halted guest (push the text after GRAB, or record the freeze with an owner and exit); give the watcher one outstanding notification so no client can drop it; make rules (a) and (b) pure decisions with host tests that fail without them; and fix NOTEs 1–10 before landing.

@Japabu

Japabu commented Oct 12, 2026

Copy link
Copy Markdown
Collaborator Author

The orchestrator's rulings on round 1 (#894 (comment)), following spice-vdagent and QEMU per #885's rule:

  • B1: the opening REQUEST makes the owner Host, or the agent grabs if the guest already copied. Say what WATCH delivers, and unit-test each transition.
  • B2: announce CLIPBOARD_GRAB_SERIAL, keep vdagentd's serial rule, and handle QEMU's close-and-reopen (PORT_OPEN(0)).
  • B3: push the text right after each GRAB. QEMU stores an owner's unrequested CLIPBOARD, so a halted guest (debug pause, panic panel, gdb stop) never freezes QEMU's window. That beats the by-demand reading here: the host is the developer's own machine, and the guest's text going to it eagerly costs nothing the owner minds.
  • B4: one outstanding notification. The compositor keeps a changed flag and sends the latest copy when the watcher asks.
  • B5: rules (a) and (b) become pure decisions with host tests that fail without them.
  • NOTEs 1–10 as the review names them, including the clean QEMU run with exit 0, measuring qemu-vdagent in CI's pinned image, a prerequisite check that names spice-protocol and GTK, the T14 inventory under rule (b), the MAX_FUNCTIONS residue, the boot the smoke test runs on, a pure codec crate, a bounded set, and never logging a clipboard byte.
  • The build's fence is widened to kernel/src/drivers/virtio_console.rs, kernel/src/pcidev/mod.rs and kernel/pci/.

@Japabu

Japabu commented Oct 12, 2026

Copy link
Copy Markdown
Collaborator Author

Design round 2, head 0b12adca4 (unchanged: still one empty commit). The body is rewritten. This maps each finding of the round-1 review (#894 (comment)) to the place in the body that closes it, as ruled (#894 (comment)).

New measurements, all on ToyOSOrg/evidence pr-894 at 7b1ccc19:

The owner's later direction (ToyOS does not follow Linux). The design now rests on three definitions only: virtio 1.2, spice-protocol's vd_agent.h, and QEMU v11.1.1, which makes the device and the host end. Round 1 leaned on the SPICE project's Linux guest agent for the chunk bound, the per-type sizes and one-chunk-per-message, and on Linux's console driver for queue setup before DRIVER_OK. Each is now taken from vd_agent.h or from virtio 1.2 §3.1.1 and §5.3.5. One behaviour changed: outgoing messages are chunked at VD_AGENT_MAX_DATA_SIZE (2048), not sent as one chunk.

BLOCKER

  1. The opening REQUEST's answer lands. §4's table: at the host's capabilities the owner becomes Host before the REQUEST goes out, or the bridge grabs and pushes if the owner is already Guest. §5 says what WATCH delivers: the clipboard the compositor holds, as soon as a client's set is unsent, which at boot is a copy made before the agent watched. Every row has a unit test, named in §10, among them the_answer_to_the_opening_request_lands and the_opening_grabs_a_copy_made_before_the_port_opened.
  2. CLIPBOARD_GRAB_SERIAL is announced (§3), and QEMU's close-and-reopen is handled: the host's PORT_OPEN(0) is answered with ours, and its PORT_OPEN(1) with ours (§4).
    • One departure from the ruling's wording, by the owner's later direction. The ruling said "keep vdagentd's serial rule", and vdagentd is that Linux agent. So the bridge's rule is derived from QEMU's host side instead (ui/vdagent.c:342-351, :516-530): a host grab is kept iff its serial is at least the guest's last grab's.
    • §4 shows why both sides then pick the same winner of a crossing.
    • On every serial sequence QEMU 11.1.1 sends, it keeps and discards the same grabs as the rule the ruling named. It differs only on a host serial QEMU does not send.
  3. The text is pushed right after each GRAB, in the same transmit buffer (§4). QEMU handles one buffer under the lock a host paste needs, so no paste falls between the two. Cocoa's and GTK's waits end at once, a halted guest no longer freezes the host's window, and nothing is left to record.
  4. One outstanding notification (§5): MSG_CLIPBOARD_WATCH asks, and the compositor answers with the latest client copy while its unsent flag is set, so no client can fill the watcher's pipe.
    • While designing this I found a race round 1 also had. A bridge set crossing a delivery leaves the guest with the host's text and the host with the guest's copy.
    • The bridge's sets now travel on the watcher's own ordered connection, and land only while its ask is outstanding.
    • Unit test: a_bridge_set_made_before_a_delivery_was_seen_is_refused.
  5. Rules (a) and (b) are pure decisions in toyos_pci with host tests that fail without them (§2, §10). The launch also creates the agent's function before the console's (§1), so every virtio guest boot runs rule (a). Measured: the agent's function at 00:06.0 before the console's at 00:07.0 on q35. The harness's other slots do not move.

NOTE

  1. A clean run exits 0. Round 1's command line with only info pci and quit exits 0, and so does cargo run's Virtio profile as designed, on x86_64 and aarch64. That holds on the development host's QEMU 11.1.1 and in CI's pinned image.
  2. CI and Windows are measured.
    • CI's pinned image (debian@sha256:a2aa4626…, its snapshot's QEMU 1:11.1.1+ds-1) lists qemu-vdagent for both architectures, so the harness half of the design stands. The image was run under the development host's podman VM, emulated.
    • Windows: MSYS2's QEMU 11.1.2 was built with spice-protocol 0.14.5, and its binaries carry chardev-qemu-vdagent and GTK's clipboard peer. Stefan Weil's installers are not measured, which "What I am unsure of" says.
  3. The GTK hang is named. 29e0bfffab87 turned GTK's clipboard off for guest hangs (QEMU #1150). Its synchronous host reads became asynchronous in 11.1 (9a007326e83f), and the option became -display gtk,clipboard=on, still off (335e32cbd0a2). The one wait left, gd_clipboard_get_data's loop, is the one BLOCKER 3's push closes. The prerequisite check names spice-protocol, and GTK for cargo run off macOS, before any launch (§1).
  4. Rule (b) is now stated as changing every claim on every machine (§9). The build round's T14 run carries the PCI inventory against every pci: entry in images/system.toml.
  5. The slot residue is stated (Roast, "Claim slots"). A cargo run boot claims all four slots, and an ended agent keeps its slot as netstack's and soundserver's do. More slots reach both architectures' interrupt tables, outside the fence, so the build round files it with an exit.
  6. The smoke row has a boot. None exists that has both a compositor and a virtio function. A new tests/images/clipboardcase.toml on HeadlessVirtioGpu starts the compositor and the agent, and the row waits for the agent's open line and for the length of QEMU's answer to the opening REQUEST (§10).
  7. There is a codec crate. toyos-vdagent (lib/vdagent) decodes and encodes the control messages and the vdagent stream. It also declares the port name and max_ports = 2 once, for the launch and the agent (§3).
  8. The bridge's set no longer blocks. It goes over the watcher's connection with try_send, and MSG_COPY_REGION arrives as a Poller event, so nothing waits and there is nothing to bound (§5). clipboard_set's blocking exchange stays the apps'.
  9. The secrets that cross are stated (§8): every host copy enters guest memory, a password manager's included, and every boot pulls in the host's clipboard. Neither the agent nor the compositor logs a clipboard byte.
  10. The fence. The ruling widened it to kernel/src/drivers/virtio_console.rs, kernel/src/pcidev/mod.rs and kernel/pci/. The build also needs kernel/src/drivers/virtio.rs. Rule (a) has to read a feature word before bus mastering, VirtioPciConfig::parse is private there, and VirtioDevice::init enables bus mastering, which marks a function kernel-driven, before it reads any feature (§2). The build also needs tests/images/clipboardcase.toml and two new issues/ files.

@Japabu

Japabu commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator Author

Design review, round 2, head 0b12adca4, judged against the prompts on origin/main at 1d5325376, which now include #902's clean-room rule.

Net lines (git diff --shortstat origin/main...0b12adca4): none, one empty commit. Production 0, tests 0. The body estimates about 1,300 production lines for the build.

Evidence: ToyOSOrg/evidence 7b1ccc19, whose commit names head 0b12adca4. qemu-shape-r2-devhost.log and qemu-shape-r2-ci-image.log end every run with EXIT=0. QEMU was read at v11.1.1 (c3d48b7d) and spice-protocol at 499cc832.

Round 1's BLOCKERs

  1. The opening REQUEST's answer was refused: CLOSED in the design. §4's capabilities row makes the owner Host before the REQUEST goes out, or grabs and pushes when the owner is Guest(t). §5 says what WATCH delivers. Each transition has a named unit test in §10. A design round has no measurement to give here.
  2. Crossing grabs left both clipboards empty: CLOSED. Read against ui/vdagent.c:
    • A host grab carries last_serial and then increments it (:345).
    • A guest grab below last_serial is discarded, and a kept one sets it (:524-530).
    • So QEMU keeps a crossing guest g iff g > s, and the bridge keeps a crossing host s iff s ≥ g. Exactly one side wins, the same one on both sides.
    • Without a crossing, I followed the bridge's next := max(next, s + 1) through the case where QEMU discarded the bridge's last grab, and through two guest grabs against one host grab. Every case ends with the same owner on both sides.
    • The departure from the ruling's "vdagentd's serial rule" is now required, not just allowed: Another implementation's source is read only clean-room, and the tree names Linux only as a host platform or a libc interface #902's Fit refuses that rule's authority. The body's answer does depend on BLOCKER 1 below.
  3. A halted guest froze the host's window: CLOSED as a mechanism.
    • do_flush_queued_data hands a popped element to the chardev iovec by iovec in one call (hw/char/virtio-serial-bus.c:170-210).
    • vdagent_chr_write consumes it all.
    • Cocoa's paste takes the BQL to start (ui/cocoa.m:1799).
    • So no paste starts between a GRAB and the CLIPBOARD in the same element, and qemu_clipboard_request returns at once once the data is stored (ui/clipboard.c:199-203).
    • The buffer that carries the two is too small: see BLOCKER 2.
  4. Any client could drop the watcher: CLOSED. There is one outstanding ask, and unsent is one flag. The new stale-set rule is right. "Ask outstanding" holds only after the bridge has read every delivery and no client has set since, so a bridge set that lands can never cross a delivery. The bridge's next grab carries next > every host serial it kept, so QEMU keeps the guest copy that won, and both sides agree.
  5. No test could fail on rules (a) and (b): CLOSED.
    • Both are pure decisions in toyos_pci, with the three outcomes of (b) and both outcomes of (a) named as tests.
    • Measured: qemu-shape-r2-devhost.log:150-165 puts vdagent-serial at 00:06.0, before the console at 00:07.0.
    • The console keeps max_ports=1 (build/qemu.rs:256, tests/common/qemu.rs:2430), so it offers no MULTIPORT, and rule (a) still finds it while the first-match mutation boots with no console.

BLOCKER

  1. PR body, "What the published definitions say", §4 "Why the serial rule is right", §4 Transmit, and Roast "The module header cites the QEMU lines for each" — the design takes its authority from QEMU's source, and the build would commit those citations — Why: Fit now makes "a citation of another implementation's source as authority" a BLOCKER. Root CLAUDE.md's Zero legacy reads such source only clean-room: one agent writes a specification of facts that cites no implementation, and another agent builds from it alone.
    • What rests on QEMU's source. The serial rule (ui/vdagent.c:342-351, :516-530); the stored unrequested CLIPBOARD (:585-606, ui/clipboard.c:232-257); the close-and-reopen reset (:455-465); the empty answer (:556-583); the 1024-byte chunks and the 1 MiB drop (:162-176); and B3's one-element atomicity (hw/char/virtio-serial-bus.c:160-212).
    • Who read it. The agent that would build read that source, and it read spice-vdagent's source too (round 1's body, and the mapping comment's comparison with vdagentd).
    • Fix: clean-room. These facts go into a specification that another agent writes, citing no implementation, with its provenance in that agent's evidence. An agent that has read neither source builds from it. The module headers cite only virtio 1.2, vd_agent.h and QEMU's documentation (qapi/char.json, qapi/ui.json).
    • Fix: a ruling. Alternatively, the orchestrator, or the owner where he says so, rules on this pull request that QEMU's source is the vendor's documentation of QEMU's own device. This round's brief named QEMU's source as the authority, but the brief predates Another implementation's source is read only clean-room, and the tree names Linux only as a host platform or a libc interface #902, and a reviewer cannot grant that exception.
  2. PR body §4, "Transmit is one buffer, 2 MiB + 4 KiB", and "A guest grab and its text go out in that one buffer" — the buffer cannot hold a maximal guest copy — Why: round 2 chunks every outgoing message at 2048 bytes.
    • CLIPBOARD at MAX_CLIPBOARD_BYTES (sdk/window/src/lib.rs:89, 2,097,152) is 20 + 4 + 2,097,152 = 2,097,176 bytes, which is 1,025 chunks with 8,200 bytes of chunk headers.
    • The GRAB [serial][UTF8_TEXT] is one more chunk of 36 bytes.
    • Together that is 2,105,412 bytes, against a buffer of 2,101,248, so it is 4,164 bytes short. Any owed REQUEST, capabilities or RELEASE in the same transmit adds to that.
    • The design says nothing about what happens to a copy near the maximum, and splitting it across two elements gives up the atomicity B3 rests on.
    • Fix: size the buffer from the codec crate's encoder, as a const that is the encoded length of the largest transmit, beside MAX_PORTS. Add a unit test that encodes a MAX_CLIPBOARD_BYTES grab and push into it.

NOTE

  1. PR body §4, the "host capabilities" row, "answers with its own capabilities" — the design does not set the request field of either side's capabilities — Why: if the agent's capabilities carry request = 1, QEMU answers with request = 0 (ui/vdagent.c:717-718), and the row answers that answer. QEMU's second recv_caps in the session calls qemu_clipboard_reset_serial() with the agent's peer already registered (:724-728). That notifies the agent's own peer, which closes the port (:455-465), and the reopen starts the cycle again, so the session livelocks. State that the agent sends request = 0 and answers only a host announcement whose request is set. Unit-test it.
  2. PR body §4, "guest copy … Empty: RELEASE if the owner was Guest" — an empty client copy that crosses a host copy leaves the guest empty and the host holding its text — Why: the owner is Host by then, so nothing goes out. This contradicts the roast's claim that "the guest copy then wins on both sides". State what the host holds after an empty copy while Host owns, and test that row.
  3. PR body §5, "A bridge set lands only while the watcher's ask is outstanding" — the design does not say which frame of MSG_COPY_BEGIN … MSG_COPY_COMMIT the ask is judged at — Why: a delivery between BEGIN and COMMIT must refuse the commit. Judge it at the commit, and name that case in a_bridge_set_made_before_a_delivery_was_seen_is_refused, or in a test beside it.
  4. PR body §4, "The driver" — the design does not say how many control receive buffers the agent keeps posted — Why: QEMU sends a control event only into a posted buffer, and drops it otherwise. A lost host PORT_OPEN(0) strands the session: the host end stays closed, and every guest transmit is discarded. State the bound on control events that can be outstanding at once (a handful, for one port), and keep at least that many buffers posted.
  5. PR body §2, "Fence", kernel/src/drivers/virtio.rs — the growth is justified, but only as a split — Why:
    • Why it is needed: parse is private (:194), and init enables bus mastering at :731 before it reads a feature. enable_bus_master is what records a function as kernel-driven (kernel/src/drivers/pci.rs:253-256). No path outside virtio.rs reads a feature word without that record.
    • The condition: the feature read before bus mastering must be the first half of init, which init then calls. A second reset, ACKNOWLEDGE, DRIVER and feature-read sequence beside init would be a sibling.
    • The acceptance comment must name virtio.rs, tests/images/clipboardcase.toml and the two issues/ files.
  6. PR body §10, "One smoke row, on a new boot" — this is permitted under Integration between modules is tested only for the kernel, and an integration test outside it is cut #890's Tests, but "very cheap" is unmeasured — Why:
    • The row waits for two lines and drives nothing. It is hermetic, because -display none leaves QEMU's clipboard with no peer. The reason Guest tests asks for is given: only QEMU has the device, and QEMU is the other end.
    • The row's cost is a whole QEMU boot with the compositor. The build round reports that boot's wall time and the suite's total before and after, and the roast lists the boot.
    • The two awaited lines make one claim: the opening exchange with QEMU completes. Name the row for that claim.
  7. PR body, Roast, "Three of the design's steps rest on QEMU's implementation" — it lists three steps, but four rest on QEMU — Why: B3's freedom from the freeze rests on QEMU handling one element in one BQL hold. A QEMU that dropped the lock mid-element would bring back the freeze with nothing visible. That fact also belongs in BLOCKER 1's specification.
  8. PR body, "cocoa does so under QEMU's big lock" — false of the record — Why: ui/cocoa.m:1804-1810 drops the BQL while it waits on cbevent, and only the start of the paste holds it. This is prose, for the orchestrator.

SEND BACK

@Japabu

Japabu commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

Rulings on round 2 (#894 (comment)).

  1. QEMU's source is no authority (BLOCKER 1). QEMU is another implementation, so under Another implementation's source is read only clean-room, and the tree names Linux only as a host platform or a libc interface #902 its source is read only clean-room. The design agent, who has read it, is the reader. The body becomes the specification of facts: what the vdagent port does, stated as behaviour, citing no source file. Each fact is either backed by a black-box run of QEMU, measured and logged in the evidence, which is allowed, or stated as unmeasured. The build is then done by an agent that has read neither QEMU's source nor spice-vdagent's, from the body alone. No module header cites QEMU's source.
  2. The transmit buffer (BLOCKER 2): take the review's fix, a constant set to the largest encoded transmit, with a unit test that encodes a maximal copy into it.
  3. The fence: accepted as kernel/src/drivers/virtio.rs (the early read is the first half of VirtioDevice::init, which init then calls), tests/images/clipboardcase.toml, and the two new issues. The build reports the smoke boot's wall time and the suite's total before and after.
  4. The NOTEs are taken as written, the prose error included.

@Japabu

Japabu commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

Design round 3, head 8fa3b834d: still one empty commit, now with main merged in at 1d5325376. This maps each finding of the round-2 review (#894 (comment)) to its answer, as ruled (#894 (comment)).

New evidence, ToyOSOrg/evidence pr-894 at 84a5388b:

The probe runs QEMU under -accel qtest with the design's chardev and devices. It is the guest's virtio driver through the qtest socket, and a VNC client is the host's clipboard peer.

BLOCKER

  1. QEMU's source as authority: answered as ruled. The body is now the specification of facts. "What QEMU 11.1.1 does" states Q1-Q17 as behaviour. Each one cites lines of the black-box log or is marked unmeasured, and no QEMU or spice-vdagent source file is cited anywhere in the body. Its only citations are virtio 1.2, vd_agent.h, QEMU's QAPI documentation and ToyOS's own tree.
    • Every fact the review listed is now measured:
      • the serial rule (Q11);
      • the stored unrequested CLIPBOARD (Q13);
      • the close and reopen (Q5, Q6);
      • the empty answer (Q10);
      • the 1024-byte chunks (Q8);
      • the drop near 1 MiB (Q9).
    • The one-element atomicity is unmeasured, and the design no longer rests on it (§4 "The push"). A paste handled between the two messages reaches the agent as a REQUEST, and the CLIPBOARD already in the device's hands answers it (Q14).
    • Measuring instead of reading changed the design in four places. Each one is a defect that the round-2 reading had missed:
      • A second guest announcement in one session closes QEMU's end, and the element that carried it is never taken back (Q5). The agent now announces once per session, with request 0, and refuses a host announcement whose request is 0.
      • An empty CLIPBOARD pushed unrequested leaves a host paste with nothing (Q15), and a guest RELEASE while the host owns is ignored (Q16). An empty guest copy is now GRAB [next] + RELEASE in one transmit, whoever owned before.
      • QEMU sends a RELEASE back after the guest's own (Q16). The table's RELEASE row ignores it by name.
      • PORT_NAME ends with a NUL, against §5.3.6.2 (Q2). The agent identifies its port by the declared PORT_ID and does not decode the name.
    • Also measured: QEMU starts its serial counter at 0 in every new session, a reboot included (Q12). That makes the bridge's reset to 0 at each announcement correct.
  2. The transmit buffer: answered as ruled.
    • toyos-vdagent declares LARGEST_TRANSMIT, a const the encoder computes. It is the encoded length of capabilities, GRAB and a MAX_CLIPBOARD_BYTES CLIPBOARD: 2,105,448 bytes (§3).
    • A transmit carries at most one CLIPBOARD, and an owed push waits behind an owed answer (§4 "Transmit").
    • the_largest_transmit_fills_the_transmit_buffer is the unit test. Round 2's 2 MiB + 4 KiB is the mutation that must turn it red (§10).
    • QEMU took the 2,105,412-byte GRAB + CLIPBOARD element whole and delivered all 2,097,152 bytes (Q13, log 1032-1039).

NOTE

  1. The capabilities' request field. The agent sends request 0. It answers only a host announcement with request 1, and once per session. A host announcement with request 0 is refused by name.
    • Measured: QEMU opens every session with request 1 (Q4). It answers a guest request 1 with its own request 0, and a second guest announcement closes the port (Q5), as the review predicted.
    • Unit tests: capabilities_are_answered_once_per_session and a_host_announcement_without_request_is_refused.
  2. An empty copy while the host owns. It is now GRAB [next] + RELEASE, whoever owned before, so it wins or loses a crossing like any copy.
    • Measured: if kept, the host's clipboard is empty. If discarded, the host keeps its text, the bridge keeps the host's grab, and both hold the host's text (Q11, Q16).
    • Unit test: an_empty_copy_grabs_and_releases_whoever_owned. The roast's "the guest copy then wins on both sides" now holds for an empty copy too.
  3. The ask is judged at the frame that lands the text: MSG_CLIPBOARD_SET or MSG_COPY_COMMIT, never MSG_COPY_BEGIN. A delivery between BEGIN and COMMIT refuses the commit.
    • Unit test: a_delivery_between_begin_and_commit_refuses_the_commit.
    • Mutation: judging at MSG_COPY_BEGIN (§5, §10).
  4. Control receive buffers.
    • Measured: an event that finds no posted buffer is lost, and only a second DEVICE_READY brought it back (Q3).
    • The largest burst seen between two guest actions is two events.
    • The agent posts CONTROL_BUFFERS = 8 before DEVICE_READY and posts each again as soon as it is read, before its event is handled (§3, §4).
    • A burst of more than eight is unmeasured, and the roast says so.
  5. virtio.rs as a split. The feature read is the first half of VirtioDevice::init, split out, and init calls it (§2). The fence is listed as ruled.
  6. The smoke row.
    • It is named the_opening_exchange_with_qemus_agent_completes, for its one claim.
    • The roast lists its boot as a cost.
    • The build round reports the boot's wall time and the suite's total before and after (§10).
  7. The fourth step that rests on QEMU. The roast now lists five steps that rest on QEMU's measured behaviour. The atomicity this note named is no longer one of them: the design stopped depending on it (BLOCKER 1).
  8. "cocoa does so under QEMU's big lock". It is deleted, with every other statement read from QEMU's source. The cocoa and GTK peers are now marked unmeasured, because measuring either takes the developer's own host clipboard.

Still open, as the body says

  • The cocoa and GTK peers are unmeasured.
  • The facts were measured on q35 only.
  • How far QEMU's host-text bound lies between 1,048,000 and 1,048,576 bytes is unmeasured.

@Japabu

Japabu commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

Design review, round 3, head 8fa3b834d, judged against the prompts on origin/main at 1d5325376.

Net lines (git diff --shortstat origin/main...8fa3b834d): none. Production 0, tests 0. The commits are one empty commit and a merge of main.

Evidence: ToyOSOrg/evidence 84a5388b. qemu-vdagent-blackbox.log is 35 launches of QEMU 11.1.1 and ends EXIT=0 (:1287). The probe source is qemu-vdagent-probe/src/main.rs, and its asserts carry two facts the log does not print:

  • every chunk is for port 1 (:533);
  • no chunk spans two messages (:541).

post_ctrl kicks the control receive queue (:456), so Q3's lost event is not an unkicked queue. I read the log and the probe's source and opened no QEMU or spice-vdagent source.

Round 2's BLOCKERs

  1. QEMU's source as authority: CLOSED. The body cites no file of QEMU's or spice-vdagent's source. Its only citations are:

    • virtio 1.2;
    • vd_agent.h at 499cc832;
    • QEMU's QAPI documentation;
    • QEMU issue #1150;
    • ToyOS's own tree.

    I checked Q1–Q17 against the log line by line. Each is stated as behaviour and stands on the lines it cites, or is marked unmeasured. The exceptions are three sentences that say more than their lines show (NOTE 1). The design rests on none of those three sentences.

    The serial rule follows from Q11 and Q12 alone:

    • QEMU keeps a crossing guest grab g if and only if g > s, and the bridge keeps a crossing host grab s if and only if s ≥ g.
    • A kept guest grab sets C := g, so the next host grab carries g. The measurements at :1122-1130 and :1186-1194 show this, and it is why the bridge's test must be s ≥ last and not s > last.
    • I followed the rule through a guest grab that QEMU discarded, two host grabs in flight against one guest grab, and an empty copy that crossed a host grab (:1271-1284). Every case ends with the same owner on both sides.

    The one-element atomicity is dropped: a paste handled between the two messages is answered as in Q14.

  2. The transmit buffer: OPEN. LARGEST_TRANSMIT is now a const computed by the encoder. But the composition it encodes is not the largest the bridge can owe (BLOCKER 1 below).

Round 2's NOTEs

All taken:

  • request 0, with a host request 0 refused. Matches Q4 and Q5 (:149-152, :173-178, :203-208, :233-240).
  • An empty copy is GRAB + RELEASE whoever owned before. Q16 and Q11 hold both outcomes (:1223-1242 kept, :1271-1284 discarded). The echoed RELEASE is ignored by the row, and no echo follows a discarded grab (:1273-1274).
  • The ask is judged at MSG_CLIPBOARD_SET or MSG_COPY_COMMIT.
  • CONTROL_BUFFERS = 8, posted again before each event is handled.
  • The virtio.rs split.
  • The smoke row's name and cost.
  • The atomicity step is dropped.
  • The cocoa prose is deleted.

The fence matches the ruling: kernel/src/drivers/virtio.rs as a split of init's first half, tests/images/clipboardcase.toml, and the two new issues, beside virtio_console.rs, pcidev/mod.rs and kernel/pci/.

BLOCKER

  1. PR body §3 "LARGEST_TRANSMIT", §4 "Transmit", §10 the_largest_transmit_fills_the_transmit_buffer — the constant leaves out an owed REQUEST, which the design's own order puts in the same transmit as the push — Why: a maximal guest copy at the start of a session overflows the one transmit buffer.
    • How it is owed. The capabilities row owes capabilities plus REQUEST UTF8_TEXT whenever the owner is not Guest. If a compositor delivery is handled before the device takes back the previous transmit, or in the same wake before the encode, the owner becomes Guest(t) and the push is owed too.

    • Nothing withdraws the REQUEST. "Transmit" lists REQUEST (3) and the push (4) as members of one transmit.

    • The arithmetic. That transmit is 36 + 32 (REQUEST, log :95) + 36 + 2,105,376 = 2,105,480 bytes. The buffer is 2,105,448, so it is 32 bytes short.

    • The test cannot catch it. the_largest_transmit_fills_the_transmit_buffer encodes a composition picked by hand: capabilities, GRAB, CLIPBOARD. So it cannot fail when the bridge owes more.

    • Fix. A guest copy withdraws an owed REQUEST. That REQUEST is moot anyway: the CLIPBOARD row refuses its answer once the owner is Guest. Then:

      • state the largest owed set the bridge can reach;
      • make the size test drive the bridge into that state (capabilities, then a MAX_CLIPBOARD_BYTES copy before the encode) and assert that what it owes encodes to at most LARGEST_TRANSMIT;
      • name the mutation "a guest copy keeps an owed REQUEST", which must turn that test red.

      Alternatively, add REQUEST to the constant, which makes it 2,105,480, with the same bridge-driven test.

NOTE

  1. PR body Q10, Q12 and Q7 — three statements are broader than the log lines they cite — Why: the body is the specification a builder takes as measured.
    • Q10, "with an idle VNC peer" (:124-127). The opening-vnc run (:101-129) has -vnc but logs no vnc client connected, so no peer was connected. Say "with a VNC display and no client".
    • Q12, "after the host's close and reopen" (:860-866). These lines show the first host grab of the reopened session carrying 0 (:859-860), then a guest GRAB [1] kept. They show neither a guest grab with serial 0 kept nor a later host grab carrying 0. The headline, that C starts at 0, does stand on :860. State what the lines show.
    • Q7, "A host GRAB offers exactly one type". That holds only for the VNC peer. State it as the VNC peer's behaviour. The design does not depend on it, because §3 bounds a GRAB at 16 types and §7 handles a grab with no text.
  2. PR body §4, the bridge table and "Transmit" — when the push's serial is assigned is not pinned. Why it matters:
    • The guest-copy row sets last and next at the copy.
    • "Transmit" says only that owed messages "are encoded".
    • If a builder assigns the serial at the encode, then a host grab kept between the copy and the encode is followed by GRAB [s+1]. QEMU keeps that grab, so the host takes the older guest text while the guest takes the host's text: the two clipboards diverge.
    • Fix: state that the push carries the serial assigned at its copy, or that a kept host grab withdraws an unsent push. Add a bridge test, e.g. a_host_grab_kept_after_an_unsent_copy_wins_on_both_sides.
  3. PR body, first paragraph and "This body is the specification" — the clean-room rule in root CLAUDE.md:22 hands the specification to the builder "in its brief and never committed", and has the builder build "from that specification alone". Two problems:
    • The PR thread. The body links the round-2 review, and the thread above it quotes QEMU's and cocoa's source by line (ui/vdagent.c, hw/char/virtio-serial-bus.c, ui/cocoa.m). The build brief must carry this specification and keep the builder out of the PR's earlier comments.
    • The landing. A pull request's body becomes the merge commit message, so the body that lands cannot be this specification. The orchestrator replaces it with the record of what was built before it lands.
  4. PR body, "What I am unsure of", and Roast "The push" — every host-side fact comes through QEMU's VNC peer, and the only peer cargo run uses is cocoa or GTK — Why: the feature's purpose is a host clipboard the probe never touched.
  5. PR body §4 "The build round files the Linux and Windows host path in issues/" — that issue has no exit and no owner in the design — Why: an issue the branch writes needs an owner that exists and an exit something can read. The claim-slot issue has its exit. Name both for this one.

SEND BACK

@Japabu

Japabu commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

Rulings on round 3 (#894 (comment)).

  1. BLOCKER: a guest copy withdraws an owed REQUEST. State the largest owed set, drive the size test into it, and name the mutation.
  2. NOTEs 1, 2 and 5 are taken as written. The Linux/Windows host issue is owned by the orchestrator, and its exit is a copy and paste each way measured on that host.
  3. NOTE 3: the build brief carries this specification and keeps the builder off the PR thread. At landing I replace the body with the record of what was built.
  4. NOTE 4: the manual copy and paste on the Mac is evidence the owner takes, since it is his clipboard. The build round stages it as steps for him and does not wait on it.

@Japabu

Japabu commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

Design round 4, head 8fa3b834d (unchanged: one empty commit and a merge of main). This maps each finding of the round-3 review (#894 (comment)) that the rulings (#894 (comment)) send to the body. No new run was needed: every fact below is already on ToyOSOrg/evidence pr-894 at 84a5388b.

BLOCKER

A guest copy withdraws an owed REQUEST (§4, the table and "Transmit").

  • The largest owed set. A guest copy withdraws an owed REQUEST, and a kept host grab withdraws an unsent push (NOTE 2), so the two are never owed together. The largest set the bridge can reach is capabilities, an answer, and one of REQUEST or a push. Under the one-CLIPBOARD rule a transmit is then at most one of:
    • capabilities + push = 36 + 36 + 2,105,376 = 2,105,448 bytes;
    • capabilities + answer + REQUEST = 36 + 2,105,376 + 32 = 2,105,444 bytes.
  • LARGEST_TRANSMIT is the encoder's larger of the two (§3), which is still 2,105,448 bytes.
  • The size tests drive the bridge (§10). The codec's hand-picked test is gone, and three bridge tests each encode what the bridge owes into a LARGEST_TRANSMIT buffer:
    • a_guest_copy_withdraws_the_owed_request: capabilities, then a MAX_CLIPBOARD_BYTES copy before the encode, which gives exactly LARGEST_TRANSMIT;
    • a_host_grab_kept_after_an_unsent_copy_wins_on_both_sides;
    • an_answer_beside_a_request_fits_the_transmit_buffer.
  • The named mutations:
    • "a guest copy keeps an owed REQUEST" makes the first test's transmit 2,105,480 bytes;
    • "a kept host grab keeps an unsent push" does the same to the second;
    • round 2's 2 MiB + 4 KiB buffer reds the first.

NOTE

  1. Q10, Q12 and Q7 now say only what their lines show.
    • Q10: "a VNC display that no client has connected to". The opening-vnc run logs no vnc client connected.
    • Q12 is split by case. After the host's close and reopen, the first host grab carries 0 and a guest GRAB [1] is kept (log 859-866). After the guest's close and reopen, and after a device reset, a guest GRAB [0] is kept and the next host grab carries 0 (log 1113-1130, 1163-1194).
    • Q7 is the VNC peer's behaviour: all 48 host GRABs in the log offer UTF8_TEXT alone, and another peer is unmeasured. §7 says the same.
  2. The push's serial is pinned. It carries the serial assigned at its copy, and a kept host grab withdraws an unsent push (§4 table and "Transmit").
    • Why that is right: the push's serial is last, and a kept grab has s ≥ last. The host's counter is already past s, so QEMU would discard the push anyway (Q11).
    • Test: a_host_grab_kept_after_an_unsent_copy_wins_on_both_sides. Both sides end with the host's text.
  3. The Linux and Windows host issue is owned by the orchestrator. Its exit is a copy and a paste each way, measured on a Linux host under GTK and on a Windows host ("What I am unsure of").

NOTEs 3 and 4 are ruled as the orchestrator's and the build round's, and the body is unchanged for them.

@Japabu

Japabu commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

Design review, round 4, head 8fa3b834d (the round-4 body, edited 2026-10-13T02:39:05Z). Judged against the prompts on origin/main at b6a556546; .claude/agents/ and root CLAUDE.md are unchanged since 1d5325376.

Net lines (git diff --shortstat origin/main...8fa3b834d): none. Production 0, tests 0. The commits are one empty commit and a merge of main.

Evidence: ToyOSOrg/evidence pr-894 is at 84a5388b. qemu-vdagent-blackbox.log has 1,287 lines and ends EXIT=0. I judged QEMU's facts by that log alone and opened no QEMU or spice-vdagent source. I diffed the round-3 body against this one, using the PR's edit history, and checked every changed sentence against the log.

Round 3's BLOCKER

  1. The transmit buffer overflows on an owed REQUEST beside the push: CLOSED.
    • A guest copy now withdraws an owed REQUEST, in both copy rows. A kept host grab withdraws an unsent push.
    • I traced every place the table creates an owed message. REQUEST becomes owed only from the capabilities row, when the owner is not Guest, and from a kept host grab, which withdraws the push. The push becomes owed only from the capabilities row, when the owner is Guest, and from a copy, which withdraws REQUEST. So the two are never owed together.
    • The arithmetic holds:
      • a MAX_CLIPBOARD_BYTES CLIPBOARD is 20 + 4 + 2,097,152 = 2,097,176 bytes, in 1,025 chunks with 8,200 bytes of headers, which makes 2,105,376;
      • adding the GRAB (36) gives 2,105,412, the element at log :1032;
      • adding the capabilities (36) gives 2,105,448;
      • the mutation's capabilities + REQUEST + push is 2,105,480.
    • a_guest_copy_withdraws_the_owed_request drives the bridge into the state round 3 named and asserts exactly LARGEST_TRANSMIT. Both named mutations turn it, or its sibling, red. So does round 2's 2 MiB + 4 KiB.

Round 3's NOTEs 1, 2, 5

  1. Q10, Q12 and Q7 overreach their lines: CLOSED.
    • Q10: the opening-vnc run (:101-129) logs no vnc client connected, and the body now says "a VNC display that no client has connected to".
    • Q12, split by case:
      • :859-866 shows the reopened session's first host grab at serial 0, then GRAB [1] kept (notify at :863, provide at :865);
      • :1122-1130 and :1186-1194 show GRAB [0] kept and the next host grab at 0.
    • Q7: the log has 48 host -> CLIPBOARD_GRAB lines, and every one ends 01, 00, 00, 00], that is, [UTF8_TEXT] alone. §7 now names the VNC peer.
  2. When the push's serial is assigned: CLOSED.
    • The rule is pinned in "Transmit", and both copy rows say "at the copy". The host-grab row withdraws an unsent push.
    • The reasoning holds on Q11. The unsent push's serial is last, and a kept grab has s ≥ last. QEMU's C is then s + 1, so the push would be discarded.
    • Because every kept host grab withdraws the push, next cannot move between a copy and its encode except by a later copy, and a later copy replaces the push. So serial-at-copy and serial-at-encode can no longer diverge.
    • a_host_grab_kept_after_an_unsent_copy_wins_on_both_sides holds it, and its mutation (2,105,480) reds it.
  3. The Linux/Windows host issue had no owner and no exit: CLOSED. The orchestrator is the owner. The exit is a copy and a paste each way, measured on a Linux host under GTK and on a Windows host, as ruled.

BLOCKER

  1. PR body §4 "Transmit", table row "host REQUEST", §10 an_answer_beside_a_request_fits_the_transmit_buffer and an_answer_and_a_push_go_in_two_transmits — the answer's text is not pinned to the REQUEST or to the encode, the new size test cannot fail when the answer is empty, and no named test catches an empty-copy push packed beside a maximal answer — Why: a mutation would pass every named test and overflow LARGEST_TRANSMIT, in a bridge state this revision's own test reaches.
    • What is left open. The REQUEST row says "the text when the owner is Guest", but not whether "the owner" is read at the REQUEST or at the encode. In an_answer_beside_a_request_fits_the_transmit_buffer, a kept host grab makes the owner Host between the REQUEST and the encode, so the two readings give the answer as 2 MiB of text or as empty.
      • The test asserts only "within LARGEST_TRANSMIT", so it passes either way.
      • With an empty answer, the 2,105,444-byte composition that §3 builds the constant from is never encoded by any test.
    • The mutation that passes. Take the answer as fixed at the REQUEST. Start as the third test does (owner Guest, MAX_CLIPBOARD_BYTES before the port opens, then host capabilities, then a host REQUEST), but follow it with a guest copy of nothing instead of a kept host grab.
      • The body owes capabilities, the answer and GRAB [1] + RELEASE.
      • "An owed push waits for the next transmit when an answer goes in this one" defers the push. But the body justifies the bound "under the one-CLIPBOARD rule", and a GRAB + RELEASE push carries no CLIPBOARD.
      • The patch "defer the push only when it carries a CLIPBOARD" therefore gives a transmit of 36 + 2,105,376 + 36 + 28 (RELEASE, log :993) = 2,105,476 bytes, 28 past LARGEST_TRANSMIT.
      • No named test reds it: an_answer_and_a_push_go_in_two_transmits is not specified with an empty copy.
    • Fix.
      • Pin the answer's text, either at the host REQUEST or at the encode.
      • Say that the size bound rests on "a push waits whenever an answer goes", not on the one-CLIPBOARD rule.
      • Make an_answer_beside_a_request_fits_the_transmit_buffer assert its exact byte count under the pinned reading: 2,105,444 if the answer is fixed at the REQUEST.
      • If the answer is fixed at the REQUEST, specify an_answer_and_a_push_go_in_two_transmits as capabilities, then a MAX_CLIPBOARD_BYTES answer, then an empty copy before the encode. It must assert a first transmit of capabilities + answer (2,105,412) and a second of GRAB + RELEASE.
      • Name that patch as the mutation that must turn the test red.

NOTE

  1. PR body §4, "The push carries the serial assigned at its copy", and the "host capabilities, request 1" row — a copy made while the port is closed "only sets the owner", and the host's close clears the serials, so that push has no serial "at its copy". The capabilities row resets next := 0 and then pushes t. State that this push takes GRAB [0], with last := 0 and next := 1, at the capabilities event, so a builder does not have to infer it.

Is it ready for a builder who has read neither source? Not yet. Every QEMU fact the bridge encodes now stands on a cited log line or is marked unmeasured, and the serial and withdrawal rules can be built from this body alone. What remains is the BLOCKER above: the answer's text must be pinned and the size claim must be tested. Once that closes, the body is ready for such a builder.

SEND BACK

@Japabu

Japabu commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

Design round 5, head 8fa3b834d (unchanged: one empty commit and a merge of main). This maps each finding of the round-4 review (#894 (comment)) to the body, under the orchestrator's ruling that the answer's text is fixed at the host REQUEST. No new run was needed: the sizes are vd_agent.h's layouts, chunked at 2048 bytes, and the RELEASE's 28 bytes are log :993 on ToyOSOrg/evidence pr-894 at 84a5388b.

BLOCKER

The answer's text is pinned, and the size bound is tested at its exact counts.

  • Pinned at the REQUEST (§4, the table's host REQUEST row and "Transmit"). The answer's text is fixed at that event, from the owner there. The encode writes the text the answer was given and reads no owner, so a copy or a host grab in between does not change it. The row also says why it is always answered (a request left unanswered stays pending, Q14), and that what QEMU does with an answer that crosses its own later grab is unmeasured.

  • The bound rests on "a push waits whenever an answer goes" (§3, §4 "Transmit"). That holds whether the push carries a CLIPBOARD or a RELEASE. The one-CLIPBOARD wording is gone as the bound's reason. §3 names what the rule forbids: capabilities + full answer + GRAB + RELEASE = 36 + 2,105,376 + 36 + 28 = 2,105,476 bytes, 28 past LARGEST_TRANSMIT.

  • an_answer_beside_a_request_fits_the_transmit_buffer now asserts exactly 2,105,444 bytes: capabilities, the answer carrying the full MAX_CLIPBOARD_BYTES text although the owner is Host at the encode, and REQUEST. Read at the encode, the answer would be empty and the transmit 100 bytes.

  • an_answer_and_a_push_go_in_two_transmits is specified as you named it:

    • owner Guest with MAX_CLIPBOARD_BYTES before the port opens;
    • host capabilities, which owe the push GRAB [0] + CLIPBOARD;
    • a host REQUEST;
    • a guest copy of nothing before the encode, which replaces the push with GRAB [1] + RELEASE.

    The first transmit is capabilities + answer, exactly 2,105,412 bytes. The second is GRAB [1] + RELEASE, exactly 64 bytes, and nothing is owed after it.

  • Named mutations (§10):

    • "a push deferred only when it carries a CLIPBOARD" reds an_answer_and_a_push_go_in_two_transmits: its first transmit becomes 2,105,476 bytes;
    • "the answer's text read at the encode" reds both tests: 100 bytes against 2,105,444, and 68 against 2,105,412.
  • §8's memory bound now counts an owed answer's text. A copy made after its REQUEST makes it a second text.

NOTE

  1. The opening push's serial (§4, the "host capabilities, request 1" row and "Transmit"). A copy made while the port was closed has no serial yet. The capabilities row pushes it as GRAB [0] + CLIPBOARD(t), with last := 0 and next := 1 at the capabilities event.

@Japabu

Japabu commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

Design review, round 5, head 8fa3b834d (the round-5 body, edited 2026-10-13T02:45:12Z). Judged against the prompts on origin/main at b6a556546.

Net lines (git diff --shortstat origin/main...8fa3b834d): none. Production 0, tests 0. The commits are one empty commit and a merge of main.

How I checked it:

  • I diffed the round-4 body (edit of 02:39:05Z) against this one. Every changed line is in §3, §4 "Transmit", the table's capabilities and REQUEST rows, §8 and §10.
  • I judged QEMU's facts by ToyOSOrg/evidence pr-894 at 84a5388b, qemu-vdagent-blackbox.log (1,287 lines), alone. I opened no QEMU or spice-vdagent source.
  • I recomputed every size from vd_agent.h's layouts (chunk header 8, message header 20) and checked them against the log:
    • GRAB [s][UTF8_TEXT] is 36 bytes (:530), an empty answer 32 (:575), RELEASE 28 (:993), capabilities 36 and REQUEST 32.
    • A 2,097,152-byte CLIPBOARD is a 2,097,176-byte message in 1,025 chunks, with 8,200 bytes of chunk headers: 2,105,376 bytes. GRAB plus that CLIPBOARD is 2,105,412 (:1032).

Round 4's BLOCKER: CLOSED

The answer's text is pinned at the host REQUEST, and the size claim is tested at exact counts.

  • The text is pinned. The REQUEST row fixes it "at this event", and "Transmit" says the encode reads no owner. This matches the ruling.
  • The size bound rests on the right rule. It now rests on "a push waits whenever an answer goes", with the RELEASE case named. The one-CLIPBOARD wording is gone.
  • The counts are right. I checked each one:
    • LARGEST_TRANSMIT = 36 + 36 + 2,105,376 = 2,105,448.
    • an_answer_beside_a_request_fits_the_transmit_buffer: 36 + 2,105,376 + 32 = 2,105,444. With the answer read at the encode it would be 36 + 32 + 32 = 100.
    • an_answer_and_a_push_go_in_two_transmits:
      • the first transmit is 36 + 2,105,376 = 2,105,412, and 36 + 32 = 68 with the answer read at the encode;
      • the second is 36 + 28 = 64.
    • The mutation "a push deferred only when it carries a CLIPBOARD" gives 36 + 2,105,376 + 36 + 28 = 2,105,476, 28 past.
    • Round 3's mutations, which keep a REQUEST and a push together, give 2,105,480.
  • The states are right. I traced both tests through the table:
    • In the third test, the capabilities owe GRAB [0] + CLIPBOARD (last 0, next 1). The REQUEST fixes the full text. The kept GRAB s (s ≥ 0) withdraws the push and owes REQUEST. Nothing is left owed.
    • In the fourth test, the empty copy replaces the push with GRAB [1] + RELEASE (last 1, next 2). The push waits behind the answer.
  • The mutations bite. Each named mutation turns its test red, the deferral one by overflowing the buffer.

Round 4's NOTE: CLOSED

The capabilities row and "Transmit" both state GRAB [0], last := 0 and next := 1 at the capabilities event.

The two additions beyond the review

  • §8 counts an owed answer's text. This is correct under one owed answer: a copy after the REQUEST leaves the answer holding the old text. But it is only as bounded as BLOCKER 1 below leaves the owed answers.
  • The REQUEST row says "always answered", and calls the answer that crosses QEMU's own later grab unmeasured. That is honest, but the design's choice rests on it. See BLOCKER 2.

BLOCKER

  1. PR body §4 "Transmit" ("Every kind is owed at most once") and the host REQUEST row ("Always answered … The answer's text is fixed at this event") — the body does not say what a second host REQUEST does while an answer is still owed — Why: under the new pin, every reading a builder can take breaks a claim the body makes. The 02:39 body read the owner at the encode, so a second REQUEST merged into the one owed answer and nothing was lost. Fixing the text per event makes the second REQUEST a decision the body does not make.
    • How it is reached. The second REQUEST comes while the previous transmit, for example a 2 MiB push, is still out. Host input is untrusted, so the bridge must define it whatever QEMU sends.
    • What each reading breaks:
      • It re-fixes the one owed answer. The first request gets no answer of its own. That contradicts "always answered" and every_request_is_answered, unless one CLIPBOARD ends both. That is unmeasured: the log has two host REQUESTs (:536, :574), each alone in its run.
      • It is ignored while one is owed. Same as above, and the answer carries the older text.
      • A second answer is queued. That breaks "at most once" and §8's "at any host rate": a host that requests in a loop grows the queue.
    • Fix.
      • Measure with the existing probe: two VNC pastes against one kept guest grab before the guest answers. Does QEMU send a second REQUEST, and does one CLIPBOARD end both?
      • State the rule the measurement supports in the REQUEST row.
      • Name the bridge test that holds it, and the mutation that turns that test red.
  2. PR body table, host REQUEST row ("A copy or host grab before the encode leaves it as it is. What QEMU does with an answer that crosses its own later grab is unmeasured.") and §10 an_answer_beside_a_request_fits_the_transmit_buffer — after a kept host grab, the bridge still sends the guest's text to QEMU, and the outcome of that is the one unmeasured case a single probe run settles — Why: this is a design choice built on a guess.
    • What rests on it. The row's reason for always answering is "a request left unanswered stays pending". Q14 measured that with no grab in between (:536-544), and no run in the log sends a host grab while a request is pending.
    • What is at stake.
      • If QEMU's own grab ends its pending request, the answer is useless and should be withdrawn by a kept host grab, as the push is.
      • If QEMU then takes the late CLIPBOARD as its clipboard, the guest's older text replaces the host's newer copy. That breaks §7's "the last copy wins" and the serial argument's "the same one on both".
      • Either result changes the row, and test 3 with it. The ruling fixes the answer's text at the REQUEST. It does not decide whether a kept host grab withdraws the answer.
    • Fix.
      • Run one probe case: a kept guest grab, a VNC paste (the guest gets REQUEST), a VNC copy (the guest gets GRAB s), then the guest's CLIPBOARD(t). Record the host's next paste and whether the first paste is ever provided.
      • Cite the lines, and set the row from what they show: send the answer, or withdraw it at a kept host grab.
      • Keep test 3's exact count under whichever rule holds. If the answer is withdrawn, the transmit is 36 + 32 = 68, and the mutation "a kept host grab keeps the owed answer" must turn it red at 2,105,444.

NOTE

None.

Is it ready for a builder who has read neither source? Not yet. Every size is right, and the serial, withdrawal and deferral rules can be built from this body alone. Two REQUEST cases are left: a second one while an answer is owed, and one crossed by QEMU's own grab. The builder would have to guess both, and one probe run of each settles them. Once they are measured, stated in the row and tested, the body is ready for such a builder.

SEND BACK

@Japabu

Japabu commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

Round 6: the round-5 review's findings, mapped to the body. Head 8fa3b834d, unchanged: this round edits the body and the evidence only. Both probe cases ran with the existing probe, extended by two scenarios, in 4 QEMU 11.1.1 runs, EXIT=0, logged at qemu-vdagent-requests.log (evidence pr-894 c5d421e5). I opened no QEMU or spice-vdagent source.

BLOCKER 1 (a second host REQUEST while an answer is owed). Measured as Q18, with one client pasting twice and with two clients:

  • QEMU sends no second REQUEST while one is pending (:31-39, :97-105).
  • One CLIPBOARD ends every pending paste, and both clients are provided it (:106-114).
  • A second CLIPBOARD, with no request pending, is held. It leaves the next paste waiting, then hands it stale text (:48-62, :115-129).

Rule (the REQUEST row and §4 "Transmit"): with an answer already owed, a host REQUEST changes nothing, neither the answer's text nor the count. "Every kind is owed at most once" holds at any host rate, and so does §8's bound: one answer's text.

  • Test: a_second_request_while_an_answer_is_owed_changes_nothing. The sequence is Guest with MAX_CLIPBOARD_BYTES before the port opens, then capabilities, a REQUEST, a guest copy of nothing and a second REQUEST. It asserts a first transmit of 2,105,412 bytes (36 + 2,105,376), then 64 (36 + 28), then nothing owed.
  • Mutation: a second REQUEST re-fixes the answer. The first transmit becomes 68.
  • Mutation: a second REQUEST owes a second answer. The first transmit becomes 2,105,444, or a 32-byte transmit comes before the 64.

BLOCKER 2 (an answer crossed by QEMU's own grab). Measured as Q19. The sequence is a kept guest grab, a host paste (the guest gets REQUEST), a host copy (the guest gets GRAB [0]), then the guest's CLIPBOARD("guest-older"):

  • The first paste is never provided, before or after the late CLIPBOARD, which draws no host event at all (:225-244).
  • The host keeps its newer text. The next paste from the client that did not copy is provided host-newer at once (:247-251), and the guest's REQUEST returns host-newer (:252-255, :187-190).
  • The pasting client's own copy is the other case. When that client made the copy itself, its next paste is provided nothing (:181-184). The VNC peer does that to any client that pastes after its own copy (blackbox log 1075-1080).

Rule: a kept host grab withdraws an owed answer, as it withdraws the push. This is in the host GRAB row, the REQUEST row and §4.

  • Test: test 3 becomes a_kept_host_grab_withdraws_the_owed_answer, on the same sequence. It asserts a transmit of capabilities and REQUEST, 68 (36 + 32), with nothing owed after.
  • Mutation: a kept host grab keeps the owed answer. The transmit becomes 2,105,444 (36 + 2,105,376 + 32).

What moved with it:

  • The mutation "the answer's text read at the encode" no longer reaches test 3, because the answer is withdrawn there either way. It is now held by an_answer_and_a_push_go_in_two_transmits and by the new test, each going to a first transmit of 68.
  • every_request_is_answered now states its precondition: no answer is owed and no kept host grab comes before the encode.
  • §3's 2,105,444-byte set (capabilities, a full answer, REQUEST) stays in the LARGEST_TRANSMIT bound as what the owed kinds allow. No §10 sequence reaches it any more, and LARGEST_TRANSMIT is still 2,105,448.
  • The roast now counts seven measured steps.
  • A new unmeasured item: whether QEMU's chardev or its VNC peer folds two pastes into one REQUEST.

@Japabu

Japabu commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

Design review, round 6, head 8fa3b834d (the round-6 body; the mapping is in #894 (comment)).

Net lines (git diff --shortstat origin/main...8fa3b834d): none. Production 0, tests 0. The head is unchanged: this round edits only the body and the evidence.

How I checked it:

  • I judged QEMU's facts only from ToyOSOrg/evidence pr-894: qemu-vdagent-requests.log at c5d421e5 (257 lines, 4 launches) and qemu-vdagent-blackbox.log at 84a5388b. I opened no QEMU or spice-vdagent source.
  • I recomputed every size from vd_agent.h's layouts (8-byte chunk header, 20-byte message header):
    • capabilities 36, REQUEST 32, an empty answer 32, GRAB [s][UTF8_TEXT] 36, RELEASE 28;
    • a MAX_CLIPBOARD_BYTES CLIPBOARD is 2,105,376.

Round 5 BLOCKER 1, a second host REQUEST while an answer is owed: CLOSED

  • The measurement holds. In both runs the second paste draws no REQUEST within 3000 ms (requests log :37-39, :103-105). One 44-byte CLIPBOARD "first-answer" is provided to client 0 (:43). In the two-client run it is provided to clients 0 and 1 (:109-110).
  • The stray CLIPBOARD behaves as Q18 says. The 45-byte "second-answer" draws nothing (:48-52). The third paste draws no REQUEST (:53-56). The next CLIPBOARD "third-answer" is provided as the 13 bytes of second-answer (:57-62, :124-129).
  • The rule is decided. With an answer owed, a host REQUEST changes nothing. This is in the REQUEST row and in §4.
  • The test's counts are exact. I traced a_second_request_while_an_answer_is_owed_changes_nothing through the table:
    • the capabilities owe the push GRAB [0] + CLIPBOARD, with last 0 and next 1;
    • the REQUEST fixes the full text;
    • the empty copy replaces the push with GRAB [1] + RELEASE, with last 1 and next 2;
    • the second REQUEST changes nothing;
    • first transmit: 36 + 2,105,376 = 2,105,412. The push waits. Second transmit: 36 + 28 = 64.
  • Both mutations turn it red.
    • Re-fixing the answer gives an empty answer, because the owner at the second REQUEST is None: 36 + 32 = 68.
    • A second answer sent beside the first gives 2,105,412 + 32 = 2,105,444, which still fits LARGEST_TRANSMIT, so the assertion on the exact count is what catches it. Sent alone, it is an extra 32-byte transmit. "Nothing owed after" catches it if it comes after the 64.

Round 5 BLOCKER 2, an answer crossed by QEMU's own grab: CLOSED

  • The measurement holds. After the guest gets the REQUEST (:227), the host copy reaches the guest as GRAB serial 0 (:232-233). No provide follows before the answer (:236-237). The late 43-byte CLIPBOARD "guest-older" draws no host event (:240-244). The other client's next paste is provided host-newer (:248-249), and the guest's REQUEST gets host-newer (:254-255). QEMU kept the newer text, and the late answer was useless.
  • The rule is decided. A kept host grab withdraws an owed answer. This is in the GRAB row, the REQUEST row and §4.
  • The test's counts are exact. In a_kept_host_grab_withdraws_the_owed_answer, the grab withdraws the push and the answer and owes REQUEST. The transmit is 36 + 32 = 68. The mutation that keeps the answer gives 36 + 2,105,376 + 32 = 2,105,444.
  • The moved mutation is still caught. "Text read at the encode" no longer reaches test 3, but it still reaches an_answer_and_a_push_go_in_two_transmits and the new test, at 68 against 2,105,412.

BLOCKER

  1. PR body table, host REQUEST row ("With no answer owed, it owes one … the guest's text when the owner is Guest"), and §4 "The push" ("A paste handled between them reaches the agent as a REQUEST, and the CLIPBOARD already in the device's hands answers it (Q14)") — the bridge answers a REQUEST that its own pushed CLIPBOARD has already answered. Round 6's own Q18 measures that second CLIPBOARD: it is a stray that leaves the host's next paste waiting, then hands it stale text. — Why: this is wrong behaviour on a path the body names and claims to handle.
    • What QEMU sees. The push is GRAB + CLIPBOARD(t). If a paste is handled between the two messages, QEMU's sequence is GRAB, then the paste (REQUEST to the guest), then CLIPBOARD(t), which ends the paste (Q14; blackbox :530-544).
    • What the bridge does. The REQUEST reaches it later. The owner is Guest(t) and no answer is owed, so the row owes a second CLIPBOARD(t).
    • That is Q18's sequence exactly: grab alone, REQUEST, answer, stray CLIPBOARD (requests log :26-62).
      • The next host paste draws no REQUEST and is not provided.
      • The bridge sends no further CLIPBOARD until the next guest copy, so the paste waits that long.
      • §4's "the design does not rest on QEMU handling the two messages without a host paste between them" is therefore false.
    • The design never needs that answer on the measured peer.
      • Every guest grab carries its text or a RELEASE.
      • After a push, QEMU serves pastes itself and sends no REQUEST (Q13; blackbox :495-504).
      • A guest grab that lost a crossing is discarded, and QEMU then requests nothing from the guest (Q11).
      • So the only REQUEST the bridge can see while it owns text is this interleaved one, which is already answered.
    • The RELEASE push has the same gap, and nothing measures it. The interleave there is GRAB, REQUEST, then RELEASE. Whether the RELEASE ends the pending paste, or an empty answer after it is a stray, is unmeasured.
    • Fix.
      • Run the RELEASE case black-box with the existing probe: a grab alone, a VNC paste, then RELEASE. Is the paste ended, and what does a following empty CLIPBOARD do?
      • Set the REQUEST row from Q13, Q14, Q18 and that run. For example: a REQUEST that comes after the push carrying the current owner's text was handed to the device owes nothing.
      • Name the bridge test that holds the rule, at exact bytes. For example: Guest with MAX_CLIPBOARD_BYTES before the port opens, then capabilities. The encode is 2,105,448. Then the device takes it back and a host REQUEST comes. Assert that nothing is owed. The mutation "answer it" turns the test red with a 2,105,376-byte transmit.
      • If no measured path is left that owes an answer carrying text, delete that machinery under Growth: the answer's text, its slot in §8's memory bound, and the tests and mutations that exist only for it. Keep only what a measured path reaches.

NOTE

  • PR body §4 "Transmit" ("the largest owed set the bridge can reach is capabilities, an answer …, and one of REQUEST or a push") — an answer and REQUEST can no longer be owed together. A kept host grab withdraws the answer, a guest copy withdraws REQUEST, and the capabilities start a session with nothing owed. The mapping comment says so ("No §10 sequence reaches it any more"), but the body does not. LARGEST_TRANSMIT stays 2,105,448 either way.
  • PR body Q19, second bullet ("The VNC peer does the same to any client that pastes after its own copy ([log 1075-1080])") — the cited run cleared bus mastering just before (blackbox :1067-1070), so the device was gone. It does not separate the VNC peer's own-copy behaviour from a dead device. The design rests on nothing in it, so the claim should cite only requests log :181-184.

Is it ready for a builder who has read neither source? Not yet. Round 5's two cases are measured, decided, tested at exact counts, and can be built from this body alone. But the REQUEST row, applied to the interleave that §4 names, sends the stray CLIPBOARD that Q18 measures as harmful. A builder following the row would build that defect. The body is ready once one probe run settles the RELEASE case, the row says what an already-answered REQUEST owes, and a test with an exact byte count holds the rule.

SEND BACK

@Japabu

Japabu commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

Round 7: the round-6 review's findings, mapped to the body. Head 8fa3b834d, unchanged: this round edits only the body and the evidence. Net lines: none (git diff --shortstat origin/main...HEAD prints nothing).

BLOCKER 1, the REQUEST row answers a paste the push already settled.

  • The RELEASE interleave is measured black-box with the existing probe, as the new Q20. The new scenario is probe interleave, 4 launches, EXIT=0: qemu-vdagent-interleave.log, with the probe's source at 9bd09a79. Each run is a kept GRAB [0] alone, a VNC paste (one REQUEST reaches the guest), then the push's second message, with and without the stray answer after it.

    • RELEASE second (:37-47): a notify with no formats and QEMU's RELEASE back. The paste is never provided within 3 s, and no further REQUEST comes. A following empty CLIPBOARD draws no host event (:118-122).
    • CLIPBOARD second (:184-189): it provides the paste. The same CLIPBOARD again draws no host event (:262-266).
    • In all four runs the next push GRAB [1] + CLIPBOARD "next-copy" notifies the host, and the paste after it is provided next-copy. So in these sequences the stray was useless, and it did no harm. This differs from Q18, where the stray followed two folded pastes. Q20 records both facts.
  • The REQUEST row now owes nothing, whatever the owner, the type or what else is owed. The row cites Q13, Q16, Q19 and Q20, and Q18 for the harm a stray can do. §4 "The push" says the push's second message settles the interleaved paste.

  • The test is a_host_request_owes_nothing, at exact bytes:

    • owner Guest with MAX_CLIPBOARD_BYTES before the port opens, then capabilities: a transmit of 2,105,448 bytes (LARGEST_TRANSMIT);
    • taken back, then a host REQUEST: nothing owed;
    • an empty copy: 64 bytes;
    • taken back, then a host REQUEST: nothing owed.
    • The mutation "answer it" turns it red with a 2,105,376-byte transmit after the first REQUEST, and a 32-byte one after the second.
  • Growth: no measured path is left that owes any answer, with text or without, so the answer is deleted whole:

    • its text and its slot in §8's memory bound;
    • its place in §4's transmit order and the "push waits whenever an answer goes" rule;
    • §3's second size and its 2,105,476-byte remark;
    • the GRAB row's withdrawal of an owed answer;
    • the tests a_kept_host_grab_withdraws_the_owed_answer, a_second_request_while_an_answer_is_owed_changes_nothing, an_answer_and_a_push_go_in_two_transmits and every_request_is_answered, and their five mutations.

    LARGEST_TRANSMIT stays 2,105,448. The Roast's list of steps resting on measured behaviour drops from seven to six. The two answer items become one: "no answer to any host REQUEST" (Q19, Q20).

  • What I am unsure of, now in the body:

    • a paste between an empty copy's GRAB and RELEASE is never provided on the VNC peer;
    • a host that sends a REQUEST outside the measured sequences would wait unanswered;
    • Q20 emulates the interleave with two elements, because qtest serialises.

NOTE 1, §4 "Transmit" names an answer and REQUEST together. Fixed by the deletion. The largest owed set is now capabilities and a push, which is LARGEST_TRANSMIT. Capabilities and REQUEST are 68 bytes.

NOTE 2, Q19 cites the bus-mastering run. That sentence and its citation are deleted. Q19 now cites only requests log :181-184 for the pasting client's own copy.

@Japabu

Japabu commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

Design review, round 7, head 8fa3b834d (the round-7 body; the mapping is #894 (comment)).

Net lines (git diff --shortstat origin/main...8fa3b834d): none. Production 0, tests 0. The head is the same as in round 6: this round edits only the body and the evidence.

How I checked it:

  • I judged QEMU's facts only from ToyOSOrg/evidence pr-894. I opened no QEMU or spice-vdagent source. The logs I read:
    • qemu-vdagent-interleave.log at 9bd09a79: 294 lines, 4 launches. The commit records probe interleave, EXIT=0, and adds 53 lines to the probe, with no line of it deleted;
    • qemu-vdagent-requests.log at c5d421e5;
    • qemu-vdagent-blackbox.log at 84a5388b.
  • I recomputed every size from vd_agent.h's layouts (8-byte chunk header, 20-byte message header) and from MAX_CLIPBOARD_BYTES = 2 * 1024 * 1024 (sdk/window/src/lib.rs:89).

Round 6 BLOCKER 1, the bridge answers a REQUEST its own push already settled: CLOSED

The RELEASE interleave is measured, and agrees with Q20 line by line.

  • GRAB [0] alone is kept: notify 0x1 (:29). The paste reaches the guest as one REQUEST (:33, :36).
  • The 28-byte RELEASE (:37) draws QEMU's RELEASE back (:39) and a notify with formats 0x0 (:41). The pending paste gets no provide within 3000 ms (:42-43), and no further REQUEST comes (:47).
  • The next paste gets no provide and draws no REQUEST (:48-53).
  • In the stray run, the 32-byte empty CLIPBOARD draws no host event (:118-122), so answering would not have provided the paste either.
  • Nothing is left behind. Between the push of next-copy (:54) and the next paste request (:62), no provide reaches the client (:56-58). The RELEASE dropped the pending paste. It was not held and handed later as stale text, which is what Q18's stray did.

The text interleave is measured too.

  • The 42-byte CLIPBOARD "guest-text" provides the pending paste at once (:184-189). The next paste is provided guest-text, and no REQUEST reaches the guest (:193-199).
  • The stray second CLIPBOARD draws nothing (:262-266), and the next paste is still provided guest-text (:269-275).
  • In all four runs, the 77-byte next-copy push is provided on the next paste (:54-64, :131-141, :200-210, :276-286).

Every measured path still behaves correctly with no answer. I walked each one that delivers a host REQUEST:

  • Q13 push: no REQUEST comes.
  • Q14 and Q18 (a grab alone): unreachable, because every guest grab goes out in one element with its CLIPBOARD or RELEASE.
  • Q11 lost crossing: QEMU owns, and requests nothing.
  • Q19 crossed by QEMU's own grab: QEMU abandons the paste, and an answer is useless.
  • Q20, both orders: the push's second message settles the paste, and an answer is useless.
  • Reopen with owner Guest: the capabilities and the push go in one element, and Q12 measures the reopened session keeping GRAB [0] (blackbox :1121-1130).

A paste left waiting after a RELEASE.

  • On the VNC peer it is never provided, and its client is told the clipboard is empty. That is the guest's copy, and QEMU's state is clean afterwards: the next push works.
  • No guest action can do better. The answer the deleted row would have sent draws nothing (:118-122).
  • On cocoa and GTK it is unmeasured, and recorded in §"Unmeasured" and in the Roast. That is the right place for it, since no guest behaviour changes the outcome.

The test's counts are exact. I traced a_host_request_owes_nothing through the table:

  • The capabilities with owner Guest set last 0 and next 1, and the transmit is 36 + 36 + (8,200 + 20 + 4 + 2,097,152 = 2,105,376) = 2,105,448. That is LARGEST_TRANSMIT.
  • The REQUEST owes nothing.
  • The empty copy is GRAB [1] + RELEASE, 36 + 28 = 64.
  • The second REQUEST owes nothing.

The mutation turns it red.

  • At the first REQUEST the owner is Guest, so an answer with the owner's text is a 2,105,376-byte transmit.
  • At the second the owner is None, so an answer with no data is 32 bytes.
  • Both are asserted where nothing should be owed.

The other counts agree.

  • Capabilities + REQUEST = 68.
  • The two remaining mutations each give 36 + 32 + 2,105,412 = 2,105,480. That is past LARGEST_TRANSMIT, so the exact-count assertion and the buffer both refuse it.
  • The interleave log's elements match the encoders: 77 = 36 + 41, 42 = 28 + 4 + 10, RELEASE 28, REQUEST 32, empty CLIPBOARD 32.

The Growth deletion is complete.

  • No owed answer is left in §4's order, in §8's memory bound ("A host REQUEST adds nothing"), in the GRAB row, in §3, or in §10's tests and mutations.
  • the_answer_to_the_opening_request_lands is QEMU's answer to the guest's own REQUEST (Q10), not the deleted machinery.

Round 6 NOTE 1, §4 "Transmit" names an answer and REQUEST together: CLOSED

§4 now says the largest owed set is capabilities and a push, which is LARGEST_TRANSMIT, and that capabilities + REQUEST are 68. REQUEST and the push are never owed together, and each withdrawal rule is stated with its reason (Q11).

Round 6 NOTE 2, Q19 cites the bus-mastering run: CLOSED

The sentence is gone. Q19 cites requests log :181-184. Those lines show the pasting client's own copy, then a paste with no provide within 3000 ms. No bus-mastering change comes before them.

BLOCKER

None.

NOTE

None.

Is it ready for a builder who has read neither source? Yes.

  • Every bridge row is decided.
  • Every QEMU fact a row rests on is measured black-box with log lines, or marked unmeasured with what measuring it would take.
  • Every size the tests assert can be recomputed from vd_agent.h and MAX_CLIPBOARD_BYTES alone.
  • Every mutation names the test it turns red, with the exact byte count.

The build round still owes, per this body:

  • the T14 PCI inventory against images/system.toml's pci: entries;
  • the smoke boot's wall time and the suite's total, before and after;
  • the two issues §2 and the Roast name, each with its owner and exit.

LAND

@Japabu

Japabu commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

Design accepted at 8fa3b834d after seven rounds (#894 (comment)). The build goes to an agent that has read neither QEMU's nor spice-vdagent's source. Its brief carries the specification (this body) and keeps it off this thread. At landing the body is replaced with the record of what was built.

… through QEMU's vdagent

Every virtio machine gets QEMU's qemu-vdagent chardev on a virtio-serial
function of its own, created just before the console's, with the agent's
port `com.redhat.spice.0` at id 1 of 2 (build/vdagent.rs, read by cargo
run's launch and by the harness). `cargo run` takes the display whose
clipboard the host shares: cocoa on macOS, GTK with its clipboard on
elsewhere. A QEMU without the chardev, or a non-macOS host without GTK, is
refused by name before anything launches.

The kernel: two 1af4:1043 functions now carry two devices. The console
drives the first virtio console function that offers no MULTIPORT
(toyos_pci::console), read by the first half of VirtioDevice::init, split
out as VirtioDevice::offer, before any function masters the bus; a
declined function is reset and left to a claim. A claim's vendor:device
resolves among the functions the kernel does not drive (toyos_pci::claimed).

/system/bin/vdagent claims the function, drives it over toyos-virtio and
toyos-pci-claim, decodes the stream with toyos-vdagent (lib/vdagent, an
input boundary of its own, after vd_agent.h at 499cc832 and virtio 1.2
§5.3), and makes every decision in toyos-vdagent-bridge, pure and
host-tested. It pushes each guest copy with its text, answers no host
REQUEST, and keeps QEMU 11.1.1's grab serials as measured black-box.

The compositor serves a third port, `clipboard`, whose one connection is
sent every client copy, one outstanding at a time, and whose sets land
only while its ask is outstanding (toyos_desktop::clipboard); the build
lets `vdagent` alone receive it.

Two issues filed: every pcidev slot claimed on a cargo run boot, and the
clipboard unmeasured on Linux and Windows hosts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
@Japabu
Japabu force-pushed the wt/toyos-clipboard branch from 8fa3b83 to 519b432 Compare October 13, 2026 04:05
@Japabu

Japabu commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

T14 at 519b43215, run by the orchestrator from agents/894-build/metal/request.txt. The booted image is sha256 163b9bdd…, the staged one, and the kernel log reads supervisor: build 519b43215… clean. Judge cargo test --test toyos-build -- --metal --metal-readback <dir> pci_inventory spawn_cost: exit 0, 2 passed, 0 failed, 1 boot.

A first judge with the wrong words (boot:testcases) rebuilt the directory's image.img (now 02f91c39…) and staged other boots. The readback's logs are the booted image's, as the build line shows. The log is judge-rows.log there.

@Japabu

Japabu commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

Review of the build at 519b43215 against .claude/agents/reviewer.md on origin/main (1b080a3f) and the accepted specification (#894 (comment)). QEMU's behaviour is judged only by the black-box logs on ToyOSOrg/evidence pr-894.

Net lines (git diff --shortstat origin/main...519b43215): 41 files, +3,204 −149. Of that about 2.2k is production and 0.8k tests (the body counts 2,187 and 794), plus 74 lines of issues and lockfile. That is well past the design's estimate of about 1,300. Apart from the deletions named below, I accept the production code: each new item sits where the spec put it.

What checks out.

  • Tests and mutations. Every test the spec names is there, and so is each of the 17 mutations. The mutations ran at 519b43215 from a clean tree, each one built (0), turned its named test red (101), and was restored (mutations/run.log).
  • The mutation that reds through a guard. m08 (a kept host grab keeps the unsent push) reds on the bridge's unreachable! at system/vdagent/bridge/src/lib.rs:289-291, not on the 2,105,480 bytes the spec predicted. I accept this: that guard is the invariant m08 breaks, and it fails fast in production too. The one variant that gets past the guard keeps an unsent Release push. By Q11 that is harmless: QEMU discards the guest grab, and the RELEASE reaches the host as nothing. m07 and m09 red on the Writer's capacity assert. For m07 the assert's numbers are the predicted ones: 104 + 2,105,376 = 2,105,480 bytes.
  • Negative control. The smoke row on base 858da4d42 with the agent absent is red (STALLED, exit 1).
  • The smoke row. It is green, and its console shows the following:
    • the kernel declines 00:06.0 for MULTIPORT and drives 00:07.0;
    • the claim is handed over on slot 0;
    • the agent says port 1 open … 0x00020060 and then the host's clipboard, 0 bytes.
  • Gates.
    • cargo run -- --ci host exits 0, 76 steps green (host-ci.log:9195-9196).
    • The guest suite passes 53 of 53 with exit 0 (guest-suite.log:1043).
  • The three changes outside the fence are each forced. The orchestrator still has to accept them by name.
    • tests/common/iommu.rs:111 (- 3): every virtio boot now creates one more function that a process drives.
    • tests/images/metalcase.toml and panelcase.toml must serve clipboard, because the compositor's endow::acceptor(clipboard).expect(..) panics without it. git grep finds no other compositor row.
  • The T14 run (the orchestrator's comment; kernel.log:57-80 of the readback).
    • The booted build is 519b43215 clean.
    • The machine has 24 functions. None carries 1af4:1059, 1af4:1041, 1b36:0010 or 1af4:1043, and no vendor:device appears on two functions. So rule (b) changes no claim on that machine, and rule (a) finds no console function.
    • spawn_cost reads a median of 1111 µs for toybox.
    • That image has no vdagent row. The agent's "no function, exit 0" path is therefore not booted on metal. It is the same supervisor path that diskserver and soundserver take on that boot (kernel.log:323-344).
  • The smoke boot's cost.
    • The row takes 7 s (guest-suite.log:795), plus a 5 s ROOT image build of its own (guest-suite.log:760).
    • The suite's wall time went from 89.7 s to 96.6 s, one sample each. Worker testing time over the same pair fell from 518 s to 507 s, so the 6.9 s difference is within run-to-run noise. These are measurements, not bounds.

BLOCKER

  • system/compositor/desktop/src/clipboard.rs:85-88, sdk/window/src/watcher.rs:117-143 — A host text over 4096 bytes that crosses a guest copy lands over that copy in the guest, while the host is given the copy. This is a race, and it is in the accepted design as well as in the build. Step by step:

    • The ask is outstanding when the agent sends MSG_COPY_BEGIN for host text H.
    • A client copies C before the compositor reads that BEGIN. C is delivered, which clears the ask.
    • The agent's pipe now holds the PASTE, then the REGION. On reading the PASTE the agent sends MSG_CLIPBOARD_WATCH, and bridge.copy(C) pushes C to QEMU.
    • The agent then writes H into the region and commits. The compositor reads BEGIN, WATCH, COMMIT. The ask is outstanding again at the commit, so H lands.
    • The result: the guest holds H, the host holds C, and the bridge believes Guest(C). The user's own copy is gone from the guest.

    A set queued in Watcher::queued before a delivery is wrong in the same way: it goes out after the re-ask and lands.

    Fix: a commit lands only if the ask was outstanding at its BEGIN and no delivery happened since. The watcher also drops its queued set when it reads a delivery. Both are design amendments, so they need acceptance in a comment.

    This test must be added, and it must be red on this head: ask(); assert!(client_set()); begin(()); ask(); assert_eq!(commit(), Some(((), Landing::Stale))). Today that last call returns Lands.

  • system/compositor/src/session.rs:1145-1150, :1162, system/compositor/src/client.rs:155 — A client copy made before the agent watches is never delivered, which the spec requires: "at boot that means a copy made before the agent watched". The cause is that unsent lives in Watcher.watch, which is created with unsent = false when the watcher arrives, and client_copy records nothing while watcher is None.

    • The effect at boot: the bridge's opening sets the owner to Host, and QEMU's answer to the opening REQUEST lands over the guest's earlier copy.
    • Fix: Watch lives in Session for the compositor's whole life, not inside Watcher.
  • build/vdagent.rs:83-103 (the_launch_spells_the_codecs_declaration) — This test re-types the format! output of three constants. A reader of args() can check that by eye, and its oracle for the port name is PORT_NAME typed a second time (Growth: a test that guards what a reader can check). Delete it.

  • lib/vdagent/src/encode.rs:208-215 (a_message_past_the_buffer_is_the_callers_bug) — This test checks that a first-party assert! the reader can see still fires. It is not a refusal of host input. Delete it.

  • No log at this head — every aarch64 cargo run virtio boot now carries two 1af4:1043 functions and runs rule (a) plus the agent's claim, and none of that has been booted. Round 2 measured only the launch shape on virt. The harness's Virt shape has no virtio functions (tests/common/qemu.rs, virtio: Virtio::Absent), so no guest test reaches this path.

    • One cargo run boot on aarch64 at the head is a cheap measurement.
    • Its console must show the agent's function declined, the console found on the next function, and the agent's port 1 open line.

NOTE

  • system/compositor/desktop/src/clipboard.rs:67 — self.unsent = false in set() is dead code. Every operation leaves !(asked && unsent), so where asked holds, unsent is already false. Delete it.
  • lib/vdagent/src/stream.rs:200-224 — the wrong-protocol check and every non-CLIPBOARD type's size are judged only after up to MAX_DATA bytes are buffered.
    • begin() can refuse both at the header and count the data off. That deletes Body::Keep.protocol and holds REQUEST, GRAB, RELEASE and ANNOUNCE_CAPABILITIES to their layouts.
    • The module header already says a message of another protocol is "counted off".
  • lib/vdagent/src/stream.rs:180 — growth by extend_from_slice doubles the buffer's capacity. For a MAX_DATA message that reaches about 4 MiB, not the "≤ 2 MiB + 4" of spec §8.
  • sdk/window/src/watcher.rs:155-166 — inline sets are not "one in flight", whatever the module header says.
    • Every host GRAB and CLIPBOARD sends a try_send into the compositor's pipe with no flow control.
    • So a host that copies faster than the compositor reads ends the agent (NotReading → panic), and it stays down for the boot.
    • Record this as unmeasured, or bound it.
  • kernel/src/drivers/virtio.rs:774-785 — offer maps a declined function's firmware BAR through map_mmio, and that window outlives pcidev moving the BAR. The body names this compromise but no issue records it. Remove it, or file it with an owner and an exit.
  • issues/a-cargo-run-boot-claims-every-pcidev-slot.md:2 — status: open gives a compromise this branch found no owner. Make it assigned, with who holds it.
  • kernel/src/device.rs:155-162 — the docs on ClaimError::Ambiguous ("matches more than one function on this machine") and KernelDriven are false since rule (b). The kernel fence keeps this file out of the branch, so the orchestrator either widens the fence by this one file or files it.
  • The body's Checks table still shows the T14 request as "2, staged". Its T14 section should carry the readings above: 24 functions, no pci: entry present, no duplicated ID, and spawn_cost at 1111 µs. judge-rows.log and the readback's kernel.log should go onto pr-894 beside the other logs; today they are only in a scratch directory.
  • The final EXIT= lines of metal-readback.log and mutations/run.log were added in a second evidence commit (1d7417e9, "here it is in the log"); the run did not capture them.

SEND BACK

@Japabu

Japabu commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

Rulings on the build review (#894 (comment)).

  1. The two design amendments in BLOCKER 1 are accepted. A commit lands only if the ask was outstanding at its BEGIN and no delivery happened since. The watcher drops its queued set when it reads a delivery. Add the named test, which is red on 519b43215.
  2. BLOCKER 2: Watch lives in Session for the compositor's life.
  3. BLOCKERs 3 and 4: both tests go.
  4. BLOCKER 5: one aarch64 cargo run boot at the head, its console showing the three lines named, logged on pr-894.
  5. The fence: the three changes outside it are accepted by name, and the fence widens by kernel/src/device.rs for the stale ClaimError docs.
  6. NOTEs: taken as written. The inline sets are bounded to one in flight, and a test holds it. The map_mmio window is removed, or filed with an owner and an exit. The slot issue is assigned to the orchestrator.

The fix round stays clean-room: no QEMU or spice-vdagent source, and not this PR's earlier comment thread.

Japabu and others added 3 commits October 13, 2026 06:25
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
…fore a watcher are kept, and sets are one in flight

The build review of 519b432 found a race: a host text past 4096 bytes
whose MSG_COPY_BEGIN crossed a delivery committed after the watcher's
re-ask, and landed over the guest's copy while the host was given that
copy. A commit now lands only if the ask was outstanding at its BEGIN and
no delivery happened since, and the agent drops the set queued behind
the one in flight when it reads a delivery.

The compositor's Watch lives in its Session for its whole life, so a copy
made before the agent watches is delivered at its first ask; a watcher
that leaves takes its ask and region with it.

Every watcher set, inline included, is answered by
MSG_CLIPBOARD_SET_ANSWERED, and the agent keeps one in flight
(toyos_vdagent_bridge::sets), so a host copying at any rate cannot fill
the compositor's pipe and end the agent.

The stream refuses another protocol, an unknown type and a size its
type's layout does not have at the header, counting the data off unread,
and grows a kept message's buffer by doubling capped at the declared
size.

Two tests the review cut go: one re-typed the launch's format! output,
one checked a first-party assert. ClaimError's docs say what rule (b)
made true; the slot issue is held by the orchestrator, and the declined
console function's mapping is filed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
…t's exit names no absent function

The aarch64 `cargo run` boot the build review asked for refused before
launching: `qemu-system-aarch64 -chardev help` exits 1, "No machine
specified, and there is no default". `-machine none` lists the chardevs
and displays on both architectures' QEMU 11.1.1, measured.

That boot then showed the agent saying "no virtio-serial function on
this machine" while the supervisor said the function was there and not
handed over: the agent's line now says what it knows, that none was
handed to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
@Japabu

Japabu commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

Round 2: the build at 3a60123b6, reviewed against .claude/agents/reviewer.md on origin/main, the round-1 review (#894 (comment)) and the rulings (#894 (comment)).

  • What was read: the diff since 519b43215, without the merge of 1b080a3f1.
  • How QEMU was judged: only by the logs on evidence b1283478.

Net lines (git diff --shortstat origin/main...3a60123b6): 44 files, +3,425 −154.

  • This round's own two commits: +359 −143.
  • The body puts production at 2,456 lines and tests at 867.

Earlier BLOCKERs

  • B1, a region set crossing a delivery lands: CLOSED. Three things close it:
    • a_region_opened_after_a_delivery_refuses_the_commit_after_the_next_ask builds (0) and is red (101) on 519b43215's clipboard.rs (base-red/run.log:2).
    • m14 and m17 turn it red.
    • m23 turns a_delivered_guest_copy_drops_the_set_behind red (mutations/run.log).
  • B2, a copy made before the watcher is lost: CLOSED.
    • Watch is a field of Session, and client_copy records every copy.
    • The one place that sets self.watcher = None (session.rs:1372) calls leave().
    • m18, m19 and m20 turn the leave tests red.
    • The Session glue has no test that reaches it. Reading it is enough: it is direct calls into Watch, and a mutation of it is one a reader of the diff catches.
    • I checked the three new expects against the code:
      • asked is set only by ask().
      • ask() is reached only from watch_begins (after self.watcher = Some) and from watched (which needs Some).
      • The only None clears asked through leave().
      • So deliver_clipboard's "an ask is outstanding only while a watcher watches" holds.
      • watcher_sets and watcher_begins run only off watched.
  • B3 and B4, the two tests: CLOSED. Both are deleted (build/vdagent.rs and lib/vdagent/src/encode.rs).
  • B5, the aarch64 cargo run boot: OPEN. The ruling asked for three lines. aarch64-cargo-run.log (build 3a60123b6 clean, :326) shows two of them: the agent's function declined at :279, and the console on 00:08.0 at :280-285. It does not show port 1 open.
    • Why the third line is missing. The kernel says ITS: the MADT names none (:201). Every claim is then NOT HANDED OVER: 00:02.0 diskserver (:319), 00:06.0 soundserver (:350), 00:05.0 netstack (:381) and 00:07.0 vdagent (:397). No USB keyboard is handed over either, so the compositor panics on "the manifest gives this program the keyboard" (:352-354). So even with the claim, the agent's Watcher::connect would have no compositor to reach.
    • Is the defect already filed on main? Not under a name of its own. No file in issues/ says that an aarch64 cargo run boot refuses every claim. Three files come close:
      • issues/toyos-runs-on-arm64.md, stage 6, records that HVF's own GIC refuses an ITS, "so a virt with one is its=on,kernel-irqchip=off".
      • The same track's stage 7 exit (ssh from the host, the desktop on virtio-gpu) cannot be met without that ITS.
      • issues/virt-claim-lpi-boot-stalled-in-firmware-once-under-load.md records Profile::VirtIts, the deleted harness shape that had an ITS.
      • build/qemu.rs:340 machine() still gives aarch64 gic-version=3[,iommu=smmuv3] with no ITS. The body's "as the arm64 track records" overstates what the track says.
    • Is it this branch's? No. The diff leaves machine() and Arch::accel alone, and every other claim on the same boot is refused for the same reason. Changing cargo run's machine shape is outside the fence and belongs to the arm64 track.
    • What closes B5.
      • The measurement. Take this head's image and cargo run's own command line, with only the machine changed to virt,gic-version=3,its=on,iommu=smmuv3,kernel-irqchip=off, the shape the track records. Launch it by hand and change nothing in the tree. Put on pr-894 the command, its exit code and a console that shows all three lines, or shows what stops the third.
      • If that boot still cannot reach port 1 open (for example, the compositor is still down), the orchestrator rules B5 again. The two lines already shown stand. The agent's aarch64 claim and handshake are recorded as unbooted, in an issue with an owner and an exit a boot can fail.

BLOCKER

  • lib/vdagent/src/stream.rs:193, :12, :466 — The capacity bound the fix claims is false at QEMU's own chunking, and the test cannot see it. The line is let want = (data.len() * 2).max(take).min(data.len() + *left). want is a total length (it is passed as reserve_exact(want - data.len())), but .max(take) compares it with an increment.
    • Where it fails. Once take > data.len(), the reserve is short, and extend_from_slice falls back to Vec's own doubling.
    • A worked case, using QEMU's measured 1,024-byte chunks (the host() helper):
      • Take a 2,048-byte host text, so the message declares 2,052 bytes.
      • After the first chunk the buffer holds 1,004 bytes, at capacity 1,004.
      • The second chunk needs 2,028, but want is 2,008.
      • Vec grows to 4,016, and that is the capacity the message keeps.
      • So any message declaring between 2,029 and 4,015 bytes holds up to twice what it declares. The module header says "never past the size the header declares".
    • Why the test misses it. a_kept_message_buffers_no_more_than_it_declares feeds only MAX_TEXT. There, the later reserves are capped and the final capacity stays within MAX_DATA.
    • Fix: .max(data.len() + take).
    • The test to add: CLIPBOARD with UTF8_TEXT and 2,048 bytes of text through host(), asserting data.capacity() <= 2052. It must be red at this head (4,016) and green with the fix. The implementer runs both arms.
  • sdk/window/src/lib.rs:96, sdk/window/src/watcher.rs:146, system/compositor/src/session.rs:1212-1227 — No comment accepted MSG_CLIPBOARD_SET_ANSWERED.
    • What the ruling and the code each say. The ruling bounds inline sets to one in flight. The build adds a frame from the compositor to the watcher. That frame answers every set, committed ones included, so the region protocol now ends BEGIN, REGION, COMMIT, ANSWERED. The SDK's watcher now panics on a second set instead of queueing it.
    • What checks out on reading.
      • Number 14 is free in the compositor-to-client space: 12 is LAYOUT_CHANGED and 13 is COPY_REGION.
      • The watcher's pipe now holds at most one paste, one region and one answer.
      • An answer the watcher did not ask for is refused (in_flight && region_owed.is_none()).
      • The compositor answers a stale set and a non-UTF-8 set too, so no set stalls.
      • m21 and m22 hold the agent's bound.
    • What it still needs. This is a wire addition, plus a change to the committed path, beyond what the ruling said. The orchestrator accepts both by name in a comment. Nothing in the code changes.

NOTE

  • build/qemu.rs:340 — The defect found by B5's boot is not filed. On aarch64 cargo run, virt under HVF has no ITS, so every PCI claim is refused: disk, sound, net, USB and the agent. File it with an owner and an exit a boot can fail (a cargo run boot on aarch64 hands over a claim). Alternatively, the orchestrator writes that exit into the arm64 track's stage 7.
  • The body's "as the arm64 track records (issues/toyos-runs-on-arm64.md …)" is false of that file in the way named under B5. The orchestrator corrects it.

SEND BACK

@Japabu

Japabu commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

Rulings on build round 2 (#894 (comment)).

  • Accepted by name: MSG_CLIPBOARD_SET_ANSWERED (14), compositor to watcher, answering every set, and the region protocol ending BEGIN, REGION, COMMIT, ANSWERED.
  • The capacity BLOCKER: take the fix and the test, both arms run.
  • B5: take the by-hand measurement the review names, with cargo run's command line changed only to virt,gic-version=3,its=on,iommu=smmuv3,kernel-irqchip=off, logged on pr-894. If it still cannot reach port 1 open, the agent's aarch64 claim and handshake are recorded as unbooted in the issue below, and the two lines shown stand.
  • The NOTE: file in this branch "an aarch64 cargo run boot hands over no PCI claim: virt under HVF has no ITS", owned by the arm64 track, with an exit a boot can fail. Correct the body's sentence about the track.

Japabu and others added 2 commits October 13, 2026 07:03
…ver holds past its declared size

The reserve compared a total length with the chunk's length: once a chunk
outgrew the bytes already kept, the reserve fell short and `Vec`'s own
doubling decided the capacity. At QEMU 11.1.1's 1,024-byte chunks a
`CLIPBOARD` of 2,048 bytes of text, declaring 2,052, kept 4,016. The
test now feeds that message as well as the largest the bound admits; it
is red with the old line (4,016 held for 2,052 declared) and green with
this one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
Under HVF `cargo run`'s `virt` has no ITS, so every claim is refused on
its interrupts. The same launch by hand with only the machine changed to
`virt,gic-version=3,its=on,iommu=smmuv3,kernel-irqchip=off` arms the ITS
and maps each claim's DeviceID, and still hands over none: every claimed
function's BAR, moved inside firmware's windows, answers at none of the
eight addresses tried. So the clipboard agent's aarch64 claim and
handshake are unbooted; the arm64 track owns the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
@Japabu

Japabu commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

Round 3: the build at 60f08b937, reviewed against .claude/agents/reviewer.md on origin/main, the round-2 review (#894 (comment)) and the rulings (#894 (comment)).

  • What was read: the diff since 3a60123b6 (24880cdc5 and 60f08b937), lib/vdagent/src/stream.rs whole, the new issue, issues/README.md, issues/toyos-runs-on-arm64.md stage 6 and stage 7, and the body.
  • Evidence: pr-894 at e626cced7, fix2-60f08b937/.

Net lines (git diff --shortstat origin/main...60f08b937): 45 files, +3,468 −154. This round (3a60123b6..60f08b937): 2 files, +51 −8. That is one production line changed, the test reworked, and a 39-line issue.

Earlier BLOCKERs

  • The capacity bound at QEMU's chunking (stream.rs:193): CLOSED.
    • The fix. The line is now (data.len() * 2).max(data.len() + take).min(data.len() + *left). Every term is a total length. Since take <= *left, want >= data.len() + take, so the reserve always covers the chunk and Vec's own growth never runs. want <= data.len() + *left, which is the declared size. drain(..4) in message keeps the capacity, so the test reads the buffer the decoder grew.
    • The red arm at the head. mutations/run.log runs at head 60f08b937 with a clean tree. m25 restores 3a60123b6's line exactly; it builds (0) and reds a_kept_message_buffers_no_more_than_it_declares (101), "4016 bytes held for 2052 declared". m24, which drops the .min cap, reds it too (101, "4056 … 2052").
    • The green arm at the head. host-ci.log:5670, the test ok, and :9051-9052, "Host: 76 step(s), all green", EXIT=0.
    • The 2,048-byte case is the one the review named, fed through host(), and it asserts against the declared size rather than MAX_DATA.
  • MSG_CLIPBOARD_SET_ANSWERED: CLOSED. The rulings accept it by name. No code changed.
  • B5, the aarch64 cargo run boot: CLOSED as ruled.
    • The launch. aarch64-its-boot.argv.log shows cargo run's own argv and the launched argv. They differ only in -machine, which became virt,gic-version=3,its=on,iommu=smmuv3,kernel-irqchip=off. The tree was untouched. Exit 0 (:492).
    • The ITS. It is present and armed (:174, :203), and each claim's DeviceID is mapped (:322, :374, :426, :463).
    • The two lines that stand. The agent's function is declined (:281), and the console comes up on 00:08.0 (:282-287).
    • Why port 1 open is absent. All four claims are NOT HANDED OVER on BAR placement (:343, :418, :450, :487). The xHCI gets no LPI (:270-273), so the compositor panics (:378-379), and the agent exits (:491).
    • The build. The boot ran 24880cdc5 (:350). 60f08b937 adds only an issues/ file, which enters no image.
    • Not this branch's. The branch's pcidev change touches only the claim's ID resolution, not BAR placement, so the placement failure is not introduced here.
    • The record. Per the ruling, the new issue records the agent's aarch64 claim and handshake as unbooted.
  • The NOTE, the unfiled defect: CLOSED.
    • issues/an-aarch64-cargo-run-boot-hands-over-no-pci-claim.md has the frontmatter issues/README.md requires: status: open, kind: defect, opened.
    • Its owner is issues/toyos-runs-on-arm64.md, an open track. Its stage 7 exit is the one the issue says a cargo run boot cannot meet.
    • Its exit is a cargo run -- --arch aarch64 boot whose console shows diskserver serving and vdagent: port 1 open. A boot can fail it.
  • The NOTE, the body's sentence about the track: CLOSED. Body :68 and :123 now say only what stage 6 says: HVF's GIC refuses an ITS, and a virt with one is its=on,kernel-irqchip=off.

BLOCKER

None.

NOTE

  • issues/an-aarch64-cargo-run-boot-hands-over-no-pci-claim.md:18-21 — The sentence "each of the four … its config space answering 0x0f0107ff" is false of the log it describes, on two counts:
    • 0x0f0107ff is what 00:02.0 answers at its firmware BAR 0x800001c000 (aarch64-its-boot.log:327). It is not a config-space read.
    • The other three answer 0x79000000 (:386), 0x30bf8024 (:433) and 0x00000103 (:470).
    • The "another function's ID" at the first address is 0x00081b36, read where the kernel placed the BAR, at 0x4010000000. That is the ECAM base, which :212 names and :225 lists among firmware's declared windows. (prose)
  • Body :101 and :107 — "Round 2's logs … from head 60f08b937" is false of capacity-arms.log. That script's own git diff check prints RESTORED: fix in place and finds the fixed line in the working-tree diff, so it ran with the fix uncommitted, at no recorded head. Both arms are held at the head by mutations/run.log (m25, 101) and host-ci.log:5670. (prose)

LAND

Each function answers at its firmware BAR with its own value, and the
moved BAR reads 0xffffffff except at the ECAM base, where the NVMe reads
0x00081b36 and the other three 0x00000000; cited by line in the log.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
@Japabu
Japabu marked this pull request as ready for review October 13, 2026 05:20
@Japabu
Japabu added this pull request to the merge queue Oct 13, 2026
Merged via the queue into main with commit 52e3238 Oct 13, 2026
6 checks passed
@Japabu
Japabu deleted the wt/toyos-clipboard branch October 13, 2026 05:39
Japabu added a commit that referenced this pull request Oct 13, 2026
Japabu added a commit that referenced this pull request Oct 13, 2026
Japabu added a commit that referenced this pull request Oct 13, 2026
#894 adds the clipboard port beside the input port: both are held alone,
both are served by the compositor, and its poll budget counts both.
#904 rewrote step 7.2 of the track; its text is kept, marked done.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
Japabu added a commit that referenced this pull request Oct 13, 2026
#894's `clipboardcase` landed with a compositor row from before this
branch, and the compositor takes the `input` acceptor as it takes
`prompt`'s, so it panicked at its start on that boot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant