112 lines
5.5 KiB
Markdown
112 lines
5.5 KiB
Markdown
# 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`
|
|
- `packages/cli/systemd/mosaic-bus.service.in`, one `EnvironmentFile=`
|
|
line for the session credential, and its paragraph in
|
|
`packages/cli/README.md` (lead decision 118)
|
|
- 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.
|
|
- A confined Claude Code session can't reach Jason's
|
|
`~/.claude/.credentials.json`, which is how every role session logs
|
|
in today (Darkwing, 2026-10-12T19:52Z: the bus host has no
|
|
`ANTHROPIC_*` or `CLAUDE_CODE_OAUTH_TOKEN` variable). Binding that
|
|
file in is ruled out: Claude Code replaces it by rename on refresh,
|
|
and the session would hold a refresh token that can log out Jason and
|
|
every seat. The credential comes from a 0600 file under
|
|
`~/.config/mosaic-dev/secrets/`, which the bus unit loads with
|
|
`EnvironmentFile=`, as `CLAUDE_CODE_OAUTH_TOKEN` (or
|
|
`ANTHROPIC_API_KEY`), and `ENV_ALLOW` passes it on
|
|
(`packages/seat/src/session.mjs:61-66`). Which one is Jason's
|
|
choice. Sessions get nothing from `~/.claude`. With no credential a
|
|
Claude Code launch refuses before it starts, naming the missing file.
|
|
- 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. The row lands
|
|
only after Jason has chosen the credential and its file is in place;
|
|
without it the PM would not log in after the restart.
|