Skip to content

Commit a855c18

Browse files
rfc: ADR-0009 sandbox cartridge + ADR-0010 cross-machine federation (epic #87 items 2 + 3) (#93)
## Summary Second coupled RFC pair for epic #87 Tier B. Coupled because **sandbox-mcp is machine-local by construction** and the federation design must remain aware of that — federation coordinates *which peer runs a sandbox*; sandbox handles themselves never cross machines. **No code in this PR.** Pure design docs. ## ADR-0009 — Sandbox cartridge Multi-provider, tier-gated code execution as **one MCP cartridge** with five swappable backends: | Backend | Isolation | Cold-start | When | |---|---|---|---| | \`local\` | Process (bubblewrap) | <100ms | Default; no SaaS dependency | | \`e2b\` | Firecracker microVM | ~500ms | Best LLM-coding ergonomics | | \`modal\` | Container | ~1s | Persistent runtimes | | \`codesandbox\` | Container | ~2s | Web-dev workflows | | \`replit\` | Container | ~3s | Long-running educational use | Tier gating: \`capabilities.network: true\` flips \`sandbox_exec\` from tier-2 to **tier-4**, requiring master approval. Policy engine (ADR-0007) computes effective tier per-call. Wires explicitly with \`panic-attack-mcp\` (pre-flight static analysis) + \`vordr-mcp\` (post-flight integrity). Three-cartridge **canonical untrusted-execution flow**. ## ADR-0010 — Cross-machine coord federation Promote \`local-coord-mcp\` from loopback-only to federated. Three pillars: 1. **DID identity** — \`did:boj:peer:<base32-ML-DSA-public-key>\`; private keys never leave the machine 2. **ML-KEM-1024 key exchange + AEAD** — post-quantum on the wire; aligns EXHIBIT-B end-to-end 3. **Federated quarantine** — master visibility across machines; master-uniqueness invariant federated via HOTSTUFF-style election Three topology variants: mesh / hub / **hub-and-rim** (recommended for production — dedicated routing machine keeps the federation up when any LLM-peer machine goes down). Opt-in per machine via \`COORD_FEDERATED=true\` + signed roster. **No insecure-federation mode** — federation without crypto is not federation. Implementation plan is six staged sub-RFCs (~6 weeks total) so the campaign can be reviewed in chunks rather than as a single mega-PR. ## Why this pair Sandbox is the *biggest* execution-blast-radius operation BoJ exposes; federation is the *most architecturally far-reaching* extension. Both depend on ADR-0007 (policy DSL) landing first. Both touch the trust-tier model from different angles — execution and identity. Coupling them in one review surfaces the interactions early. ## Review focus Recommend reading **Open questions** in both: - ADR-0009: default provider choice (\`local\` vs \`e2b\`); GPU support; output streaming protocol - ADR-0010: DID method registration with W3C; roster mutability; split-brain handling on partition heal; performance ceiling The federation RFC is the largest commitment in epic #87 — call out anything in the staged plan that feels off-sequence. ## What this doesn't change - No code touched. - No existing cartridges affected. - ADR-0002 (BoJ-only MCP) stands; federation is *in-cartridge*, not *between-MCPs*. ## Sequencing Per epic #87: this is Tier B step 2 of 3. The third pair (items 5 + 6: webhooks + sampling) follows. All three pairs are independent of each other and of the code PRs (#88/#89/#91). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 2f8cd93 commit a855c18

3 files changed

Lines changed: 533 additions & 0 deletions

File tree

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
<!-- SPDX-License-Identifier: MPL-2.0 -->
2+
<!-- Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk> -->
3+
4+
# 9. Sandbox cartridge — multi-provider, tier-gated code execution
5+
6+
Date: 2026-05-20
7+
8+
## Status
9+
10+
Proposed (RFC — implementation tracked in epic #87 item 2)
11+
12+
## Context
13+
14+
BoJ's multi-agent architecture (master / journeyman / apprentice supervision via `local-coord-mcp`) has no execution substrate. Agents on the bus can call tools, read resources, claim tasks, but cannot **run code**. This is a gap because:
15+
16+
- Common agent workflows include "write code, run it, observe output, iterate" — the canonical LLM-coding loop has nowhere to land
17+
- The trust-tier model has no expression in execution: writing code is a "small_write" (creating a file), but *running* that code is closer to "destructive" (arbitrary side effects)
18+
- `panic-attack-mcp` does static analysis pre-execution, `vordr-mcp` does post-execution integrity — there's a deliberate gap in the middle where execution should sit
19+
- Existing cartridges that *do* run code (`browser-mcp`'s `execute_js`, container-mcp's full container lifecycle) are general-purpose and not tier-gated; they're hard to reason about as "the place agents execute untrusted code"
20+
21+
The estate also can't pull in standard tools without violating ADR-0002 (BoJ-only MCP) — a third-party `e2b-mcp` or `modal-mcp` would be a standalone MCP, which is forbidden. The capability must enter BoJ as a cartridge.
22+
23+
## Decision
24+
25+
Build **`sandbox-mcp`** — a multi-provider, tier-gated execution cartridge. One MCP-facing cartridge surface; pluggable backends.
26+
27+
### Provider abstraction
28+
29+
```
30+
sandbox-mcp
31+
├── ABI (Idris2) — Sandbox.idr, Execution.idr, Provider.idr
32+
├── FFI (Zig) — uniform sandbox_* calls
33+
└── Adapter (Deno) — provider modules:
34+
├── e2b.js — e2b.dev firecracker microVMs
35+
├── modal.js — Modal.com containers
36+
├── codesandbox.js — CodeSandbox sandpack runners
37+
├── replit.js — Replit Repl Spaces
38+
└── local.js — local Podman + bubblewrap (no network)
39+
```
40+
41+
Provider selection by env (`SANDBOX_PROVIDER=e2b|modal|codesandbox|replit|local`) or per-call argument. The MCP surface is provider-agnostic; calls don't change shape when switching backends.
42+
43+
### Tool surface
44+
45+
```
46+
sandbox_create — provision a fresh sandbox; returns sandbox_id
47+
sandbox_exec — execute a command/script inside an existing sandbox
48+
sandbox_read — read a file from inside the sandbox
49+
sandbox_write — write a file into the sandbox (input only)
50+
sandbox_install — install a language toolchain or package
51+
sandbox_destroy — release the sandbox; idempotent
52+
sandbox_list — list active sandboxes belonging to the calling peer
53+
```
54+
55+
Each tool maps to one provider call. `sandbox_exec` is the high-frequency one; the rest exist so the LLM doesn't have to redo provisioning across multiple `exec` calls.
56+
57+
### Tier model
58+
59+
Per ADR-0007:
60+
61+
| Tool | Tier | Required role | Master approval |
62+
|---|---|---|---|
63+
| `sandbox_create` | 2 | apprentice | no — but the sandbox itself is bounded by the tier of operations it can run |
64+
| `sandbox_exec` | 2-4 (depends on capabilities) | journeyman | only if `capabilities` includes `network` |
65+
| `sandbox_read` | 1 | apprentice | no |
66+
| `sandbox_write` | 1 | apprentice | no — write into sandbox, not host |
67+
| `sandbox_install` | 2 | journeyman | no — bounded to sandbox |
68+
| `sandbox_destroy` | 0 | apprentice | no |
69+
| `sandbox_list` | 0 | apprentice | no — read-only |
70+
71+
`sandbox_exec` is the policy hot-spot. Each sandbox has declared **capabilities** at create-time:
72+
73+
```
74+
capabilities: {
75+
network: boolean, # internet access
76+
filesystem: "ro" | "rw",
77+
duration_s: number, # hard timeout, max 1800
78+
memory_mb: number, # max 4096
79+
cpu_quota: number, # 0.1 .. 4.0 cores
80+
}
81+
```
82+
83+
`network: true` flips `sandbox_exec` from tier-2 to tier-4 (per policy). The policy engine (ADR-0007) computes the effective tier per-call from the sandbox's capabilities + the calling peer's role.
84+
85+
### Provider differences (deliberately surfaced)
86+
87+
The cartridge does **not** abstract over provider semantics in a leaky way. Five provider properties are exposed as part of `sandbox_create`'s response:
88+
89+
- `isolation_level`: `microvm` | `container` | `process` — e2b is microvm, Modal is container, local-bubblewrap is process
90+
- `cold_start_ms`: provider-typical, helps the LLM decide whether to reuse vs. create
91+
- `language_support`: declared by provider; `["python", "node", "rust", "go"]` etc.
92+
- `egress_policy`: `none` | `allowlist:<urls>` | `unrestricted`
93+
- `attestation`: hash/signature of the sandbox image (Modal supports this; e2b partial; local depends on bubblewrap setup)
94+
95+
This lets the LLM (or the policy engine) make informed choices. If the user policy requires `attestation: required`, `local` provider is the only valid choice.
96+
97+
### Wiring into `panic-attack-mcp` and `vordr-mcp`
98+
99+
The execution lifecycle is:
100+
101+
1. **Pre-flight** (optional but encouraged): `panic-attack_scan` on the code about to be executed. Static analysis catches banned constructs before runtime.
102+
2. **Execution**: `sandbox_create` + `sandbox_write` + `sandbox_exec`.
103+
3. **Post-flight** (optional): `vordr_verify` on artefacts the sandbox produced, before they leave the sandbox boundary.
104+
105+
These wirings are *recommendations*, not enforcements. The LLM composes them via the `audit-repo`-style prompt patterns from PR #89. A future `execute-untrusted-code` prompt template can encode the canonical 3-step flow.
106+
107+
### Cleanup discipline
108+
109+
Sandboxes are bound to peer tokens (`coord_register`). When a peer's session ends (token expires, coord watchdog fires), `sandbox-mcp` releases all sandboxes owned by that peer. Prevents orphan-sandbox cost explosions.
110+
111+
## Consequences
112+
113+
### Positive
114+
115+
- **Closes the execution gap** — agents on the BoJ bus now have a tier-gated place to run code, instead of either (a) doing nothing useful with code or (b) executing in `browser-mcp.execute_js` which has wrong tier semantics.
116+
- **One MCP surface, many providers** — addresses the "use e2b for now, switch to Modal later" reality without disrupting consuming prompts.
117+
- **Tier expression in execution**`capabilities.network: true` flipping `sandbox_exec` to tier-4 makes the tier system load-bearing for the highest-blast-radius operation BoJ performs.
118+
- **Composes with existing cartridges**`panic-attack-mcp` pre-flight + `sandbox-mcp` execution + `vordr-mcp` post-flight is the architecturally-shaped flow. Three separate cartridges, one workflow.
119+
- **Provider differences exposed honestly**`isolation_level`, `attestation`, `egress_policy` published per provider; LLM (and policy) can choose appropriately.
120+
- **Bounded resources** — every sandbox has duration/memory/CPU caps, set at create-time and enforced by the provider.
121+
122+
### Negative
123+
124+
- **External-provider dependency** — three of five backends (e2b, Modal, CodeSandbox, Replit) are paid SaaS. Pricing structure varies. Mitigation: `local` backend (Podman + bubblewrap) for users who refuse SaaS dependency; documented as the "always-available" floor.
125+
- **API drift** — provider APIs change. Mitigation: pin provider SDK versions in the cartridge's Deno imports; expose `provider_version` in `sandbox_list` so drift is observable.
126+
- **Auth surface** — each provider needs an API key (`E2B_API_KEY`, `MODAL_TOKEN`, etc.). Adds ~5 env vars to glama.json.
127+
- **Cost surprises** — LLM agents can spin up sandboxes faster than humans monitor cost. Mitigation: per-peer rate limits via ADR-0007 policy (max N sandboxes per hour, max M minutes total runtime per day).
128+
- **Sandbox-as-pivot risk** — a compromised sandbox with `network: true` becomes a pivot point for outbound attacks. Mitigation: `egress_policy` strict default (no internet); requires explicit policy override to enable.
129+
130+
## Non-goals
131+
132+
- **Not a generic compute provider** — sandbox-mcp is for short-lived, LLM-driven code execution. Not for long-running services. Use `container-mcp`, `k8s-mcp`, or provider-specific cartridges for those.
133+
- **Not a CI runner**`buildkite-mcp` / `circleci-mcp` / `laminar-mcp` cover CI. Sandbox is for ad-hoc agent execution.
134+
- **Not a debugger**`dap-mcp` exists for that.
135+
- **Not state-preserving across peer sessions** — sandbox lifetime ≤ peer session lifetime. No "resume yesterday's sandbox". Persisting requires a different cartridge.
136+
- **Not GPU-enabled in v1** — provider APIs for GPU exist (Modal, Replit), but tier-gating GPU usage adds complexity. Revisit in a follow-up.
137+
138+
## Open questions
139+
140+
1. **Default provider**`local` (always available, no SaaS) or `e2b` (best LLM-coding ergonomics)? Recommend `local` as default with strong docs on enabling SaaS providers for production use.
141+
142+
2. **Sandbox sharing** — can two peers on the coord bus share a sandbox (e.g. journeyman creates, apprentice executes)? Risk: cross-peer privilege escalation. Recommend disallow for v1; revisit when there's a concrete use case.
143+
144+
3. **Output streaming**`sandbox_exec` is naturally streaming (stdout/stderr come over time). MCP doesn't have great primitives for this yet. v1: return on completion with full output; v2: SSE streaming once ADR-0011 (webhooks/notifications) lands.
145+
146+
4. **Filesystem semantics** — when does `sandbox_write` accept binary content? Base64? Streaming? Recommend base64 for v1 (simplest); revisit with chunked streaming when SSE lands.
147+
148+
5. **Network policy granularity**`egress_policy` as an allow-list of URLs is straightforward for e2b/Modal but harder for `local` (would need iptables rules in bubblewrap). Recommend allowlist for SaaS providers; documented "best-effort" for local.
149+
150+
6. **`sandbox-mcp` vs container-mcp scope** — there's overlap with `container-mcp`'s lifecycle. Recommend: `container-mcp` is for managing persistent containers (services, databases, build environments); `sandbox-mcp` is for ephemeral, untrusted, agent-driven execution. Both can coexist; their tier and lifetime semantics differ.
151+
152+
## Implementation sketch
153+
154+
When this RFC is accepted:
155+
156+
1. New cartridge `cartridges/sandbox-mcp/` with the standard Idris2/Zig/Deno triple.
157+
2. Five provider adapters under `cartridges/sandbox-mcp/adapter/providers/`. `local` lands first (no SaaS dependency to bring up); `e2b` second (best LLM-coding ergonomics); others in subsequent PRs.
158+
3. Manifest declares 7 tools (`sandbox_create`/`exec`/`read`/`write`/`install`/`destroy`/`list`).
159+
4. New default policy entries in `policies/boj-default.ncl` (depends on ADR-0007 landing first).
160+
5. New env vars: `SANDBOX_PROVIDER`, `E2B_API_KEY`, `MODAL_TOKEN`, etc. — declared in glama.json.
161+
6. Tests: per-provider integration suite (mocked transport for SaaS, real bubblewrap for local).
162+
7. Documentation: `docs/cartridges/SANDBOX-MCP.md` covering provider selection, policy authoring, and the panic-attack → sandbox → vordr flow.
163+
164+
## Linked
165+
166+
- ADR-0007 (trust-tier policy DSL) — the policy bundle is the precondition for tier-gated execution.
167+
- ADR-0010 (cross-machine federation) — sandboxes are *machine-local*; cross-machine peer cannot share a sandbox handle. Federation needs to be aware.
168+
- `panic-attack-mcp` cartridge — pre-flight static analysis.
169+
- `vordr-mcp` cartridge — post-flight integrity verification.
170+
- Epic #87 item 2 (this).

0 commit comments

Comments
 (0)