Summary / 24. Execution sandbox / Document
Spec 2026-09-06
Shared copy: the operator's name and machine names are replaced by roles. Nothing else is changed.
Rendering…
# Build Spec 24 — Execution Sandbox (containing arbitrary generated code under a deterministic control plane with zero learned entries) > Derived from `papers/24-execution-sandbox.md` — "Containing Arbitrary Generated Code Under a Deterministic Control Plane with Zero Learned Entries." > This document drives the build of component 24: the **execution sandbox** — the boundary every one of the harness's > twenty-two stages must pass through the moment it stops being a plan and starts being *code that runs*: the candidate patch, > the synthesized test, the one-off probe script, the repaired fragment. By component 23 the harness knows, as a declared and > evidence-gated state machine, exactly *what order* to run its stages in. Component 24 is the thing that runs the `EXECUTE` > stage's payload and decides what that payload may do. Its central claim — made structural here — is that execution must be > treated as hostile by construction. The sandbox does **not** ask whether a piece of generated code is safe; it assumes both > the code **and the agent that generated it** are untrusted, and it derives — from the task, not from the model's intentions — > the minimum capabilities that task requires, enforcing every filesystem, process, network, device, tool, and secret access > through deterministic policy that lives entirely outside the model, tracking the provenance of sensitive data so that > individually legitimate operations cannot compose into prohibited flows, running candidate code in ephemeral least-privilege > environments physically separated from Piper's persistent memory and credentials, and destroying those environments after each > candidate or bounded repair episode. The one-sentence human form of the whole design, and the sentence that separates this > component from every other, is: **the sandbox should remain safe even if Piper deliberately tries to escape it — "Piper knows > she shouldn't" is not part of the threat model.** Throughout the harness the model is a *trusted collaborator* held to account > by deterministic gates; the sandbox cannot afford that trust, because it is guarding a *program*, and a program does not have > intentions. The invariant that governs the entire design, and that every later section is an elaboration of, is **the L column > contains at most one entry** — and at this component the invariant is sharpened to its limit. The entire sandbox — capability > derivation, policy enforcement, information-flow and provenance checks, isolation-tier selection, resource envelopes, and > side-effect observability — is deterministic software on CPU [D], holding **zero learned entries by construction**. The model > is **not a contributor** to the sandbox; it is the **threat** the sandbox contains. There is no "one learned entry" inside the > boundary, because the sandbox's whole job is to be the fixed, non-learning surface against which the learned thing is *tested*. > **An empty L column is a permitted, correct state** — and here it is the **only** correct state: a sandbox with any learned > entry in its enforcement path has already failed, because it has re-introduced the very entity — a model sampling from a > distribution that contains attack payloads — into the one place whose correctness must not be probabilistic. The only learned > elements anywhere in the system are **outside** the sandbox boundary: the harness's own model calls, which produce the > candidate code the sandbox then *contains*. Inside the boundary there is nothing to learn, and nothing that learns. The paper > assembles the evidence along five independent boundaries — authority (Progent, MiniScope), data flow (CaMeL, tacit), process > (Docker hardening, gVisor), machine (Firecracker), and language/runtime (WebAssembly/WASI) — traces the capability model's > lineage from Dennis & Van Horn through seL4, Capsicum, and revocable resource-and-effect capabilities (Lingering > Authority/PORTICO), and closes with the honest-measurement machinery (SandboxEval and hostile-fixture CI) that turns "does the > box actually hold" from a hope into a check. This spec pins the paper's design to exact primitives: the exact > `CapabilityManifest` schema (the ExecutionCapability envelope — `filesystem_read[]`, `filesystem_write[]`, > `network_destinations[]`, `devices[]`, `tools[]`, `secrets[]`, plus the `cpu` / `memory` / `wall_time` resource envelope), the > exact `PolicyRule` schema (Progent's `tool` + `arguments` + `effect`∈{allow,forbid} + `fallback_action`∈{terminate,notify,error}), > the exact `DataLabel` provenance tag (`source`, `sensitivity`, `allowed_sinks[]`), the exact `StructuredEvidence` record > (`exit_code`, `stdout_hash`, `stderr_hash`, `resource_counters`, `provenance_tagged_files_touched[]`, > `network_flows_denied[]`), the exact D/L invariant at its zero-learned extreme, the exact five boundaries with their > enforcement points, the exact four-tier escalation ladder with its deny-by-default tier-selection policy, the exact > denial-by-absence construction principle and revocation-by-expiry lifecycle, the exact honest-measurement protocol (51-property > adversarial suite + hostile-fixture CI + boundary CVE audit), and an exact build sequence. --- ## 1. Purpose & Scope Build a **deterministic containment boundary plus a zero-learned enforcement plane** — a software layer that, given a candidate artifact (a patch, a test, a probe script, a repaired fragment) and the workflow state's declared task (§23's state schema), decides *what that artifact may touch when it runs*, *which requests it may make*, *which flows sensitive data may take*, *which isolation tier it executes in*, *which resources are even present for it to reach*, *what structured evidence comes back out*, and *when the environment is destroyed* — all of it deterministic software on CPU, with no learned model anywhere inside the boundary. The scope is the **execution boundary only**. Components 01–22 own *what* each stage computes; component 23 owns *what order* the stages run in; this component owns *what the running code may do*, which is the one question where a wrong answer is not a quality defect but a containment failure. The answer it defends is the most conservative one available: **deny by absence, enforce by construction, and assume the payload is hostile.** The paper's discipline compresses into a handful of load-bearing moves that carry the entire design. **Hostile by construction**: the sandbox does not distinguish "injection," "echo," and "hallucination" from "correct code," and it must not need to — its guarantee is over the entire output distribution, not over the subset the model meant well. **Deny by default** is derived, not assumed: the control plane computes the *minimum* capability set a task requires (§3.1), and anything not in that set is not "blocked" — it is *absent* (§3.4). **Structured-evidence out, nothing else**: the only thing that crosses the boundary back into the harness is a bounded, hashed, provenance-tagged evidence record — never the sandbox's prose logs, never the model's narration of what happened. **Zero-L**: the sandbox is the one component whose learned column is empty *by construction*; the model is not a contributor but the threat. Three principles decide every design choice. **P1 The code is the threat; the control plane is the containment.** Every mechanism — the capability manifest, the policy engine, the provenance checker, the tier selector, the resource envelope, the evidence extractor — is deterministic software on CPU [D], and the model never appears in the enforcement path. **P2 The D/L invariant at its extreme.** The control plane holds zero learned entries by construction; **the L column contains at most one entry**, and here the bound is met at zero — **an empty L column is a permitted, correct state**, and it is the **only** correct state, because a learned entry in the enforcement path is the threat re-admitted to the one place its correctness must not be probabilistic. The only learned elements in the system are **outside** the boundary — the harness's model calls that emit the candidate code. **P3 Layered, not single-layer.** Isolation must be layered — five boundaries, four tiers — because each layer fails, and the failure of any one must not be the failure of the whole (CVE-2019-5736 is the standing proof that a single layer is a single point of assumption). The build lives in `build/24-execution-sandbox/`, writes nothing into the live harness, and is kept **only if** the sandbox strictly dominates its own absence — measured on the honest instruments of §9: escape attempts contained, prohibited flows denied, sensitive-data exposure blocked, resource-exhaustion survived, and a zero-leak structured-evidence extraction — A/B-tested against the uncontained baseline and against the intermediate ablations (no provenance checker; no denial-by-absence; no tier escalation; no ephemeral destruction). --- ## 2. Round 1 vs later rounds (toward the 100% target) The goal is the paper's full **100%** spec. **~80%** is only the expected **first-round landing**; later rounds climb from there. **BUILD-NOW = the Round 1 build** (rows 1–10); **DEFERRED = later rounds** (rows 11–13), **not excluded** but gated by the same §9 A/B protocol. Every later round of this component still builds within the same `build/24-execution-sandbox/` tree. | # | Component (paper §2/§3/§4/§5/§6) | Classification | |---|---|---| | 1 | Capability-manifest derivation — MiniScope least privilege, a pure function of the task producing the ExecutionCapability envelope (§3.1) | **BUILD-NOW** | | 2 | Policy engine — Progent ALLOW/FORBID + Fallback (terminate/notify/error) over tool names and arguments (§3.2) | **BUILD-NOW** | | 3 | Provenance / information-flow checker — DataLabel tags + CaMeL-style flow denial + tacit-style purity at sinks (§3.3) | **BUILD-NOW** | | 4 | Denial-by-absence construction — the manifest as a *build recipe*, not a blacklist (§3.4) | **BUILD-NOW** | | 5 | Isolation-tier selector + profiles — deny-by-default, least-privilege tier choice (§5) | **BUILD-NOW** | | 6 | Tier backend: hardened container — capability dropping + seccomp + user namespaces + read-only root (§5.1) | **BUILD-NOW** | | 7 | Tier backend: Wasm/WASI — wasmtime + WASI import allowlisting (§5.4) | **BUILD-NOW** | | 8 | Structured-evidence extraction — the `StructuredEvidence` record, the only residue that crosses back (§6) | **BUILD-NOW** | | 9 | Destroy / ephemeral lifecycle — per-candidate teardown, revocation-by-expiry (§5.3, §3.4) | **BUILD-NOW** | | 10 | SandboxEval subset + hostile-fixture CI — the adversarial suite's property families plus fork-bomb/egress/secret-grep/injected-instruction fixtures (§9) | **BUILD-NOW** | | 11 | Boundary CVE audit loop — runtime/kernel advisory tracking, tier-exposure recomputation (§9.3) | **BUILD-NOW** | | 12 | Tier backend: gVisor (`runsc`) — userspace kernel, ~53/68 allowlisted syscalls (§5.2) | **DEFERRED** — requires installing the `runsc` OCI runtime (72h-age gate + scrutiny); Round 1 ships the hardened container + Wasm tiers, and adds gVisor only when a workload needs a userspace kernel without a VM | | 13 | Tier backend: Firecracker microVM — machine boundary via KVM (§5.3) | **DEFERRED** — requires KVM and host-level privileges the Round 1 host may not grant; reserved for "arbitrary hostile native execution" and adopted when the harness actually generates native hostile payloads | **The cost rule (explicit).** The L column holds **zero** entries in this component — not one, not "at most one," zero. No learned model is consulted at any point in the enforcement path: not to derive a manifest, not to evaluate a policy rule, not to judge a flow, not to select a tier, not to decide a resource budget, not to read an evidence record. The model's entire presence is **outside** the boundary: it emits the candidate code (§1) that the sandbox then contains. The DEFERRED rows (gVisor, Firecracker) must clear the same bar as in every other component: strictly dominate the deterministic Round 1 tiers on §9's measures — specifically, contain a payload the Round 1 tiers demonstrably fail to contain — at a defensible cost, or be rejected. **Design decision: ZERO-L, DETERMINISTIC-ONLY.** Round 1 puts no model anywhere in the sandbox, inside or at the edge. The full deterministic control plane (rows 1–5) plus the two Round 1 tier backends (rows 6–7) plus the evidence/lifecycle machinery (rows 8–9) and the honest-measurement rig (rows 10–11) is the build. There is no L slot to fill. A sandbox that *would* need a learned entry has already failed, and Round 1 is constructed so that need never arises. --- ## 3. The Deterministic Control Plane (D) The control plane is the software that decides, before and during execution, what the running code is *allowed to do* — and it is, by construction, entirely outside the model. It has four parts: capability derivation (what the task needs), policy enforcement (what the request may be), information-flow and provenance (what the data may do), and denial-by-absence (what is never present to be asked for). Recapitulating the invariant at the architecture level, because this is the section it governs: > **the L column contains at most one entry.** The entire control plane — capability derivation, the policy engine, the information-flow and provenance checks, the isolation-tier selector, the resource envelopes, and side-effect observability — is deterministic software on CPU [D], holding **zero learned entries by construction**, and here it is exactly **zero** — **an empty L column is a permitted, correct state**, and here it is the **only** correct state. Concretely, the control plane's pipeline is: ``` CANDIDATE ARTIFACT + DECLARED TASK enter → capability-manifest derivation [D] → ExecutionCapability envelope from the task, never the model (§3.1) → policy engine [D] → ALLOW/FORBID + Fallback over every tool/operation request (§3.2) → provenance / flow checker [D] → DataLabel tags; a prohibited flow is denied, never negotiated (§3.3) → denial-by-absence construction [D] → the environment is *built* with exactly the manifest's resources (§3.4) → isolation-tier selector [D] → deny-by-default least-privilege tier, escalating by capability (§5) → structured-evidence extraction [D] → the hashed, bounded residue; the only thing that crosses back (§6) → destroy / ephemeral lifecycle [D] → teardown per candidate; revocation-by-expiry (§5.3, §3.4) ``` The model never derives a capability, never evaluates a policy rule, never judges a flow, never selects a tier, never sets a resource budget, and never reads the evidence. Every function in this section is deterministic; the L column contributes nothing here. All artifacts land in `build/24-execution-sandbox/`. ### 3.1 Capability derivation: MiniScope least privilege [D] The control plane's first act is to *derive* the minimum capability set for a task, because "minimum" cannot be assumed; it must be computed. **MiniScope** [7] is the prior that makes this concrete: given a tool surface, it reconstructs the permission hierarchy and grants a task exactly the permissions its declared tools require — no more — with **1–6% latency overhead** across 10 applications, which is the number that licenses treating least privilege as the *default* rather than an opt-in. In the harness's terms, capability derivation takes the workflow state's declared task (§23's state schema) and produces a **capability manifest**: the set of filesystem paths, network endpoints, devices, tools, and secrets that the candidate code for *this* task may touch. Anything not in the manifest is not merely "blocked" — it is *absent* (§3.4). The derivation is a **pure function of the task, not of the model**, so the same task always derives the same manifest, and the manifest can be audited, diffed, and regression-tested like any other artifact. Its signature, in effect, is `task → manifest`, with no learned term in the map. The manifest's exact shape — the `ExecutionCapability` envelope — is pinned in §6.1. ### 3.2 The policy engine: Progent ALLOW/FORBID + Fallback [D] Given a manifest, the policy engine holds every request to it. **Progent** [6] supplies the enforcement discipline: symbolic rules over **tool names and arguments**, with exactly two rule effects — **ALLOW** and **FORBID** — and a **Fallback action** (terminate, notify the user, or return an error message, the last being the default) for requests the rules do not decide. The word to insist on is *symbolic*: Progent's SMT-solved monotonic narrowing and expansion means the allowed region is a *computed* region, and a request either lies inside it (allowed) or outside it (forbidden or fallback) — never in an interpretive gray zone the model resolves. On AgentDojo its enforcement cuts the attack success rate (ASR) from **39.9% to 1.0%** and the attack success with jailbreak (ASB) from **70.3% to 3.9%** — the teeth of the boundary, measured. Mapped onto this harness, the policy engine is the deterministic gate that every tool invocation and every filesystem/network/device access passes through, comparing the requested operation and arguments against the capability manifest. It runs *outside* the model, on the same authority that evaluates the workflow's gate predicates (§23), and a forbidden request is not "helpfully reinterpreted," not "routed to a safer tool," not "asked about" — it is refused, and the refusal is an observable event the workflow can react to. The exact `PolicyRule` record is pinned in §6.2. ### 3.3 Information-flow and provenance: CaMeL and tacit [D] The authority boundary (§3.2) authorizes actions; the data-flow boundary makes sure authorized actions cannot *compose* into prohibited flows. This is the layer that catches the classic exfiltration — read a secret, write it to the network — where each step is individually legitimate. Authority-per-action is blind to this; only *provenance tracking* can see the composition. **CaMeL** [8] provides the runtime form: a **privileged planner** separated from a **policy-enforcing interpreter**, with **capability-tagged data flow** so that a value whose provenance is a sensitive source can never reach an unauthorized sink. The interpreter, not the model, decides each flow, which is why CaMeL's guarantee is *provable* (77% of AgentDojo solved vs 84% undefended) rather than statistical. **tacit** [9] provides the compile-time form: **Scala 3 capture checking** tracks which capabilities each function may exercise, and a **`Classified[T]`** type whose `map` accepts **only pure functions** (empty capture set) means a sensitive value can be transformed in place but never released — **100% security across 131 trials** in classified mode for both Sonnet 4.6 and MiniMax M2.5. The two are the same invariant at two levels of the stack: *a tainted or sensitive value may be processed, but its flow out of the boundary is a deterministic decision, never the model's.* In the harness, provenance tags (the `DataLabel` of §6.3) ride on every value the sandbox produces and consumes; the flow checker is software on CPU [D], and an attempted prohibited flow is an event, not a negotiation. ### 3.4 Denial by absence [D] The last element closes the loop the other three leave open: a resource that is not *denied* but *absent* cannot be reached by any sequence of authorized actions, because there is no handle to it at all. **Deny resources by absence rather than instruction.** A secret that is never mounted into the sandbox cannot be read, however clever the request; a network namespace with no routes cannot egress, however plausible the code; a device node that does not exist cannot be opened. This is the difference between a *filter* (which sits in the path and can be bypassed or misconfigured) and a *construction* (which never puts the resource in the environment to begin with). The capability manifest of §3.1 is therefore not a blacklist to be enforced so much as a **build recipe**: the sandbox is constructed with exactly the resources the manifest names, and everything else is simply not there. This is the mechanism that makes the sandbox's guarantee *structural* rather than *behavioral* — and it is the reason the D/L invariant can be stated as zero rather than one: absence is enforced by what was built, not by anything that runs. The ephemeral-destruction and revocation-by-expiry requirements that complete this principle are specified in §8. --- ## 4. The Five Boundaries (the layered structure) A sandbox is not one mechanism but a *set* of boundaries, each answering a different question about what a piece of running code can do. They are frequently conflated into "the sandbox"; keeping them distinct is the first act of design, because each has a different failure mode, a different guarantee, and a different point in the pipeline where it is enforced. They are layered and complementary — no one of them is sufficient, and dropping any one leaves a hole the other four do not cover. A hostile program must lose at every one of them: | Boundary | The question it answers | The machinery (paper §) | Enforcement point | Fails on | |---|---|---|---|---| | **Authority** | *May the agent request this action with these arguments?* | Progent ALLOW/FORBID + Fallback [6]; MiniScope least-privilege derivation [7] (§2.1) | Before anything runs — the policy engine (§3.2) | Tool/argument requests that carry authority | | **Data-flow** | *May this value, with this provenance, flow to this sink?* | CaMeL policy-enforcing interpreter [8]; tacit capture checking + `Classified[T]` [9] (§2.2) | During execution — the flow checker (§3.3) | Confused-deputy / exfiltration composition | | **Process** | *May this process touch this file, socket, or device?* | capability dropping, seccomp, user namespaces, read-only root; Landlock (§2.3) | In the kernel | A process reaching outside its granted view | | **Machine** | *May this code share a kernel with Piper at all?* | Firecracker microVM / KVM [11] (§2.4) | In the hypervisor | A kernel-level escape that defeats the process rung | | **Runtime** | *Is this program's semantics small enough to verify?* | Wasm/WASI; vWasm/rWasm verified compiler [12] (§2.5) | In the compiler | Any construct the environment's kernel or hypervisor cannot express | Each is enforced by a different piece of the stack, so no single compromise — a prompt injection, a confused deputy, a kernel bug, a hypervisor bug, a compiler bug — defeats the whole. The assignment of machinery to sections is fixed: **§3 owns the authority and data-flow boundaries** (the deterministic control plane), **§5 owns the process and machine boundaries** (the isolation tiers), and **the capability model of §7 owns the runtime boundary's conceptual foundation** (the capability lineage that makes WASI import allowlisting and denial-by-absence one coherent story). ### 4.1 Authority — the request itself carries the danger The outermost boundary constrains not what a process can *touch* but what it can *ask for*. An agent with a tool surface — read file, run command, push commit, send message — is dangerous before any bytes are written, because the *request itself* carries authority. **Progent** [6] answers "may the agent even *ask* for this tool with these arguments" in software; **MiniScope** [7] solves the harder upstream problem of *what is the least privilege this task actually needs*. The authority boundary is what makes "trusted agent, untrusted inputs" the *wrong* frame: injection and echo payloads arrive through the tool request itself, and Progent and MiniScope treat the request as data to be authorized, never as an intention to be honored. ### 4.2 Data-flow — legitimate actions composing into prohibited flows A program that is allowed to read a file and allowed to make a network call is not dangerous until it does both with the same data — the classic confused-deputy and exfiltration pattern. Authority-per-action is blind to this because each action is individually legitimate; only *provenance tracking* sees the composition. CaMeL's runtime provenance and tacit's compile-time capture checking are the same idea at two levels, and both answer "where may sensitive information flow" with a *deterministic* answer — which is exactly why they belong inside the control plane of §3 rather than in the model's conscience. ### 4.3 Process — the classical kernel-mediated rung Below the request and the flow sits the *process* — the actual Unix process that will execute the generated code. This boundary is the classical one: limit what the process can see and touch through the operating system's own facilities — Linux capabilities, seccomp filtering, user namespaces, a read-only root filesystem, and Landlock. These are **practitioner mechanisms, not papers** — seccomp is a Linux kernel feature (2005, with the BPF-filtered form in 2012), Landlock merged in Linux 5.13 (2021), and the discipline itself is Docker/OCI configuration. The posture to carry forward is **deny by default**: the process starts with nothing and receives only what the task's derived capability set (§3.1) grants it — no ambient filesystem, no ambient network, no device nodes, no secrets in the environment. ### 4.4 Machine — hardware-backed isolation When the generated code is *arbitrarily hostile native code* — a fork-bomb, a kernel-exploit attempt, a rootkit — no amount of in-process filtering is a real guarantee, because the process shares a kernel with Piper. The machine boundary gives the hostile code its own *virtual machine*, backed by the hypervisor, so that "escape the process" requires "escape the hypervisor," a categorically harder problem. **Firecracker** [11] is the canonical instance (its numbers are in §5.3). A machine boundary is the fallback for the cases the process boundary cannot name. ### 4.5 Runtime — verified and capability-oriented execution The innermost boundary constrains the *language* the code is written in. If generated code is compiled to a sandboxed intermediate representation whose semantics are small enough to verify, then "what can this code do" becomes a property of the compiler rather than of the environment. **WebAssembly and WASI** are the practical instance, and Bosamiya, Lim, and Parno's vWasm/rWasm [12] make the "provably" honest (numbers in §5.4). WASI's **import allowlisting** means a module can reach *only* the host capabilities it was explicitly given — the capability model applied at the module boundary. --- ## 5. The Four Isolation Tiers (the escalating ladder) The control plane decides what is *allowed*; the isolation tiers decide what is *physically reachable*. They are an escalating ladder, each rung trading overhead for a categorically stronger guarantee, and the sandbox climbs it according to how hostile the code is assumed to be. The ladder is also the answer to a specific historical failure that proves single-layer isolation is insufficient. | Tier | Rung | Guarantee | Cost (measured) | Assumed hostility | |---|---|---|---|---| | 1 | **Hardened container** | Capability dropping, seccomp, user namespaces, read-only root | Millisecond boot; lowest overhead | Untrusted enough to filter, not hostile enough to jailbreak a kernel | | 2 | **gVisor (`runsc`)** | Userspace kernel; ~53 (no host net) / ~68 (with host net) allowlisted syscalls | ~2.2× simple syscalls, ~2.8× vs native; open/close on external tmpfs ~216× | Untrusted enough to justify a userspace kernel | | 3 | **Firecracker microVM** | Own kernel, KVM hypervisor; "escape" = "escape the hypervisor" | <5 MiB overhead, 125 ms boot, 150 microVMs/sec/host | Arbitrary hostile native execution (the default assumption) | | 4 | **Wasm/WASI** | Verified runtime; import allowlisting; small verifiable semantics | ~0.2% sandboxing-pass cost; near-native | Any task that fits WebAssembly's model — *descends* back down in cost | ### 5.1 Hardened containers — the cheap first rung The first rung is a standard container with the operating system's own defenses turned up to maximum: **capability dropping** (start with none, add back only what the task needs), **seccomp** filtering (restrict the syscall surface), **user namespaces** (map the container's root to an unprivileged host user), and a **read-only root filesystem**. These are practitioner mechanisms, not papers, and they appear here as engineering context. Their value is cost: a hardened container boots in milliseconds and carries the lowest overhead of any tier. Their limit is the one §5.5 makes vivid: they all *share the host kernel*, so a kernel-level escape is an escape from all of them at once. ### 5.2 gVisor — a userspace kernel, measured The second rung addresses the shared-kernel problem without giving up container speed, by moving the kernel boundary into userspace. gVisor is Google's **application kernel** — a userspace kernel (with the `runsc` OCI runtime) that intercepts the process's syscalls and re-implements them against a small, constrained host surface, so a process running under gVisor talks to the *userspace* kernel, not the host kernel, for most operations. The citable research is Young et al., *The True Cost of Containing* [10], **USENIX HotCloud '19** (UW-Madison). Its measured numbers are the ones this paper relies on: gVisor exposes **~53 allowlisted syscalls** (no host networking) or **~68** (with host networking), against **~306 for LXC** and **~36 for Firecracker** — deny-by-default with a small allowlisted surface. The cost is structural to the userspace-kernel design: simple syscalls run **~2.2× slower** than a standard container (about 2.8× vs native in the fastest configuration), but syscall-heavy workloads degrade far worse — open/close on an external tmpfs reaches **~216×**. The lesson is a tradeoff: gVisor buys a much smaller, constrained syscall surface at a steep per-syscall price, so it is the right rung when the code is untrusted *enough* to justify a userspace kernel but *not* hostile enough to justify a virtual machine. ### 5.3 Firecracker — the machine boundary The third rung is the one that stops assuming good faith in the *kernel*. **Firecracker** [11] (Agache et al., NSDI 2020) is the AWS microVM manager, and its numbers make per-task virtual machines the practical default rather than a luxury: **~50K lines of Rust** (~96% fewer than QEMU), **less than 5 MiB** memory overhead per microVM, boot in **as little as 125 ms**, **up to 150 microVMs per second per host**, and it is the engine under **Lambda and Fargate**. A hostile native program in a Firecracker microVM shares nothing with Piper but the hypervisor: its filesystem, network, devices, and secrets are all whatever the harness constructed for it (§3.4), and "escape the sandbox" now means "escape KVM," a categorically harder and better-audited target. This is the rung the thesis reserves for "arbitrary hostile native execution": when the code is assumed hostile — as this component assumes by default — the machine boundary is the one that makes the assumption *safe* to hold. ### 5.4 Wasm/WASI — the verified runtime boundary The fourth rung inverts the question. Instead of giving hostile code a weaker environment, it gives it a *safer language*: compile the generated code to WebAssembly and constrain it with WASI, so that "what can this code do" is a property of a small, verifiable semantics rather than of a large, shared kernel. **Bosamiya, Lim, and Parno** [12] (USENIX Security 2022, **Distinguished Paper Award**, plus the Internet Defense Prize, tied 2nd) provide the proof: **vWasm** (an F\*-verified Wasm-to-native compiler) and **rWasm** (a safe-Rust runtime, `#![forbid(unsafe)]`, `#![no_std]`) — and the sandboxing pass itself costs **only ~0.2%** runtime, with the slowdown living almost entirely in the unverified optimization portion. WASI's **import allowlisting** supplies the capability side: a module reaches only the host capabilities it was explicitly handed. This is the "lightweight verified or capability-oriented runtime": where the generated code's task fits inside WebAssembly's model, the sandbox can offer a *verified* guarantee at near-native cost — a strictly better deal than any filter, and the reason the ladder *descends* back down in cost at its top rung. ### 5.5 Why single-layer isolation fails — CVE-2019-5736 The ladder exists because of a specific, documented failure of believing one layer was enough. **CVE-2019-5736** is a runc container-escape vulnerability — cited here as a **CVE/advisory, not a paper** — in which a process *inside* a container, with root-in-container, could overwrite the host's `runc` binary via `/proc/self/exe` file-descriptor mishandling and achieve host root code execution. It affected runc through 1.0-rc6 (Docker < 18.09.2, containerd, CRI-O), carried a CVSS score around 8.6 (High), and the same flaw existed in LXC. The lesson is not "containers are unsafe"; it is that **every layer assumes the one beneath it**, and a single layer is a single point of assumption. The rung's defenses — capability dropping, seccomp, user namespaces — all shared the host kernel and the `runc` entry path, so one kernel-adjacent bug defeated the whole rung. A machine boundary would have contained the escape at the hypervisor; a runtime boundary (Wasm, where `/proc/self/exe` does not exist) would have made the attack unexpressible. The five boundaries of §4 and the four rungs of this section are the same conclusion stated twice: **isolation must be layered, because each layer fails, and the failure of any one must not be the failure of the whole.** ### 5.6 The tier-selection policy (deny-by-default, least-privilege profile) The selector is deterministic software [D] that maps `(task, derived-capability-set, assumed-hostility)` to a tier and a *profile*. Its policy is two clauses. **Deny-by-default:** the selector defaults to the **most restrictive tier that can host the task's derived capability set** — a task whose capability manifest fits WebAssembly's model runs under Wasm/WASI; only a task that *requires* native or privileged capabilities the safer tiers cannot express escalates. **Least-privilege profile selection:** within a tier, the profile is the smallest one that grants exactly the manifest's resources (§3.4) — the minimum syscall surface for a container, the minimum import set for a Wasm module, the minimum device/network set for a microVM. The escalation is one-directional and evidence-driven: a task escalates from Wasm to container, or container to gVisor to microVM, only when a concrete capability in its manifest is inexpressible at the lower rung, never because the model *preferred* a looser environment. An escalation is a logged event, auditable like any other control-plane decision. --- ## 6. The Core Schemas (the build's JSON contracts) The following schemas are the load-bearing contracts of the build. Their shapes are EXACTLY these fields, no more and no fewer, and the label "EXACTLY these fields" is itself part of the contract — a field added for convenience is a spec violation. ### 6.1 The CapabilityManifest record (EXACTLY these fields) — the ExecutionCapability envelope The paper's capability derivation (§3.1) — "the set of filesystem paths, network endpoints, devices, tools, and secrets that the candidate code for *this* task may touch" — plus the resource envelopes the control plane enforces, compiles to exactly these nine fields. The first six are the **capability grant** (what may be reached); the last three are the **resource envelope** (the bounds the sandbox enforces on CPU, memory, and wall-clock time). Together they are the **ExecutionCapability envelope**. ```json CapabilityManifest { filesystem_read: string[] // absolute paths the candidate may read; anything not listed is absent (§3.4) filesystem_write: string[] // absolute paths the candidate may write; anything not listed is absent network_destinations: string[] // host:port / CIDR endpoints the candidate may reach; empty = no egress at all devices: string[] // device nodes constructed into the environment (e.g. [] or ["/dev/null"]) tools: string[] // tool names the candidate may invoke (the least-privilege tool surface) secrets: string[] // opaque secret HANDLES injected at call time, never values (M1 — secrets never enter model context) cpu: float // CPU budget, in cores (the resource envelope's compute bound) memory: integer // memory budget, in bytes (the resource envelope's RAM bound) wall_time: integer // wall-clock budget, in seconds (the resource envelope's time bound) } ``` The load-bearing decisions, enforced not advisory: - **`filesystem_read` / `filesystem_write` / `devices` are build recipes, not blacklists.** The environment is constructed with exactly these paths and nodes mounted; everything else is absent (§3.4). A path outside the manifest has no handle to reach. - **`network_destinations` is closed.** An empty list means the network namespace has no routes — egress is impossible by construction, not by a filter that might be misconfigured. - **`tools` is the least-privilege tool surface.** It is the derived permission set of §3.1 (MiniScope), and a request for a tool off the list is refused by the policy engine (§3.2), never routed. - **`secrets` holds handles, never values.** A secret is injected by the tool layer at call time; the manifest names which handles exist, never their contents, so a secret is present only as an opaque reference the sandbox cannot print (§4.5 of the ruleset: M1). - **`cpu` / `memory` / `wall_time` are the resource envelope.** Exceeding any bound is a resource-exhaustion event (§9.1), terminated deterministically — never a negotiation with the model. ### 6.2 The PolicyRule record (EXACTLY these fields) The policy engine of §3.2 is Progent's discipline made exact: a symbolic rule over a tool name and its arguments, with exactly two effects and a fallback for the undecided case. The paper names the vocabulary precisely — there is no "substitute" and no "deny" keyword; the vocabulary is allow/forbid with a fallback. ```json PolicyRule { tool: string // the tool (or operation) name this rule constrains (Progent's "tool names") arguments: object // the symbolic argument constraints this rule applies to (Progent's "arguments") effect: enum // {allow, forbid} — the rule's Effect field; there is no third value fallback_action: enum // {terminate, notify, error} — taken when the policy cannot decide; default = error } ``` - **`effect` is a closed two-valued enum.** A request is either inside the allowed region (`allow`) or outside it (`forbid`); there is no interpretive gray zone, and the allowed region is *computed* (SMT monotonic narrowing/expansion), not heuristic. - **`fallback_action` carries exactly the three forms the paper names** — terminate, notify the user, return an error message — with `error` the default. It fires only when the rules do not decide; it never fires because the model was consulted. - **Refusal is observable.** A `forbid` or a fallback is an event the workflow's gate predicates (§23) can react to — it is never "helpfully" reinterpreted, never routed to a safer tool, never asked about. ### 6.3 The DataLabel record (EXACTLY these fields) — the provenance tag The provenance machinery of §3.3 (CaMeL's capability-tagged flow, tacit's `Classified[T]`) rides on a tag attached to every value the sandbox produces and consumes. The paper's "a value whose provenance is a sensitive source" compiles to exactly these three fields. ```json DataLabel { source: string // the provenance origin of the value (e.g. "repo:README", "task:spec", "env:secret") sensitivity: enum // {public, sensitive} — the provenance-derived label (the paper's "sensitive source" vs not) allowed_sinks: string[] // the sinks this value is permitted to flow to; a flow to any sink not listed is denied } ``` - **`source` is the provenance, not the content.** The label records *where the value came from*, so an untrusted source taints the value even if the value itself looks benign. - **`sensitivity` is the provenance-derived level.** A value from a sensitive source is labeled `sensitive` regardless of what it contains; a `sensitive` value may be *transformed* (tacit's `map` over pure functions) but never *released* to an unauthorized sink. - **`allowed_sinks` is a closed set.** The flow checker (§3.3) denies — deterministically, as an event — any flow to a sink not in the list. An individually legitimate read and an individually legitimate write compose into a denied flow the moment the sensitive value reaches a sink outside its allowance. ### 6.4 The StructuredEvidence record (EXACTLY these fields) The paper's extraction discipline — "extract only structured evidence" — names the record explicitly: "exit code, stdout/stderr bounded and hashed, resource counters, provenance-tagged files touched, network flows denied." That compiles to exactly these six fields, and this record is the **only** thing that crosses the boundary back into the harness. ```json StructuredEvidence { exit_code: integer // the candidate process's exit code stdout_hash: string // bounded stdout, hashed — raw text never crosses back stderr_hash: string // bounded stderr, hashed — raw text never crosses back resource_counters: object // CPU time, peak memory, bytes read/written (the resource envelope's actuals) provenance_tagged_files_touched: string[] // the DataLabel-tagged files the candidate touched, with their labels network_flows_denied: string[] // the flows the flow checker denied during execution (each an event) } ``` - **`stdout_hash` / `stderr_hash` are bounded and hashed.** The raw streams are truncated to a bound and hashed; the hash is the only residue. The sandbox's output is not its logs in prose, not the model's narration — it is this record. - **`resource_counters` are the envelope's actuals**, measured by the kernel/hypervisor/runtime, never self-reported by the code. - **`network_flows_denied` is the flow-checker's audit trail.** Each denied flow is an event with its DataLabel and sink; a non-empty list is itself evidence the sandbox held. - **This record is the GoEX discipline [22] made structural:** undo semantics (the environment is annihilated) and post-facto validation (this record is the only surviving residue by which any action can be checked after the fact). --- ## 7. The L Column — zero learned entries (the threat is not a contributor) Every element of this component is classified into one of two columns: **[D]** — deterministic software running on CPU — or **[L]** — a learned model. The structural invariant that governs the entire design is: > **the L column contains at most one entry.** And at this component the invariant is sharpened to its limit. The entire sandbox — capability derivation, policy enforcement, information-flow and provenance checks, isolation-tier selection, resource envelopes, and side-effect observability — is deterministic software on CPU [D], with **zero learned entries by construction**. The model is **not a contributor** to the sandbox; it is the **threat** the sandbox contains. There is no "one learned entry" inside the boundary, because the sandbox's whole job is to be the fixed, non-learning surface against which the learned thing is *tested*. **An empty L column is a permitted, correct state** — and here it is the **only** correct state: a sandbox with any learned entry in its enforcement path has already failed, because it has re-introduced the very entity — a model sampling from a distribution that contains attack payloads — into the one place whose correctness must not be probabilistic. The only learned elements anywhere in the system are **outside** the sandbox boundary: the harness's own model calls, which produce the candidate code the sandbox then *contains*. Inside the boundary there is nothing to learn, and nothing that learns. This is a stronger claim than the sibling components make, and it is the load-bearing one. Component 23 could afford exactly one learned entry — the semantic judgment inside a state — because a state's judgment could be gated by deterministic evidence before and after. The sandbox has no "after" to gate with: once hostile code has executed, the harm is done. The only place the guarantee can live is in the deterministic software that runs *before and during* execution. Hence zero, not one. ### 7.1 Why the model's output distribution includes attack payloads The claim that generated code must be treated as arbitrary is a reading of the measurement, not paranoia. Three directions converge on one point: *the distribution the model samples from contains, at non-negligible mass, outputs that are indistinguishable from attacks.* First, the tool surface is the attack surface: the MCP Safety Audit [1] showed the Model Context Protocol "allows major security exploits," and MCPTox [2] showed real-world MCP servers are themselves poisonable. Second, small models are already capable attackers: Probst, Happe, and Cito [3] found guidance raises Llama3.1 8B and Qwen2.5 7B to **67%** and Llama3.1 70B to **83%** on Linux privilege-escalation — ordinary procedures a model Piper's own harness routinely drives is already capable of producing. Third, the threat surface is broad and catalogued: Ling et al. [4] synthesize 247 papers on LLM-agent security, and Pirch et al. [5] supply the posture this paper adopts wholesale — securing AI agents *like operating systems*. Three failure modes produce three classes of payload, and each must be caught by a *mechanism*, not by a plea: **Injection** (an untrusted instruction in external text makes the model *faithfully* emit an attack), **Echo** (the model replays hostile content it was shown, functioning correctly), and **Hallucination** (the model emits a plausible, confident, *wrong* program — the most common case, because every generated program is, until executed, indistinguishable from a hallucination). The three share one property that dictates the architecture: **containment must hold for arbitrary generated text.** The sandbox cannot distinguish "injection," "echo," and "hallucination" from "correct code," and it must not need to. That is what it means for the L column to be empty here: the thing being contained is exactly the thing that, everywhere else in the harness, is the trusted learned entry. ### 7.2 Zero, not one — recapitulated at the architecture level Recapitulating the invariant at the architecture level, because this is the section it governs: > **the L column contains at most one entry.** The entire control plane — capability derivation, the policy engine, the information-flow and provenance checks, the isolation-tier selector, the resource envelopes, and side-effect observability — is deterministic software on CPU [D], holding **zero learned entries by construction**. The model is **not a contributor** to this plane; it is the **threat** the plane contains. **An empty L column is a permitted, correct state** — and here it is the **only** correct state: a control plane with any learned entry in its enforcement path has already failed, because it has put a sample from a distribution that contains attack payloads in charge of deciding what the payload may do. The only learned elements in the system are **outside** the boundary — the harness's own model calls, which emit the candidate code — and **inside** the boundary there is nothing to learn. ### 7.3 The capability model's lineage (the runtime boundary's foundation) The runtime boundary of §5.4 and the provenance machinery of §3.3 both rest on a single idea that predates every component of this harness by six decades: the **capability** — a token that *is* the authority to use a resource, rather than a name that must be checked against a separate access-control list. Dennis and Van Horn's *Programming Semantics for Multiprogrammed Computations* [13] (CACM Vol. 9, No. 3, 1966) defined the object, the capability, and the domain — and, decisively, **confinement** and **revocation**. Confinement is the property the sandbox needs *named*: a confined computation can reach only what its capabilities name. Revocation is the property §8 returns to: a capability is a thing that can be *taken back*. The lineage runs through **seL4** [14] (the capability model pushed to a formally verified microkernel), **Capsicum** [15] ("practical capabilities for UNIX" — capability mode and capability-based file descriptors), and **WASI import allowlisting** (a module's reach is its explicitly declared imports). Levy's *Capability-Based Computer Systems* [16] is the book-length survey; **Janus** [17] is the historical *ptrace*-based filter (the ancestor of the filter approach — deliberately *not* favored over the construction approach of §3.4). Every mechanism in this build — the capability manifest of §3.1, the provenance tags of §3.3, the WASI imports of §5.4, the revocable handles of §8 — is an elaboration of this 1966 definition. ### 7.4 Revocation as first class — Lingering Authority (PORTICO) The capability model's classic weakness is **lingering authority**: a capability granted for one episode that outlives the episode and can be replayed later. The sandbox's ephemerality requirement — *destroy the execution environment after each candidate or bounded repair episode* — is precisely a demand that no authority linger. **Lingering Authority: Revocable Resource-and-Effect Capabilities for Coding Agents**, by Santos-Grueiro [18] (arXiv:2606.22504, June 2026, single-author preprint; the system is named **PORTICO**), makes revocation a *first-class* capability property: capabilities carry **resource and effect scopes**, and they are **revocable mid-session** — episode-bound handles whose closure revokes them, so a stale handle replayed after the episode is rejected, not honored. Two flags attach honestly: the paper is fresh, single-author, and unrefereed (cited for its *mechanism*, confirmed from the abstract), and the claim sometimes attached to it — "grounded in Saltzer & Schroeder" — is **not in the abstract** and is flagged unverified. The revocation *property itself* is not in doubt, and it is exactly what the "destroy the environment" clause needs: authority is not merely narrowed but *expires*, and a capability that has been revoked is as absent as a resource that was never mounted (§3.4). **Revocation-by-expiry is denial-by-absence applied to time.** --- ## 8. Deny by Absence + Ephemeral Lifecycle (construction, not filter; revocation-by-expiry) ### 8.1 Construction, not filter The loop the other control-plane parts leave open is closed by a single principle: **deny resources by absence rather than instruction.** A secret that is never mounted into the sandbox cannot be read, however clever the request; a network namespace with no routes cannot egress, however plausible the code; a device node that does not exist cannot be opened. This is the difference between a *filter* (which sits in the path and can be bypassed or misconfigured) and a *construction* (which never puts the resource in the environment to begin with). The capability manifest of §3.1 is therefore not a blacklist to be enforced so much as a **build recipe**: the sandbox is constructed with exactly the resources the manifest names, and everything else is simply not there. This is the mechanism that makes the sandbox's guarantee *structural* rather than *behavioral* — and it is the reason the D/L invariant can be stated as zero rather than one: absence is enforced by what was built, not by anything that runs. Concretely, two hard construction rules: - **Secrets never enter the image.** A secret is injected by the tool layer at call time as an opaque handle (§6.1's `secrets[]`), never baked into the environment's filesystem or environment variables. An image that contains a secret is a construction failure, caught by the CI of §9.2, not a runtime risk to be filtered. - **Resources not granted are not mounted or present.** The filesystem view is exactly `filesystem_read ∪ filesystem_write` (plus nothing); the network namespace has exactly the `network_destinations` routes; the device namespace has exactly the `devices` nodes. There is no ambient `/home/agent`, no ambient `~/.ssh`, no ambient credential file — the harness's persistent memory and credentials are *physically separated* from the candidate's environment, not merely protected within it. ### 8.2 Ephemeral per-task environment; destroy after each candidate The sandbox's environment is **ephemeral and per-task**: it exists for one candidate (or one bounded repair episode) and is then destroyed. **Destroy the execution environment after each candidate or bounded repair episode.** The teardown is not optional cleanup; it is the *strongest* form of undo — undo by annihilation — and the `StructuredEvidence` record (§6.4) is the only surviving residue by which any action can be post-facto validated (GoEX's two properties [22]: undo semantics and post-facto validation with human oversight). A new candidate gets a *fresh* environment built from its own manifest, never a reused one, so no state, no capability, and no stale handle survives from one candidate to the next. ### 8.3 Revocation-by-expiry Ephemerality is the temporal form of denial-by-absence: authority is not merely narrowed but *expires*. A capability handle whose episode has closed is revoked (§7.4, PORTICO's mid-session revocation), and a stale handle replayed after the episode is rejected, not honored. A revoked capability is as absent as a resource that was never mounted. The lifecycle therefore has three deterministic stages, each a pure function of the episode, none involving a learned model: **build** (construct the environment from the manifest, §3.4) → **run** (execute under the policy engine, flow checker, tier profile, and resource envelope) → **destroy** (annihilate the environment, revoke every handle, emit the `StructuredEvidence` record). The destroy stage runs even on crash, timeout, or escape attempt — teardown is a `finally`-semantics invariant, not a happy-path courtesy, because the one case where it must not be skipped is precisely the case where the candidate misbehaved. --- ## 9. Honest Measurement — does the box actually hold? A sandbox whose confinement is asserted but never tested is a hope, and a hope is not a component. The measurement machinery has three parts, all deterministic software on CPU [D] — the sandbox's own verification is, like the sandbox, outside the model. ### 9.1 SandboxEval — the adversarial property suite The direct answer to "does the box hold" is an **adversarial test suite** that tries to break it and fails. **SandboxEval: Towards Securing Test Environment for Untrusted Code**, by Rabin, Hostetler, McGregor, Weir, and Judd [19] (arXiv:2504.00018), is the prior that frames this precisely, cited with its caveat attached: its arXiv comments state **"preliminary version, working paper"** — a *working* framework, not a finished one. Its substance is exactly the right shape: **51 properties** across the categories that matter — **sensitive-information exposure**, **filesystem manipulation/escape**, **external network communication**, **resource exhaustion** (CPU/disk/network), and **privilege escalation**. That list is, in effect, the five boundaries of §4 restated as attack families: the authority boundary tested by privilege escalation, the data-flow boundary tested by sensitive-information exposure, the process and machine boundaries tested by filesystem escape and network egress, and every boundary tested by resource exhaustion. The harness adopts SandboxEval's *shape* — an enumerated property list, each property a concrete "the sandbox must prevent X" — and runs it as the sandbox's own regression suite, so that a change to the sandbox (a new rung, a widened manifest, a relaxed seccomp profile) must *re-pass every property* or fail CI. Round 1 ships a **subset** of the 51 properties spanning all five families (the full port is a later-round hardening — DEFERRED, gated by §9.4); each property is a deterministic assertion that a specific escape is contained, with no model anywhere in the loop. ### 9.2 Hostile-fixture CI — the payloads we actually generate SandboxEval-style properties are the *specification*; the second part is the *evidence from the field*. The harness runs, as a standing CI job, a corpus of **hostile fixtures** — the concrete payloads a coding agent actually produces: **fork-bombs** (resource exhaustion), **egress probes** (network escape attempts), **secret-grepping payloads** (attempts to read credentials and exfiltrate them), and **injected-instruction fixtures** (a README or comment carrying an embedded "run this" instruction, exercising the injection/echo/hallucination triple of §1.1). The fixtures are the *positive controls*: each is a program that *should* be contained, and the CI asserts — deterministically — that each is. This is the honest-measurement answer to the attack-side literature of §1.1 [1, 2, 3]: those papers document that agents *do* produce hostile code; the hostile-fixture corpus documents that *this* sandbox contains it. The two must be kept in correspondence, not comfort. ### 9.3 The boundary audited like code The third part treats the sandbox itself as a piece of code that must be *audited*, not trusted. The sandbox's CVE exposure is tracked the way a production service's would be — **CVE-2019-5736** (§5.5) is the standing example of the failure class this audit exists to catch. The audit asks, continuously: which runtimes and kernels does the sandbox depend on, what are their open advisories, and does the current tier selection expose Piper to any of them? The loop is boring and mechanical, and that is the point — "isolation is layered" is only true if the layers are *kept* layered, and a single unpatched runc in the chain silently un-layers them. The audit's output is a tier-exposure recomputation: for each open advisory, which tier's boundary would it breach, and does the current selector ever route hostile code through that tier. ### 9.4 A/B and ablation — the component earns its place The sandbox, like every component, is **kept only if it strictly dominates its own absence** — measured, not asserted. **The baseline is the uncontained execution path** — the candidate run directly on the host, which is what the harness does *without* this component. The full sandbox is the **arm**: the manifest → policy → provenance → tier-select → construct → run → destroy → evidence pipeline of §3–§8. Because the sandbox's value is containment (not a quality-of-output improvement), the instruments are all **containment counters**, never a self-report: 1. **Escapes contained** — hostile fixtures whose escape attempt was blocked (fork-bomb terminated, egress denied, secret-grep returned nothing, injected instruction not honored). The baseline is uncontained by definition; the arm must block all of them. 2. **Prohibited flows denied** — `network_flows_denied` events (§6.4) that the flow checker caught; zero in the baseline, nonzero in the arm. 3. **Sensitive-data exposure blocked** — fixtures that attempt to read a secret and exfiltrate it; the arm's exposure count must be zero. 4. **Resource-exhaustion survived** — the sandbox survives a fork-bomb / memory bomb within its envelope; the host survives, and the run is terminated deterministically, not by a host OOM. 5. **Zero-leak evidence** — the only residue is the `StructuredEvidence` record; no prose log, no raw stdout/stderr, no model narration crosses back. **The A/B decision rule (explicit).** The sandbox is **kept iff** the arm strictly dominates the uncontained baseline on the containment instruments — escapes contained, prohibited flows denied, sensitive-data exposure blocked — with the host surviving every hostile fixture, at a defensible overhead (boot latency, per-syscall cost, evidence-extraction cost). A mechanism whose *addition* degrades a measure is a demonstrated harm and is removed outright. **Never keep a mechanism because it sounds rigorous.** If the arm is indistinguishable from the baseline on the measures that matter, the invariant's escape hatch fires: **an empty L column is a permitted, correct state**, and the machinery is pared back until the evidence changes. **The ablation rule (mechanical).** One mechanism removed at a time, everything else held fixed, the delta measured on the instruments above: 1. **Ablate the provenance/flow checker** (§3.3) — the direct test of the data-flow claim. The delta must show sensitive-data exposure blocked count *rising* (the read-then-exfiltrate fixture now succeeds) while authority stays intact. 2. **Ablate denial-by-absence** (§3.4) — replace construction with a filter that denies on access. The delta must show the secret-grepping and egress fixtures *finding handles and routes to attempt*, i.e. the boundary moving from structural to behavioral. 3. **Ablate the tier escalation** (§5) — run everything on the hardened-container tier alone. The delta must show a kernel-adjacent fixture (a CVE-2019-5736-class replay) escaping, proving single-layer isolation is insufficient. 4. **Ablate the ephemeral destroy** (§8) — reuse the environment across candidates. The delta must show a stale capability handle from a prior candidate being replayed successfully — lingering authority the sandbox no longer revokes. 5. **Ablate the zero-L contract** (§7) — introduce a learned "safety judge" into the enforcement path. The delta must show the injection/echo/hallucination fixtures *succeeding where the deterministic engine blocks them*, proving the learned entry is the threat re-admitted, not a defense. A mechanism is **DELETED** iff its removal does **not** degrade the honest measures beyond noise. The L column is never granted a single entry to "fix" a problem a deterministic mechanism can solve — that would be re-importing the threat through the back door. --- ## 10. Implementation Order (build sequence) Nine phases, each independently testable and reversible, landing in `build/24-execution-sandbox/` as standalone importable modules. The order is deliberate — it builds the control plane from the manifest outward, then the tier backends, then the evidence/lifecycle machinery, then the measurement rig — in the exact sequence the paper's mechanics require: **capability-manifest derivation first, then the policy engine (ALLOW/FORBID + Fallback), then the provenance/flow checker, then the isolation-tier selector + profiles, then the four tier backends (hardened container → gVisor → microVM → Wasm), then structured-evidence extraction, then the destroy/ephemeral lifecycle, then the SandboxEval + hostile-fixture CI, then the boundary CVE audit loop.** Rollback = delete or flag-off that phase's module; nothing earlier breaks because phases only add modules and never mutate live harness code. | Phase | Delivers | Isolated test | Rollback | |---|---|---|---| | **1. Capability-manifest derivation** | `manifest.py` (§3.1 the `CapabilityManifest` schema + the pure `task → manifest` derivation) | Derive a manifest from a fixture task; assert it names exactly the task's filesystem paths/endpoints/devices/tools/secrets and the resource envelope; assert the derivation is a pure function (same task → same manifest); assert the derived `secrets[]` holds handles, never values; assert **no LLM consulted**. | Delete `manifest.py`. | | **2. Policy engine** | `policy.py` (§3.2 the `PolicyRule` schema + the ALLOW/FORBID evaluator with Fallback) | Evaluate an allowed request → `allow`; a forbidden request → `forbid`; an undecided request → the declared `fallback_action` (`error` default); assert the allowed region is computed (symbolic), never model-resolved; assert a `forbid` is an observable event, never "helpfully" routed; assert **no LLM consulted**. | Delete `policy.py`. | | **3. Provenance / flow checker** | `provenance.py` (§3.3 the `DataLabel` schema + the flow denial over `allowed_sinks`) | Tag a value from a sensitive source; attempt a flow to an unauthorized sink → assert denied (event logged); attempt a flow to an allowed sink → assert permitted; assert a `sensitive` value may be transformed but not released; assert **no LLM consulted**. | Delete `provenance.py`. | | **4. Isolation-tier selector + profiles** | `tiers.py` (§5.6 the deny-by-default least-privilege tier choice) | Select a tier for a task that fits Wasm → assert Wasm/WASI; for a task requiring native capabilities → assert escalation, one-directional and logged; assert the profile is the smallest that grants exactly the manifest's resources; assert **no LLM consulted**. | Delete `tiers.py`. | | **5. Tier backend: hardened container** | `backends/container.py` (§5.1 capability dropping + seccomp + user namespaces + read-only root) | Build a container from a manifest; assert only the manifest's filesystem/network/device set is present (§3.4 construction); assert a secret is injected as a handle, never in the image; assert a fork-bomb is terminated within the resource envelope; assert **no LLM consulted**. | Delete `backends/container.py`. | | **6. Tier backend: Wasm/WASI** | `backends/wasm.py` (§5.4 wasmtime + WASI import allowlisting) | Compile a fixture module; assert its import set is exactly the manifest's capability set; assert an off-manifest import is refused at compile time (unexpressible, not filtered); assert the sandboxing pass imposes the ~0.2%-class overhead; assert **no LLM consulted**. | Delete `backends/wasm.py`. | | **7. Structured-evidence extraction** | `evidence.py` (§6.4 the `StructuredEvidence` schema) | Run a fixture; assert the only residue is the record — `exit_code`, `stdout_hash`/`stderr_hash` (bounded, hashed), `resource_counters`, `provenance_tagged_files_touched`, `network_flows_denied`; assert raw stdout/stderr and prose logs do **not** cross back; assert **no LLM consulted**. | Delete `evidence.py`. | | **8. Destroy / ephemeral lifecycle** | `lifecycle.py` (§8 the build → run → destroy stages with revocation-by-expiry) | Run two candidates; assert the second gets a *fresh* environment built from its own manifest, never a reused one; assert every capability handle from episode 1 is revoked before episode 2 (stale replay rejected); assert teardown runs even on crash/timeout/escape; assert **no LLM consulted**. | Delete `lifecycle.py`. | | **9. SandboxEval + hostile-fixture CI + CVE audit** | `sandboxeval.py`, `hostile_ci.py`, `audit.py` (§9 the 51-property subset + fork-bomb/egress/secret-grep/injected-instruction fixtures + advisory tracking) | Run the property subset across all five families; run each hostile fixture and assert it is contained; run the CVE audit and assert tier-exposure recomputation flags any open advisory on a dependency in the chain; assert the whole rig is deterministic and never consults a model; assert a widened manifest or relaxed seccomp profile fails CI. | Delete `sandboxeval.py` / `hostile_ci.py` / `audit.py`. | The tier backends `gVisor` and `Firecracker` (rows 12–13 of §2) are **DEFERRED** — they enter the sequence at the same position as Phase 5–6 (between the container and Wasm backends) only when a measured containment gap justifies the install and the 72h-age + scrutiny gate clears. The measurement rig (Phase 9) is the only phase that ever *invokes* the sandbox end-to-end — only to measure it, never to modify the live harness. The sandbox invents no new model, no new execution substrate of its own beyond the four tiers, and no new diagnosis path; it reuses the harness's own candidate artifacts as the things-to-contain and adds only the deterministic containment boundary around them. --- ## 11. Dependencies & Supply-Chain Per Piper's rule: nothing is installed unless >72h old AND (≥10 stars OR deep-scrutinized). Round 1 is **zero-install for the control plane (Python-only, stdlib + in-repo)** plus two runtime substrates that are already in-repo or standard tooling: | Dependency | Why | Status / age-stars | Install needed? | |---|---|---|---| | stdlib `enum` / `typing` | §6 the `effect` / `fallback_action` / `sensitivity` enums and the schema type hints | CPython stdlib. | **No** | | stdlib `json` | §6 the CapabilityManifest / PolicyRule / DataLabel / StructuredEvidence serialization | CPython stdlib. | **No** | | stdlib `dataclasses` | §6 the frozen schemas (immutable, rebuilt not mutated) | CPython stdlib. | **No** | | stdlib `subprocess` / `os` / `resource` | §3–§8 process spawn, namespace construction, resource-envelope counters | CPython stdlib. | **No** | | stdlib `hashlib` | §6.4 the bounded stdout/stderr hashing | CPython stdlib. | **No** | | Python toolchain (`python3`, pinned) | §3–§8 all deterministic services | In-repo, pinned interpreter. | **No** | | Docker / OCI runtime (already present) | §5.1 the hardened-container tier (capability dropping, seccomp, user namespaces, read-only root) | Existing host tooling; no new install. | **No** | | `wasmtime` (WASI runtime) | §5.4 the Wasm/WASI tier with import allowlisting | Standard runtime; must clear the 72h-age + scrutiny gate before first install. | **Gated** (verify age/stars, sandbox-test) | | Harness components 01–23 | §1 the candidate artifacts (patches, tests, probes) that the sandbox contains; §23's declared task schema feeds §3.1 | The harness's own stages, already vetted. | **No** | **Preferred path: zero-install control plane.** Round 1's deterministic control plane is Python-only and stdlib; the two Round 1 tier substrates are the already-present OCI runtime and `wasmtime`. The DEFERRED rows (§2 rows 12–13) are `gVisor` (`runsc`) and `Firecracker` — both *new* installs that must clear the **72h-age + star/scrutiny + sandbox-test** gate before use, and both out of Round 1 scope entirely. No model, no learned-judgment dependency, and no third-party Python package is introduced in Round 1. --- ## 12. Guardrails & Reversibility **Revocability.** - The build lives entirely in `build/24-execution-sandbox/`; it never writes into the live harness's source. - A **feature flag** (env var `SANDBOX=24|uncontained`, default `uncontained` during Round 1 A/B) routes execution; the flag alone switches between the sandbox and the uncontained baseline — no code deletion required to revert. The default stays `uncontained` until §9.4's A/B shows strict domination, because **an empty L column is a permitted, correct state** and an unproven mechanism must not be the default. - The measurement rig (`sandboxeval.py` / `hostile_ci.py` / `audit.py`) *measures* the sandbox and never modifies it. The hostile fixtures run *inside* the sandbox; the host is never the target. - The sandbox ledger (every manifest derivation, every policy decision, every denied flow, every tier escalation, every resource envelope draw, every destroy event, every revoked handle) is append-only and versioned; rolling the build back never loses it. This ledger *is* the containment traceability the paper claims the uncontained path loses: every grant, every denial, and every destruction has a name and an address. **Invariants (must hold, checked at the end of every phase).** 1. **the L column contains at most one entry** — and at this component it is exactly **zero**: the sandbox holds zero learned entries by construction, and the model is not a contributor but the threat the sandbox contains. **an empty L column is a permitted, correct state** — and here it is the **only** correct state: a sandbox with any learned entry in its enforcement path has already failed. 2. **The model/code is the threat, never a contributor** — no learned model appears in capability derivation, policy enforcement, provenance/flow checking, tier selection, resource budgeting, or evidence extraction. 3. **Deny by absence, not by instruction** — a resource not in the manifest is not mounted and not present; a secret is never in the image; the harness's persistent memory and credentials are physically separated from the candidate's environment. 4. **Layered isolation** — the five boundaries of §4 and the four tiers of §5 are each enforced at their own point in the stack; no single compromise defeats the whole, and the tier selector defaults to the most restrictive tier that can host the task. 5. **Ephemerality + revocation-by-expiry** — the environment is destroyed after each candidate or bounded repair episode; every capability handle is revoked at episode close, and a stale handle is rejected, not honored; teardown runs even on crash, timeout, or escape. 6. **Structured-evidence only** — the `StructuredEvidence` record (§6.4) is the only residue that crosses back into the harness; raw stdout/stderr and prose logs never do. 7. **Honest measurement** — the sandbox is kept only if it strictly dominates the uncontained baseline on the containment instruments of §9.4, on held-out hostile fixtures, and every property of the adversarial suite re-passes on any change to the sandbox; the boundary is CVE-audited continuously, and a single unpatched dependency in the chain fails the audit. 8. **All metrics on held-out hostile fixtures** — escapes contained, prohibited flows denied, sensitive-data exposure blocked, resource-exhaustion survival, and zero-leak evidence are reported on held-out fixtures, never tuning or in-benchmark data. --- *End of spec. This document is the build contract; the next document records the A/B outcome.*