188 lines
7.0 KiB
Markdown
188 lines
7.0 KiB
Markdown
# 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<T>(code, context, options): Promise<SandboxResult<T>> | SandboxResult<T>;
|
|
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<SandboxResult<T>>`.
|
|
|
|
## 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.
|