# The box
Workers run in the **box**: a Docker container that is deliberately capable
inside and powerless at the edge. Inside, pi has a real toolchain — git,
`gh`, Python, Node, build tools. At the edge, there is no internet, no DNS,
no secrets, and no writable code. Capability and authority live in
different layers: being useful is not the same as being trusted.
A "no" is always legible — a 403 naming the rule, a 402 naming the budget —
never a missing package, an untrusted certificate, or a hung socket.
## The lockdown
**No route.** The box joins `roster-locked`, a Docker bridge network with
IP masquerade disabled: packets to the internet leave with an address
replies can't route back to, so outbound connections simply never complete.
The host stays reachable — that's where the gateway is.
**No DNS.** With an HTTP proxy, the box needs no resolver at all — clients
hand the hostname to CONNECT and the gateway resolves it. The box runs with
`--dns 127.0.0.1`, a blackhole, so DNS can't be used as an exfiltration
side channel. Tools that insist on resolving before proxying fail fast
instead of hanging.
**One door.** `HTTP_PROXY`/`HTTPS_PROXY` (and friends) point at the gateway,
carrying the run's single-use identity token as the proxy credential — how
every request gets attributed, un-spoofably, to this worker and run
(see [gateway.md](gateway.md)).
**Fail closed.** If the locked network can't be ensured, the gateway isn't
answering, the CA is missing, or there are no model credentials to
sentinel, the runner refuses to start the box. It never falls back to open
egress.
**TLS that just works.** The box trusts the gateway's CA everywhere:
`NODE_EXTRA_CA_CERTS` gets the CA certificate, and everything that replaces
its root store (`SSL_CERT_FILE`, `CURL_CA_BUNDLE`, `REQUESTS_CA_BUNDLE`,
`GIT_SSL_CAINFO`, `PIP_CERT`) gets a combined bundle — system roots plus the
roster CA — so Go, Rust, git, curl, and pip all work through interception,
and tunneled hosts presenting real certificates still verify.
## Nothing worth stealing
No real credential enters the box, ever:
- The model auth in the box's home is a **sentinel** — same shape as the
real thing (a well-formed fake JWT where a JWT is expected), useless
everywhere. The gateway swaps in the real token in transit.
- Service tokens exposed via connections or `[[expose]]` appear as env vars
set to the sentinel string. `GH_TOKEN` works exactly where policy says
GitHub calls may go, and nowhere else — leaking the entire box
environment leaks placeholders.
- Bot tokens, SMTP credentials, and the CA private key are host-side only.
And nothing to tamper with beyond what was deliberately granted. The box
sees one legible tree under its per-run `$HOME`, and the rule that governs
every entry is **who wrote it decides the mode**: host-written mounts
read-only, worker-written mounts read-write.
```
$HOME rw fresh every run: workspace/, session/, dotfiles
├── store/ rw the worker's durable dir — survives runs,
│ worker-managed layout, host-snapshotted
├── mnt/<connection>/ granted host-dir / host-repo connections
│ (ro or rw per grant; gated repos appear as
│ per-run clones — see repos.md)
├── self/ ro the worker's own footprint: config/, identity,
│ schedule.json, journal/, and runs/ — its complete
│ raw run history — edits via actions only
└── channel/ ro the active channel's history and files, when
the run serves one
```
Plus the CA certificates (read-only) and nothing else of the deployment:
not the vault, not org.toml, not the audit logs, not any other worker's
footprint, not any other channel's live history. (`self/runs/` transcripts
do span every channel this worker has served — a worker is scoped to
channels that can be trusted, so its own record is its to read.) `$HOME` itself is ephemeral
**by design** — a run cannot plant a `.bashrc` or `.gitconfig` for a
future run to execute; only `store/` persists, and nothing in it
auto-executes.
`/tmp` is a private 2 GiB tmpfs that vanishes with the container —
downloads and scratch work go there and are never mistaken for durable
storage.
Runs end at a hard wall-clock ceiling (`docker kill` — default 30 minutes,
per-run `--ceiling`); warm chat sessions are bounded by their idle window
instead (see [channels.md](channels.md)).
## The toolbelt
The image (`box/Dockerfile`, published as `ghcr.io/manasgarg/roster-box` —
the server pulls `:latest` automatically and re-pulls on every restart) is
`node:24-bookworm-slim` plus the tools a worker actually reaches for. On a
dead network, more binaries add no egress power — the cost is only image
size:
- **VCS / GitHub**: git, `gh`, git-lfs
- **Transfer & archives**: curl, wget, rsync, unzip, zip, xz, zstd
- **Coordination**: `roster-lock <name> -- <cmd>` — run a command holding
a named store lock (see [store.md](store.md))
- **Data**: sqlite3, jq, `yq`, poppler-utils (pdftotext)
- **Search & files**: ripgrep, fd, tree, file, less
- **Python**: python3, pip, venv, `uv`
- **Node**: node 24, npm, pnpm/yarn via corepack
- **Build**: build-essential, pkg-config (native pip/npm extensions)
- **Documents & media**: pandoc, imagemagick, ffmpeg
- **Misc**: openssl, procps, moreutils, tzdata
Go and Rust toolchains and a headless browser are deliberately not
defaults — add them per deployment if a worker's job needs them: build your
own image from `box/Dockerfile` and point `[engine] image` in `org.toml` at
its tag.
## The engine and its tools
pi and the box extensions are **baked into the image** at
`/opt/roster/engine` — the box does not depend on your checkout. (For
development, `[engine] dir` in `org.toml` mounts a checkout read-only over
the baked engine.)
The extensions are the worker's hands, in two files under `box/extensions/`:
- **`web.ts`** — governed retrieval: `web_search` (keyless DuckDuckGo
search) and `fetch_pages` (fetch and extract readable markdown). Plain
HTTP through the proxy; every request judged and logged.
- **`actions.ts`** — proposals, not powers: `message_user`, `discord_send`,
`slack_send`, `send_email`, `propose_purpose_edit`, `file_task`,
`set_tasks`, `file_update`, `repo_push`, and `check_gates`. Each submits
a typed envelope to the gateway's action host and obeys the verdict that
comes back — the tools shape behavior; they never enforce safety
(see [actions-and-trust.md](actions-and-trust.md)).
Dropping a new `.ts` file into `box/extensions/` ships a new capability;
governance for whatever it does comes free, because the gateway is still
the only exit.