Initial Commit
This commit is contained in:
132
openspec/archive/2026-05-25-enterprise-sandbox/proposal.md
Normal file
132
openspec/archive/2026-05-25-enterprise-sandbox/proposal.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# 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/` — shared `SandboxConfig`, `SandboxResult`, `SandboxOptions`,
|
||||
`SandboxModule`, `ExecutionContext`, `AuditLogEntry`, `AuditStatistics`,
|
||||
`Clock`, `ProcessSpawner`, `CommandResult` types.
|
||||
- `error-codes/` — `SANDBOX_CODES` via `defineCodes("SANDBOX", [...])` +
|
||||
`SandboxError` class.
|
||||
- `schemas/` — Zod schemas for `SandboxConfig`, `SandboxOptions`,
|
||||
`SandboxModule` boundaries.
|
||||
- `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/` — `AuditLogger` with SHA-256 code hashing, search filters,
|
||||
statistics aggregation, and `onAuditLog` callback hook.
|
||||
- `resource-limiter/` — `ResourceLimiter` with sliding-window rate limiting,
|
||||
output-size truncation, injectable `Clock` for deterministic tests.
|
||||
- `module-resolver/` — `ModuleResolver` with `BLOCKED_MODULES` denylist,
|
||||
`BUILTIN_SAFE_MODULES` (`Math`/`JSON`/`Date`), custom module registry.
|
||||
- `code-transformer/` — `CodeTransformer` with the eleven `BUILTIN_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`-backed `SandboxEngine` (default). Frozen-Realm
|
||||
pattern via `vm.createContext(...)`, `vm.Script` compile +
|
||||
`runInContext({ timeout })`, captured console with output-size limit,
|
||||
module + context injection, heap snapshotting via `v8.getHeapStatistics()`.
|
||||
- `worker-engine/` — Real-`node:worker_threads`-backed `SandboxEngine`. Spawns
|
||||
an inline `Worker` (`eval: true`) with code + context + modules sent via
|
||||
`postMessage`, captures result via the message channel, enforces timeout by
|
||||
terminating the worker.
|
||||
- `spawn-engine/` — Real-`node:child_process`-backed runner equivalent to
|
||||
upstream's `bun-spawn-runner` but using `node:child_process.spawn` so it
|
||||
works under both Bun and Node. Pinned to current `process.execPath`,
|
||||
sanitised env (`SECRET`/`KEY`/`TOKEN`/`PASSWORD`/`CREDENTIAL`/`AUTH`
|
||||
patterns + `HOME`/`USER`/`SHELL`/`LOGNAME` stripped), structured JSON IPC
|
||||
via stdin/stdout, timeout via `Promise.race`.
|
||||
- `runner/` — Child-process entry point invoked by `spawn-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/` — Pluggable `IsolateEngine` over the upstream's
|
||||
isolated-vm contract (kept as a structural interface so the optional peer
|
||||
dep stays optional).
|
||||
- `pool/` — `SandboxPool` of pre-warmed engines, idle reclamation,
|
||||
recycle-on-N-executions, statistics surface.
|
||||
- `result-streamer/` — `ResultStreamer` with 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` / `executeFunction` returns the structured `SandboxResult`.
|
||||
|
||||
## 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:vm` is not a security boundary.** `vm.runInContext` shares the host
|
||||
realm; a determined attacker with code-injection can escape via prototype
|
||||
chain tricks. The package documents this explicitly: `vm-engine` is for
|
||||
*isolation against accidents*, `worker-engine` and `spawn-engine` are for
|
||||
isolation against adversaries.
|
||||
- **Timeouts under `vm.runInContext` only cover synchronous code.** A microtask
|
||||
scheduled inside the context outlives the timeout. The package documents
|
||||
this and recommends `worker-engine` for code that awaits.
|
||||
- **`node:worker_threads` `eval: true` accepts a string.** This is the
|
||||
worker's documented contract for running ad-hoc code. The boundary is the
|
||||
worker thread, not the string parser.
|
||||
Reference in New Issue
Block a user