Files
suaveplan/openspec/archive/2026-05-25-enterprise-sandbox/proposal.md
2026-05-27 23:29:53 +01:00

7.7 KiB

Proposal — Migrate @suaveplan/sandbox to genesis

  • Design: ./design.md
  • Spec delta: ./specs/sandbox/spec.md

Why

Upstream @suaveplan/sandbox (/mnt/s/web/suaveplan/packages/server/sandbox/) ships a working server-tier sandboxed execution environment built around Bun.spawn for process-isolated execution, an IsolateEngine contract for isolated-vm, a pre-warmed SandboxPool, an async ResultStreamer, a pattern-based CodeTransformer, an allowlist-based ModuleResolver, a sliding-window ResourceLimiter, and a SHA-256 AuditLogger. The genesis enterprise/sandbox.md audit calls out two cross-cutting gaps shared with every other migration: (a) no error-code registry — every failure path raises plain Error instances so consumers cannot discriminate programmatically, and (b) no OpenTelemetry surface even though sandbox execution is a load-bearing primitive in any agent or plugin host. The upstream also hard-codes a single transport (Bun.spawn pinned to process.argv[0]) with no seam for testing without an actual child process, and it lacks Zod-validated configuration on the public boundary.

Genesis cleans up the contract: every failure raises a typed SandboxError with a stable SANDBOX.* code, the engine surface gains a runner-tier abstraction that defaults to real node:vm (universally available, no Bun dependency) and real node:worker_threads for true isolation when requested, Bun.spawn is retained behind the same SandboxEngine contract for environments that prefer process boundaries, Zod schemas guard the public surface, and OTel counters + histograms tag every execution by engine, outcome, and phase.

What

Land @suaveplan/sandbox@0.1.0 at packages/devtools/tooling/sandbox/ with these feature modules (one folder per feature, sibling .md per implementation file):

  • types/ — shared SandboxConfig, SandboxResult, SandboxOptions, SandboxModule, ExecutionContext, AuditLogEntry, AuditStatistics, Clock, ProcessSpawner, CommandResult types.
  • error-codes/ — SANDBOX_CODES via defineCodes("SANDBOX", [...]) + SandboxError class.
  • schemas/ — Zod schemas for SandboxConfig, SandboxOptions, SandboxModule boundaries.
  • telemetry/ — OTel counters (sandbox_executions_total, sandbox_rule_blocks_total, sandbox_pool_acquisitions_total) and a histogram (sandbox_execution_duration_ms) tagged by engine + outcome.
  • audit-logger/ — AuditLogger with SHA-256 code hashing, search filters, statistics aggregation, and onAuditLog callback hook.
  • resource-limiter/ — ResourceLimiter with sliding-window rate limiting, output-size truncation, injectable Clock for deterministic tests.
  • module-resolver/ — ModuleResolver with BLOCKED_MODULES denylist, BUILTIN_SAFE_MODULES (Math/JSON/Date), custom module registry.
  • code-transformer/ — CodeTransformer with the eleven BUILTIN_RULES (block-process, block-require, block-import-dynamic, block-eval, block-function-constructor, block-global-this, block-proto-access, block-constructor-access, warn-while-true, warn-infinite-for, max-code-length) plus an injectable custom-rule slot.
  • vm-engine/ — Real-node:vm-backed SandboxEngine (default). Frozen-Realm pattern via vm.createContext(...), vm.Script compile + runInContext({ timeout }), captured console with output-size limit, module + context injection, heap snapshotting via v8.getHeapStatistics().
  • worker-engine/ — Real-node:worker_threads-backed SandboxEngine. Spawns an inline Worker (eval: true) with code + context + modules sent via postMessage, captures result via the message channel, enforces timeout by terminating the worker.
  • spawn-engine/ — Real-node:child_process-backed runner equivalent to upstream's bun-spawn-runner but using node:child_process.spawn so it works under both Bun and Node. Pinned to current process.execPath, sanitised env (SECRET/KEY/TOKEN/PASSWORD/CREDENTIAL/AUTH patterns + HOME/USER/SHELL/LOGNAME stripped), structured JSON IPC via stdin/stdout, timeout via Promise.race.
  • runner/ — Child-process entry point invoked by spawn-engine. Reads JSON from stdin, evaluates code via a strict-mode dynamic wrapper, writes JSON result to stdout. Ships as its own subpath export.
  • isolate-engine/ — Pluggable IsolateEngine over the upstream's isolated-vm contract (kept as a structural interface so the optional peer dep stays optional).
  • pool/ — SandboxPool of pre-warmed engines, idle reclamation, recycle-on-N-executions, statistics surface.
  • result-streamer/ — ResultStreamer with subscribe/unsubscribe, push chunks (log/result/error/complete), buffered + interval-flushed delivery, completion gating.
  • sandbox/ — createSandbox(config?) facade composing module resolver, resource limiter, audit logger, code transformer, and an engine selection. Sandbox.execute / executeFunction returns the structured SandboxResult.

Parity Checklist

Capability Upstream Migrated Notes
createSandbox(config) facade ✅ ✅ Same surface, Zod-validated config.
Sandbox.execute(code, context, opts) ✅ ✅
Sandbox.executeFunction(fn, args, opts) ✅ ✅
Sandbox.registerModule / unregisterModule ✅ ✅
Sandbox.getAuditLog / getStatistics / reset ✅ ✅
Bun.spawn process engine ✅ ⬆️ Replaced with portable node:child_process spawn (works under Bun + Node). Same isolation guarantees.
Frozen-Realm VM engine 🚫 ⬆️ New default — node:vm createContext + runInContext({ timeout }).
node:worker_threads engine 🚫 ⬆️ New — true OS-thread isolation for stronger CPU caps.
IsolateEngine (isolated-vm contract) ✅ ✅ Preserved as structural interface; tests use a real fake factory.
SandboxPool (warm + idle reclaim + recycle) ✅ ✅
ResultStreamer (subscribe/buffer/flush) ✅ ✅
CodeTransformer (10 built-in rules + max-code-length) ✅ ✅
ModuleResolver (blocked/allowed + builtins) ✅ ✅
ResourceLimiter (sliding-window rate limit + output trunc) ✅ ⬆️ Injectable Clock for deterministic tests.
AuditLogger (SHA-256 hash + filter + stats) ✅ ✅
BUILTIN_RULES export ✅ ✅
DEFAULT_CONFIG export ✅ ✅
Error-code registry 🚫 ⬆️ Added — SANDBOX.* codes via defineCodes.
OTel self-telemetry 🚫 ⬆️ Added — counters + histograms.
Zod-validated config 🚫 ⬆️ Added — SandboxConfigSchema, etc.
Subpath exports per feature 🚫 ⬆️ Added — every feature module is its own subpath.

Impact

Affected dependents

Run bunx turbo ls --filter=...@suaveplan/sandbox to enumerate. Net-new on genesis — no current dependents.

  • No dependents (verified by bunx turbo ls --filter=...@suaveplan/sandbox).

Risks

  • node:vm is not a security boundary. vm.runInContext shares the host realm; a determined attacker with code-injection can escape via prototype chain tricks. The package documents this explicitly: vm-engine is for isolation against accidents, worker-engine and spawn-engine are for isolation against adversaries.
  • Timeouts under vm.runInContext only cover synchronous code. A microtask scheduled inside the context outlives the timeout. The package documents this and recommends worker-engine for code that awaits.
  • node:worker_threads eval: true accepts a string. This is the worker's documented contract for running ad-hoc code. The boundary is the worker thread, not the string parser.