Files
2026-05-27 23:29:53 +01:00

5.1 KiB

Capability Spec Delta: api-sandbox-vm (api-sandbox-vm)

Package: @suaveplan/api-sandbox-vm Status: PROPOSED

Every requirement below carries a **Verified by:** line at authoring time.

ADDED Requirements

Requirement: Sandbox boots inside a frozen vm.Context

The package SHALL expose createVmSandbox({ pluginModulePath, policy, proxies }) that constructs a vm.Context whose globalThis is sealed and frozen. The sandbox MUST expose only the curated FrozenGlobals allowlist (URL, URLSearchParams, TextEncoder, TextDecoder, structuredClone, logger-routed console, subset of crypto). Any other global (e.g. process, Buffer, raw outbound HTTP client, raw require, import.meta.url outside the sandbox path) MUST be absent.

Scenario: process global is unreachable

  • WHEN code inside the sandbox references process
  • THEN evaluation throws a ReferenceError AND the sandbox surfaces it as a SAND_VM_*-prefixed SuaveplanError

Scenario: globalThis is frozen

  • WHEN code inside the sandbox attempts globalThis.foo = 1
  • THEN the assignment throws under strict mode AND no property is added to the sandbox globals

Verified by: src/sandbox/sandbox.test.ts::"process global is unreachable inside sandbox" AND src/sandbox/sandbox.test.ts::"globalThis is frozen"


Requirement: Dynamic code constructors are rejected

The package SHALL ensure direct calls to dynamic-code constructors (the Function family — sync, async, generator), direct dynamic-code execution, and indirect dynamic-code execution all throw inside the sandbox. The thrown error MUST be a SuaveplanError with code SAND_VM_EVAL_REJECTED or SAND_VM_INDIRECT_EVAL_REJECTED.

Scenario: direct dynamic execution is rejected

  • WHEN sandboxed code attempts to evaluate a string of source via the built-in dynamic-evaluator
  • THEN the call throws SAND_VM_EVAL_REJECTED

Scenario: indirect dynamic execution is rejected

  • WHEN sandboxed code attempts indirect dynamic evaluation through the comma-operator trick
  • THEN the call throws SAND_VM_INDIRECT_EVAL_REJECTED

Verified by: src/sandbox/sandbox.test.ts::"direct dynamic execution is rejected" AND src/sandbox/sandbox.test.ts::"indirect dynamic execution is rejected"


Requirement: fs and fetch are host-proxied only

The package SHALL install fs and outbound-HTTP proxy stubs that route every call to the host via installCapabilityProxies. Every proxy call MUST consult PermissionChecker from api-sandbox-permissions BEFORE forwarding to the host. Denied calls MUST raise PERMISSION_DENIED inside the sandbox without contacting the host.

Scenario: allowed fs read proxies to host

  • WHEN sandboxed code calls the fs-proxy with a path matching permissions.fs[]
  • THEN the request reaches the host adapter once AND the response is delivered to the sandbox

Scenario: denied outbound HTTP does not reach the host

  • WHEN sandboxed code calls the outbound-HTTP proxy with a host outside permissions.fetch[]
  • THEN the call raises PERMISSION_DENIED inside the sandbox AND no host-side outbound request is initiated AND plugin.permission_denied_total{capability="fetch"} increments

Verified by: src/proxies/fs-proxy/fs-proxy.test.ts::"allowed read proxies once to host" AND src/proxies/fetch-proxy/fetch-proxy.test.ts::"denied fetch never contacts host AND increments denial metric"


Requirement: Module loading is restricted to the plugin's own module

The package SHALL refuse vm.SourceTextModule import specifiers that resolve outside the plugin module graph. Only relative imports rooted at pluginModulePath and a fixed allowlist of platform globals (no node built-ins) are loadable. Forbidden imports raise SAND_VM_MODULE_IMPORT_FORBIDDEN.

Scenario: node built-in import is rejected

  • WHEN sandboxed code imports "node:fs"
  • THEN module instantiation throws SAND_VM_MODULE_IMPORT_FORBIDDEN

Verified by: src/sandbox/sandbox.test.ts::"node built-in imports are forbidden"


Requirement: Conformance harness exported

The package SHALL export runVmSandboxConformance(label, makeSandbox) on ./testing and the in-package fixture sandbox MUST pass it. The harness re-runs every scenario above so a future vm-based host (e.g. an alternative isolate library) is gate-checked against the same contract.

Scenario: in-package fixture passes harness

  • WHEN runVmSandboxConformance("self", makeFixtureSandbox) runs
  • THEN every harness it(...) passes

Verified by: src/testing/run-vm-conformance.test.ts::"in-package fixture passes runVmSandboxConformance"


Requirement: Disposal is idempotent

The package SHALL ensure VmSandboxHandle.dispose() releases the vm.Context, cancels every in-flight proxy call, and is safe to invoke multiple times. A second call MUST be a no-op (no error, no double-free).

Scenario: dispose twice is a no-op

  • WHEN dispose() is called twice on the same handle
  • THEN the second call returns without error AND no second teardown side-effect is observed

Verified by: src/sandbox/sandbox.spec-conformance.test.ts::"dispose is idempotent"