|
| 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