# Design — @suaveplan/sandbox ## File layout ```text packages/devtools/tooling/sandbox/ package.json # 0.1.0, server-tier, sideEffects: false vite.config.ts # multi-entry lib build, preserveModules vitest.config.ts # forks pool, istanbul, 100/100/100/100 tsconfig.lib.json # rootDir=./src, lib=ES2022, types=[node] README.md src/ index.ts # explicit named re-exports types/ sandbox.types.ts # SandboxConfig, SandboxResult, ... index.ts error-codes/ error-codes.ts # defineCodes("SANDBOX", [...]) error-codes.md error-codes.test.ts index.ts schemas/ schemas.ts # SandboxConfigSchema, ... schemas.md schemas.test.ts index.ts telemetry/ telemetry.ts # counters + histogram + record fns telemetry.md telemetry.test.ts index.ts audit-logger/ audit-logger.ts # AuditLogger class audit-logger.md audit-logger.test.ts index.ts resource-limiter/ resource-limiter.ts # ResourceLimiter with injectable Clock resource-limiter.md resource-limiter.test.ts index.ts module-resolver/ module-resolver.ts # ModuleResolver + BLOCKED + BUILTINS module-resolver.md module-resolver.test.ts index.ts code-transformer/ code-transformer.ts # CodeTransformer + BUILTIN_RULES code-transformer.md code-transformer.test.ts index.ts vm-engine/ vm-engine.ts # VmEngine (default; node:vm) vm-engine.md vm-engine.test.ts index.ts worker-engine/ worker-engine.ts # WorkerEngine (node:worker_threads) worker-engine.md worker-engine.test.ts index.ts spawn-engine/ spawn-engine.ts # SpawnEngine (node:child_process) spawn-engine.md spawn-engine.test.ts runner.ts # child entry point runner.md index.ts isolate-engine/ isolate-engine.ts # IsolateEngine (structural ivm) isolate-engine.md isolate-engine.test.ts index.ts pool/ pool.ts # SandboxPool with injectable Clock pool.md pool.test.ts index.ts result-streamer/ result-streamer.ts # ResultStreamer result-streamer.md result-streamer.test.ts index.ts sandbox/ sandbox.ts # createSandbox facade sandbox.md sandbox.test.ts index.ts ``` ## Engine selection `createSandbox({ engine })` accepts `"vm" | "worker" | "spawn" | "isolate"` plus a `customEngine` factory escape hatch. Default is `"vm"`. Engines all conform to the `SandboxEngine` contract: ```ts interface SandboxEngine { readonly name: string; execute(code, context, options): Promise> | SandboxResult; registerModule(mod): void; unregisterModule(name): boolean; reset(): void; destroy(): void; } ``` `vm-engine` and `isolate-engine` execute synchronously and wrap the result in `Promise.resolve` at the facade level. `worker-engine` and `spawn-engine` execute asynchronously. The facade always returns `Promise>`. ## Frozen-Realm pattern (vm-engine) `node:vm` shares the host realm but `createContext` lets us hand the script a context object whose intrinsics (`Object`, `Array`, etc.) are the host's. We mitigate prototype-chain escapes by: 1. `vm.createContext({})` with `codeGeneration: { strings: false, wasm: false }` to prevent `eval`/`Function` constructor inside the script. 2. `Object.freeze` on the context's `Object.prototype`, `Function.prototype`, `Array.prototype`, and the context's `Object` constructor itself. 3. Injecting `console` as a frozen proxy that delegates only to the captured log array. 4. `runInContext(wrappedCode, { timeout, breakOnSigint: true })` — synchronous timeout enforcement. The package README documents that `vm-engine` is NOT a security boundary against adversarial code; for that, use `worker-engine` (true OS-thread isolation) or `spawn-engine` (true process isolation). ## Test seams - `ResourceLimiter` and `SandboxPool` accept a `Clock` (`() => number`) defaulting to `Date.now`. Tests inject a `FakeClock` to assert sliding-window + idle-timeout behaviour without `setTimeout` flake. - `SpawnEngine` accepts a `ProcessSpawner` defaulting to `child_process.spawn`. Tests use the real spawner against a temp-file `runner.ts` fixture so the whole pipeline (stdin write, stdout read, timeout kill, JSON parse) is exercised end-to-end. - `WorkerEngine` uses real `node:worker_threads`. No stubbing — workers run inline strings through real V8. - `IsolateEngine` consumes a structural `IsolateFactory` so tests can wire a hand-written fake that implements the same contract a real `isolated-vm` instance would. The structural type means the optional peer dep stays optional and the tests stay deterministic. ## Error codes (SANDBOX.*) `defineCodes("SANDBOX", [...])` yields fifteen codes: - `INVALID_CONFIG` — Zod validation failed on config. - `INVALID_OPTIONS` — Zod validation failed on per-execution options. - `INVALID_MODULE` — Zod validation failed on a `SandboxModule`. - `BLOCKED_MODULE` — caller tried to register a denylisted module name. - `RATE_LIMITED` — sliding-window rate limit tripped. - `TIMEOUT` — execution exceeded the configured timeout. - `MEMORY_EXCEEDED` — V8 heap statistics reported usage above the limit. - `CODE_BLOCKED` — `CodeTransformer` blocked execution. - `CODE_TOO_LONG` — code exceeded `maxCodeLength`. - `ENGINE_DISPOSED` — caller invoked execute after `destroy()`. - `ENGINE_FAILED` — engine raised before producing a result envelope. - `WORKER_FAILED` — worker thread exited or errored before responding. - `SPAWN_FAILED` — child process exited non-zero without producing JSON. - `POOL_EXHAUSTED` — pool is at `maxSize` with no available engines. - `STREAM_CLOSED` — `pushChunk` after `complete()`. Every code is reachable from a call site exercised by a test. ## OTel instruments - Counter `sandbox_executions_total` — `{ engine, outcome }`. - Counter `sandbox_rule_blocks_total` — `{ rule }`. - Counter `sandbox_pool_acquisitions_total` — `{ result }` (`acquired` / `created` / `exhausted`). - Histogram `sandbox_execution_duration_ms` — `{ engine, outcome }`. ## Zod boundaries `createSandbox(config?)` parses `SandboxConfigPartialSchema` then merges with defaults. `execute(code, context?, options?)` parses the options object via `SandboxOptionsSchema`. `registerModule(mod)` parses `SandboxModuleSchema`. Failures raise `SandboxError` with `INVALID_CONFIG` / `INVALID_OPTIONS` / `INVALID_MODULE`. ## No dependents Net-new package on genesis — pure addition.