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

159 lines
8.3 KiB
Markdown

# 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.