2.7 KiB
WorkerEngine
(no submodules)
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_threadsexecution — 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+recordDurationper call.
Usage: Basic
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:
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.