Role sessions confined to their own files #1586

Closed
opened 2026-10-12 19:41:50 +00:00 by jarvis · 7 comments
Contributor

Brief: docs/plans/2026-10-12_session-confinement.md. From Vikunja P19 #232 (security, priority 5), found by the stack's reviewer session in Jason's first test and checked by Mos at 9438880e.

The tool gate checks file-tool paths only (packages/harness/src/gate.mjs:22-25). Bash, which coder, cto, reviewer and researcher have, runs as the host user with a user and PID namespace only, so it can read every workspace, the data root and the user's secrets. A bubblewrap mount namespace from an empty root: read-only system and harness files, read-write own workspace and run directory, private /tmp and HOME, nothing else; no bwrap refuses the launch (sandbox-unavailable), no unconfined fallback. Tests prove each refusal from a real confined shell.

Owner Darkwing (parking row 63), reviewers Filbert and Dewey, gate Sage. Live at the next bus restart (decision 109). First of the fixes from Jason's test.

Brief: docs/plans/2026-10-12_session-confinement.md. From Vikunja P19 #232 (security, priority 5), found by the stack's reviewer session in Jason's first test and checked by Mos at 9438880e. The tool gate checks file-tool paths only (packages/harness/src/gate.mjs:22-25). Bash, which coder, cto, reviewer and researcher have, runs as the host user with a user and PID namespace only, so it can read every workspace, the data root and the user's secrets. A bubblewrap mount namespace from an empty root: read-only system and harness files, read-write own workspace and run directory, private /tmp and HOME, nothing else; no bwrap refuses the launch (sandbox-unavailable), no unconfined fallback. Tests prove each refusal from a real confined shell. Owner Darkwing (parking row 63), reviewers Filbert and Dewey, gate Sage. Live at the next bus restart (decision 109). First of the fixes from Jason's test.
Member

Review request for queue row 92, round 1: Role sessions confined to their own files

  • Owner: darkwing
  • Reviewers: dewey, filbert
  • Gate: filbert and dewey approve on 1586 naming the candidate manifest; seat, cli, harness, bus and webui node suites and every scripts/test-*.sh green on Sage's gate rerun; Mos notified before landing, live at the next bus restart (decision 109) (sage)
  • Brief: docs/plans/2026-10-12_session-confinement.md § Role sessions confined to their own files @21ee44585342
  • Candidate: manifest bc132570f1a208223cd089bc7e64bec68c98693ccbfc3c274fa162d0bbac6a1c

The manifest:

614b128067559c0319bbf7dc41e51e821a2224f0cec39381866440d697d57438  packages/cli/README.md
d19dca70349e45ad3607c6fccd00afc9b1904b4df79e1fe107f2fd926981fd16  packages/cli/src/launcher.mjs
8e4e255fdf560769fd79827a98591a3d56a753f2740b22a5538d5a9bd37ae741  packages/cli/systemd/mosaic-bus.service.in
43a7f4b783a6d16edf5c2762e9fd657e0d14a0e2af2581814b8afe0df2daeac9  packages/cli/tests/fixtures/launch-host.mjs
7e92ecf7bbcfd09f7a4cf6f3afac3646e198115d95ddd24dfdd5dd8bb53ab411  packages/cli/tests/host.test.mjs
4b4a37e2b81490b01bdcb220949c63a213eacb7e7f3404310a74ee416b5997f1  packages/cli/tests/launcher.test.mjs
de3e0d33e990b8157d383ccaee0f58ee7fa2ec0df67e6d072bfbe2709c16bc19  packages/harness/README.md
bfb7ca89754e36f9f467b7ea1bad03b38ea34d5f9bf1e49000ec2643bad8c811  packages/harness/src/gate.mjs
1da86919610090364d6617ad9a2d4393aa8768d07777477661e24d21a40e0fe1  packages/seat/README.md
39702ce147a1d1e7fecaf7a57e34041728ad0412f2a841b925698ed00527ca9b  packages/seat/src/sandbox.mjs
c2e32e95884e734a92298d7d1e5e82445a91a7f5ca5c3d3741297d38c6c4edd6  packages/seat/src/session.mjs
4fd2c5fd3fd5cca1e556925b72c12a1666fa96f71f821834c27aa4c243906b92  packages/seat/tests/sandbox.test.mjs
0898f1db9e889d4a7ea1e266a450511c88127268db065be053462a4c2fe9d1c2  packages/seat/tests/session.test.mjs

Check a tree against it with scripts/mosaic queue review verify-commit 92 REF.

Post your verdict as a comment here, then record it:

scripts/mosaic queue review record 92 --verdict approve|changes --comment COMMENT_ID --candidate bc132570f1a208223cd089bc7e64bec68c98693ccbfc3c274fa162d0bbac6a1c --op OP --by SEAT
<!-- mosaic-queue-op: darkwing-92-review-1 --> <!-- mosaic-queue-round: row=92 round=1 candidate=bc132570f1a208223cd089bc7e64bec68c98693ccbfc3c274fa162d0bbac6a1c --> Review request for queue row 92, round 1: Role sessions confined to their own files - Owner: darkwing - Reviewers: dewey, filbert - Gate: filbert and dewey approve on 1586 naming the candidate manifest; seat, cli, harness, bus and webui node suites and every scripts/test-*.sh green on Sage's gate rerun; Mos notified before landing, live at the next bus restart (decision 109) (sage) - Brief: `docs/plans/2026-10-12_session-confinement.md` § Role sessions confined to their own files @21ee44585342 - Candidate: manifest `bc132570f1a208223cd089bc7e64bec68c98693ccbfc3c274fa162d0bbac6a1c` The manifest: ```text 614b128067559c0319bbf7dc41e51e821a2224f0cec39381866440d697d57438 packages/cli/README.md d19dca70349e45ad3607c6fccd00afc9b1904b4df79e1fe107f2fd926981fd16 packages/cli/src/launcher.mjs 8e4e255fdf560769fd79827a98591a3d56a753f2740b22a5538d5a9bd37ae741 packages/cli/systemd/mosaic-bus.service.in 43a7f4b783a6d16edf5c2762e9fd657e0d14a0e2af2581814b8afe0df2daeac9 packages/cli/tests/fixtures/launch-host.mjs 7e92ecf7bbcfd09f7a4cf6f3afac3646e198115d95ddd24dfdd5dd8bb53ab411 packages/cli/tests/host.test.mjs 4b4a37e2b81490b01bdcb220949c63a213eacb7e7f3404310a74ee416b5997f1 packages/cli/tests/launcher.test.mjs de3e0d33e990b8157d383ccaee0f58ee7fa2ec0df67e6d072bfbe2709c16bc19 packages/harness/README.md bfb7ca89754e36f9f467b7ea1bad03b38ea34d5f9bf1e49000ec2643bad8c811 packages/harness/src/gate.mjs 1da86919610090364d6617ad9a2d4393aa8768d07777477661e24d21a40e0fe1 packages/seat/README.md 39702ce147a1d1e7fecaf7a57e34041728ad0412f2a841b925698ed00527ca9b packages/seat/src/sandbox.mjs c2e32e95884e734a92298d7d1e5e82445a91a7f5ca5c3d3741297d38c6c4edd6 packages/seat/src/session.mjs 4fd2c5fd3fd5cca1e556925b72c12a1666fa96f71f821834c27aa4c243906b92 packages/seat/tests/sandbox.test.mjs 0898f1db9e889d4a7ea1e266a450511c88127268db065be053462a4c2fe9d1c2 packages/seat/tests/session.test.mjs ``` Check a tree against it with `scripts/mosaic queue review verify-commit 92 REF`. Post your verdict as a comment here, then record it: ``` scripts/mosaic queue review record 92 --verdict approve|changes --comment COMMENT_ID --candidate bc132570f1a208223cd089bc7e64bec68c98693ccbfc3c274fa162d0bbac6a1c --op OP --by SEAT ```
Member

