Lab 01 — Capability-Gated Sandbox Executor

Phase 09 · Lab 01 · Phase README · Warmup

The problem

An agent wants to run a model-proposed action — read a file, write a file, fetch a URL, call a tool. The action is untrusted: it came out of a stochastic model that may have been steered by an injected instruction in a document it read (Phase 10). "The model proposes, the application executes" (Phase 00) — and this lab is the executes clause done safely.

You will build a pure-Python model of the containment boundary a real sandbox enforces at the kernel — a container / gVisor / Firecracker microVM. The whole thing turns on one invariant:

A denied operation returns a structured result — it NEVER raises, and it NEVER performs the side effect. No partial write, no network packet, no escape.

Concretely: a Sandbox checks every op against a granted Capabilities object before performing it. Reads/writes are confined to allow-listed path prefixes (with a path-traversal defense), egress is confined to allow-listed hosts (the anti-exfiltration control), tools to an allow-list, and three resource budgets (max_ops, max_output_bytes, max_wall via an injected clock) cap a runaway. A SandboxedAgent drives a whole op sequence and stops on a hard limit — proving a hostile sequence cannot escape the grant.

What you build

PieceWhat it doesThe lesson
Capabilitiesfrozen allow-lists (fs_read, fs_write, net_hosts, allowed_tools) + budgetsleast privilege / POLA; allow-lists fail closed; authority is immutable
VFS / Netin-memory filesystem + network with a calls logtestable, offline side effects; the log proves "no exfil on denial"
has_traversal / canonical / under_allowlistthe path defensecanonicalize-then-check; /work must not authorize /workshop
Sandbox.run(op)charge budgets → check capability → then performcheck-before-effect; denials never raise, never mutate
_charge_output, max_wall clockthe resource metersa blown budget is a hard halt (the cgroup/breaker)
SandboxedAgent.run_alldrives a sequence, stops on a hard limita hostile op sequence is contained

File map

FileRole
lab.pyyour implementation (fill the # TODOs)
solution.pyreference implementation + worked example (python solution.py)
test_lab.pythe proof — 26 tests: allow/deny FS, traversal, egress, tools, budgets, no-side-effect, driver, determinism
requirements.txtpytest only

Run it

pytest test_lab.py -v                       # your lab.py (red until you implement)
LAB_MODULE=solution pytest test_lab.py -v   # the reference (green)
python solution.py                          # worked example

Success criteria

  • A read/write inside the allow-list is allowed; outside it is denied — and a denied write leaves VFS.snapshot() byte-for-byte unchanged.
  • A .. path is denied as path_traversal, and /work does not authorize /workshop.
  • A fetch to an allow-listed host works; a fetch to any other host is egress_denied and never appears in Net.calls (no exfil).
  • max_ops, max_output_bytes, and max_wall each deny with the right violation and a hard=True halt; a capability denial is hard=False (soft).
  • No denial ever raises; every denial is a SandboxResult(ok=False, violation=...).
  • SandboxedAgent.run_all stops on the first hard limit; a hostile sequence is contained.
  • All 26 tests pass under both lab and solution.

How this maps to the real stack

  • Capabilities is the OCI security config: --cap-drop=ALL, --read-only, --network=none + egress allow-list, --pids-limit, --memory, a seccomp profile. Least privilege, one flag at a time; frozen because dropped authority can't be re-acquired.
  • The fs_read/fs_write split models a mount namespace + read-only rootfs + a writable scratch mount. canonicalize-then-check is the userspace belt; pivot_root into the image is the kernel's suspenders.
  • The net_hosts allow-list models a network namespace + Kubernetes NetworkPolicy (default-deny egress). The egress check runs before the fetch — the anti-exfiltration guarantee.
  • max_ops/max_output_bytes/max_wall model cgroups (pids, memory, cpu) + an execution deadline; the hard halt models an OOM-kill / throttle. The injected clock is the same replay-safe seam as Phase 08.
  • The whole Sandbox is the model of a container / gVisor / Firecracker boundary; in prod you'd run untrusted agent code in an ephemeral Firecracker microVM (Vercel Sandbox, E2B) or a hardened gVisor pod — one per task, thrown away.

Limits of the miniature. This is an in-process model of a capability checknot a security boundary. Never run actually-untrusted code against it; real isolation is the kernel (namespaces/cgroups/seccomp) or a hypervisor (microVM), not a Python if. It also omits symlink/TOCTOU races, syscall filtering, and side-channels. What transfers is the shape of every control — check before effect, allow-list, fail closed, bound the budget — which is identical from this if up to Firecracker.

Extensions (your own machine)

  • Run it for real. Put the same eight ops behind a Docker container: --read-only with a --tmpfs /work/out, --network=none (or an egress proxy), --pids-limit 64, --memory 256m, --cap-drop=ALL, and a seccomp profile. Watch the OS enforce what your if modeled.
  • gVisor / microVM. Swap the runtime to runsc (gVisor), or run the snippet in E2B / Vercel Sandbox and compare cold-start latency and syscall behavior.
  • SSRF hardening. Extend Net to resolve hostnames to IPs and add link-local/RFC-1918 denial, so 169.254.169.254 (cloud metadata) and internal ranges are blocked even when a hostname slips through.
  • Symlink/TOCTOU. Add a symlink concept to VFS and show how a check-then-open race defeats canonicalize-then-check — then fix it by resolving symlinks inside the jail.

Interview / resume signal

"Built a capability-gated execution sandbox for agent tool/code calls — least-privilege allow-lists for filesystem, egress, and tools; canonicalize-then-check path-traversal defense; default-deny egress as the anti-exfiltration control; and cgroup-style resource budgets (ops/output/wall) with hard-halt semantics — modeling the container/microVM boundary (namespaces, cgroups, seccomp) so a hostile, injection-driven op sequence is contained with zero side effects."