3.9 KiB
@suaveplan/api-sandbox-vm — vm.Context-based sandbox host
Package: @suaveplan/api-sandbox-vm
Status: PROPOSED
Type: new-package
Stage: 8 — see umbrella openspec/changes/api-plugin-foundation/
Why
Sandboxed-tier plugins that need a JavaScript runtime (no WASM build pipeline) require an in-process execution surface that does NOT give them ambient access to the host's globals. Node's vm.Context with frozen globals — plus the worker-thread bulkhead — is the path of least resistance and is well-trodden for trusted-but-isolated JS plugins. This package owns that runtime; it does not own the permission model (that's api-sandbox-permissions) and does not own worker spawning (that's api-worker-host). It composes both.
Per design doc §2.2: sandboxed tier = worker thread + vm.Context guard + permission-gated fs/fetch proxies + CPU/memory/wall-clock budgets. api-sandbox-vm is the vm.Context half.
What changes
- ADDED:
createVmSandbox({ pluginModulePath, policy, proxies })— builds avm.Contextwith frozen globals (no dynamic-code constructors, noeval, noglobalThis.process, noimport.metato outside the sandbox), loads the plugin module viavm.SourceTextModule, returns aVmSandboxHandleexposingstart(),dispose(), and the capability-proxy receiver. - ADDED:
FrozenGlobals— the curated allowlist of globals the sandbox sees (URL,URLSearchParams,TextEncoder,TextDecoder,structuredClone,console(logger-routed),crypto(subset)). Everything else absent. - ADDED:
installCapabilityProxies(sandbox, proxies)— wires the capability proxy surface into the sandbox usingvm.Context-safe stub objects. Every call routed throughapi-sandbox-permissionsPermissionCheckerbefore reaching the proxy. - ADDED:
installFsProxy(sandbox, policy)andinstallFetchProxy(sandbox, policy)— host-proxied filesystem and outbound HTTP. Denials raisePERMISSION_DENIEDinside the sandbox. - ADDED:
runVmSandboxConformance(label, makeSandbox)(subpath./testing) — the conformance suite every sandbox host must pass; in this package it pins the contract for vm-tier specifics (frozen globals, dynamic-code-constructor rejection, indirect-eval rejection). - ADDED: Error codes (
SAND_VM_*):EVAL_REJECTED,INDIRECT_EVAL_REJECTED,MODULE_IMPORT_FORBIDDEN,SOURCE_TEXT_MODULE_FAILED.
Out of scope
- Permission policy resolution (that's
api-sandbox-permissions). - Worker spawning and lifecycle (that's
api-worker-host). - WASM runtimes (that's
api-sandbox-wasm). - Capability adapters themselves; this package only routes proxy traffic. The proxies are constructed by the host.
Source mapping
Genesis-original. Source of design intent: docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md.
| Requirement | Design doc section |
|---|---|
vm.Context with frozen globals |
§2.2 |
| No dynamic-code constructors | §2.2 |
| fs/fetch via host proxies only | §2.2, §5.2b |
| Permission checks before proxy call | §5.2b |
Verification approach
src/sandbox/sandbox.test.ts— builds a sandbox, confirms frozen globals enumerable list, confirms dynamic-code constructors and indirect-eval ((0, eval)("...")) all throwSAND_VM_*errors.src/sandbox/sandbox.spec-conformance.test.ts— pins observability: every denial emitsplugin.permission_denied_total(viaapi-sandbox-permissions) AND surfaces asPERMISSION_DENIEDinside the sandbox.src/proxies/fs-proxy.test.ts,src/proxies/fetch-proxy.test.ts— allowed-path / denied-path matrices using a realPermissionPolicy.src/testing/run-vm-conformance.test.ts— self-test of the exported harness against the in-package sandbox.tests/dep-graph.test.ts— pins runtime deps (api-types,api-sandbox-permissions,error,logger).
Migration impact
None — additive only. api-host registers this as the default sandbox implementation when host.json defaultSandbox = "vm".