AI Sandbox — Code Execution
- Area: Intelligence
- Path:
services/ai/sandbox - Kind: Isolated execution of AI-generated code (Firecracker microVMs + language packs)
- Status: v0.1.0 — foundation landed 2026-05-24. HTTP daemon (
koder-sandbox) + operator CLI (ksandbox) + Go SDK (engines/sdk/go/sandbox) ship with the subprocess runtime (real, testable). Firecracker driver is the typed stub returningSANDBOX-RUNTIME-UNAVAILABLE-001; rootfs catalog + boot pipeline + vsock agent track in sandbox#014. Pre-flip status — subprocess driver suitable for devCIsingle-tenant trusted-workload deployments; not yet for hostile multi-tenant traffic (gates G3 + G4 still open).
Role in the stack
sandbox is the missing primitive that unblocks the entire "agent that writes code" surface of the stack. Agents generate code; without a sandbox they can't run it (or worse, they run it on the host). Today every product-side prototype that wants "run this snippet" reinvents some half-baked Docker shell-out — slow, leaky, no quotas, no audit. This sector consolidates the capability into one secure isolation primitive that all AI-side consumers (kode, agents, workflow, kortex code-tools) plug into.
It is the Koder analog of E2B, Daytona, and Modal sandboxes — built on Firecracker (sub-200ms boot, ~5MB overhead per microVM) with a gVisor adapter for compatibility tail.
Boundary vs neighbors
services/ai/runtimeis disjoint — runtime serves models; sandbox runs user code.services/ai/kode,services/ai/agents,services/ai/workflow,services/ai/playgroundare the primary consumers.products/dev/kortexconsumes via "Run this" panels in the IDE.services/ai/tracereceives spans for every session + exec.infra/data/kdb-blobholds artifact storage (file IO + snapshots).- The long-standing tracking ticket
services/ai/ai/backlog/pending/017-pluggable-execution-sandbox-backends.mdis realized by this sector.
Features (v1 target)
- Firecracker microVM runtime, warm pool per language
- Sub-200ms session create from warm pool; < 1s cold boot
- Language packs: Python 3.11, Node 20, Bash + GNU coreutils
- Sync + async exec, SSE stdout/stderr streaming
- Default-deny network with allowlist (DNS proxy + iptables)
- FS quota (default 1GB), CPU quota (cgroups v2), memory cap with OOM kill
- Hard-kill on quota breach with partial-output preservation
- File IO API (upload, download, list, delete) with quota awareness
- Snapshot/restore for replay + persistent agent loops
- Per-tenant concurrent-session + daily-minute quotas
- Audit log of network connection attempts
Realized surfaces (live)
Beyond the v1 target above, these are implemented and validated:
- Incus runtime driver (
internal/runtime/incus.go) — warm pool, per-sessionVM with a bridge IP, file IO via
incus file push/pull, listening-port discovery from/proc/net/tcp. Optional capabilities are type-asserted so drivers opt in:Upstream— resolves a session's dev server tohttp://<bridge-ip>:<port>,consumed by Koder Jet for AI-sites sandbox-origin preview proxying (jet#149):
GET /v1/sandbox/sessions/{id}/upstream, invalidated on session end. The preview port auto-detects by heuristic or can be pinned per session viaPOST /v1/sandbox/sessions/{id}/preview {port}(SANDBOX-008 manual override; honored only once that port is listening, else 503).Terminal/Resizer— interactive shell overincus exec -tdriventhrough a local PTY (
creack/pty);WS /v1/sandbox/sessions/{id}/terminalbridges it (binary = stdin/stdout, JSON text = resize control). Real-VM validated on s.khost1. The subprocess driver omits the capability → 501.
- Per-session resource enforcement (SANDBOX-003) — CPUmemorydisk/pids
caps applied as a live
incus config setright after acquire (warm-pool instances are launched before the session's limits are known), withlimits.memory.enforce=hardfor OOM-kill over silent throttle. Validated in-guest (memory.max/pids.max). Each exec reportspeak_memory_kib(frommemory.peak) and an OOM verdict (SANDBOX-EXEC-KILLED-OOM, via a per-execmemory.eventsoom_kill delta). Network mode is enforced:nonemasks the NIC (default-deny baseline, VM-validated — no routable IP),openkeeps the bridge, andallowlist(SANDBOX-013, live-verified 2026-06-02) resolves+pins FQDNsCIDRs at create (pure `internalnetpolicypkg, with an SSRF/rebinding guard that rejects private/reserved destinations unless in the Koder-internal set) and enforces them via a per-instance **Incus network ACL** (security.acls.default.egress.action=reject+ allow rules, DNS to the pinned resolver) attached to eth0, torn down on destroy. Integration test proved1.1.1.1:443reachable /8.8.8.8:443` blocked. The remaining #013 item is the plan-tier gate (free→402), blocked on Koder ID entitlements (no tier claim / 402 infra yet). - Snapshot/restore (SANDBOX-016) —
POST .../snapshotarchives thesession FS (tar.gz over
incus exec,runtime.Archiver) into a tenant-scopedblobstore.Store;POST .../restoreunpacks it back. Pluggable store (FS backend now, kdb-blob drop-in later); 30-day default retention; real-VM validated (archive→extract across instances). Was a 501. sandbox.errorsaggregation (SANDBOX-009) — per-session, multi-tenantring (last 50) for browserruntime errors: `POST ...errors
(in-VM collector) +GET .../errors` (host "N errors" badge). The Chromium collector, UI badge, and "Fix it" child agent.run remain cross-component.- Host BTRFS subvolume SDK (
pkg/host/btrfs, SANDBOX-006) — importableSubvolumeManager(CreateMountSnapshotUnmountStatus + ScheduleDelete/ConfirmDelete behind a 7-day grace) for KPC's persistent per-instance rootfs; command-seam tested. Merged VM+subvolume provisioner + real-btrfs test remain. - fs.watch WS (SANDBOX-020, done) —
runtime.Watchercapability; theIncus driver polls the tree (~300ms, diffs
ListFiles) into createdmodifieddeleted events relayed overWS .../fs/watch(live file tree, "files changed" badge, preview auto-refresh). Real-VM validated. - Streaming roadmap (split from the SANDBOX-012 omnibus): gRPC Sandbox
service + REST parity (#021) DONE; remaining — VNC-over-WS (#022), per-tier rate limits + WS leak fuzz (#023).
Future integrations (direction-capture, needs RFC)
Two architecturally-natural integrations are triaged but gated on owner ratification via sandbox-RFC-002 (proposed — runtime tiers + placement, two scaling axes):
- kbox WASM runtime (SANDBOX-024, RFC-002 Axis 1 / Q1a=ratified) — a
runtime.Kboxdriver (session = host workspace;kbox runa WASI image; host-FS file IO; caps = Exec + File IO). Core + e2e shipped — real-kbox roundtrip GREEN on dev-linux-box (Exec captures output via the mounted workspace:run -d→wait→read/sandbox/.stdout|.exit→rm, since this kbox build'srunis detached + reaps fast containers). Remaining: WASM- runtime e2e + ~10 ms perf gate (blocked on boxwasmtime/WASI-image provisioning), Upstream cap, live streaming. The faster ephemeral tier below the sandbox, alongside Incus. - fleet-scheduled placement (SANDBOX-025) — the hyperscale path above
the sandbox: Fleet (
infra-RFC-004) schedules sandbox sessions across a fleet of hosts instead of the current single-host warm pool, lifting the SANDBOX-019 ceiling and removing the SPOF. Joint RFC withinfra/net/fleet.
Primary couplings
| Producer | Relationship |
|---|---|
infra/data/kdb-blob |
Artifact + snapshot storage |
infra/observe |
Pool metrics, OOM kills, session duration |
services/ai/trace |
Session + exec spans |
| Consumer | Relationship |
|---|---|
services/ai/kode |
Code-execution tool calls |
services/ai/agents |
Sandboxed step execution |
services/ai/workflow |
Code-step nodes in DAGs |
services/ai/playground |
Interactive code cells |
products/dev/kortex |
"Run this" panels in IDE |
services/ai/training |
Generated-code eval rigs |
infra/net/jet |
AI-sites preview: resolves sandbox Upstream per session (jet#149) |
RFC and bootstrap
- RFC:
sandbox-RFC-001-foundations.kmd— accepted 2026-05-09 - Bootstrap ticket:
services/ai/backlog/done/134-sandbox-bootstrap.md - Implementation tickets:
services/ai/sandbox/backlog/pending/{001..005} - Tracking ticket (long-standing):
services/ai/ai/backlog/pending/017-pluggable-execution-sandbox-backends.md
Self-hosted-first analysis (5 gates)
| Gate | Status | Notes |
|---|---|---|
| G1 Feature parity | pending | Firecracker + agent covers E2B's execfilesnapshot surface |
| G2 Performance | pending | Targets: < 200ms warm, < 1s cold, < 50ms exec dispatch |
| G3 Stability | pending | Pre-MVP; needs container-escape security review |
| G4 Capability | pending | Long-running compute deferred to runtime; distributed jobs out of v1 |
| G5 Critical-path readiness | pending | Unblocks every code-running agent surface |
- koder_kit Dart binding + KoderSandboxOutput widget (#017) —
engines/sdk/koder_kit/lib/src/sandbox/ships the Dart binding mirroringengines/sdk/go/sandbox/:SandboxClient(baseUrl, token, httpClient?)withcreateSession/exec/streamExec(SSE with sync fallback) /writeFile/readFile/close; modelsSession/CreateSessionOptions/ExecOptions/ExecResult/ExecFrame; typedSandboxException(code, message, status)with two-step JSON decode.ansi.dartminimal SGR parser (~80 LOC, zero deps) handles 8-color + bright variants, bolddimitalic/underline, resets, unknown SGR silently dropped.KoderSandboxOutput({frames, textStyle, stderrColor, autoScroll})Stateful widget subscribes to the frame stream, renders intoSelectableText.richwith auto-scroll, shows exit-code + duration status bar on done frame. Re-exported from the koder_kit barrel. 16 unit tests (parser + model round-trips + mock HTTP + typed-error rejection) PASS on dev-linux-dek. Unblocks playground cells, kortex Run panels, kruze console.
gRPC surface (SANDBOX-021, 2026-06-02)
A typed gRPC face (koder.ai.sandbox.v1.Sandbox) now sits alongside the REST router, reusing the same session.Manager (no logic fork) — for in-Stack consumers like services/ai/tools that want a typed client. This slice ships the session lifecycle + unary Exec; auth is a unary interceptor calling the shared auth.Middleware.Resolve (identical token rules to HTTP), with sxerr→gRPC status mapping and cross-tenant → NotFound. Listener on [server].grpc_addr (empty disables). Verified by a REST↔gRPC parity test (identical results against one Manager). File-op RPCs + server-streaming (ExecStream/WatchFiles) shipped; gRPC Exec p95 measured 5.8ms (bufconn+subprocess, 150ms bar). SANDBOX-021 complete — gRPC at full REST parity. (Production Incus-path exec p95 is a separate runtime-perf measurement.)
Koder ID auth — JWKS validator (SANDBOX-027)
The v0 static-key/dev auth was upgraded to verify real Koder ID access tokens locally. auth.NewMiddleware(Config{...}) optionally takes a JWKS Validator (engines/sdk/go/auth.JWKSValidator — the same pattern canvascacheembed use, no bespoke JWT); when configured, Resolve(ctx, token) (the SANDBOX-021 single decision point shared by the HTTP handler and both gRPC interceptors) verifies the bearer against the cached JWKS and projects the verified claims onto Identity{TenantID, UserID=sub, Scopes=scope}. The audience is enforced — [auth].audience (default sandbox) must match the token's aud, so a token minted for another Koder service is rejected (proven end-to-end with an RSA-signed JWKS fixture). A present-but-invalid token is rejected, never downgraded to the dev tenant. In prd the dev/static-key shortcuts are refused at startup (fail-loud) — the JWKS validator is the only valid prod auth path. Config: [auth] issuer_url / jwks_url (derived from issuer when empty) / audience, and top-level env. This makes per-tenant isolation real for consumers like Koder Kli once they present an aud=sandbox token (minted via the Koder ID token-exchange grant, id/engine#246).
WS terminal command override (SANDBOX-026)
The interactive terminal (GET /v1/sandbox/sessions/{id}/terminal) accepts ?cmd=<program> + repeatable ?arg=<argv> to launch a specific program in the PTY instead of the default interactive bash (runtime.TerminalOpts.Args; blank cmd → bash, backward compatible). The override is exec'd as argv to incus exec inside the per-tenant isolated instance — never through a host shell — so it carries no injection/escape surface. The Go SDK exposes it as sandbox.TerminalOptions{Cmd, Args}; Koder Kli uses it to launch the agent adapter directly (dropping its earlier exec <Bin>-over-bash workaround).