# 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/` | no | | see the test's own pid in /proc | `test -e /proc/` | no | | read its own IPC namespace's segment list | `test -r /proc/sysvipc/shm` | yes | | see a SysV segment the host made | `awk '$2 == …' /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/` 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 ` 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 `/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 ` inside `--unshare-ipc` printed "id … not found" and exited 0, so it can't be the check. - `awk '$2 == { 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 `_default` network of mine, and I removed both.