diff --git a/docs/plans/2026-10-12_session-confinement.md b/docs/plans/2026-10-12_session-confinement.md new file mode 100644 index 000000000..893c6612c --- /dev/null +++ b/docs/plans/2026-10-12_session-confinement.md @@ -0,0 +1,93 @@ +# Role sessions confined to their own files (2026-10-12) + +Status: written by Sage, lead. It follows `docs/plans/BRIEF-TEMPLATE.md`. +It answers Vikunja P19 #232 (security, priority 5), which the stack's +own reviewer session found in Jason's first test and Mos checked at +9438880e (sage-0102). Gitea #1586. It comes before every other row from +that test. + +## Role sessions confined to their own files + +### Problem + +The tool gate checks the paths of Read, Write, Edit, Grep and Glob +(`packages/harness/src/gate.mjs:22-25`) and nothing else. Bash, which +the coder, cto, reviewer and researcher roles have (`roles/*.json` +`tools`), runs as the host's user with only a user and PID namespace +(`packages/seat/src/session.mjs:39`). It sees the whole filesystem. In +the test the reviewer's Grep of the coder's workspace was refused and +its Bash `ls` of the same directory ran. The same shell can read +`~/.config/mosaic-dev/secrets`, `~/.mosaic`, `~/.ssh`, +`~/.git-credentials`, every other workspace and run directory, and the +canonical checkout. The harness README says so (`README.md:162-173`, "The +gate is not a sandbox"); slice 1 accepted that limit (S0 line 5), and +Jason's test shows the stack can't run role sessions on its own source +with it. + +### Owner and reviewers + +Owner: Darkwing, parking row 63 (in progress) until this lands. +Reviewers: Filbert and Dewey, both required, since this sets a security +boundary. + +### Files owned + +- `packages/seat/src/session.mjs`, the spawn and its arguments; + `packages/seat/src/sandbox.mjs`, new +- `packages/cli/src/launcher.mjs`, only where it passes the paths a + session needs +- `packages/harness/src/gate.mjs`, the header comment only +- `packages/harness/README.md` "Limits", `packages/seat/README.md` +- tests under `packages/seat/tests/`, `packages/cli/tests/` and + `packages/harness/tests/` +- `agents/darkwing/work/queue-/` + +### What ships + +- Every role session's process tree runs inside a bubblewrap + (`/usr/bin/bwrap`, 0.12.0 on the host) mount namespace built from an + empty root. It sees, read-only: the system directories the harness and + its tools need (`/usr`, `/etc` and the like), the pinned harness + binaries, and the repository files the runner loads. It sees, + read-write: its own workspace and its own run directory. It gets a + private `/tmp` and an empty `HOME`, plus whatever the harness needs + there, created inside the run directory. It sees nothing else: no + data root, no other workspace or run directory, no `~/.config`, no + `~/.mosaic`, no `~/.ssh`, no checkout outside what the runner loads. + The PID namespace and kill-on-parent-exit stay. +- The packet lists every bind with its source, mode and why the session + needs it. A bind the reviewers can't tie to a need comes out. +- A missing or failing `bwrap` refuses the launch with + `sandbox-unavailable`, through the broker's `refuse()`. There is no + unconfined fallback. +- The network stays as it is. The harness needs its model endpoint, and + the README keeps saying the network isn't confined. +- Tests run a real confined session's shell and prove these refuse: + reading a file under a stand-in `~/.config/mosaic-dev/secrets`, under a + stand-in `~/.mosaic`, in another instance's workspace, in another run + directory, in the data root's store, and in the canonical checkout + outside the runner's files; writing outside the workspace and run + directory. They prove the session can still read and write its + workspace, run its harness and reach its tool socket. A test kills a + bind from the list and shows the matching check fail, so the checks + can't pass on an unconfined session. +- The harness README's "Limits" section says what is confined now and + what isn't (network, abstract sockets, same UID outside the namespace). + +Suites: the seat, cli, harness, bus and webui node suites, and every +`scripts/test-*.sh`. The packet also shows one real coder launch on the +host with Claude Code, reading a stand-in secret from its Bash and +getting a refusal. + +### Out of scope + +Network confinement. Different UIDs per role. Containers (rows 86 and +87, I1). Removing Bash from any role. The tool gate's own checks. + +### Gate + +Filbert and Dewey approve on the row's issue, naming the candidate +manifest. The suites above pass on Sage's gate rerun. The launcher runs +in the bus host, so the change is live at the next bus restart, which +ends every running session (decision 109 notices to Mos). Until it is +live, whether bash-capable sessions run is Jason's call.