Sandbox isolation
How pond confines an agent run. The threat model and the broader hardening
roadmap live in ../run-trust.md; this spec is the
implementation reference for the sandbox profiles, providers, and the
dispatch-time fail-closed enforcement.
Code: swarm/src/sandbox.py (worker-side enforcement),
app/executors/swarm.py (dispatch-side validation + routing).
The two-sided contract
Isolation is decided in two places that must agree:
- Dispatch (trusted, in pond):
resolve_sandbox_dispatchvalidates a stage’sexecutor.sandboxblock and derives(sandbox_block, required_capability, required_tags). It is fail-closed — a stage with nosandbox.profile(and no explicitnoneopt-out) is refused before any agent runs. - Worker (untrusted host):
resolve_policyturns the storedsandbox_blockinto a concreteSandboxPolicy, and aSandboxProviderlaunches the job under it. The worker re-resolves rather than trusting a pre-computed policy.
The profile names are the shared contract — keep _SANDBOX_PROFILES
(worker) and _KNOWN_SANDBOX_PROFILES (dispatch) in sync.
Profiles
A profile fixes the isolation posture (immutable); only resource/image knobs
are overridable (_SANDBOX_OVERRIDABLE = memory, cpus, pids_limit, tmpfs_size, image) — an overrides block can never widen network/filesystem/caps.
| Profile | Filesystem | Network | Use |
|---|---|---|---|
none | host | open | dev / local only (explicit opt-out) |
untrusted-code-read | ro checkout | broker-only | read-only analysis |
untrusted-code-write (harness) | rw throwaway copy | allowlist | the full coding harness (edit/build/test/install) |
The confined profiles drop all caps, set no-new-privileges, run non-root,
read-only root with a size-capped /tmp tmpfs, and apply hardened ulimits
(nofile/nproc/core=0) plus cgroup memory/cpus/pids defaults.
Network postures
- broker-only — the job sits on a per-job
--internalDocker network whose only egress is a sidecar running the credential broker (see model-credentials); the model key never enters the job. - allowlist — broker-only plus a forward CONNECT proxy in the sidecar:
HTTPS-only, default-deny, a tunnel opens only when the host is on the allowlist
and resolves to a non-private IP (loopback / link-local / metadata / RFC1918
rejected — closes DNS-rebind). The job gets
HTTPS_PROXY/NO_PROXY. Safe defaults (pypi/npm/crates/github) are extended per-job with the dispatcher’sallow_hosts(e.g. the target’s git remote). This is what makespip/npm/gitwork while exfil/pivot stays blocked.
rw throwaway copy
untrusted-code-write mounts a disposable copy of the checkout (staged by
_stage_checkout, removed on every exit path), so edits / node_modules /
generated files all work and are discarded — the source tree is never mutated.
Providers (OCI runtimes)
A SandboxProvider turns a SandboxSpec into a running, confined process.
get_provider(name) is fail-closed on unknown names; filter_sandbox_capabilities
drops an advertised sandbox.<backend> a host can’t actually enforce.
| Provider | name | Boundary |
|---|---|---|
NoneProvider | none | passthrough (dev fallback) |
DockerProvider | docker | shared-kernel container (runc) |
GvisorProvider | gvisor | userspace kernel (runsc) |
KataProvider | firecracker | microVM (Kata + Firecracker VMM) |
The hardened backends are DockerProvider + a --runtime flag, so the harness
stays an ordinary editable OCI image — “modify the environment” = edit a
Dockerfile, not a kernel. _RuntimeProvider resolves its runtime from
$SWARM_{GVISOR,KATA}_RUNTIME or the daemon’s registered runtimes
(Firecracker-before-QEMU) and is fail-closed: it refuses to instantiate
rather than silently downgrade to runc when its runtime is absent.
Trust-tier routing
executor.sandbox.tier (trusted | byo | any) maps to a required
pool:<tier> tag the orchestrator subset-matches against worker tags, so a job
only lands on a worker in the required pool. Confined profiles default to
trusted (untrusted code never silently reaches BYO/unpinned compute);
byo is explicit-only and additionally requires a tenant:<project_id> tag
(data minimization — a BYO host only sees its own tenant’s work).
Invariants
- No profile or an unknown profile/tier/backend at dispatch → the stage fails closed, never runs unconfined.
overridescan only touch resource/image knobs, never the posture.- A hardened provider never falls back to runc.
- The model credential is reachable only via the broker on broker-only/allowlist.