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

4.8 KiB

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

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

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

ADDED Requirements

Requirement: WASM module instantiated with a permission-derived WASI snapshot

The package SHALL expose createWasmSandbox({ wasmModulePath, policy, proxies }) that instantiates the WASM module against a WASI snapshot built from the PermissionPolicy. The instantiation MUST fail with SAND_WASM_WASI_VERSION_UNSUPPORTED for unsupported WASI versions and with SAND_WASM_INSTANTIATION_FAILED for any other instantiation error.

Scenario: instantiation honours the policy

  • WHEN createWasmSandbox is called with a policy declaring permissions.fs = ["./uploads/*"]
  • THEN the resulting WASI snapshot exposes a preopen for ./uploads AND no other directory

Scenario: unsupported WASI version rejected

  • WHEN the WASM module imports wasi_snapshot_preview0 (or any version not on the supported list)
  • THEN instantiation throws SAND_WASM_WASI_VERSION_UNSUPPORTED

Verified by: src/sandbox/sandbox.test.ts::"instantiation honours the policy" AND src/sandbox/sandbox.test.ts::"rejects unsupported WASI version"


Requirement: Filesystem access via read-only preopens by default

The package SHALL map permissions.fs[] to read-only WASI preopen directories. A rule prefix rw: is required to make a preopen read-write. Paths outside the allowlist MUST NOT be preopened so the guest cannot open them. Denials surface as SAND_WASM_PREOPEN_DENIED AND increment plugin.permission_denied_total{capability="fs"}.

Scenario: read-only by default

  • WHEN permissions.fs = ["./uploads/*"] is given
  • THEN the preopen for ./uploads is read-only AND a guest write into that preopen returns the WASI errno for read-only

Scenario: write access requires explicit prefix

  • WHEN permissions.fs = ["rw:./uploads/*"] is given
  • THEN the preopen is read-write AND a guest write succeeds

Verified by: src/preopens/preopens.test.ts::"read-only by default" AND src/preopens/preopens.test.ts::"rw prefix enables write"


Requirement: Outbound socket allowlist gates network connects

The package SHALL refuse outbound socket connects whose destination host is not in permissions.fetch[]. The refusal MUST raise SAND_WASM_OUTBOUND_DENIED BEFORE any socket is opened. Denials increment plugin.permission_denied_total{capability="fetch"}.

Scenario: allowed destination connects through

  • WHEN a guest connects to api.stripe.com against permissions.fetch = ["api.stripe.com"]
  • THEN the connect proceeds to the underlying socket

Scenario: denied destination never opens a socket

  • WHEN a guest attempts to connect to evil.example.com against the same allowlist
  • THEN the connect raises SAND_WASM_OUTBOUND_DENIED AND plugin.permission_denied_total{capability="fetch"} increments AND no OS socket is created

Verified by: src/outbound/outbound.test.ts::"allowed destination connects" AND src/outbound/outbound.test.ts::"denied destination never opens a socket AND increments denial metric"


Requirement: Capability imports are permission-gated

The package SHALL expose capability proxies as WASI host imports (or component-model imports). Every imported function MUST consult PermissionChecker from api-sandbox-permissions before forwarding to the host proxy. Denied calls return a WASI errno and surface as PERMISSION_DENIED inside the guest.

Scenario: denied secret read returns errno

  • WHEN guest calls the secrets.get import for a name not in the manifest
  • THEN the import returns the WASI errno for permission-denied AND the host adapter is never invoked

Verified by: src/sandbox/sandbox.spec-conformance.test.ts::"denied capability import never reaches host adapter"


Requirement: Conformance harness exported

The package SHALL export runWasmSandboxConformance(label, makeSandbox) on ./testing. The in-package fixture sandbox MUST pass every scenario.

Scenario: fixture passes harness

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

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


Requirement: Disposal is idempotent

The package SHALL ensure WasmSandboxHandle.dispose() tears down the WASM instance, closes every preopen file descriptor, drops the outbound socket allowlist reference, and is safe to call multiple times.

Scenario: dispose twice is a no-op

  • WHEN dispose() is called twice
  • THEN the second call returns without error

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