Files
stack/agents/darkwing/work/queue-92/r2/build.md
T
jason.woltjeandClaude Opus 5.5 a4fa775a51 darkwing: row 92 round 2 rebased onto ac37cd17, candidate 8bf4141c (#1586)
Row 89 landed on packages/harness/README.md. The patch applies cleanly;
gate 14/14 green and 34/34 mutants killed on the new base.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-12 17:13:47 -05:00

8.3 KiB

Row 92, role sessions confined to their own files, round 2 build (Darkwing)

Issue #1586. Round 1 packet: agents/darkwing/work/queue-92/ (candidate manifest bc132570…). Round 1 verdicts: Dewey approved with notes N1 to N5 (comment 27942, rev 771). Filbert asked for six changes (comment 27951, rev 772).

Base ac37cd17. Round 1 was built on af01465c. Row 89 landed at ce104b27 while this packet was going out, and it edits packages/harness/README.md, one of the 13 paths (Dewey's N5). So I applied the patch to a detached worktree of ac37cd17 (it applied cleanly), reran the gate and the mutants there, and rebuilt the patch and manifest. Commit 4643911a holds the same round 2 on af01465c; this packet replaces it. Only the harness README's digest differs between the two manifests. Its added and removed lines are the same 57 either way, and row 89's lines in that file sit outside them.

build.patch is git diff --cached --binary ac37cd17 of the 13 paths. Apply it with git apply --index to a detached worktree of ac37cd17, and sha256sum -c candidate-manifest.sha256 then gives 13/13 OK.

What changed since round 1

No source file moved. sandbox.mjs, session.mjs, launcher.mjs, gate.mjs, the unit and the other test files are byte for byte round 1's. Five files moved:

File What changed For
packages/seat/tests/sandbox.test.mjs five rows in the confined-shell table; standIns makes a host /dev/shm file and a SysV segment Filbert 1 and 2
packages/cli/tests/launcher.test.mjs two tests: a pi launch with no credential, a runner killed from the host Filbert 3 and 4
packages/cli/README.md the refusal table row names the proxy set; a paragraph on how a Pi session logs in; the shell clause covers Claude Code's Bash Filbert 5 and 6, Dewey N1 and N2
packages/seat/README.md one sentence: Pi logs in only through ENV_ALLOW's variables Filbert 6, Dewey N1
packages/harness/README.md Limits: the credential variable is readable from bash; /proc/net/unix lists the host's socket names Dewey N2 and N3

Filbert's changes

  1. The private /proc, /dev and IPC namespace. Four rows in checks():

    Row Command May
    read a stand-in in the host's /dev/shm cat /dev/shm/<the test's scratch name> no
    see the test's own pid in /proc test -e /proc/<the test's pid> no
    read its own IPC namespace's segment list test -r /proc/sysvipc/shm yes
    see a SysV segment the host made awk '$2 == <id> …' /proc/sysvipc/shm no

    standIns now takes t. It writes the /dev/shm file (0600) and makes a 4096-byte segment with ipcmk -M. t.after removes both. session() checks on the host that the file, the test's own /proc/<pid> and the segment all exist before any row runs, so a refusal inside means the sandbox hid them. The readable row stops the segment row from passing just because /proc/sysvipc is missing.

    I tried ipcs -m -i <id> first. It exits 0 even when it prints "not found", so it can't tell the namespaces apart. The awk over /proc/sysvipc/shm exits 1 in a private IPC namespace and 0 in a shared one (probe below).

  2. pi runs in the sandbox. A row runs <REPO>/node_modules/.bin/pi --version and needs rc 0. The table's session is pi, and so is the direct binds() sandbox in the second test.

  3. The launcher's sessionExit call. "a runner killed from the host ends its launch as killed, not failed" launches the PM, waits for its claim, sends SIGKILL to the runner's host pid (bwrap's child) and reads the launch log's end entry: reason killed, exit code null, signal SIGKILL.

  4. A pi launch with no credential. "a pi launch needs no session credential" drops the fixture's stand-in, checks that none of the SESSION_CREDENTIALS variables is left in the env, and launches the PM (pi).

  5. cli README refusal row. It names all three sets now.

  6. pi and ~/.pi/agent. The cli Sessions paragraph says a Pi session logs in only through the variables ENV_ALLOW passes, never reads the operator's ~/.pi/agent, has no launch credential check, and so fails at its first turn on a provider whose login lives only in those files. The seat README's sandbox section says the same in one sentence. No code, as Filbert asked.

Dewey's notes

  • N1 is Filbert's change 6, above.
  • N2. The cli Sessions paragraph now says a shell the harness opens, Claude Code's Bash or Pi's bash, can read the credential variables. The harness Limits bullet "The gate is not the sandbox" says the same.
  • N3. The Limits bullet on the network now names /proc/net/unix: it lists the host's Unix sockets by name, path ones included, and a name gives no access to a path socket the mounts hide.
  • N4 (seccomp, --disable-userns) stays outside this row, as Dewey said.
  • N5. Row 89 landed first, so this round is rebased onto it (above), and the gate below ran on that tree.

Probes

Run 2026-10-12 on this host, outside the tests, with a 4096-byte segment from ipcmk -M, removed afterwards with ipcrm -m:

  • ipcs -m -i <id> inside --unshare-ipc printed "id … not found" and exited 0, so it can't be the check.
  • awk '$2 == <id> { f = 1 } END { exit !f }' /proc/sysvipc/shm exited 1 inside --unshare-ipc, 0 in the same sandbox without it, and 0 on the host.
  • Under bwrap's --dev, /dev/shm exists and is empty.

After the seat suite and after the mutant run, /dev/shm held no mosaic-sandbox-* file and ipcs -m showed no 4096-byte segment of ours.

Mutants

mutants.sh here: round 1's 26 unchanged, then 8 more. It ran with a copy of agents/darkwing/work/queue-59-review/r1/mut.mjs (sha256 a1ed6bca…, round 1's helper). It ran twice, with no other seat's gate or mutant run going: on af01465c from 21:53:34Z to 21:58:15Z, and on ac37cd17 from 22:08:34Z to 22:12:57Z (mutants.txt is the second). 34 of 34 killed both times, each by the same test; only the timings differ. Every source file was restored and still matches the manifest.

Mutant Filbert's Killed by
no-ipc-namespace (--unshare-ipc dropped) survived the row "see a SysV segment the host made"
host-proc (--ro-bind /proc) survived the row "see the test's own pid in /proc"
host-dev (--dev-bind /dev) survived the row "read a stand-in in the host's /dev/shm"
pi-no-node-modules survived the row "run pi, the session's harness"
launcher-raw-exit (resolve({ code, signal })) survived "a runner killed from the host ends its launch as killed, not failed"
credential-every-harness survived "a pi launch needs no session credential"
pdeathsig-term killed the hard-death recovery test, as in Filbert's run
workspace-read-only killed the row "write and read its workspace"

In each of the four table mutants, both sandbox tests failed on that one row and no other: the session test and the expose test's session run. exit-code-not-mapped (round 1) is now also caught by the host-kill test.

Filbert's other five I didn't repeat. no /etc, 0755 home/tmp and recover on start time alone stay as they noted: the first isn't demonstrable offline, the second is equivalent, the third is a second guard. claude bin at its real path only and sandbox check for Claude Code only were already killed in their run.

Gate

gate.sh, run sequentially in ~/darkwing-scratch/r92/wt3 (ac37cd17 plus this patch, node_modules symlinked), 22:05:10Z to 22:08:27Z, after a check that no other seat's gate or mutant run was going (gate-summary.txt). All 14 suites exited 0, 0 skipped:

  • node: seat 33, cli 120 (round 1's 118 plus the two launcher tests), harness 65, bus 108, webui 86 (row 89 and the rows before it added harness, bus and webui tests since af01465c);
  • scripts: auth 15, conductor 17, config 24, discord 66, extension-package 18, foundation 44, queue 27, release 14, task 26 (DOCKER_HOST at a dead socket).

The seat count doesn't move because the five new rows run inside the two existing sandbox tests. The run on af01465c (21:58:30Z to 22:01:56Z) gave the same results with harness 59, bus 102 and webui 84. Each test-release left an empty <dir>_default network of mine, and I removed both.