Chapter 6 — Sandbox and Governed Execution
# Chapter 6 — Sandbox and Governed Execution
Chapter 5’s engine said yes to one tool call. This chapter follows that call all the way down — through admission, policy, approval, and into a container with no network — and then all the way back up, where the result is classified before the model is allowed to see it. That last clause is the chapter’s thesis: in this runtime, permission decides what the agent may do, and a separate, monotone decision decides what the agent may see. ADR-004 (“all execution is sandboxed”) and ADR-003 (fail closed) meet in the middle of a single tool call.
The lab runs on your real machine’s sandbox toolchain — no model backend needed — and captures what degraded actually looks like when the doctor reports it. If you have Docker and gVisor, you will see them verified; if you do not, you will see the graceful degradation that is itself one of this chapter’s claims.
A note on citations. Tracked files are cited
path:line; the authoring corpus underdevdocs/is cited by document and line and is not part of the public clone (.gitignore:41). Captured at commit1bc57c62; see the pin note in the front matter.
# Where you are
From Chapter 5 you have the decision machinery: effects, scopes, risk,
the allow/ask/deny model, HITL, the audit chain. This chapter adds the
enforcement side (capability/sandbox/) and traces the journey — the
nine-step invocation pipeline that every tool call traverses. You need
the Chapter 3 contracts (descriptor, trust class) and Chapter 5’s
vocabularies. No model backend is needed anywhere.
By the end you will be able to:
- State the sandbox backend contract and where the policy vocabulary lives (hint: not where the backends live).
- Explain image digest pinning — its precedence and its failure mode.
- Walk one tool call through all nine pipeline steps, citing each.
- Read an insertion decision and compute the more restrictive of two.
# Technical requirements
| Requirement | Version / notes |
|---|---|
| OS / toolchain | As Chapters 1–5 (Linux primary; toolchain pin go1.27.2, go.mod:5) |
| Model backend | Not needed — the turn runs on the offline backend |
| Sandbox | Docker and gVisor runsc welcomed but not required — the doctor’s degraded output is part of the lab |
Captured on the Chapter 1 machine (Docker 29.9.2, runsc release-20260406.0),
commit 1bc57c62.
# 6.1 ADR-004 as a boot condition
The decision record is short and absolute
(docs/ARCHITECTURE.md:263-271): every command goes through the sandbox
chain into a verified backend — gVisor by default, Docker as the
alternative — the previous local host-exec runner was deleted, and a
guard test keeps it deleted. The consequence line is the semantics of
“degraded”: a missing sandbox toolchain degrades boot to the doctor
screen instead of allowing unsandboxed execution. Degraded never means
unsandboxed; it means visible.
You can see all of it live. In a scratch workspace, the doctor at the pin reports (excerpt; full output in the lab transcript):
- runner: missing [warning] (no runner status (not spawned yet))
- sandbox_backend: ok [required] (runsc version release-20260406.0
spec: 1.1.0-rc.1)
- docker: degraded [warning] (degraded: runsc runtime not registered)
- audit_chain: ok [required] (no audit chain yet (created on first agent run))
- runtime_image: degraded [required] (unpinned (tag-based; resolved from the
local daemon at boot — set security.sandbox.image_digest to pin))
Result: readyFour rows teach four different things. sandbox_backend: ok verifies
the toolchain (runsc version quoted — the doctor doesn’t trust
presence, it interrogates). docker: degraded — on a machine where
Docker the daemon runs fine — means the gVisor runtime is not
registered with Docker, so the default backend pairing is only half
present: degraded is a pairing verdict, not a health verdict.
runtime_image: degraded is §6.3’s pin warning, surfaced as a required
row that still does not block. And Result: ready with two degraded
rows is the whole governance philosophy of boot in one line: fail
closed, degrade loudly, never quietly allow. (The runner row is Chapter
1’s D10 seam; the audit row is Chapter 5’s chain waiting for its first
entry.)
The platform hints — kvm and ptrace for gVisor
(--sandbox-platform, Chapter 3’s flag table) — select how runsc
virtualizes, not whether it does.
# 6.2 The backends, and where the policy vocabulary lives
The enforcement side is capability/sandbox/. Its Backend interface
(capability/sandbox/contract_types.go:83-96) is the contract an
isolation technology satisfies: name → verify → describe capabilities →
validate a policy → apply a policy → return the applied policy. Two ship:
gVisor runsc (default) and Docker. An unsupported backend is a hard
error, and a local (bare host) backend “does not exist and is rejected
by config validation” (devdocs/HANDOFF-docs/API_SURFACES.md:306-326).
Here is the wrinkle Chapter 5 set up and this chapter resolves: the
policy vocabulary is not in capability/sandbox/ — it is in
governance/sandbox/, because “these structs are the contract between
authored policy and enforcement; backends … implement the enforcement
ports against these types” (governance/sandbox/sandbox.go:1-4). The
vocabulary is three types — SandboxPolicy (security intent),
SandboxConfig (runtime knobs), NetworkRule (one egress/ingress rule)
— and one of its honesty rules you already know from ADR-003: declared
network rules do not relax network isolation
(devdocs/HANDOFF-docs/API_SURFACES.md:319-321; per-rule egress is not
enforceable at the packet level, so opening the container network
because rules exist would be an unsafe fallback).
The same split holds for the downgrade guard: a manifest that declares
the gVisor runtime while the configured backend is Docker is a hard
error — gVisor is the only runtime providing the full security model
(app/envcomposition/security_validate.go).
Sharing the runner. Reasoning paradigms do not own command
execution either — the command runner and the command policy arrive as
wired dependencies on the paradigm contract
(cognitionzoo/paradigm/deps.go:33-34: CommandRunner and
CommandPolicy in paradigm.Deps). One enforcement seam, threaded
everywhere; no paradigm can grow its own.
The structure in one picture — vocabulary owned by governance, the five-verb contract in capability, the runner shared by wiring:
flowchart TD
subgraph GV["governance/sandbox — the vocabulary"]
SP["SandboxPolicy — security intent"]
SC["SandboxConfig — runtime knobs"]
NR["NetworkRule — one rule<br/>declared rules never relax isolation"]
end
subgraph CS["capability/sandbox — the enforcement"]
BE["Backend interface<br/>name → verify → describe → validate → apply"]
GS["gVisor runsc — default<br/>platform hints: kvm · ptrace"]
DB["Docker — alternative"]
IMG["image pin precedence<br/>@sha256 → image_digest → daemon → unpinned-loud"]
end
SP --> BE
SC --> BE
NR --> BE
BE --> GS
BE --> DB
IMG --> BE
DEPS["paradigm.Deps<br/>CommandRunner · CommandPolicy"] --> BE
GUARD["security_validate<br/>gVisor declared + Docker configured = hard error"] -.-> BE# 6.3 The image: pinned, or loud
Every sandboxed command runs in a container built from one image, and
the tree treats that image as a supply-chain fact
(capability/sandbox/command_runner.go:24-29): the digest is resolved
at boot — from the local daemon or from
security.sandbox.image_digest — and “an unpinned boot emits
sandbox.image_unpinned.”
The pin precedence is recorded in the code with a decision ID
(capability/sandbox/image.go:33-40, SBH-1 D-13): an explicit
@sha256: reference wins, then the operator-configured image_digest,
then a daemon-resolved repo digest, and last — PinUnpinned — the tag
is used with a loud warning, which is exactly the
runtime_image: degraded [required] row from the doctor capture above.
There is no silent path: the image is either cryptographically pinned or
the boot tells you it isn’t, in a row you cannot miss and that CI cannot
suppress.
# 6.4 The journey of one tool call
Every capability invocation traverses the same nine steps, all inside
CapabilityRegistry.InvokeCapability
(capability/registry/invoke.go:32) — the pipeline the whitepaper
documents (§5.2) and Book 1 summarized:
| # | Step | What gates it |
|---|---|---|
| 1 | Delegate and allowlist | a scoped registry view must contain the capability |
| 2 | Session-scoped lookup | a capability registered on the envelope wins over the registry |
| 3 | Availability | the handler may declare itself unavailable |
| 4 | Policy evaluation | risk.Classify (Ch 5) → policy engine returns allow / deny / require-approval |
| 5 | Prechecks | doom-loop detection, write-path restriction |
| 6 | Schema validation and coercion | unknown keys rejected, defaults injected, values coerced |
| 7 | Safety budgets | per-capability and per-provider call caps; session byte and token caps |
| 8 | Execution | panic-recovered; commands traverse the sandbox chain: command authorization → shell guard → enforcing runner → container |
| 9 | Postchecks and metrics | the result passes back through the insertion decision (§6.5) |
Step 8’s sandbox chain is Chapter 5’s vocabulary executing:
governance/authorization/command_authorization.go decides, the shell
guard blocks-or-escalates unparsable commands, and the enforcing runner
launches the container — the same runner that was wired through
paradigm.Deps (§6.2).
One consumer of this pipeline refuses to run without it. ReWOO’s
constructor takes the permission checker as a mandatory option — and its
error vocabulary says why
(cognitionzoo/rewoo/errors.go:5-9):
The journey as a flow — the nine steps, the ask-detour, and the classified return:
flowchart TD
M["model decision"] --> S1["1 · allowlist + delegation"]
S1 --> S2["2 · session-scoped lookup"]
S2 --> S3["3 · availability"]
S3 --> S4["4 · policy evaluation<br/>risk.Classify → allow / deny / ask"]
S4 -->|ask| H["HITL approval (Chapter 5)"]
S4 -->|deny| STOP["denied — audited"]
H -->|approved| S5
S4 -->|allow| S5["5 · prechecks — doom-loop · write-path"]
S5 --> S6["6 · schema validation + coercion"]
S6 --> S7["7 · safety budgets"]
S7 --> S8["8 · sandboxed execution<br/>command authz → shell guard → runner → container"]
S8 --> S9["9 · postchecks + metrics"]
S9 --> INS{"insertion decision<br/>from the trust class"}
INS -->|builtin / workspace-trusted| D["direct — full text"]
INS -->|llm-generated · tool-result · remote-approved| SU["summarized — ≤220 chars/block"]
INS -->|provider-local · remote-declared| MO["metadata-only — data nil'd"]
INS -->|policy or composition| HR["hitl-required · denied"]ErrNoPermissionChecker is returned when a ReWOO execution is attempted without a permission checker. Governance is unconditional: a ReWOO turn …
— the sentence completes itself in the type system: without the
checker, there is no execution to attempt (cognitionzoo/rewoo/new.go:28-35).
# 6.5 The answer comes back classified
Step 9 hands the result to the insertion decision — the mechanism whitepaper §5.3 calls this paper’s thesis, and the code is short enough to quote the whole default:
// capability/result/result.go:166-186 (excerpt)
func DefaultInsertionDecision(descriptor descriptor.CapabilityDescriptor,
disposition ContentDisposition) InsertionDecision {
switch descriptor.TrustClass {
case agentspec.TrustClassBuiltinTrusted, agentspec.TrustClassWorkspaceTrusted:
return InsertionDecision{Action: agentspec.InsertionActionDirect, …}
case agentspec.TrustClassLLMGenerated, agentspec.TrustClassToolResult:
return InsertionDecision{Action: agentspec.InsertionActionSummarized, …}
case agentspec.TrustClassRemoteApproved:
return InsertionDecision{Action: agentspec.InsertionActionSummarized, …}
case agentspec.TrustClassProviderLocalUntrusted, agentspec.TrustClassRemoteDeclared:
return InsertionDecision{Action: agentspec.InsertionActionMetadataOnly, …}
}
…
}One declared trust class — Chapter 3’s descriptor field — yields both
the admission input (Chapter 5’s risk.Classify) and the visibility
answer here. Five actions exist, and they form a total order:
// capability/result/result.go:376-390
func InsertionRestrictiveness(action agentspec.InsertionAction) int {
switch action {
case agentspec.InsertionActionDirect: return 0
case agentspec.InsertionActionSummarized: return 1
case agentspec.InsertionActionMetadataOnly: return 2
case agentspec.InsertionActionHITLRequired: return 3
case agentspec.InsertionActionDenied: return 4
default: return 4
}
}The default arm is fail-closed in one line: an unrecognized action
scores maximally restrictive. Composition can only tighten —
moreRestrictiveInsertionDecision returns whichever candidate is more
restrictive (capability/result/result.go::315-324) — and a manifest’s insertion policy
overrides only if it does not loosen
(capability/registry/capability_policy_eval.go:32).
The consequences are visible at the consumption sites. A direct
insertion’s budget is zero because nothing needs summarizing; a
summarized one is capped at 220 characters per block
(cognitionzoo/react/capability_insertion.go:59-62); and a
metadata-only decision nils the observation’s data entirely while the
envelope still travels — the paradigm learns that something happened
without learning what (cognitionzoo/react/react_act_node.go:438-441).
# 6.6 Hands-on: the journey, verified at both ends
The lab (docs/relurpic-internals/labs/ch06-sandbox/) captures the
journey from three angles; the transcript holds the full evidence.
1. Readiness, on your machine. Build from the pin and run the doctor in a scratch workspace:
go build -o /tmp/relurpify-ch06/relurpish ./app/relurpish
/tmp/relurpify-ch06/relurpish doctor --workspace /tmp/ch06-wsCaptured (§6.1’s excerpt): the sandbox row interrogating runsc’s
version, the Docker degraded pairing, the unpinned-image row, and
Result: ready. On a machine without the toolchain the same rows
degrade instead of vanishing — capture yours and diff the shapes.
2. The turn. The same product-path test as Chapter 5, re-run for this chapter because the journey’s middle steps belong here:
go test ./app/relurpish/e2e -run TestBootTurnToolCall -count=1Captured: ok … 0.125s — admission, evaluation, budget, execution, and
the classified return all exercised (the harness isolates the container
and the human; §5.6’s note applies verbatim).
3. The enforcement machinery.
go test ./capability/... -count=1Captured: 12 packages ok, including capability/sandbox (1.118s —
it exercises real lifecycle logic against the committed fake-docker
fixture).
4. The sites. Read the five declaration sites this chapter cites —
the Backend interface, the pin precedence, the pipeline entry, the
restrictiveness order, and ReWOO’s mandatory option — and check each
claim above against the line. That habit is the book’s deliverable.
# Verification block
| Chapter claim | Where captured | Lab file |
|---|---|---|
| Degraded semantics, live (runsc verified; Docker pairing; unpinned image) | §6.6 step 1 | transcript.txt §1 |
| Governed tool-call turn passes the product path | §6.6 step 2 | transcript.txt §2 |
| Capability machinery green (12 packages) | §6.6 step 3 | transcript.txt §3 |
| Backend contract; policy vocabulary owned by governance | §6.2 | manual: capability/sandbox/contract_types.go::83-96, governance/sandbox/sandbox.go:1-7 |
| Pin precedence with a loud unpinned fallback | §6.3 | manual: capability/sandbox/image.go::33-40, capability/sandbox/command_runner.go::24-29 |
| Insertion order is total and fail-closed | §6.5 | manual: capability/result/result.go::376-390 |
# Summary
- ADR-004 is a boot condition: the toolchain is interrogated, not
assumed; degradation is loud, paired, and never a path to unsandboxed
execution (
app/relurpish/runtime/doctor.go::627-636rows;Result: readywith warnings). - The sandbox policy vocabulary lives in
governance/sandbox/— the contract between authored policy and enforcement — while the backends live incapability/sandbox/behind a five-verb interface. The command runner and command policy reach paradigms only as wired dependencies (cognitionzoo/paradigm/deps.go::33-34). - The container image is pinned by precedence (explicit digest >
configured digest > daemon-resolved) or used with a loud
sandbox.image_unpinnedwarning — never silently (capability/sandbox/image.go::33-40). - Every tool call traverses nine pipeline steps inside
InvokeCapability(capability/registry/invoke.go::32), from allowlist to insertion; ReWOO refuses to run without its permission checker because “governance is unconditional” (cognitionzoo/rewoo/errors.go::5-9). - The result returns through an insertion decision derived from the
trust class, on a total restrictiveness order where unrecognized
actions score maximally restrictive and composition can only tighten
(
capability/result/result.go::315-390).
# Check yourself
- The doctor reports
docker: degradedon a machine where the Docker daemon is healthy. What is degraded, and why doesn’t the row block? - Name the pin precedence in order. Which step does a bare
image: latestin a manifest take, and what does the boot emit? - At which pipeline step does the model’s claim first meet a governance judgment, and what makes that judgment non-bypassable for ReWOO specifically?
- A capability’s trust class yields
summarized; a manifest policy demandsmetadata-only. Which wins, and which function computes it? What if the manifest demandeddirectinstead? - After a
metadata-onlydecision, the paradigm’s observation still exists. What does it contain, and why is that not a leak?
Answers. 1. The pairing — gVisor is not registered as a Docker
runtime, so the default backend combination is half-present; the row is
a warning because sandbox_backend (the required row) verified runsc
itself, and ADR-004’s failure mode is degrade-and-explain, not
unsandboxed fallback. 2. Explicit @sha256: ref, then
security.sandbox.image_digest, then a daemon-resolved repo digest,
then PinUnpinned — the tag with the sandbox.image_unpinned warning
(capability/sandbox/image.go::33-40, capability/sandbox/command_runner.go::27-29). A bare tag resolves
through the daemon or lands unpinned-loud. 3. Step 4, policy evaluation
— the declared effects meet risk.Classify and the policy engine
there. ReWOO cannot skip it because its executor is constructed with
WithPermissionChecker and errors at construction time without one
(cognitionzoo/rewoo/errors.go::5-9) — the gate is structural, not advisory.
4. metadata-only wins; moreRestrictiveInsertionDecision
(capability/result/result.go::315-324) picks it (2 > 1). A direct demand loses
(0 < 1): manifest policies only tighten
(capability/registry/capability_policy_eval.go::32). 5. The observation’s Data is nil
while its envelope (decision, action type) still travels
(cognitionzoo/react/react_act_node.go::438-441) — the paradigm knows that something
happened and how it was classified, never what; the payload stayed in
the artifact/summary layers the model never receives.
# Try it yourself
- Break the downgrade guard. In a scratch clone, configure the
Docker backend in a workspace whose security policy declares the
gVisor runtime, and boot the doctor. Capture the hard error, then
find its enforcement site in
app/envcomposition/security_validate.go. - Compute a ladder. Write down the five insertion actions in
restrictiveness order from memory, then check against
InsertionRestrictiveness(capability/result/result.go::376-390). Which arm would a future action enum value fall into, and what does that choice imply about adding one?
# Where next
The governed turn left something behind: knowledge chunks, ingest pipelines, an AST index, and a graph store. Chapter 7 opens the knowledge substrate — the world the bi-directional compiler reads and writes — including the scale ceilings the project states about itself in its own docs.
The lab for this chapter lives in
docs/relurpic-internals/labs/ch06-sandbox/ — the command card, the
captured transcript from the pin, and verify.sh ch06 to check your
machine against the deterministic outputs.