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/— sharedSandboxConfig,SandboxResult,SandboxOptions,SandboxModule,ExecutionContext,AuditLogEntry,AuditStatistics,Clock,ProcessSpawner,CommandResulttypes.error-codes/—SANDBOX_CODESviadefineCodes("SANDBOX", [...])+SandboxErrorclass.schemas/— Zod schemas forSandboxConfig,SandboxOptions,SandboxModuleboundaries.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/—AuditLoggerwith SHA-256 code hashing, search filters, statistics aggregation, andonAuditLogcallback hook.resource-limiter/—ResourceLimiterwith sliding-window rate limiting, output-size truncation, injectableClockfor deterministic tests.module-resolver/—ModuleResolverwithBLOCKED_MODULESdenylist,BUILTIN_SAFE_MODULES(Math/JSON/Date), custom module registry.code-transformer/—CodeTransformerwith the elevenBUILTIN_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-backedSandboxEngine(default). Frozen-Realm pattern viavm.createContext(...),vm.Scriptcompile +runInContext({ timeout }), captured console with output-size limit, module + context injection, heap snapshotting viav8.getHeapStatistics().worker-engine/— Real-node:worker_threads-backedSandboxEngine. Spawns an inlineWorker(eval: true) with code + context + modules sent viapostMessage, captures result via the message channel, enforces timeout by terminating the worker.spawn-engine/— Real-node:child_process-backed runner equivalent to upstream'sbun-spawn-runnerbut usingnode:child_process.spawnso it works under both Bun and Node. Pinned to currentprocess.execPath, sanitised env (SECRET/KEY/TOKEN/PASSWORD/CREDENTIAL/AUTHpatterns +HOME/USER/SHELL/LOGNAMEstripped), structured JSON IPC via stdin/stdout, timeout viaPromise.race.runner/— Child-process entry point invoked byspawn-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/— PluggableIsolateEngineover the upstream's isolated-vm contract (kept as a structural interface so the optional peer dep stays optional).pool/—SandboxPoolof pre-warmed engines, idle reclamation, recycle-on-N-executions, statistics surface.result-streamer/—ResultStreamerwith 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/executeFunctionreturns the structuredSandboxResult.
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:vmis not a security boundary.vm.runInContextshares the host realm; a determined attacker with code-injection can escape via prototype chain tricks. The package documents this explicitly:vm-engineis for isolation against accidents,worker-engineandspawn-engineare for isolation against adversaries.- Timeouts under
vm.runInContextonly cover synchronous code. A microtask scheduled inside the context outlives the timeout. The package documents this and recommendsworker-enginefor code that awaits. node:worker_threadseval: trueaccepts a string. This is the worker's documented contract for running ad-hoc code. The boundary is the worker thread, not the string parser.