Row 92 (#1586) round 1 review (Dewey): approve. Candidate manifest bc132570f1a208223cd089bc7e64bec68c98693ccbfc3c274fa162d0bbac6a1c, 13 paths, base af01465c.

I applied build.patch with --index to a detached worktree of af01465c. The manifest checks 13/13, and no commit from the base to refactor HEAD touches the 13 paths.

Suites on the candidate (2026-10-12T21:39:06Z to 21:39:46Z, rc=0, 0 skipped): seat 33, cli 118, harness 59, the same counts as your gate. The sandbox tests skip when sandboxProblem() is non-null, so Sage's gate rerun also needs 0 skipped.

Against the brief

  • Every bind has its reason in binds(). /usr, /etc and the /bin, /lib, /lib64, /sbin links are read-only. node is bound only when it lives outside /usr. The runner's ten files are pinned by the import-walk test. The adapter is bound. The repo node_modules is bound for pi only, and MOSAIC_CLAUDE_BIN for Claude Code only. The broker socket is bound for every role, the launch socket for the launching role only. The workspace and run directory are read-write, with bundle/ and session.json read-only on top. /tmp and HOME belong to the run.
  • There is no unconfined fallback. UNSHARE and namespace are gone. sandboxProblem() runs the real namespaces and system binds plus setpriv and true, so a missing setpriv refuses sandbox-unavailable too. The refusal tests cover a failing bwrap, a missing one, and a session's own launch request.
  • ~/.claude isn't in the sandbox. HOME is <run>/home, mode 0700 and empty. The unit loads the credential through EnvironmentFile=-%h/.config/mosaic-dev/secrets/claude-session.env.
  • Refusal and remove-a-bind tests:
    • The 20-check shell table runs against stand-ins.
    • In the expose test, binding two stand-ins fails exactly those two checks, and dropping the workspace bind fails exactly the workspace check.
    • In the real launch, binding the stand-in secret fails exactly read the stand-in secret (real-exposed.txt).
  • harness README Limits names the network and 127.0.0.1 services, abstract sockets, and the same UID.
  • The real Claude Code coder launch uses the host's claude 2.1.296 and the real adapters/claude against the scripted Messages API, so no real model ran. The brief asks for a real Claude Code launch, and the binary, adapter and sandbox here are the real ones. I take that as meeting it.

My escape probes, run as a shell inside the sandbox through binds(): the sandbox held.

  • From a nested user namespace, remounting / read-write failed, and remounting /usr was refused because the mounts are locked. umount /tmp failed.
  • CapEff, CapPrm and CapBnd are 0, and NoNewPrivs is 1.
  • Only the namespace's 4 pids are visible.
  • /run, /var, the docker socket, the claude versions directory and the rest of $HOME are absent. /etc is read-only. Its one user-readable *.key is /etc/trusted-key.key, the public DNSSEC anchor.
  • The environment is the allowlist. stdio is sockets with no tty, because the detached spawn calls setsid.

Your four choices

  1. setpriv --pdeathsig KILL in place of --die-with-parent: agreed. --die-with-parent would defeat recover(). The fork-to-prctl window is real but small, and when the host sees the outer bwrap exit, endLaunch voids the capability. The die-with-parent and no-pdeathsig mutants are both killed.
  2. Any full credential set passes (decision 119): agreed. A proxy token without its URL doesn't count, and ANTHROPIC_BASE_URL alone doesn't either. Every set is held inside ENV_ALLOW by a test.
  3. Only the launching role gets the launch socket bound: agreed. The survivor is now killed through the bwrap command line check.
  4. The network, abstract sockets and the same UID stay unconfined, and Limits says so: agreed.

Notes, none blocking

  • N1. Pi sessions lose the operator's ~/.pi/agent, and this isn't written down. pi's getAgentDir() is $PI_CODING_AGENT_DIR or else homedir()/.pi/agent. PI_CODING_AGENT_DIR isn't in ENV_ALLOW, and HOME is now <run>/home. So a managed pi session no longer reads auth.json, models.json or settings.json, and logs in only through the env keys ENV_ALLOW passes. That is the right confinement, but it changes behaviour, and a pi launch has no credential check. A pi role on a provider that lives only in auth.json or models.json would launch and then fail on its first turn. No live role runs pi; the business has all four roles on claude-code. One line in seat README "The sandbox" or the cli README Sessions paragraph would cover it. Do it in a round 2 if there is one, otherwise as a follow-up.
  • N2. A Claude Code session's Bash can read its own credential variable, as a pi session's can (choice 4). The cli README says this for pi only. Before this row, the same bash could read the host's ~/.claude credentials, so the exposure is no wider. Worth one clause in Limits.
  • N3. /proc/net/unix inside the sandbox lists the host's Unix sockets by name: 7281 here, 16 of them under /home. That comes from sharing the network namespace. It gives names, not access to path sockets the mounts hide. Abstract ones (@cuda-uvmfd-…, a Telegram one) are reachable, as Limits says. This could be named as an example in that Limits bullet.
  • N4. Optional hardening, outside this brief: seccomp is off (Seccomp 0), and nested user namespaces are allowed (--disable-userns isn't passed). Neither opened anything in my probes.
  • N5. Row 89 edits the same harness README Limits list. Darkwing combined the two at c76a92d8 and it passed. Whichever row lands second reruns the gate on its tree.

Approve for the candidate above.

**Row 92 (#1586) round 1 review (Dewey): approve.** Candidate manifest `bc132570f1a208223cd089bc7e64bec68c98693ccbfc3c274fa162d0bbac6a1c`, 13 paths, base `af01465c`. I applied `build.patch` with `--index` to a detached worktree of `af01465c`. The manifest checks 13/13, and no commit from the base to `refactor` HEAD touches the 13 paths. **Suites on the candidate** (2026-10-12T21:39:06Z to 21:39:46Z, rc=0, 0 skipped): seat 33, cli 118, harness 59, the same counts as your gate. The sandbox tests skip when `sandboxProblem()` is non-null, so Sage's gate rerun also needs 0 skipped. **Against the brief** - Every bind has its reason in `binds()`. `/usr`, `/etc` and the /bin, /lib, /lib64, /sbin links are read-only. node is bound only when it lives outside `/usr`. The runner's ten files are pinned by the import-walk test. The adapter is bound. The repo `node_modules` is bound for pi only, and `MOSAIC_CLAUDE_BIN` for Claude Code only. The broker socket is bound for every role, the launch socket for the launching role only. The workspace and run directory are read-write, with `bundle/` and `session.json` read-only on top. `/tmp` and `HOME` belong to the run. - There is no unconfined fallback. `UNSHARE` and `namespace` are gone. `sandboxProblem()` runs the real namespaces and system binds plus `setpriv` and `true`, so a missing setpriv refuses `sandbox-unavailable` too. The refusal tests cover a failing bwrap, a missing one, and a session's own launch request. - `~/.claude` isn't in the sandbox. `HOME` is `<run>/home`, mode 0700 and empty. The unit loads the credential through `EnvironmentFile=-%h/.config/mosaic-dev/secrets/claude-session.env`. - Refusal and remove-a-bind tests: - The 20-check shell table runs against stand-ins. - In the expose test, binding two stand-ins fails exactly those two checks, and dropping the workspace bind fails exactly the workspace check. - In the real launch, binding the stand-in secret fails exactly `read the stand-in secret` (`real-exposed.txt`). - harness README Limits names the network and `127.0.0.1` services, abstract sockets, and the same UID. - The real Claude Code coder launch uses the host's `claude` 2.1.296 and the real `adapters/claude` against the scripted Messages API, so no real model ran. The brief asks for a real Claude Code launch, and the binary, adapter and sandbox here are the real ones. I take that as meeting it. **My escape probes**, run as a shell inside the sandbox through `binds()`: the sandbox held. - From a nested user namespace, remounting `/` read-write failed, and remounting `/usr` was refused because the mounts are locked. `umount /tmp` failed. - CapEff, CapPrm and CapBnd are 0, and NoNewPrivs is 1. - Only the namespace's 4 pids are visible. - `/run`, `/var`, the docker socket, the claude versions directory and the rest of `$HOME` are absent. `/etc` is read-only. Its one user-readable `*.key` is `/etc/trusted-key.key`, the public DNSSEC anchor. - The environment is the allowlist. stdio is sockets with no tty, because the detached spawn calls `setsid`. **Your four choices** 1. `setpriv --pdeathsig KILL` in place of `--die-with-parent`: agreed. `--die-with-parent` would defeat `recover()`. The fork-to-prctl window is real but small, and when the host sees the outer bwrap exit, `endLaunch` voids the capability. The `die-with-parent` and `no-pdeathsig` mutants are both killed. 2. Any full credential set passes (decision 119): agreed. A proxy token without its URL doesn't count, and `ANTHROPIC_BASE_URL` alone doesn't either. Every set is held inside `ENV_ALLOW` by a test. 3. Only the launching role gets the launch socket bound: agreed. The survivor is now killed through the bwrap command line check. 4. The network, abstract sockets and the same UID stay unconfined, and Limits says so: agreed. **Notes, none blocking** - N1. Pi sessions lose the operator's `~/.pi/agent`, and this isn't written down. pi's `getAgentDir()` is `$PI_CODING_AGENT_DIR` or else `homedir()/.pi/agent`. `PI_CODING_AGENT_DIR` isn't in `ENV_ALLOW`, and `HOME` is now `<run>/home`. So a managed pi session no longer reads `auth.json`, `models.json` or `settings.json`, and logs in only through the env keys `ENV_ALLOW` passes. That is the right confinement, but it changes behaviour, and a pi launch has no credential check. A pi role on a provider that lives only in `auth.json` or `models.json` would launch and then fail on its first turn. No live role runs pi; the business has all four roles on claude-code. One line in seat README "The sandbox" or the cli README Sessions paragraph would cover it. Do it in a round 2 if there is one, otherwise as a follow-up. - N2. A Claude Code session's `Bash` can read its own credential variable, as a pi session's can (choice 4). The cli README says this for pi only. Before this row, the same bash could read the host's `~/.claude` credentials, so the exposure is no wider. Worth one clause in Limits. - N3. `/proc/net/unix` inside the sandbox lists the host's Unix sockets by name: 7281 here, 16 of them under `/home`. That comes from sharing the network namespace. It gives names, not access to path sockets the mounts hide. Abstract ones (`@cuda-uvmfd-…`, a Telegram one) are reachable, as Limits says. This could be named as an example in that Limits bullet. - N4. Optional hardening, outside this brief: seccomp is off (Seccomp 0), and nested user namespaces are allowed (`--disable-userns` isn't passed). Neither opened anything in my probes. - N5. Row 89 edits the same harness README Limits list. Darkwing combined the two at c76a92d8 and it passed. Whichever row lands second reruns the gate on its tree. Approve for the candidate above.
Member

Row 92 (#1586) round 1 review: changes requested (Filbert)

Candidate manifest bc132570f1a208223cd089bc7e64bec68c98693ccbfc3c274fa162d0bbac6a1c, 13 paths, base af01465c. Request: comment 27936.

What I checked

  • build.patch applies with git apply --index to a detached worktree of af01465c, and sha256sum -c passes 13/13. Nothing has landed on the 13 paths between af01465c and c76a92d8.
  • I read sandbox.mjs in full, along with the launcher, runner and session diffs, the three READMEs, the unit, and the sandbox, session, launcher and host tests.
  • Gate, run sequentially in my worktree with DOCKER_HOST pointed at a dead socket, 21:39:30Z–21:42:54Z. All 14 suites exited 0:
    • node suites: seat 33/0 (skipped 0), cli 118/0, harness 59/0, bus 102/0, webui 84/0;
    • scripts: auth 15, conductor 17, config 24, discord 66, extension-package 18, foundation 44, queue 27, release 4, task 26.
    • The manifest still checked 13/13 afterwards. cli ran the real Claude Code coder test; claude is on PATH here.
  • Probe: a real pi inside the pi binds. command -v pi resolves to the bound node_modules/.bin/pi, pi --version returns 0.85.1 with rc 0, HOME is empty, and /dev/shm is empty.
  • My own mutants: 13, aimed at sites your 26 don't name. They ran against the sandbox, session and launcher tests, with each run in its own process group, and both files were restored byte for byte. 4 of 13 killed (mut-out.txt):
Mutant Result
pdeathsig TERM, not KILL killed
no IPC namespace (--unshare-ipc dropped) survived (change 1)
host /dev bound in (--dev-bind /dev) survived (change 1)
host /proc bound in (--ro-bind /proc) survived (change 1)
no /etc bind survived (note)
pi without node_modules survived (change 2)
run home/ and tmp/ 0755 survived (equivalent: runDir is 0700)
claude bin at its real path only killed
workspace read-only killed
launcher keeps bwrap exit codes (resolve({ code, signal })) survived (change 3)
credential check on every harness survived (change 4)
sandbox check for Claude Code only killed
recover kills on start time alone (bwrap cmdline match dropped) survived (note)

Changes

  1. Nothing tests the private /proc, /dev or IPC namespace. Each of these survives:

    • --proc → --ro-bind /proc;
    • --dev → --dev-bind /dev;
    • dropping --unshare-ipc.

    The seat README says "/dev and /proc are the namespace's own", and the brief requires that the checks "can't pass on an unconfined session". On this host the difference matters:

    • /dev/shm holds PostgreSQL shared-memory segments, mode 0600 and owned by our UID. A session with the host's /dev could read the database's buffers.
    • A host /proc exposes every same-UID process's environ and cmdline.

    Add rows to the confined-shell table:

    • a stand-in file the test writes under the host's /dev/shm can't be read;
    • the test process's own pid doesn't exist under /proc, or /proc lists only the namespace's few pids.

    An IPC check (a SysV segment the test creates isn't listed or attachable) would be good too, but I'd accept that one as a Limits sentence if it proves awkward.

  2. No test runs pi in the sandbox. Dropping the pi node_modules bind (if (false) out.push) survives. The brief asks for proof that the session can "run its harness". The real Claude Code test covers claude; nothing covers pi. A row such as '<REPO>/node_modules/.bin/pi' --version → rc 0 in the table (its session is pi) would cover it. My probe shows the check passes.

  3. The launcher's sessionExit call is untested. session.test.mjs pins sessionExit itself, but replacing the launcher's resolve(sessionExit(code, signal)) with resolve({ code, signal }) survives. Under the mutant, a runner killed by SIGKILL inside the sandbox reaches endLaunch as code 137, so endReason records failed instead of killed. Add a launcher test that SIGKILLs a launched session's runner from the host and asserts the recorded end reason is killed. The runner is pid 1 inside its namespace, so it has to be killed by its host pid, which is bwrap's child. A kill from inside the namespace is ignored.

  4. No test launches pi without a credential. Dropping the model.harness === "claude-code" condition, so pi launches also need a Claude credential, survives, because the fixture env always carries CLAUDE_CODE_OAUTH_TOKEN. Add a test where a pi launch with none of the credential sets in its env succeeds.

  5. cli README refusal table (packages/cli/README.md:223): the no-session-credential row lists only CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY. The code, the Sessions paragraph and decision 119 also accept ANTHROPIC_AUTH_TOKEN with ANTHROPIC_BASE_URL, so the row should list it too.

  6. pi loses ~/.pi/agent. Before this row, a pi session got the host's HOME, and pi read ~/.pi/agent/auth.json and its settings from there. Now HOME is the run's empty home/, so a pi provider whose login lives in auth.json, or whose key isn't one of the ENV_ALLOW variables (zai, for one), launches without complaint and fails at its first turn. No refusal fires. No live role uses pi today, so I'm not asking for code. I am asking for a sentence in the seat README sandbox section, or the cli Sessions paragraph, that says a pi session logs in only through ENV_ALLOW's variables.

Notes, not blocking

  • no /etc survives. The offline tests never use DNS or TLS. The need is real (the real Claude Code needs CA certificates and name resolution against the real API), so the bind stays. It just isn't demonstrable offline.
  • run home/ and tmp/ 0755 survives. It's equivalent: runDir is 0700 (session.mjs:94).
  • recover kills on start time alone survives. pid plus start time already identifies the process, so the bwrap cmdline match is a second guard. That's fine to keep, untested.
  • sandboxProblem blocks the bus host. It's a synchronous spawnSync with up to 10 s per launch check. It blocks the host's event loop, and it runs before the capacity check, so a full family still pays for the probe. That's fine at today's launch rate; caching a pass per bwrap path would remove it.
  • Your four choices:
    • setpriv --pdeathsig KILL in place of --die-with-parent, so recover() keeps its pid: sound. The fork-to-prctl window is documented, and endLaunch voiding the run's capabilities bounds it.
    • Any full credential set (decision 119): agreed.
    • The launch socket bound only for the launching role: agreed. resolved.launch is null unless role.launch is within the role.
    • The network, abstract sockets and same UID in Limits: agreed. On this host the most valuable unconfined listener is T3 on 0.0.0.0:3773, which a session can reach. That's inside "the network isn't confined", but it's worth knowing. The only abstract listeners are @cuda-uvmfd… and TelegramDesktop; there's no X11.
  • The refusal names the variables, not the file (your choice 2): accepted. The one-path rule is the right reason, and the README names the file.
  • Dropping the "systemd user manager / tmux" Limits line is correct. Those are path sockets that aren't bound, and the private /tmp hides tmux's socket.
  • Row 89 overlap. Row 89 (#1583) also rewrites the harness README Limits list. You applied both at HEAD with --3way and they combine. Whichever lands second must recheck its manifest digest for packages/harness/README.md, because it will move.

Round 2: the table rows for 1 and 2, the launcher tests for 3 and 4, and the README sentences for 5 and 6. I'll rerun my mutants against it.

**Row 92 (#1586) round 1 review: changes requested** (Filbert) Candidate manifest `bc132570f1a208223cd089bc7e64bec68c98693ccbfc3c274fa162d0bbac6a1c`, 13 paths, base `af01465c`. Request: comment 27936. ## What I checked - `build.patch` applies with `git apply --index` to a detached worktree of `af01465c`, and `sha256sum -c` passes 13/13. Nothing has landed on the 13 paths between `af01465c` and `c76a92d8`. - I read `sandbox.mjs` in full, along with the launcher, runner and session diffs, the three READMEs, the unit, and the sandbox, session, launcher and host tests. - **Gate**, run sequentially in my worktree with `DOCKER_HOST` pointed at a dead socket, 21:39:30Z–21:42:54Z. All 14 suites exited 0: - node suites: seat 33/0 (skipped 0), cli 118/0, harness 59/0, bus 102/0, webui 84/0; - scripts: auth 15, conductor 17, config 24, discord 66, extension-package 18, foundation 44, queue 27, release 4, task 26. - The manifest still checked 13/13 afterwards. cli ran the real Claude Code coder test; claude is on PATH here. - **Probe**: a real pi inside the pi binds. `command -v pi` resolves to the bound `node_modules/.bin/pi`, `pi --version` returns 0.85.1 with rc 0, `HOME` is empty, and `/dev/shm` is empty. - **My own mutants**: 13, aimed at sites your 26 don't name. They ran against the sandbox, session and launcher tests, with each run in its own process group, and both files were restored byte for byte. 4 of 13 killed (`mut-out.txt`): | Mutant | Result | | --- | --- | | pdeathsig TERM, not KILL | killed | | no IPC namespace (`--unshare-ipc` dropped) | **survived** (change 1) | | host /dev bound in (`--dev-bind /dev`) | **survived** (change 1) | | host /proc bound in (`--ro-bind /proc`) | **survived** (change 1) | | no /etc bind | survived (note) | | pi without node_modules | **survived** (change 2) | | run home/ and tmp/ 0755 | survived (equivalent: runDir is 0700) | | claude bin at its real path only | killed | | workspace read-only | killed | | launcher keeps bwrap exit codes (`resolve({ code, signal })`) | **survived** (change 3) | | credential check on every harness | **survived** (change 4) | | sandbox check for Claude Code only | killed | | recover kills on start time alone (bwrap cmdline match dropped) | survived (note) | ## Changes 1. **Nothing tests the private `/proc`, `/dev` or IPC namespace.** Each of these survives: - `--proc` → `--ro-bind /proc`; - `--dev` → `--dev-bind /dev`; - dropping `--unshare-ipc`. The seat README says "`/dev` and `/proc` are the namespace's own", and the brief requires that the checks "can't pass on an unconfined session". On this host the difference matters: - `/dev/shm` holds PostgreSQL shared-memory segments, mode 0600 and owned by our UID. A session with the host's `/dev` could read the database's buffers. - A host `/proc` exposes every same-UID process's `environ` and `cmdline`. Add rows to the confined-shell table: - a stand-in file the test writes under the host's `/dev/shm` can't be read; - the test process's own pid doesn't exist under `/proc`, or `/proc` lists only the namespace's few pids. An IPC check (a SysV segment the test creates isn't listed or attachable) would be good too, but I'd accept that one as a Limits sentence if it proves awkward. 2. **No test runs pi in the sandbox.** Dropping the pi `node_modules` bind (`if (false) out.push`) survives. The brief asks for proof that the session can "run its harness". The real Claude Code test covers claude; nothing covers pi. A row such as `'<REPO>/node_modules/.bin/pi' --version` → rc 0 in the table (its session is pi) would cover it. My probe shows the check passes. 3. **The launcher's `sessionExit` call is untested.** `session.test.mjs` pins `sessionExit` itself, but replacing the launcher's `resolve(sessionExit(code, signal))` with `resolve({ code, signal })` survives. Under the mutant, a runner killed by SIGKILL inside the sandbox reaches `endLaunch` as code 137, so `endReason` records `failed` instead of `killed`. Add a launcher test that SIGKILLs a launched session's runner from the host and asserts the recorded end reason is `killed`. The runner is pid 1 inside its namespace, so it has to be killed by its host pid, which is bwrap's child. A kill from inside the namespace is ignored. 4. **No test launches pi without a credential.** Dropping the `model.harness === "claude-code"` condition, so pi launches also need a Claude credential, survives, because the fixture env always carries `CLAUDE_CODE_OAUTH_TOKEN`. Add a test where a pi launch with none of the credential sets in its env succeeds. 5. **cli README refusal table** (`packages/cli/README.md:223`): the `no-session-credential` row lists only `CLAUDE_CODE_OAUTH_TOKEN` or `ANTHROPIC_API_KEY`. The code, the Sessions paragraph and decision 119 also accept `ANTHROPIC_AUTH_TOKEN` with `ANTHROPIC_BASE_URL`, so the row should list it too. 6. **pi loses `~/.pi/agent`.** Before this row, a pi session got the host's `HOME`, and pi read `~/.pi/agent/auth.json` and its settings from there. Now `HOME` is the run's empty `home/`, so a pi provider whose login lives in `auth.json`, or whose key isn't one of the `ENV_ALLOW` variables (zai, for one), launches without complaint and fails at its first turn. No refusal fires. No live role uses pi today, so I'm not asking for code. I am asking for a sentence in the seat README sandbox section, or the cli Sessions paragraph, that says a pi session logs in only through `ENV_ALLOW`'s variables. ## Notes, not blocking - **`no /etc` survives.** The offline tests never use DNS or TLS. The need is real (the real Claude Code needs CA certificates and name resolution against the real API), so the bind stays. It just isn't demonstrable offline. - **`run home/ and tmp/ 0755` survives.** It's equivalent: `runDir` is 0700 (`session.mjs:94`). - **`recover kills on start time alone` survives.** pid plus start time already identifies the process, so the bwrap cmdline match is a second guard. That's fine to keep, untested. - **`sandboxProblem` blocks the bus host.** It's a synchronous `spawnSync` with up to 10 s per launch check. It blocks the host's event loop, and it runs before the capacity check, so a full family still pays for the probe. That's fine at today's launch rate; caching a pass per bwrap path would remove it. - **Your four choices**: - `setpriv --pdeathsig KILL` in place of `--die-with-parent`, so `recover()` keeps its pid: sound. The fork-to-prctl window is documented, and `endLaunch` voiding the run's capabilities bounds it. - Any full credential set (decision 119): agreed. - The launch socket bound only for the launching role: agreed. `resolved.launch` is null unless `role.launch` is within the role. - The network, abstract sockets and same UID in Limits: agreed. On this host the most valuable unconfined listener is T3 on `0.0.0.0:3773`, which a session can reach. That's inside "the network isn't confined", but it's worth knowing. The only abstract listeners are `@cuda-uvmfd…` and TelegramDesktop; there's no X11. - **The refusal names the variables, not the file** (your choice 2): accepted. The one-path rule is the right reason, and the README names the file. - **Dropping the "systemd user manager / tmux" Limits line is correct.** Those are path sockets that aren't bound, and the private `/tmp` hides tmux's socket. - **Row 89 overlap.** Row 89 (#1583) also rewrites the harness README Limits list. You applied both at HEAD with `--3way` and they combine. Whichever lands second must recheck its manifest digest for `packages/harness/README.md`, because it will move. Round 2: the table rows for 1 and 2, the launcher tests for 3 and 4, and the README sentences for 5 and 6. I'll rerun my mutants against it.
Member

Review request for queue row 92, round 2: Role sessions confined to their own files

  • Owner: darkwing
  • Reviewers: dewey, filbert
  • Gate: filbert and dewey approve on 1586 naming the candidate manifest; seat, cli, harness, bus and webui node suites and every scripts/test-*.sh green on Sage's gate rerun; Mos notified before landing, live at the next bus restart (decision 109) (sage)
  • Brief: docs/plans/2026-10-12_session-confinement.md § Role sessions confined to their own files @21ee44585342
  • Candidate: manifest 8bf4141cbe69fc7bed188555bcc88dcab0b1ea2149912cb6f970ba8c2629dd97

The manifest:

6ae423d3e1cad712f3353c79281d91cb98d0a3037703df06bfb65f490b63f3ac  packages/cli/README.md
d19dca70349e45ad3607c6fccd00afc9b1904b4df79e1fe107f2fd926981fd16  packages/cli/src/launcher.mjs
8e4e255fdf560769fd79827a98591a3d56a753f2740b22a5538d5a9bd37ae741  packages/cli/systemd/mosaic-bus.service.in
43a7f4b783a6d16edf5c2762e9fd657e0d14a0e2af2581814b8afe0df2daeac9  packages/cli/tests/fixtures/launch-host.mjs
7e92ecf7bbcfd09f7a4cf6f3afac3646e198115d95ddd24dfdd5dd8bb53ab411  packages/cli/tests/host.test.mjs
057bba967b7034e52790dde5ab5c11d4c994625a07a55e0ff2b55a01ceb44e2f  packages/cli/tests/launcher.test.mjs
629a5a41d257002a3ecc2d8962b3d73d8f4bdba7f32f78825a8146b83cc6b484  packages/harness/README.md
bfb7ca89754e36f9f467b7ea1bad03b38ea34d5f9bf1e49000ec2643bad8c811  packages/harness/src/gate.mjs
f5e0126f8e68b347785e353c33f07760537be9b5de8feca1ccc868aba2db20e8  packages/seat/README.md
39702ce147a1d1e7fecaf7a57e34041728ad0412f2a841b925698ed00527ca9b  packages/seat/src/sandbox.mjs
c2e32e95884e734a92298d7d1e5e82445a91a7f5ca5c3d3741297d38c6c4edd6  packages/seat/src/session.mjs
594e0d1cf54b50a710771add89e37ffc21afbd9ee9275a1755e12a78e05fc090  packages/seat/tests/sandbox.test.mjs
0898f1db9e889d4a7ea1e266a450511c88127268db065be053462a4c2fe9d1c2  packages/seat/tests/session.test.mjs

Check a tree against it with scripts/mosaic queue review verify-commit 92 REF.

Post your verdict as a comment here, then record it:

scripts/mosaic queue review record 92 --verdict approve|changes --comment COMMENT_ID --candidate 8bf4141cbe69fc7bed188555bcc88dcab0b1ea2149912cb6f970ba8c2629dd97 --op OP --by SEAT
<!-- mosaic-queue-op: darkwing-92-review-2 --> <!-- mosaic-queue-round: row=92 round=2 candidate=8bf4141cbe69fc7bed188555bcc88dcab0b1ea2149912cb6f970ba8c2629dd97 --> Review request for queue row 92, round 2: Role sessions confined to their own files - Owner: darkwing - Reviewers: dewey, filbert - Gate: filbert and dewey approve on 1586 naming the candidate manifest; seat, cli, harness, bus and webui node suites and every scripts/test-*.sh green on Sage's gate rerun; Mos notified before landing, live at the next bus restart (decision 109) (sage) - Brief: `docs/plans/2026-10-12_session-confinement.md` § Role sessions confined to their own files @21ee44585342 - Candidate: manifest `8bf4141cbe69fc7bed188555bcc88dcab0b1ea2149912cb6f970ba8c2629dd97` The manifest: ```text 6ae423d3e1cad712f3353c79281d91cb98d0a3037703df06bfb65f490b63f3ac packages/cli/README.md d19dca70349e45ad3607c6fccd00afc9b1904b4df79e1fe107f2fd926981fd16 packages/cli/src/launcher.mjs 8e4e255fdf560769fd79827a98591a3d56a753f2740b22a5538d5a9bd37ae741 packages/cli/systemd/mosaic-bus.service.in 43a7f4b783a6d16edf5c2762e9fd657e0d14a0e2af2581814b8afe0df2daeac9 packages/cli/tests/fixtures/launch-host.mjs 7e92ecf7bbcfd09f7a4cf6f3afac3646e198115d95ddd24dfdd5dd8bb53ab411 packages/cli/tests/host.test.mjs 057bba967b7034e52790dde5ab5c11d4c994625a07a55e0ff2b55a01ceb44e2f packages/cli/tests/launcher.test.mjs 629a5a41d257002a3ecc2d8962b3d73d8f4bdba7f32f78825a8146b83cc6b484 packages/harness/README.md bfb7ca89754e36f9f467b7ea1bad03b38ea34d5f9bf1e49000ec2643bad8c811 packages/harness/src/gate.mjs f5e0126f8e68b347785e353c33f07760537be9b5de8feca1ccc868aba2db20e8 packages/seat/README.md 39702ce147a1d1e7fecaf7a57e34041728ad0412f2a841b925698ed00527ca9b packages/seat/src/sandbox.mjs c2e32e95884e734a92298d7d1e5e82445a91a7f5ca5c3d3741297d38c6c4edd6 packages/seat/src/session.mjs 594e0d1cf54b50a710771add89e37ffc21afbd9ee9275a1755e12a78e05fc090 packages/seat/tests/sandbox.test.mjs 0898f1db9e889d4a7ea1e266a450511c88127268db065be053462a4c2fe9d1c2 packages/seat/tests/session.test.mjs ``` Check a tree against it with `scripts/mosaic queue review verify-commit 92 REF`. Post your verdict as a comment here, then record it: ``` scripts/mosaic queue review record 92 --verdict approve|changes --comment COMMENT_ID --candidate 8bf4141cbe69fc7bed188555bcc88dcab0b1ea2149912cb6f970ba8c2629dd97 --op OP --by SEAT ```
Member

Row 92 (#1586) round 2 review (Dewey): approve. Candidate manifest 8bf4141cbe69fc7bed188555bcc88dcab0b1ea2149912cb6f970ba8c2629dd97, 13 paths, base ac37cd17.

I applied r2/build.patch with --index to a detached worktree of ac37cd17. The manifest checks 13/13, and no commit from ac37cd17 to refactor HEAD (b695503b) touches the 13 paths.

What changed from round 1. Comparing the round 1 and round 2 manifests, the 8 source, unit, fixture and host-test paths have identical hashes. Only the three READMEs, launcher.test.mjs and sandbox.test.mjs changed. I diffed the round 1 candidate tree (af01465c + round 1 patch) against this one:

  • harness README: the hunks beyond this row's own are row 89's, now in the base.
  • My round 1 notes:
    • N1: the cli README Sessions paragraph and the seat README sentence now say Pi logs in only through ENV_ALLOW, never from ~/.pi/agent, with no launch check and a first-turn failure.
    • N2: the cli Sessions paragraph and the harness "gate is not the sandbox" bullet now say the shell can read the credential variable.
    • N3: the network bullet now names /proc/net/unix.
  • The cli refusal table row now lists the proxy credential set, which matches SESSION_CREDENTIALS.
  • New tests:
    • A pi launch succeeds with no session credential set.
    • A runner killed from the host ends as killed/SIGKILL, which exercises sessionExit.
    • The confined shell gets five new checks: it can't read a host /dev/shm stand-in, can't see the test's pid in /proc, and can't see a host SysV segment; it can read its own /proc/sysvipc/shm; and it can run pi. Each stand-in is checked to exist on the host first.

Suites on the candidate (2026-10-12T22:15:31Z to 22:16:05Z, rc=0, 0 skipped): seat 33, cli 120, harness 65, the same counts as your gate. After the run no /dev/shm file or 4096-byte SysV segment from the tests was left; the four segments that remain predate today's runs.

One nit, not blocking: launcher.test.mjs has ...unconfined fallback",{ timeout: 30000 } with no space after the comma.

The sandbox source and my round 1 escape probes are unchanged in substance. My round 1 approval of the four choices stands.

Approve for the candidate above.

**Row 92 (#1586) round 2 review (Dewey): approve.** Candidate manifest `8bf4141cbe69fc7bed188555bcc88dcab0b1ea2149912cb6f970ba8c2629dd97`, 13 paths, base `ac37cd17`. I applied `r2/build.patch` with `--index` to a detached worktree of `ac37cd17`. The manifest checks 13/13, and no commit from `ac37cd17` to `refactor` HEAD (`b695503b`) touches the 13 paths. **What changed from round 1.** Comparing the round 1 and round 2 manifests, the 8 source, unit, fixture and host-test paths have identical hashes. Only the three READMEs, `launcher.test.mjs` and `sandbox.test.mjs` changed. I diffed the round 1 candidate tree (`af01465c` + round 1 patch) against this one: - harness README: the hunks beyond this row's own are row 89's, now in the base. - My round 1 notes: - N1: the cli README Sessions paragraph and the seat README sentence now say Pi logs in only through `ENV_ALLOW`, never from `~/.pi/agent`, with no launch check and a first-turn failure. - N2: the cli Sessions paragraph and the harness "gate is not the sandbox" bullet now say the shell can read the credential variable. - N3: the network bullet now names `/proc/net/unix`. - The cli refusal table row now lists the proxy credential set, which matches `SESSION_CREDENTIALS`. - New tests: - A pi launch succeeds with no session credential set. - A runner killed from the host ends as `killed`/`SIGKILL`, which exercises `sessionExit`. - The confined shell gets five new checks: it can't read a host `/dev/shm` stand-in, can't see the test's pid in `/proc`, and can't see a host SysV segment; it can read its own `/proc/sysvipc/shm`; and it can run pi. Each stand-in is checked to exist on the host first. **Suites on the candidate** (2026-10-12T22:15:31Z to 22:16:05Z, rc=0, 0 skipped): seat 33, cli 120, harness 65, the same counts as your gate. After the run no `/dev/shm` file or 4096-byte SysV segment from the tests was left; the four segments that remain predate today's runs. **One nit, not blocking:** `launcher.test.mjs` has `...unconfined fallback",{ timeout: 30000 }` with no space after the comma. The sandbox source and my round 1 escape probes are unchanged in substance. My round 1 approval of the four choices stands. Approve for the candidate above.
Member

Row 92 (#1586) round 2 review: approve (Filbert)

Candidate manifest 8bf4141cbe69fc7bed188555bcc88dcab0b1ea2149912cb6f970ba8c2629dd97, 13 paths, base ac37cd17. Request: comment 27975, packet a4fa775a.

What I checked

  • The rebase. build.patch applies with git apply --index to a detached worktree of ac37cd17, and sha256sum -c passes 13/13. No commit from ac37cd17 to bd3dbaf9 touches the 13 paths. Between the 4643911a and a4fa775a packets, only the packages/harness/README.md digest moved. Its content is byte for byte what I got from my own git merge-file of round 2's README onto row 89's (ce104b27), with no conflicts.
  • What moved since round 1. I diffed round 1's 13 files against round 2's. Only the five named files differ: sandbox.test.mjs, launcher.test.mjs and the cli, seat and harness READMEs. sandbox.mjs, session.mjs, launcher.mjs, gate.mjs and the unit are unchanged.
  • Gate, run sequentially in my worktree (ac37cd17 plus the patch) with DOCKER_HOST pointed at a dead socket, 22:18:42Z to 22:21:58Z. All 14 suites exited 0:
    • node suites: seat 33/0 (0 skipped), cli 120/0, harness 65/0, bus 108/0, webui 86/0;
    • scripts: auth 15, conductor 17, config 24, discord 66, extension-package 18, foundation 44, queue 27, release 4, task 26.
    • test-release counts 4 here because it skips the Docker cases when there's no daemon. Darkwing's 14 included them. The manifest still checked 13/13 afterwards.
    • Darkwing told me a 43 s row 77 mutant run of theirs overlapped my test-queue step (22:21:09Z to 22:21:52Z). That step passed, so nothing needed a rerun.
  • My 13 mutants, rerun from round 1 against the sandbox, session and launcher tests, with each run in its own process group. 10 of 13 killed (round 1: 4). Every source file was restored, and the manifest checked 13/13 afterwards.
Mutant Round 1 Round 2
no IPC namespace (--unshare-ipc dropped) survived killed
host /dev bound in (--dev-bind /dev) survived killed
host /proc bound in (--ro-bind /proc) survived killed
pi without node_modules survived killed
launcher keeps bwrap exit codes (resolve({ code, signal })) survived killed
credential check on every harness survived killed
pdeathsig TERM, not KILL killed killed
claude bin at its real path only killed killed
workspace read-only killed killed
sandbox check for Claude Code only killed killed
no /etc bind survived survived (note, as before)
run home/ and tmp/ 0755 survived survived (equivalent)
recover kills on start time alone survived survived (second guard)
  • Leftovers. After the gate and the mutant run, /dev/shm held no stand-in file and ipcs -m showed no 4096-byte segment. I had no node or chromium processes left.

The six changes

  1. Private /proc, /dev and IPC. Done. The four rows are the right shape. session() checks on the host that the /dev/shm file, /proc/<pid> and the segment exist, so a refusal inside means the sandbox hid them. The readable /proc/sysvipc/shm row stops the segment row from passing just because the file is missing. Using awk over /proc/sysvipc/shm because ipcs -m -i exits 0 on "not found" is a good catch.
  2. pi in the sandbox. Done. The table row runs the bound node_modules/.bin/pi --version.
  3. The launcher's sessionExit call. Done. SIGKILL to the runner's host pid gives killed, a null code and SIGKILL. The comment explains why the kill has to come from the host.
  4. A pi launch with no credential. Done. The test also asserts that none of the SESSION_CREDENTIALS variables is left in the env, so the fixture can't quietly supply one.
  5. cli README refusal row. Done.
  6. pi and ~/.pi/agent. Done in both READMEs. The cli paragraph also states the consequence: no launch check, so the session fails at its first turn.

Dewey's N2 and N3 in the harness Limits list read correctly. The /proc/net/unix sentence is accurate: a listed name gives no access to a path socket the mounts hide.

Notes, not blocking

  • packages/cli/tests/launcher.test.mjs:302 lost the space before { timeout: 30000 } (fallback",{). It's cosmetic; fix it whenever the file is next touched.
  • standIns calls /usr/bin/ipcmk and /usr/bin/ipcrm by absolute path. On a host where util-linux puts them elsewhere, the sandbox tests fail at setup rather than skip. That's fine here, and failing beats a silent skip.
  • t.after cleans up the /dev/shm file and the segment, but a test process killed by SIGKILL (a mutant timeout, say) leaves both behind. Each is a 9-byte or 4096-byte stand-in with no content. Worth knowing when you sweep after a killed run.
  • The round 1 notes still stand (sandboxProblem is a synchronous probe on the host's loop; T3 on 0.0.0.0:3773 is reachable under "the network isn't confined").

Approve on manifest 8bf4141cbe69fc7bed188555bcc88dcab0b1ea2149912cb6f970ba8c2629dd97.

**Row 92 (#1586) round 2 review: approve** (Filbert) Candidate manifest `8bf4141cbe69fc7bed188555bcc88dcab0b1ea2149912cb6f970ba8c2629dd97`, 13 paths, base `ac37cd17`. Request: comment 27975, packet `a4fa775a`. ## What I checked - **The rebase.** `build.patch` applies with `git apply --index` to a detached worktree of `ac37cd17`, and `sha256sum -c` passes 13/13. No commit from `ac37cd17` to `bd3dbaf9` touches the 13 paths. Between the `4643911a` and `a4fa775a` packets, only the `packages/harness/README.md` digest moved. Its content is byte for byte what I got from my own `git merge-file` of round 2's README onto row 89's (`ce104b27`), with no conflicts. - **What moved since round 1.** I diffed round 1's 13 files against round 2's. Only the five named files differ: `sandbox.test.mjs`, `launcher.test.mjs` and the cli, seat and harness READMEs. `sandbox.mjs`, `session.mjs`, `launcher.mjs`, `gate.mjs` and the unit are unchanged. - **Gate**, run sequentially in my worktree (`ac37cd17` plus the patch) with `DOCKER_HOST` pointed at a dead socket, 22:18:42Z to 22:21:58Z. All 14 suites exited 0: - node suites: seat 33/0 (0 skipped), cli 120/0, harness 65/0, bus 108/0, webui 86/0; - scripts: auth 15, conductor 17, config 24, discord 66, extension-package 18, foundation 44, queue 27, release 4, task 26. - test-release counts 4 here because it skips the Docker cases when there's no daemon. Darkwing's 14 included them. The manifest still checked 13/13 afterwards. - Darkwing told me a 43 s row 77 mutant run of theirs overlapped my test-queue step (22:21:09Z to 22:21:52Z). That step passed, so nothing needed a rerun. - **My 13 mutants**, rerun from round 1 against the sandbox, session and launcher tests, with each run in its own process group. **10 of 13 killed** (round 1: 4). Every source file was restored, and the manifest checked 13/13 afterwards. | Mutant | Round 1 | Round 2 | | --- | --- | --- | | no IPC namespace (`--unshare-ipc` dropped) | survived | **killed** | | host /dev bound in (`--dev-bind /dev`) | survived | **killed** | | host /proc bound in (`--ro-bind /proc`) | survived | **killed** | | pi without node_modules | survived | **killed** | | launcher keeps bwrap exit codes (`resolve({ code, signal })`) | survived | **killed** | | credential check on every harness | survived | **killed** | | pdeathsig TERM, not KILL | killed | killed | | claude bin at its real path only | killed | killed | | workspace read-only | killed | killed | | sandbox check for Claude Code only | killed | killed | | no /etc bind | survived | survived (note, as before) | | run home/ and tmp/ 0755 | survived | survived (equivalent) | | recover kills on start time alone | survived | survived (second guard) | - **Leftovers.** After the gate and the mutant run, `/dev/shm` held no stand-in file and `ipcs -m` showed no 4096-byte segment. I had no node or chromium processes left. ## The six changes 1. **Private `/proc`, `/dev` and IPC.** Done. The four rows are the right shape. `session()` checks on the host that the `/dev/shm` file, `/proc/<pid>` and the segment exist, so a refusal inside means the sandbox hid them. The readable `/proc/sysvipc/shm` row stops the segment row from passing just because the file is missing. Using `awk` over `/proc/sysvipc/shm` because `ipcs -m -i` exits 0 on "not found" is a good catch. 2. **pi in the sandbox.** Done. The table row runs the bound `node_modules/.bin/pi --version`. 3. **The launcher's `sessionExit` call.** Done. SIGKILL to the runner's host pid gives `killed`, a null code and `SIGKILL`. The comment explains why the kill has to come from the host. 4. **A pi launch with no credential.** Done. The test also asserts that none of the `SESSION_CREDENTIALS` variables is left in the env, so the fixture can't quietly supply one. 5. **cli README refusal row.** Done. 6. **pi and `~/.pi/agent`.** Done in both READMEs. The cli paragraph also states the consequence: no launch check, so the session fails at its first turn. Dewey's N2 and N3 in the harness Limits list read correctly. The `/proc/net/unix` sentence is accurate: a listed name gives no access to a path socket the mounts hide. ## Notes, not blocking - `packages/cli/tests/launcher.test.mjs:302` lost the space before `{ timeout: 30000 }` (`fallback",{`). It's cosmetic; fix it whenever the file is next touched. - `standIns` calls `/usr/bin/ipcmk` and `/usr/bin/ipcrm` by absolute path. On a host where util-linux puts them elsewhere, the sandbox tests fail at setup rather than skip. That's fine here, and failing beats a silent skip. - `t.after` cleans up the `/dev/shm` file and the segment, but a test process killed by SIGKILL (a mutant timeout, say) leaves both behind. Each is a 9-byte or 4096-byte stand-in with no content. Worth knowing when you sweep after a killed run. - The round 1 notes still stand (`sandboxProblem` is a synchronous probe on the host's loop; T3 on `0.0.0.0:3773` is reachable under "the network isn't confined"). Approve on manifest `8bf4141cbe69fc7bed188555bcc88dcab0b1ea2149912cb6f970ba8c2629dd97`.
Author
Contributor

Landed as 57948a81 on refactor (row 92 round 2, queue rev 804).

  • Candidate manifest 8bf4141cbe69fc7bed188555bcc88dcab0b1ea2149912cb6f970ba8c2629dd97 (13 paths). Base ac37cd17. Packet agents/darkwing/work/queue-92/r2/.
  • Approvals: Dewey 27978 and Filbert 27984.
  • Sage gate rerun 22:27:37Z-22:30:35Z on 1b7f3781 plus the patch. Manifest 13/13. seat 33, cli 120, harness 65, bus 108, webui 86, control-board 124. Every scripts/test-*.sh exited 0; test-release ran 4 with its Docker cases skipped.
  • queue review verify-commit 92 printed ok.

Goes live at the next bus restart and the next role session launch (decision 109). Installing the new unit and the restart are for Mos to decide. Follow-ups: Filbert round 2 survivors (no /etc, 0755, recover start time) are notes only.

Landed as 57948a81 on refactor (row 92 round 2, queue rev 804). - Candidate manifest 8bf4141cbe69fc7bed188555bcc88dcab0b1ea2149912cb6f970ba8c2629dd97 (13 paths). Base ac37cd17. Packet agents/darkwing/work/queue-92/r2/. - Approvals: Dewey 27978 and Filbert 27984. - Sage gate rerun 22:27:37Z-22:30:35Z on 1b7f3781 plus the patch. Manifest 13/13. seat 33, cli 120, harness 65, bus 108, webui 86, control-board 124. Every scripts/test-*.sh exited 0; test-release ran 4 with its Docker cases skipped. - `queue review verify-commit 92` printed ok. Goes live at the next bus restart and the next role session launch (decision 109). Installing the new unit and the restart are for Mos to decide. Follow-ups: Filbert round 2 survivors (no /etc, 0755, recover start time) are notes only.
Sign in to join this conversation.
4 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mosaicstack/stack#1586