# Capability Spec Delta: api-sandbox-wasm (api-sandbox-wasm) **Package**: `@suaveplan/api-sandbox-wasm` **Status**: ACCEPTED > 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"`