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