Move docs/ package mirror into document-navigator plugin

Relocate the ~6k-file docs tree from the repo root into
plugins/document-navigator/docs/ so the plugin is self-contained and
portable. docs/superpowers/ (specs and plans) stays at the repo root.

Moved via cp+rm rather than git mv because directory rename() fails with
EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the
identical-content files at commit time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -0,0 +1,108 @@
# @suaveplan/sandbox - Server-tier sandboxed code execution: node:vm, node:worker_threads, and node:child_process engines, pre-warmed pool, audit logger, sliding-window rate limiter, pattern-based code transformer, async result streamer.
Server-tier sandboxed code execution for `@suaveplan` plugin hosts and agent runtimes. Ships four `SandboxEngine` implementations (a `node:vm` default, real `node:worker_threads` for OS-thread isolation, real `node:child_process` for process isolation, and a structural `isolated-vm` adapter), a pre-warmed `SandboxPool`, an async `ResultStreamer`, a pattern-based `CodeTransformer`, an allowlist-based `ModuleResolver`, a sliding-window `ResourceLimiter`, and a SHA-256 `AuditLogger` — all behind a `createSandbox()` facade with Zod-validated config and full OpenTelemetry instrumentation.
<!-- begin:auto-docs -->
## Documentation tree
- [audit-logger](./src/audit-logger/audit-logger.md) — `AuditLogger` records every sandbox execution with a structured `AuditLogEntry`. Each entry carries an ISO-8601 timesta…
- [audit-logger](./src/audit-logger/audit-logger.md) — `AuditLogger` records every sandbox execution with a structured `AuditLogEntry`. Each entry carries an ISO-8601 timesta…
- [code-transformer](./src/code-transformer/code-transformer.md) — `CodeTransformer` is a defence-in-depth layer that pattern-matches dangerous code constructs before they reach the engi…
- [code-transformer](./src/code-transformer/code-transformer.md) — `CodeTransformer` is a defence-in-depth layer that pattern-matches dangerous code constructs before they reach the engi…
- [error-codes](./src/error-codes/error-codes.md) — `@suaveplan/sandbox` raises typed `SandboxError` instances rather than plain `Error` so consumers can discriminate fail…
- [error-codes](./src/error-codes/error-codes.md) — `@suaveplan/sandbox` raises typed `SandboxError` instances rather than plain `Error` so consumers can discriminate fail…
- [isolate-engine](./src/isolate-engine/isolate-engine.md) — `IsolateEngine` implements the `SandboxEngine` contract over a structural `IsolateFactory` interface that mirrors the `…
- [isolate-engine](./src/isolate-engine/isolate-engine.md) — `IsolateEngine` implements the `SandboxEngine` contract over a structural `IsolateFactory` interface that mirrors the `…
- [module-resolver](./src/module-resolver/module-resolver.md) — `ModuleResolver` is the gatekeeper between user code and the host's module surface. It maintains an allowlist of module…
- [module-resolver](./src/module-resolver/module-resolver.md) — `ModuleResolver` is the gatekeeper between user code and the host's module surface. It maintains an allowlist of module…
- [pool](./src/pool/pool.md) — `SandboxPool` amortises engine startup cost across many short executions by maintaining a warm pool of `SandboxEngine`…
- [pool](./src/pool/pool.md) — `SandboxPool` amortises engine startup cost across many short executions by maintaining a warm pool of `SandboxEngine`…
- [resource-limiter](./src/resource-limiter/resource-limiter.md) — `ResourceLimiter` enforces two complementary controls on the sandbox boundary: a sliding-window rate limit (so a single…
- [resource-limiter](./src/resource-limiter/resource-limiter.md) — `ResourceLimiter` enforces two complementary controls on the sandbox boundary: a sliding-window rate limit (so a single…
- [result-streamer](./src/result-streamer/result-streamer.md) — `ResultStreamer` delivers sandbox output incrementally rather than as a single end-of-execution dump. The most common u…
- [result-streamer](./src/result-streamer/result-streamer.md) — `ResultStreamer` delivers sandbox output incrementally rather than as a single end-of-execution dump. The most common u…
- [sandbox](./src/sandbox/sandbox.md) — `createSandbox` is the public entry point of the package. It composes the engine, `ModuleResolver`, `ResourceLimiter`,…
- [sandbox](./src/sandbox/sandbox.md) — `createSandbox` is the public entry point of the package. It composes the engine, `ModuleResolver`, `ResourceLimiter`,…
- [schemas](./src/schemas/schemas.md) — Zod schemas guard every public boundary of `@suaveplan/sandbox`. The package never trusts its caller; instead, every ar…
- [schemas](./src/schemas/schemas.md) — Zod schemas guard every public boundary of `@suaveplan/sandbox`. The package never trusts its caller; instead, every ar…
- [spawn-engine](./src/spawn-engine/spawn-engine.md) — `SpawnEngine` uses `node:child_process.spawn` for the strongest isolation the package offers: a fresh OS process spawne…
- [runner](./src/spawn-engine/runner.md) — `runner.ts` is the child-process entry point invoked by `SpawnEngine`. The parent process spawns it via `node:child_pro…
- [spawn-engine](./src/spawn-engine/spawn-engine.md) — `SpawnEngine` uses `node:child_process.spawn` for the strongest isolation the package offers: a fresh OS process spawne…
- [telemetry](./src/telemetry/telemetry.md) — `@suaveplan/sandbox` ships first-class OpenTelemetry instrumentation so consumers running the sandbox at any scale can…
- [telemetry](./src/telemetry/telemetry.md) — `@suaveplan/sandbox` ships first-class OpenTelemetry instrumentation so consumers running the sandbox at any scale can…
- [vm-engine](./src/vm-engine/vm-engine.md) — `VmEngine` is the default sandbox engine. It uses `node:vm` directly: caller code is compiled with `new vm.Script(...)`…
- [vm-engine](./src/vm-engine/vm-engine.md) — `VmEngine` is the default sandbox engine. It uses `node:vm` directly: caller code is compiled with `new vm.Script(...)`…
- [worker-engine](./src/worker-engine/worker-engine.md) — `WorkerEngine` uses real `node:worker_threads` for true OS-thread isolation. Each `execute` call spawns a fresh `Worker…
- [worker-engine](./src/worker-engine/worker-engine.md) — `WorkerEngine` uses real `node:worker_threads` for true OS-thread isolation. Each `execute` call spawns a fresh `Worker…
<!-- end:auto-docs -->
## Install
```bash
bun add @suaveplan/sandbox
```
## Quick start
```ts
import { createSandbox } from "@suaveplan/sandbox";
const sandbox = createSandbox({ timeout: 5000, engine: "vm" });
const result = await sandbox.execute("2 + 3");
console.log(result.result); // 5
sandbox.destroy();
```
## Engine selection
| Engine | Boundary | Use when |
|---|---|---|
| `vm` (default) | `node:vm` context | Trusted accident-isolation, fastest path. |
| `worker` | `node:worker_threads` | Adversarial code, OS-thread isolation. |
| `spawn` | `node:child_process` | Adversarial code, full process isolation. |
| `isolate` | `isolated-vm` (optional dep) | Strongest in-process isolation. |
Wire `engine: "isolate"` with a `customEngine` factory that constructs a real
`IsolateEngine` over your installed `isolated-vm` peer dependency.
## Modules
Co-located docs for every public surface:
- [error-codes](./src/error-codes/error-codes.md) — `SANDBOX_CODES`,
`SandboxError`.
- [schemas](./src/schemas/schemas.md) — Zod boundaries.
- [telemetry](./src/telemetry/telemetry.md) — OTel instruments + recorders.
- [audit-logger](./src/audit-logger/audit-logger.md) — execution audit log.
- [resource-limiter](./src/resource-limiter/resource-limiter.md) —
rate-limit + output truncation.
- [module-resolver](./src/module-resolver/module-resolver.md) —
denylist/allowlist + builtins.
- [code-transformer](./src/code-transformer/code-transformer.md) —
regex-based dangerous-pattern guard.
- [vm-engine](./src/vm-engine/vm-engine.md) — default engine.
- [worker-engine](./src/worker-engine/worker-engine.md) — OS-thread engine.
- [spawn-engine](./src/spawn-engine/spawn-engine.md) — process engine.
- [runner](./src/spawn-engine/runner.md) — spawn-engine child entry point.
- [isolate-engine](./src/isolate-engine/isolate-engine.md) — `isolated-vm`
structural adapter.
- [pool](./src/pool/pool.md) — pre-warmed engine pool.
- [result-streamer](./src/result-streamer/result-streamer.md) — async
streaming output.
- [sandbox](./src/sandbox/sandbox.md) — `createSandbox` facade.
## Security model
`vm-engine` shares the host realm and is NOT a security boundary against
adversarial code — `vm.runInContext` can be escaped via prototype-chain
tricks. For adversarial isolation, use `worker-engine` or `spawn-engine`,
both of which sit behind real OS-level boundaries. The `code-transformer`
is a defence-in-depth layer that catches the obvious accidents
(`process.exit`, `require`, `eval`, etc.); it is not a substitute for an
engine-level boundary.
## Tier
Server (`node:vm` / `node:worker_threads` / `node:child_process` / `node:crypto`).
Targets Node ≥ 20 (and Bun, which provides the same APIs).

View File

@@ -0,0 +1,65 @@
# AuditLogger
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`AuditLogger` records every sandbox execution with a structured `AuditLogEntry`. Each entry carries an ISO-8601 timestamp, a short SHA-256 code hash (first 16 hex chars — long enough to identify the script across executions, short enough to keep audit pages readable), execution duration, success boolean, memory delta, and an optional error string. The logger is the only place where raw user code is hashed; downstream consumers see only the hash, so audit trails can be persisted without leaking the script content.
Search filters, statistics aggregation, and a synchronous `onLog` hook let the logger plug into any monitoring stack — feed entries into Prometheus via the callback, persist them to a database from `getEntries()`, or surface aggregate health in a dashboard via `getStatistics()`.
## Features
- Short SHA-256 code hashing (first 16 hex chars) via `node:crypto`.
- Filter by success boolean, ISO timestamp window, or row limit.
- `getStatistics()` aggregates total runs, success/failure counts,
success rate, average duration, and total memory used.
- `onLog` callback fires synchronously per entry for streaming integrations.
- `clear()` resets the in-memory log for long-lived loggers.
## Usage: Basic
```ts
import { AuditLogger } from "@suaveplan/sandbox/audit-logger";
const log = new AuditLogger();
log.log("1 + 1", 10, true, 100);
console.log(log.getStatistics().totalRuns); // 1
```
## Usage: Advanced
Stream entries to an external sink while preserving the in-memory log for
ad-hoc queries:
```ts
import { AuditLogger } from "@suaveplan/sandbox/audit-logger";
const log = new AuditLogger(async (entry) => {
await fetch("https://audit.example.com", {
method: "POST",
body: JSON.stringify(entry),
});
});
```
## API Reference
- `new AuditLogger(onLog?)` — constructor.
- `log(code, duration, success, memoryUsed, error?): AuditLogEntry`.
- `getEntries(): readonly AuditLogEntry[]`.
- `search(filter: AuditFilter): readonly AuditLogEntry[]`.
- `getStatistics(): AuditStatistics`.
- `clear(): void`.
## Implementation Notes
The logger stores entries in insertion order in a plain array; for large audit
windows callers should periodically drain via `getEntries()` + `clear()`. The
hash truncation is deterministic — the same code string always hashes to the
same 16-char prefix — so log readers can grep by hash across multiple log
snapshots. The `onLog` callback is invoked synchronously on the calling
thread; expensive sinks should defer work via `queueMicrotask` to avoid
blocking the audit path.

View File

@@ -0,0 +1,70 @@
# CodeTransformer
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`CodeTransformer` is a defence-in-depth layer that pattern-matches dangerous code constructs before they reach the engine. The eleven built-in rules catch the obvious foot-guns — `process.exit`, `require`, dynamic `import`, `eval`, the Function constructor, `globalThis`, prototype-chain probes, and the `constructor['constructor']` escape — plus two warning-only rules for infinite loops and a `maxCodeLength` guard that caps script size at 100KB by default.
The transformer is regex-based by design. A full AST parser would catch contrived bypasses (commented-out code that gets reassembled at runtime, string concatenation that builds a denylisted identifier) but at the cost of pulling in a megabyte of parser. The engine-level boundary (`vm-engine`, `worker-engine`, `spawn-engine`) is the actual security layer; the transformer raises the floor on accidental harm and surfaces clearly-named blocks for malicious harm.
## Features
- Ten built-in security rules covering the documented OWASP-recommended
dangerous-pattern catalogue for in-process script execution.
- One `max-code-length` guard for script size.
- `enableBuiltinRules: false` opt-out for callers shipping their own rules.
- `customRules` slot for additive caller rules.
- `transform(code)` returns `{ code, blocked, blockedBy?, applied, warnings }`.
- `wouldBlock(code)` returns `{ blocked, reason? }` for cheap pre-flight checks.
- Every block fires an OTel counter via `recordRuleBlock(rule)`.
## Usage: Basic
```ts
import { CodeTransformer } from "@suaveplan/sandbox/code-transformer";
const t = new CodeTransformer();
const r = t.transform("1 + 1");
console.log(r.blocked, r.applied);
```
## Usage: Advanced
Extend the transformer with a custom auditing rule that rewrites a vendor
SDK call into a logged wrapper before execution:
```ts
import { CodeTransformer } from "@suaveplan/sandbox/code-transformer";
const t = new CodeTransformer({
customRules: [
{
name: "audit-vendor-call",
pattern: /\bvendor\.send\(/g,
replacement: "vendor.__audited_send(",
blocking: false,
},
],
});
```
## API Reference
- `BUILTIN_RULES: readonly TransformRule[]`.
- `new CodeTransformer(config?: CodeTransformerConfig)`.
- `transform(code: string): TransformResult`.
- `wouldBlock(code: string): { blocked: boolean; reason?: string }`.
- `getRules(): readonly TransformRule[]`.
## Implementation Notes
The rule list is iterated in declaration order; the first matching blocking
rule wins. Replacement rules apply globally (the `g` flag is forced on every
regex) so multiple occurrences are rewritten in one pass. Warning-only rules
that aren't replacements only append a warning string — they never mutate the
code. Pattern strings (vs. literal regex) get the `g` flag added in
`ruleRegex` so caller-supplied string patterns behave consistently with
caller-supplied regex patterns.

View File

@@ -0,0 +1,68 @@
# Error codes
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`@suaveplan/sandbox` raises typed `SandboxError` instances rather than plain `Error` so consumers can discriminate failure modes programmatically. Every code is registered via `@suaveplan/error`'s `defineCodes("SANDBOX", [...])`, producing namespaced string values of the form `SANDBOX.<REASON>` that survive worker, child-process, and RPC boundaries because the comparison is value-equality on a string rather than `instanceof` against a class identity that does not cross realms.
The registry is the contract between the package and its consumers: every code listed here is reachable from a real call site inside `src/` and is exercised by a corresponding test. The set is intentionally small — fifteen reasons cover everything the package can fail at — and growing it requires an OpenSpec change so downstream code never assumes a code exists that isn't actually thrown.
## Features
- Fifteen registered codes covering every public failure path.
- `SandboxError` extends `SuaveplanError`, so cause-chain traversal, OTel
span-event mapping, and Zod-validated wire format all work.
- Optional structured `details` payload threaded through `SuaveplanError`'s
`context` field for diagnostic redaction.
## Usage: Basic
```ts
import { SANDBOX_CODES, SandboxError } from "@suaveplan/sandbox/error-codes";
throw new SandboxError(
SANDBOX_CODES.TIMEOUT,
"Script execution timed out",
{ details: { timeoutMs: 5000 } }
);
```
## Usage: Advanced
Discriminate at the call site without `instanceof`:
```ts
import { SANDBOX_CODES, SandboxError } from "@suaveplan/sandbox/error-codes";
function handle(err: unknown): void {
if (
err &&
typeof err === "object" &&
"code" in err &&
(err as SandboxError).code === SANDBOX_CODES.RATE_LIMITED
) {
// back off
return;
}
throw err;
}
```
## API Reference
- `SANDBOX_CODES` — frozen registry produced by `defineCodes`. Indexed by
reason (`SANDBOX_CODES.TIMEOUT` → `"SANDBOX.TIMEOUT"`).
- `SandboxCode` — union of every registered code value.
- `SandboxError(code, message, options?)` — `options.cause`, `options.details`.
## Implementation Notes
Codes are sorted by lifecycle order — config validation first, then runtime
limits, then engine-specific failures, then pool/streamer failures — so a
reader following the file matches the order consumers see failures in. The
`details` field is always assigned (defaulting to `{}`) so consumers can read
it without an optional-chain when they know they constructed the error with
context.

View File

@@ -0,0 +1,78 @@
# IsolateEngine
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`IsolateEngine` implements the `SandboxEngine` contract over a structural `IsolateFactory` interface that mirrors the `isolated-vm` package's API. This keeps `isolated-vm` an optional peer dependency: consumers who want its stronger isolation guarantees wire their own factory backed by `new ivm.Isolate(...)`, consumers who don't pay no install cost. Tests use a hand-written fake factory that satisfies the same structural contract.
`isolated-vm` provides genuine V8 isolate separation — a separate heap, separate intrinsics, hard memory + timeout limits enforced inside V8 itself. When a consumer needs stronger isolation than `vm-engine` offers but cannot afford the per-call cost of `worker-engine` or `spawn-engine`, `IsolateEngine` plus a real `isolated-vm` factory is the right pick.
## Features
- Structural `IsolateFactory` / `IsolateInstance` / `IsolateContext` /
`IsolateReference` / `IsolateScript` interfaces.
- `executeSync` companion for pool callers.
- Captured console via injected `__log`/`__warn`/`__error` proxies.
- Module + context-variable injection.
- Heap stats via `getHeapStatisticsSync` (returns null after destroy).
- `reset` disposes + re-creates the underlying isolate.
## Usage: Basic
```ts
import ivm from "isolated-vm";
import { IsolateEngine } from "@suaveplan/sandbox/isolate-engine";
const engine = new IsolateEngine({
timeout: 5000,
maxOutputSize: 1024,
memoryLimit: 32 * 1024 * 1024,
factory: {
createIsolate: (memLimit) =>
new ivm.Isolate({ memoryLimit: memLimit / (1024 * 1024) }),
},
});
```
## Usage: Advanced
Provide a custom factory backed by a test double for deterministic unit
tests:
```ts
import { IsolateEngine } from "@suaveplan/sandbox/isolate-engine";
const engine = new IsolateEngine({
timeout: 5000,
maxOutputSize: 1024,
memoryLimit: 32 * 1024 * 1024,
factory: {
createIsolate: () => myDeterministicIsolate,
},
});
```
## API Reference
- `IsolateFactory` — `createIsolate(memoryLimit): IsolateInstance`.
- `IsolateInstance` — `createContextSync`, `compileScriptSync`,
`getHeapStatisticsSync`, `dispose`.
- `IsolateContext` — `global: IsolateReference`, `release`.
- `IsolateReference` — `setSync`, `getSync`.
- `IsolateScript` — `runSync(context, { timeout? })`.
- `new IsolateEngine(options: IsolateEngineOptions)`.
- `execute`, `executeSync`, `registerModule`, `unregisterModule`,
`getHeapStatistics`, `reset`, `destroy`.
## Implementation Notes
The console proxy is set up by injecting three reference functions
(`__log`/`__warn`/`__error`) into the isolate's global, then prepending a
`const console = { log: ..., warn: ..., error: ... }` to the compiled script.
The real `isolated-vm` API serialises across the realm boundary with
`ExternalCopy` and `Reference`; the structural type intentionally does not
prescribe that machinery because consumer adapters may wrap arguments
differently depending on their isolation needs.

View File

@@ -0,0 +1,70 @@
# ModuleResolver
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`ModuleResolver` is the gatekeeper between user code and the host's module surface. It maintains an allowlist of modules the sandbox may consult, refuses to register anything on the denylist (`fs`, `net`, `child_process`, `cluster`, `dgram`, `dns`, `http`, `https`, `http2`, `tls`, `worker_threads`, `process`, `os`, `path`, `readline`, `repl`, `vm`), and ships three built-in safe modules (`Math`, `JSON`, `Date`) auto-loaded when their names appear in the allowed list.
The resolver is the only place in the package where module names are reasoned about. Engines consume `resolver.getModules()` and inject the returned exports into the execution context; they never reason about names directly. This makes the security boundary auditable in one file rather than spread across four engine implementations.
## Features
- Hard-coded denylist of 17 dangerous Node module names.
- Three built-in safe modules with curated exports.
- `register` / `unregister` for custom modules.
- `isBlocked` / `isAllowed` / `getBlockedModules` introspection helpers.
- `BLOCKED_MODULES` and `BUILTIN_SAFE_MODULES` exported for downstream
composition.
## Usage: Basic
```ts
import { ModuleResolver } from "@suaveplan/sandbox/module-resolver";
const resolver = new ModuleResolver(["Math"]);
resolver.register({ name: "config", exports: { factor: 21 } });
console.log(resolver.getModules().length); // 2
```
## Usage: Advanced
Surface the denylist to a UI that shows users why their module registration
was rejected:
```ts
import {
BLOCKED_MODULES,
ModuleResolver,
} from "@suaveplan/sandbox/module-resolver";
function offerSuggestions(name: string): string {
if (BLOCKED_MODULES.has(name)) {
return `"${name}" is denylisted — use the sandbox's host bridge instead.`;
}
return "";
}
```
## API Reference
- `new ModuleResolver(allowedNames?: readonly string[])`.
- `register(module: SandboxModule): void` — throws `SandboxError(BLOCKED_MODULE)`.
- `unregister(name: string): boolean`.
- `getModules(): readonly SandboxModule[]`.
- `isBlocked(name: string): boolean`.
- `isAllowed(name: string): boolean`.
- `getBlockedModules(): readonly string[]`.
- `BLOCKED_MODULES: ReadonlySet<string>`.
- `BUILTIN_SAFE_MODULES: Readonly<Record<string, () => SandboxModule>>`.
## Implementation Notes
The denylist is hard-coded rather than configurable because it lists Node
intrinsics whose mere presence inside a sandbox compromises isolation
regardless of whether the engine is a vm context, a worker, or a child
process. The `BUILTIN_SAFE_MODULES` factory returns a fresh module per call so
custom rebinding by one sandbox does not leak into another sharing the
resolver constructor.

View File

@@ -0,0 +1,76 @@
# SandboxPool
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`SandboxPool` amortises engine startup cost across many short executions by maintaining a warm pool of `SandboxEngine` instances. The pool pre-warms to `minSize` on construction, lazily grows up to `maxSize` under load, recycles engines that exceed `maxExecutionsPerEngine` (so leak-prone engines like long-lived workers don't accumulate state), and reclaims idle engines above `minSize` whose `lastUsed` exceeds `idleTimeoutMs` via the explicit `cleanupTick()` call.
The pool's clock is injectable — defaulting to `Date.now` in production, replaced by a `FakeClock` in tests so the idle-reclaim behaviour can be asserted without flake. The cleanup is invoked explicitly rather than on an internal timer so the consumer controls scheduling and the pool stays single-threaded.
## Features
- Pre-warmed up to `minSize` on construction.
- Lazy growth up to `maxSize` on acquisition.
- `POOL_EXHAUSTED` surfaced in a `SandboxResult` when the pool cannot grow.
- Per-engine execution count, recycle on threshold.
- `cleanupTick()` reclaims idle engines down to `minSize`.
- Injectable `Clock` for deterministic tests.
- OTel `recordPoolAcquisition` for each acquisition (acquired / created /
exhausted).
- Aggregate `stats()` surface.
## Usage: Basic
```ts
import { VmEngine } from "@suaveplan/sandbox/vm-engine";
import { SandboxPool } from "@suaveplan/sandbox/pool";
const pool = new SandboxPool({
minSize: 2,
maxSize: 8,
engineConfig: { timeout: 5000, maxOutputSize: 1024, memoryLimit: 5e7 },
factory: (cfg) => new VmEngine(cfg),
});
const r = await pool.execute("1 + 1");
pool.destroy();
```
## Usage: Advanced
Drive cleanup against a `FakeClock` so idle reclamation can be asserted:
```ts
let now = 0;
const pool = new SandboxPool({
engineConfig,
factory,
minSize: 1,
maxSize: 4,
idleTimeoutMs: 1000,
clock: () => now,
});
// run a few executions, then advance time:
now = 5000;
pool.cleanupTick();
```
## API Reference
- `new SandboxPool(config)`.
- `execute(code, context?, options?): Promise<SandboxResult>`.
- `size()`, `available()`, `inUse()`, `stats()`.
- `warmUp()`, `cleanupTick()`, `destroy()`.
- `DEFAULT_POOL_CONFIG` — exported defaults for downstream composition.
## Implementation Notes
`acquire` prefers idle engines over creating new ones — this keeps warmed
engines reusable rather than stranding them behind a fresh allocation when an
old engine is right there. Recycling is reactive (on release, after the
threshold is crossed) rather than proactive (at acquisition time) so a
recycle never delays the path that needs the engine. The `clock` lookup
happens once per `lastUsed`/`createdAt` write so callers can swap the clock
implementation without re-engineering the pool's call graph.

View File

@@ -0,0 +1,75 @@
# ResourceLimiter
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`ResourceLimiter` enforces two complementary controls on the sandbox boundary: a sliding-window rate limit (so a single caller cannot exhaust the engine with rapid-fire executions) and an output-size cap (so log capture cannot grow unbounded inside long-running scripts). Both controls operate against an injectable `Clock` so tests can advance time deterministically rather than relying on `setTimeout` and observed wall-clock drift.
The limiter has no opinion about *what* it limits — the same instance is reused across executions, and the rate-limit window slides naturally as time passes. When the configured `maxExecutionsPerWindow` is `undefined`, the limiter is effectively a no-op for rate control; the output truncation still applies.
## Features
- Sliding-window rate limiting tracked via a timestamp array, pruned on every
`checkRateLimit()` call.
- Injectable `Clock` (`() => number`); defaults to `Date.now`.
- `truncateOutput(str)` returns the original string when under the limit,
otherwise a truncation with a sentinel `[output truncated at N bytes]`.
- Total + window-scoped execution counters for observability.
- `reset()` clears state without re-allocating the limiter.
## Usage: Basic
```ts
import { ResourceLimiter } from "@suaveplan/sandbox/resource-limiter";
const limiter = new ResourceLimiter({
maxExecutionsPerWindow: 10,
rateLimitWindow: 60_000,
maxOutputSize: 1024,
});
limiter.checkRateLimit();
limiter.recordExecution();
```
## Usage: Advanced
Drive the limiter against a `FakeClock` in tests to assert the sliding-window
behaviour without flaky waits:
```ts
import { ResourceLimiter } from "@suaveplan/sandbox/resource-limiter";
let now = 0;
const limiter = new ResourceLimiter({
maxExecutionsPerWindow: 1,
rateLimitWindow: 1000,
maxOutputSize: 1024,
clock: () => now,
});
limiter.checkRateLimit();
limiter.recordExecution();
now = 2000; // advance past the window
limiter.checkRateLimit(); // succeeds
```
## API Reference
- `new ResourceLimiter(config: LimiterConfig)`.
- `checkRateLimit(): void` — throws `SandboxError(RATE_LIMITED)`.
- `recordExecution(): void`.
- `getTotalExecutions(): number`.
- `getWindowExecutions(): number`.
- `truncateOutput(s: string): string`.
- `reset(): void`.
## Implementation Notes
The timestamp array is rebuilt on every `checkRateLimit()` call via
`Array.prototype.filter`. For limiters with very high throughput (>10k
windowed events) a circular buffer would be more efficient; the current
implementation prefers clarity because rate limits at this layer are typically
a low single-digit count of events per window.

View File

@@ -0,0 +1,69 @@
# ResultStreamer
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`ResultStreamer` delivers sandbox output incrementally rather than as a single end-of-execution dump. The most common use case is long-running scripts that emit logs over many seconds: subscribers receive `StreamChunk`s as soon as the buffer either reaches `maxBufferSize` or the next `flushIntervalMs` tick fires, instead of waiting for the whole execution to finish. A final `complete` chunk carries the structured result envelope (success / duration / memoryUsed / error) so subscribers can tear down their UI state at the right moment.
The streamer enforces a strict completion lifecycle: once `complete()` has fired, any further `pushChunk` raises `SandboxError(STREAM_CLOSED)`. This prevents post-completion writes from corrupting downstream state and surfaces programming errors immediately rather than letting them silently drop chunks.
## Features
- Four chunk types: `log`, `result`, `error`, `complete`.
- Per-chunk wall-clock timestamp and elapsed-since-start milliseconds.
- Configurable `maxBufferSize` (auto-flush threshold) and
`flushIntervalMs` (auto-flush cadence).
- Subscribe / unsubscribe with returned disposer.
- `complete` is idempotent (second call is a no-op).
- Strict post-complete `STREAM_CLOSED` enforcement.
## Usage: Basic
```ts
import { ResultStreamer } from "@suaveplan/sandbox/result-streamer";
const s = new ResultStreamer();
s.subscribe((c) => console.log(c.type, c.data));
s.start();
s.pushLog("processing...");
s.complete({ success: true, result: 42, error: undefined, duration: 100, memoryUsed: 0, logs: [] });
```
## Usage: Advanced
Pipe stream chunks straight into a Server-Sent-Events response:
```ts
import { ResultStreamer } from "@suaveplan/sandbox/result-streamer";
const s = new ResultStreamer({ flushIntervalMs: 50 });
s.subscribe((c) => {
res.write(`event: ${c.type}\ndata: ${c.data}\n\n`);
});
s.start();
// ...drive the sandbox, push chunks, await complete
```
## API Reference
- `new ResultStreamer(config?: Partial<ResultStreamerConfig>)`.
- `start()`, `stop()`, `destroy()`.
- `subscribe(listener): () => void`.
- `pushLog(message)`, `pushError(error)`, `pushResult(data)`.
- `complete(result: SandboxResult): void`.
- `flush()`, `bufferSize()`, `isCompleted()`, `listenerCount()`.
- `DEFAULT_STREAMER_CONFIG` — exported defaults.
## Implementation Notes
`pushChunk` auto-flushes when the buffer reaches `maxBufferSize`, so callers
that don't run the interval flusher (`flushIntervalMs: 0`) still see chunks
land at subscribers without manual `flush()` calls — useful for tests that
want deterministic delivery. The `elapsed` field is recorded against
`startTime`; when the streamer is used without `start()`, `elapsed` carries
the value relative to `performance.now() - 0`, which is the host's high-res
timer origin — a slight oddity but a tolerable one because the documented
contract is to call `start()` first.

View File

@@ -0,0 +1,79 @@
# createSandbox (facade)
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`createSandbox` is the public entry point of the package. It composes the engine, `ModuleResolver`, `ResourceLimiter`, `CodeTransformer`, and `AuditLogger` into a single ergonomic surface. Callers configure the sandbox via a Zod-validated partial config that gets merged with `DEFAULT_CONFIG`; the resolved config is exposed on `sandbox.config` for diagnostic purposes.
Each `execute` call walks the configured pipeline: check the rate limiter, transform the code against the security rules, record the execution, drive the engine, and log the audit entry. The pipeline is the same regardless of whether the underlying engine is `vm`, `worker`, `spawn`, or a custom `isolate` — engine selection is the only thing that varies.
## Features
- Zod-validated config / options / module boundaries.
- Engine selection via the `engine` config key (`vm` default).
- `customEngine` escape hatch for advanced configurations (e.g. wiring a
real `isolated-vm` factory).
- Audit log + statistics + per-call `onAuditLog` callback.
- Sliding-window rate limiter trips a `SandboxError(RATE_LIMITED)`.
- Code transformer auto-blocks `process.*`, `require`, `eval`, etc.
- `executeFunction` wraps a host function for sandbox execution.
- `reset()` clears rate-limit + audit state but keeps the engine alive.
- `destroy()` disposes the engine permanently.
## Usage: Basic
```ts
import { createSandbox } from "@suaveplan/sandbox/sandbox";
const sandbox = createSandbox();
const r = await sandbox.execute("2 + 3");
console.log(r.result); // 5
sandbox.destroy();
```
## Usage: Advanced
Wire a custom engine factory for true `isolated-vm` isolation:
```ts
import ivm from "isolated-vm";
import { createSandbox } from "@suaveplan/sandbox/sandbox";
import { IsolateEngine } from "@suaveplan/sandbox/isolate-engine";
const sandbox = createSandbox({
engine: "isolate",
customEngine: (_kind, cfg) =>
new IsolateEngine({
...cfg,
factory: {
createIsolate: (mem) =>
new ivm.Isolate({ memoryLimit: mem / (1024 * 1024) }),
},
}),
});
```
## API Reference
- `createSandbox(options?: CreateSandboxOptions): Sandbox`.
- `Sandbox.execute(code, context?, options?): Promise<SandboxResult>`.
- `Sandbox.executeFunction(fn, args?, options?): Promise<SandboxResult>`.
- `Sandbox.registerModule(mod): void`.
- `Sandbox.unregisterModule(name): boolean`.
- `Sandbox.getAuditLog(filter?): readonly AuditLogEntry[]`.
- `Sandbox.getStatistics(): AuditStatistics`.
- `Sandbox.reset(): void`.
- `Sandbox.destroy(): void`.
- `Sandbox.config: SandboxConfig` (resolved).
## Implementation Notes
`syncEngineModules` re-registers the resolver's module list on the engine
before every execute. This is wasteful in the steady state (the engine
already knows about the modules) but keeps the engine's internal module
view in lockstep with the resolver's view without requiring engines to
implement a "diff against my current modules" API. Engines themselves
deduplicate by name on `registerModule`, so the redundant calls are cheap.

View File

@@ -0,0 +1,65 @@
# Schemas
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Zod schemas guard every public boundary of `@suaveplan/sandbox`. The package never trusts its caller; instead, every argument that crosses the API boundary is parsed against a strict schema and rejected with a typed `SandboxError` (`INVALID_CONFIG` / `INVALID_OPTIONS` / `INVALID_MODULE`) when the shape does not match. This means downstream consumers get the same validation guarantees whether they pass typed TypeScript objects or arbitrary JSON deserialised from a network boundary.
The `parseConfig` helper additionally merges parsed partials with `DEFAULT_CONFIG` so the rest of the package consumes a fully resolved `SandboxConfig` rather than a partial. This keeps the merge logic in one place rather than re-implementing it across the facade and the engines.
## Features
- Strict-mode schemas (`.strict()`) so unknown keys are rejected.
- `parseConfig`, `parseOptions`, `parseModule` helpers that translate Zod
errors into `SandboxError` with the right code.
- `SandboxEngineKindSchema` enum used as the discriminant for engine selection.
- `safeParse` under the hood so the error path stays on the catch-side and
the success path stays on the happy path.
## Usage: Basic
```ts
import { parseConfig } from "@suaveplan/sandbox/schemas";
const cfg = parseConfig({ timeout: 1000, engine: "worker" });
```
## Usage: Advanced
When validating untrusted input (HTTP, IPC), surface the raw Zod issues to
the caller via `SandboxError.details.issues`:
```ts
import { SandboxError } from "@suaveplan/sandbox/error-codes";
import { parseConfig } from "@suaveplan/sandbox/schemas";
try {
parseConfig(JSON.parse(httpBody));
} catch (err) {
if (err instanceof SandboxError) {
return Response.json({ issues: err.details.issues }, { status: 400 });
}
throw err;
}
```
## API Reference
- `SandboxConfigPartialSchema` — `z.ZodObject` for `Partial<SandboxConfig>`.
- `SandboxOptionsSchema` — `z.ZodObject` for `SandboxOptions`.
- `SandboxModuleSchema` — `z.ZodObject` for `SandboxModule`.
- `SandboxEngineKindSchema` — `z.ZodEnum<['vm','worker','spawn','isolate']>`.
- `parseConfig(raw): SandboxConfig` — merges with `DEFAULT_CONFIG`.
- `parseOptions(raw): SandboxOptions`.
- `parseModule(raw): SandboxModule`.
## Implementation Notes
`onAuditLog` is validated via `z.custom` rather than `z.function` because Zod's
function schema would otherwise wrap the callback in a runtime validator and
break referential equality, breaking the "consumer-passes-its-own-callback"
contract. The custom validator only checks `typeof === "function"` (or
undefined), which is the actual invariant the consumer cares about.

View File

@@ -0,0 +1,66 @@
# Runner (spawn-engine child entry)
## Purpose
`runner.ts` is the child-process entry point invoked by `SpawnEngine`. The parent process spawns it via `node:child_process.spawn`, writes a JSON `RunnerInput` envelope to its stdin (closing the pipe to signal end-of-input), and reads a JSON `RunnerOutput` envelope from its stdout. The runner runs inside a fresh OS process with a sanitised environment — that process boundary is the security guarantee, not anything the runner itself does.
The runner exports `runRunner(input)` as a pure function so the parent can also test the evaluation pipeline without spawning a child. The `main()` entry point reads stdin, parses JSON, calls `runRunner`, and writes stdout — exposed for tests that want to drive the full I/O surface.
## Features
- Pure `runRunner(input): RunnerOutput` for in-process testing.
- `readStdinAll()` async stdin slurper.
- `main()` orchestrator.
- Auto-invokes `main()` when the file is launched directly (matched by
filename ending in `runner.{js,ts,cjs,mjs}`).
- `stringify(value)` JSON-safe fallback used by the captured console.
## Usage: Basic
The runner is normally invoked by `SpawnEngine`; consumers do not call it
directly. To test in isolation:
```ts
import { runRunner } from "@suaveplan/sandbox/runner";
const out = runRunner({
code: "1 + 1",
context: {},
modules: [],
maxOutputSize: 1024,
});
console.log(out.result); // 2
```
## Usage: Advanced
Drive the full stdin/stdout pipeline against the runner file by spawning a
real child:
```ts
import { spawn } from "node:child_process";
const child = spawn(process.execPath, ["./dist/spawn-engine/runner.js"], {
stdio: ["pipe", "pipe", "pipe"],
});
child.stdin.write(JSON.stringify({ code: "1 + 1", context: {}, modules: [], maxOutputSize: 1024 }));
child.stdin.end();
```
## API Reference
- `RunnerInput` — `{ code, context, modules, maxOutputSize }`.
- `RunnerOutput` — `{ success, result, error, logs, memoryUsed }`.
- `runRunner(input: RunnerInput): RunnerOutput`.
- `readStdinAll(): Promise<string>`.
- `main(): Promise<void>`.
- `stringify(value: unknown): string`.
## Implementation Notes
The runner builds a function scope via
`Reflect.construct(Function, [...keys, source])` — this is the controlled
boundary the runner owns; the parent is the one that decides whether to
spawn it. Strict mode is enforced via the prologue. The
auto-invoke guard checks `process.argv[1]` against a filename regex so
re-importing the runner from a test never triggers the main loop.

View File

@@ -0,0 +1,74 @@
# SpawnEngine
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`SpawnEngine` uses `node:child_process.spawn` for the strongest isolation the package offers: a fresh OS process spawned per execution, pinned to `process.execPath`, with a sanitised environment that strips identity-revealing keys (`HOME`/`USER`/`SHELL`/`LOGNAME`) and case-insensitive substring matches against `SECRET`/`KEY`/`TOKEN`/`PASSWORD`/`CREDENTIAL`/`AUTH`. Code is sent to the child over stdin as a JSON envelope; the result is read from stdout the same way. Timeouts are enforced with `child.kill("SIGKILL")` so the running script cannot intercept termination.
The child process runs `runner.ts` (shipped via the `./runner` subpath export). The engine resolves the runner path relative to its own module URL by default, but callers can pin a custom path via the `runnerPath` option — useful for tests that want to drive the engine against a controlled runner fixture.
## Features
- Real `node:child_process.spawn` — true OS-process isolation.
- Sanitised env via `sanitiseEnv` (exported for direct testing).
- Injectable `ProcessSpawner` seam (defaults to `node:child_process.spawn`).
- Configurable `runnerPath` and `execPath`.
- Timeout enforcement via `SIGKILL` after `Promise.race`.
- Failure modes: timeout, no-output, parse-error, all surfaced via the
structured `SandboxResult` `error` field.
## Usage: Basic
```ts
import { SpawnEngine } from "@suaveplan/sandbox/spawn-engine";
const engine = new SpawnEngine({
timeout: 5000,
maxOutputSize: 1024,
memoryLimit: 50 * 1024 * 1024,
});
const r = await engine.execute("1 + 1");
engine.destroy();
```
## Usage: Advanced
Inject a custom spawner for tests, plus a stripped-down env for tightest
control:
```ts
import { SpawnEngine } from "@suaveplan/sandbox/spawn-engine";
const engine = new SpawnEngine({
timeout: 5000,
maxOutputSize: 1024,
memoryLimit: 5e7,
env: { PATH: process.env.PATH },
spawner: (cmd, args, opts) => myFakeSpawn(cmd, args, opts),
});
```
## API Reference
- `new SpawnEngine(options: SpawnEngineOptions)`.
- `execute(code, context?, options?): Promise<SandboxResult>`.
- `registerModule(mod): void`.
- `unregisterModule(name): boolean`.
- `getRunnerPath(): string`.
- `getSanitisedEnv(): Record<string, string>`.
- `reset(): void`.
- `destroy(): void`.
- `sanitiseEnv(env)` — pure helper exported for direct testing.
## Implementation Notes
The `raceChild` helper consolidates the four ways a child can finish:
clean exit with parseable JSON (`ok`), clean exit with non-JSON or empty
output (`no-output` / `parse-error`), error event before close
(`parse-error`), and timeout (`timeout`). Settling is idempotent — only the
first outcome counts — so a timeout that races with a clean exit doesn't
double-resolve. The kill on timeout is wrapped in try/catch because
`child.kill` raises on already-exited processes on some platforms.

View File

@@ -0,0 +1,60 @@
# Telemetry
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`@suaveplan/sandbox` ships first-class OpenTelemetry instrumentation so consumers running the sandbox at any scale can observe execution health without wrapping the API in their own counters. Every public failure mode and every successful execution increments a tagged counter, and every execution duration lands in a histogram tagged by engine kind and outcome.
The meter resolves lazily from the globally registered `MeterProvider`. When no provider is registered the OTel API returns no-op instruments, so the helpers can be called from every code path unconditionally — production code that has opted out of telemetry pays only the no-op cost.
## Features
- Four instruments on the `@suaveplan/sandbox` meter.
- 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 }`.
- Four record helpers so call sites never construct attribute bags manually.
## Usage: Basic
```ts
import { recordExecution } from "@suaveplan/sandbox/telemetry";
recordExecution("vm", "success");
```
## Usage: Advanced
Combine the duration histogram with the success counter to derive a p95
latency by engine kind, scoped by outcome, in your OTel collector:
```promql
histogram_quantile(0.95,
sum(rate(sandbox_execution_duration_ms_bucket{outcome="success"}[5m]))
by (engine, le))
```
## API Reference
- `sandboxExecutionsCounter: Counter`
- `sandboxRuleBlocksCounter: Counter`
- `sandboxPoolAcquisitionsCounter: Counter`
- `sandboxExecutionDurationHistogram: Histogram`
- `recordExecution(engine, outcome): void`
- `recordRuleBlock(rule): void`
- `recordPoolAcquisition(result): void`
- `recordDuration(engine, outcome, ms): void`
## Implementation Notes
Instruments are created at module-load time on a memoised meter — the OTel API
caches by name, so re-importing this module from another package does not
register duplicate instruments. The recording helpers are deliberately
parameter-positional (not options bags) so call sites stay short and the
attribute bag construction happens in one place rather than being repeated at
every recording site.

View File

@@ -0,0 +1,86 @@
# VmEngine
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`VmEngine` is the default sandbox engine. It uses `node:vm` directly: caller code is compiled with `new vm.Script(...)` and executed via `script.runInContext(ctx, { timeout })`. The context is created with `codeGeneration: { strings: false, wasm: false }` so `eval` and the Function constructor are syntactically unavailable inside the sandbox even if the code-transformer's regex filter fails. Context variables and registered modules are injected as own properties of the context object; the host's intrinsics (`Object`, `Array`, etc.) are reachable through the shared realm.
`VmEngine` is NOT a security boundary against adversarial code. `node:vm`'s
context-sharing model means a determined attacker with code-injection can
escape via `this.constructor.constructor("...")` or prototype-chain tricks.
For adversarial isolation use `worker-engine` (OS-thread isolation) or
`spawn-engine` (process isolation).
## Features
- Real `node:vm` execution — synchronous, deterministic.
- Frozen intrinsics flag (`codeGeneration: { strings: false, wasm: false }`).
- Captured console (`log`/`warn`/`error`) with output-size cap.
- Module + context-variable injection per execution.
- V8 heap statistics via `v8.getHeapStatistics()`.
- OTel `recordExecution` + `recordDuration` emission per call.
- `executeSync` companion for pool callers that prefer synchronous APIs.
## Usage: Basic
```ts
import { VmEngine } from "@suaveplan/sandbox/vm-engine";
const engine = new VmEngine({
timeout: 5000,
maxOutputSize: 1024,
memoryLimit: 50 * 1024 * 1024,
});
const r = await engine.execute("1 + 1");
console.log(r.result); // 2
engine.destroy();
```
## Usage: Advanced
Inject a host bridge as a module so user code can interact with the
controlled-vocabulary API rather than the host's intrinsics:
```ts
import { VmEngine } from "@suaveplan/sandbox/vm-engine";
const engine = new VmEngine({
timeout: 5000,
maxOutputSize: 1024,
memoryLimit: 50 * 1024 * 1024,
modules: [
{
name: "host",
exports: {
fetchProduct: (id: string) => loadProduct(id),
},
},
],
});
const r = await engine.execute('host.fetchProduct("sku-42")');
```
## API Reference
- `new VmEngine(config: SandboxEngineConfig)`.
- `execute(code, context?, options?): Promise<SandboxResult>`.
- `executeSync(code, context?, options?): SandboxResult`.
- `registerModule(mod): void`.
- `unregisterModule(name): boolean`.
- `getHeapStatistics(): { usedHeapSize, heapSizeLimit } | null`.
- `reset(): void`.
- `destroy(): void`.
## Implementation Notes
The compiled script wraps the user code in `(${code})` so a bare expression
(e.g. `1 + 1`) returns its value rather than throwing a syntax error from a
missing semicolon. An IIFE (`function(){...}()`) compiles to
`(function(){...}())` which is also valid. The wrapper is intentionally
lightweight; callers who want statement-level execution should use
`executeFunction` on the facade or wrap their code in a function themselves.
The `breakOnSigint: true` flag is set so a SIGINT delivered to the host
terminates the running script rather than killing the entire process.

View File

@@ -0,0 +1,70 @@
# WorkerEngine
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`WorkerEngine` uses real `node:worker_threads` for true OS-thread isolation. Each `execute` call spawns a fresh `Worker` constructed from an inline source string (`eval: true`), posts the code + context + modules over the structured-clone message channel, and awaits the structured reply. Intrinsics are NOT shared with the host realm — the worker has its own V8 isolate, its own heap, and its own event loop. Timeouts are enforced by calling `worker.terminate()`, which is a hard stop the running script cannot intercept.
Use `WorkerEngine` when the code being sandboxed may be adversarial: a true OS-thread boundary defeats the prototype-chain escapes that compromise `vm-engine`. The cost is per-call worker startup, which is non-trivial; pair with `SandboxPool` when latency matters.
## Features
- Real `node:worker_threads` execution — OS-thread isolation.
- Worker spawned per call, terminated after every result.
- `worker.terminate()` enforces timeout — script cannot intercept.
- Structured-clone passes context + modules.
- Output-size cap applied inside the worker before postMessage.
- OTel `recordExecution` + `recordDuration` per call.
## Usage: Basic
```ts
import { WorkerEngine } from "@suaveplan/sandbox/worker-engine";
const e = new WorkerEngine({
timeout: 5000,
maxOutputSize: 1024,
memoryLimit: 50 * 1024 * 1024,
});
const r = await e.execute("2 + 2");
e.destroy();
```
## Usage: Advanced
Wrap the engine in a pool to amortise startup:
```ts
import { SandboxPool } from "@suaveplan/sandbox/pool";
import { WorkerEngine } from "@suaveplan/sandbox/worker-engine";
const pool = new SandboxPool({
minSize: 2,
maxSize: 8,
engineConfig: { timeout: 5000, maxOutputSize: 1024, memoryLimit: 5e7 },
factory: (cfg) => new WorkerEngine(cfg),
});
```
## API Reference
- `new WorkerEngine(config: SandboxEngineConfig)`.
- `execute(code, context?, options?): Promise<SandboxResult>`.
- `registerModule(mod): void`.
- `unregisterModule(name): boolean`.
- `reset(): void`.
- `destroy(): void`.
## Implementation Notes
The inline worker source is a single string literal. The worker uses a
runtime `Reflect.construct(Function, [...keys, source])` to wrap the user
code in an isolated function scope — this is acceptable inside the worker
because the worker IS the security boundary. Outside the worker, in the host,
no dynamic code construction happens. The post-message channel uses
structured clone, which copies primitives + plain objects + typed arrays but
not functions or symbols — module exports must therefore be data, not host
references; for host bridges use `spawn-engine` plus an IPC channel.