Files
suaveplan/openspec/archive/2026-05-26-api-sandbox-vm/proposal.md
2026-05-27 23:29:53 +01:00

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 a vm.Context with frozen globals (no dynamic-code constructors, no eval, no globalThis.process, no import.meta to outside the sandbox), loads the plugin module via vm.SourceTextModule, returns a VmSandboxHandle exposing start(), 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 using vm.Context-safe stub objects. Every call routed through api-sandbox-permissions PermissionChecker before reaching the proxy.
  • ADDED: installFsProxy(sandbox, policy) and installFetchProxy(sandbox, policy) — host-proxied filesystem and outbound HTTP. Denials raise PERMISSION_DENIED inside 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 throw SAND_VM_* errors.
  • src/sandbox/sandbox.spec-conformance.test.ts — pins observability: every denial emits plugin.permission_denied_total (via api-sandbox-permissions) AND surfaces as PERMISSION_DENIED inside the sandbox.
  • src/proxies/fs-proxy.test.ts, src/proxies/fetch-proxy.test.ts — allowed-path / denied-path matrices using a real PermissionPolicy.
  • 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".