5.1 KiB
Capability Spec: api-sandbox-vm
Package: @suaveplan/api-sandbox-vm
Status: SHIPPED
Every requirement below carries a
**Verified by:**line.
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
ReferenceErrorAND the sandbox surfaces it as aSAND_VM_*-prefixedSuaveplanError
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_DENIEDinside the sandbox AND no host-side outbound request is initiated ANDplugin.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"