docs: Darkwing row 110 sandbox reach: a role session can open broker.sock, so the tester bind needs a process proof (#1604)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-13 18:46:46 -05:00
co-authored by Claude Opus 5.5
parent 503a81b506
commit af191a3a64
@@ -0,0 +1,41 @@
# Row 110 (#1604): can a role session reach broker.sock? For Sage's ruling 2
Sage's placement ruling (commit 503a81b50, brief "Placement ruling" point 2) asks the packet to say, from the row 92 sandbox code, whether a role session can reach `broker.sock`, and if it can, to test that a role session's tester bind refuses. Read at HEAD 503a81b50.
## Yes, it can
- Every managed session runs under bwrap. The launcher refuses a launch with `sandbox-unavailable` when bwrap doesn't run (packages/cli/src/launcher.mjs:174-175). There is no unsandboxed fallback.
- The sandbox ro-binds the broker socket at its host path, because the runner claims its capability and calls the bus through it (packages/seat/src/sandbox.mjs:108). A read-only bind doesn't stop `connect()`. The runner does exactly that on every turn.
- The coder, cto, researcher and reviewer roles hold the `bash` tool (roles/*.json `limits.tools`). Any of them can run `node -e` and open the socket. The PM holds read, write, edit, grep, find and ls, with no bash, so the PM can't open it today. That rests on the PM's tool list, not on the bus, and a later change to roles/pm.json would undo it.
So a `tester: { business }` field with no proof lets any bash-holding session bind as tester. The broker can't tell that connection from Mos's, because server.mjs takes one request per connection and has no peer credentials. The ruling's test ("a role session's tester bind refuses") can't pass with no proof at all.
## What would make it refuse
The human proof already solves the same problem the other way round. `verifyHuman` (packages/bus/src/human.mjs) takes `{ pid, startTime, nonce, business }` from human-cli.mjs, checks the nonce in that process's environment and the CLI's path, then walks the parent chain. It refuses an ancestor that matches a launch record (the outer bwrap pid, bound by `bindLaunch`) or carries an agent marker.
I propose the tester gets the same proof shape from its own shim, `packages/bus/src/tester-cli.mjs`, with a different walk:
- refuse an ancestor that matches a launch record, or that carries `MOSAIC_RUN_ID`, `MOSAIC_BUS_CAP` or `MOSAIC_AGENT_RUN` (a managed run's markers; session.mjs sets `MOSAIC_RUN_ID` for every session);
- allow `CLAUDECODE`, `CLAUDE_CODE_ENTRYPOINT`, `CODEX_THREAD_ID` and the agent binaries, since a tester is meant to run from inside Mos or a T3 seat.
Inside the sandbox this fails twice over. The session has its own PID namespace, so the pid the shim reports names a different host process, and the nonce check fails. If a caller somehow named its host pid, the walk reaches the bwrap pid, which is a launch record. The comment in human.mjs is honest about the limit: these are cooperative same-UID checks. An unsandboxed process running as Jason can still pass, and that is the intended tester.
This is a proof, so it reverses "with no human proof" in ruling 2. It isn't human proof. It proves "not inside a managed run", which is the property the ruling wants.
## Tests
- packages/bus/tests: a tester chain with a launch-record ancestor refuses; a chain with `MOSAIC_RUN_ID` refuses; a chain with `CLAUDECODE` binds; a request carrying `cap` with `tester`, or `human` with `tester`, refuses as `invalid-envelope` (the "already bound as a role or reader" limit, since a connection binds per request).
- packages/seat/tests/sandbox.test.mjs: a real bwrap session runs tester-cli.mjs against a test broker and gets the refusal. It skips where `sandboxProblem()` is non-null, as that file's other bwrap cases do.
## Files this adds to row 110
- `packages/bus/src/tester-cli.mjs` (new) and `packages/bus/src/human.mjs` (`verifyTester` beside `verifyHuman`, sharing the process walk)
- `packages/seat/tests/sandbox.test.mjs` (the real-sandbox case only)
## What I ask Sage to rule
1. A tester binds with a process proof from `tester-cli.mjs`, refused under any managed run as above, instead of no proof.
2. The three files above join row 110.
If Sage keeps "no proof", the packet would have to say that any bash-holding role can bind as tester, and the refusal test can't be written.