3.8 KiB
@suaveplan/api-sandbox-wasm — WASI sandbox host
Package: @suaveplan/api-sandbox-wasm
Status: PROPOSED
Type: new-package
Stage: 8 — see umbrella openspec/changes/api-plugin-foundation/
Why
Sandboxed-tier plugins compiled to WebAssembly with the WASI ABI need a host that can load them with the narrowest possible capability set. WASI's capability model is at the file-descriptor and socket level, which lines up exactly with the manifest's permissions.fs[] (preopens) and permissions.fetch[] (outbound-socket allowlist). This package owns that translation.
Per design doc §2.2 and the §14 open question on WASI granularity, the reference answer is: read-only fs preopens of every listed path + outbound socket allowlist derived from permissions.fetch[]. Writeable preopens require an explicit rw: rule prefix. This package locks that decision down.
What changes
- ADDED:
createWasmSandbox({ wasmModulePath, policy, proxies })— instantiates the WASM module with a WASI snapshot wired to awasi_snapshot_preview1(or preview2) implementation built from thePermissionPolicy. Returns aWasmSandboxHandle. - ADDED:
buildWasiPreopens(policy)— mapspermissions.fs[]to read-only preopen directories;rw:rules become read-write preopens. Paths outside the allowlist are not preopened, so the guest cannot open them. - ADDED:
buildWasiOutboundAllowlist(policy)— mapspermissions.fetch[]to a sockets allowlist consulted before each outbound socket connect; non-matching destinations raisePERMISSION_DENIEDand emitplugin.permission_denied_total{capability="fetch"}. - ADDED:
installCapabilityImports(sandbox, proxies)— exposes capability proxies as WASI host functions (or component-model imports), every call permission-checked first. - ADDED:
runWasmSandboxConformance(label, makeSandbox)(./testing) — conformance suite pinning WASI-specific behaviour (preopen scoping, capability import permission gating, dispose semantics). - ADDED: Error codes (
SAND_WASM_*):INSTANTIATION_FAILED,WASI_VERSION_UNSUPPORTED,OUTBOUND_DENIED,PREOPEN_DENIED.
Out of scope
- WASM build toolchain. This package consumes a pre-built
.wasmfile produced by the plugin author. - WASI preview2 component-model conversion utilities. The default supports preview1; preview2 is opt-in via host config.
- Permission policy resolution (that's
api-sandbox-permissions). - Worker spawning (that's
api-worker-host).
Source mapping
Genesis-original. Source of design intent: docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md.
| Requirement | Design doc section |
|---|---|
| WASI permission-model mapping | §2.2, §14 (resolved) |
| Read-only preopens by default | §14 |
| Outbound socket allowlist | §14, §5.2b |
PERMISSION_DENIED surfaced inside the plugin |
§5.2b |
Verification approach
src/sandbox/sandbox.test.ts— instantiate a fixture.wasm(small WASI hello-world built in CI), assert preopens reflectpermissions.fs[], assert outbound allowlist reflectspermissions.fetch[].src/preopens/preopens.test.ts— read-only vsrw:rule prefix matrix.src/outbound/outbound.test.ts— allowed / denied destination matrix; denied connect raisesSAND_WASM_OUTBOUND_DENIEDand never opens a socket.src/sandbox/sandbox.spec-conformance.test.ts— observability: denials emitplugin.permission_denied_totalwith the right labels;PERMISSION_DENIEDreaches the guest.src/testing/run-wasm-conformance.test.ts— self-test of the harness against the fixture.tests/dep-graph.test.ts— runtime deps includeapi-types,api-sandbox-permissions,error,logger.
Migration impact
None — additive only. api-host registers this when host.json defaultSandbox = "wasm".