# 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"`