Files
stack/docs/plans/2026-10-12_session-confinement.md
T

4.4 KiB

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, next after their row 63 packet (e176bb53). 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-<row>/

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.