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
createWasmSandboxis called with a policy declaringpermissions.fs = ["./uploads/*"] - THEN the resulting WASI snapshot exposes a preopen for
./uploadsAND 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
./uploadsis 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.comagainstpermissions.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.comagainst the same allowlist - THEN the connect raises
SAND_WASM_OUTBOUND_DENIEDANDplugin.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.getimport 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"