run_command — funnels through Workspace._run_unlogged. A Sandbox transforms
that one call: it computes the environment the child runs under and (in the OS/
container backends) wraps the argv in a launcher that denies network and confines
writes. It never runs the command — the Workspace still does — so the seam
stays a single chokepoint and prepare() is a pure transform (design invariant #5).
Scope is Threat C only (ADR-0042): it closes runtime/substrate gaming — an
inherited PYTEST_ADDOPTS/PYTHONPATH, a phone-home, a fork bomb — not a model
that authors weak tests (Threat B) or guts the graded ones (Threat A). Workspace-
discovered config (conftest.py, pyproject addopts) is a file, not env, and is
out of scope here by decision (the adequacy/diff-scope lever, ADR-0040).
Backends, weakest→strongest, all behind the same prepare():
none— identity argv + the full inherited environment (today’s behavior; the back-compat escape hatch).hermetic-env— identity argv + an env scrubbed to a language-neutral allowlist, soPYTEST_ADDOPTS/NODE_OPTIONS/RUBYOPT/PYTHONPATH/CLASSPATHvanish by construction (no per-language whack-a-mole). The portable floor: pure Python, every OS, no dependencies. The default.sandbox-exec— the allowlisted env + argv wrapped in the macOS launcher with a network-deny profile (Apple-deprecated, an optional native fast-path).bwrap— Linux bubblewrap: the allowlisted env + a read-only root with only the workspace writable + a network namespace unshared (Increment 2).container— a Podman/Docker run: net-deny, read-only rootfs with the workspace bind-mounted writable, a clean env, kernel-enforced pid cap. The one genuinely cross-platform strong sandbox (Increment 2); needs the runtime + an image.
RLimits) ride ExecSpec.preexec_fn for the direct-exec backends
and container flags for the container backend. They ship off by default:
preexec_fn runs between fork and exec, which is not thread-safe against the
multithreaded eval runner (ADR-0026), so they are an opt-in toggle, not baked into
the default.
Classes
Bwrap
bwrap (Linux): a bubblewrap sandbox — read-only root, writable workspace, no network.
The Linux-native strong backend (ADR-0042 Increment 2). Binds the whole root read-only,
re-binds the workspace writable, mounts a fresh /tmp, and (unless network is allowed)
unshares the network namespace. The env is allowlist-scrubbed via the child’s inherited
environment (no --clearenv needed — subprocess sets it).
Args:
allow_network: When True, keep the network namespace (no --unshare-net).
rlimits: Optional resource ceilings applied via preexec_fn (opt-in; see RLimits).
Bwrap.prepare(self, argv: 'list[str]', cwd: 'Path') -> 'ExecSpec'
Wrap argv in bubblewrap with cwd the only writable path.
Args:
argv: The command’s argument vector.
cwd: The workspace root, re-bound writable inside the otherwise read-only sandbox.
Returns:
An ExecSpec whose argv is the bwrap invocation, under the scrubbed environment.
Container
container: a Podman/Docker run — the cross-platform strong sandbox (ADR-0042 Increment 2).
Net-denied by default, a read-only rootfs with the workspace bind-mounted writable at
/workspace, a fresh tmpfs /tmp, and a kernel-enforced pid cap. The guest env is the
image’s own toolchain plus only the portable vars forwarded via -e — the host PATH/
VIRTUAL_ENV are meaningless inside and are not passed. Needs the runtime on PATH and
an image carrying the task’s toolchain.
Args:
image: The container image (e.g. docker.io/library/python:3.12-slim).
runtime: The container CLI (podman or docker).
allow_network: When True, use --network bridge instead of --network none.
rlimits: Resource ceilings; the pid cap is always applied, CPU/FSIZE are guest-managed.
Container.prepare(self, argv: 'list[str]', cwd: 'Path') -> 'ExecSpec'
Wrap argv in a podman run (or docker run) with the workspace bind-mounted.
Args:
argv: The command’s argument vector, run as the container’s command.
cwd: The workspace root, bind-mounted read-write at /workspace.
Returns:
An ExecSpec whose argv is the container-runtime invocation; its own env is the
host environment (so the runtime CLI resolves), while the guest env is set via -e.
ExecSpec
What to exec and under what environment — the pure output of Sandbox.prepare.
A small spec rather than a bare (argv, env) tuple because resource limits can’t be
expressed as env: preexec_fn carries POSIX child setup for the backends that use it.
The Workspace executes this; the sandbox never does.
Args:
argv: The argument vector to actually exec (identity, or launcher-wrapped).
env: The complete environment the child runs under (never inherited implicitly).
preexec_fn: Optional POSIX child setup (resource limits); None for the env-only
backends, which keeps execution thread-safe (see RLimits).
HermeticEnv
hermetic-env: identity argv + an allowlist-scrubbed environment (the portable default).
Closes the env-injection sub-route of Threat C on every OS with no dependencies. It does
not deny network (no OS mechanism at this layer) — that needs sandbox-exec/bwrap/
container.
Args:
rlimits: Optional resource ceilings applied via preexec_fn (opt-in; see RLimits).
HermeticEnv.prepare(self, argv: 'list[str]', cwd: 'Path') -> 'ExecSpec'
Return argv unchanged under an allowlist-scrubbed environment.
Args:
argv: The command’s argument vector.
cwd: The working directory (unused — this backend transforms only the environment).
Returns:
An ExecSpec with argv verbatim and the environment filtered to the allowlist.
NoSandbox
none: identity — the full inherited environment, exactly today’s behavior (escape hatch).
NoSandbox.prepare(self, argv: 'list[str]', cwd: 'Path') -> 'ExecSpec'
Return argv unchanged under the full inherited environment.
Args:
argv: The command’s argument vector.
cwd: The working directory (unused — identity backend imposes no confinement).
Returns:
An ExecSpec with argv verbatim and the whole os.environ.
RLimits
POSIX resource ceilings a backend imposes on the child (ADR-0042 Increment 2).
Off by default (opt-in): applied via preexec_fn for the direct-exec backends, which
is not thread-safe between fork and exec against the multithreaded eval runner
(ADR-0026). Best-effort — a limit that cannot be lowered is skipped, never a crash.
Args:
cpu_seconds: RLIMIT_CPU ceiling (CPU-seconds) — a runaway loop is killed.
fsize_bytes: RLIMIT_FSIZE ceiling — a command cannot fill the disk.
pids: The container pid cap (--pids-limit) — a fork bomb cannot wedge the host.
RLimits.preexec(self) -> 'Callable[[], None] | None'
Return a child-setup callable that lowers CPU/FSIZE limits, or None off-POSIX.
Returns:
A no-arg callable for subprocess’ preexec_fn, or None when resource is
unavailable (non-POSIX).
Sandbox
Transforms a command invocation for hermetic execution (ADR-0042); never runs it.
Sandbox.prepare(self, argv: 'list[str]', cwd: 'Path') -> 'ExecSpec'
Return the ExecSpec the Workspace should exec for argv under cwd.
Args:
argv: The command’s argument vector (post shlex.split).
cwd: The workspace root the command runs in.
Returns:
The transformed ExecSpec (argv, environment, optional child setup).
SandboxExec
sandbox-exec (macOS): allowlisted env + argv wrapped in the launcher, network denied.
Increment 1 confines the network only; write-confinement is deferred to the container
backend (Increment 2), which does it cleanly with a read-only rootfs — macOS write
profiles are finicky and the launcher itself is Apple-deprecated (kept as a native
fast-path, never the sole backend, ADR-0042).
Args:
allow_network: When True, skip the wrap and run env-scrubbed only (network open).
rlimits: Optional resource ceilings applied via preexec_fn (opt-in; see RLimits).
SandboxExec.prepare(self, argv: 'list[str]', cwd: 'Path') -> 'ExecSpec'
Return the allowlisted env with argv wrapped in the launcher (unless network is allowed).
Args:
argv: The command’s argument vector.
cwd: The working directory (unused — Increment 1 confines network, not writes).
Returns:
An ExecSpec under the scrubbed environment: argv verbatim when network is allowed,
else wrapped in sandbox-exec with a network-deny profile.
Functions
hermetic_env(source: 'Mapping[str, str]') -> 'dict[str, str]'
Filter source to the language-neutral allowlist — the portability trick (ADR-0042).
Dropping everything unlisted removes env-based rigging for every language at once,
rather than blocklisting each runtime’s injection var. PATH/VIRTUAL_ENV survive so
legitimate verification (python -m pytest, py_compile) still resolves the interpreter.
Args:
source: The environment to filter (usually os.environ).
Returns:
A new dict holding only the allowlisted keys (exact names + LC_*).
make_sandbox(mode: 'str', *, allow_network: 'bool' = False, rlimits: 'RLimits | None' = None, image: 'str' = '', runtime: 'str' = 'podman') -> 'Sandbox'
Construct the Sandbox for a config sandbox_mode (ADR-0042).
Args:
mode: One of none / hermetic-env / sandbox-exec / bwrap / container.
allow_network: Passed to the OS/container backends; ignored by none/hermetic-env,
which cannot gate network at their layer.
rlimits: Optional resource ceilings (opt-in; ignored by none).
image: The container image — required when mode == "container".
runtime: The container CLI for mode == "container" (podman or docker).
Returns:
The matching Sandbox.
Raises:
ValueError: For an unknown mode, or container without an image.