Files
suaveplan/openspec/archive/2026-05-26-api-sandbox-wasm/proposal.md
2026-05-27 23:29:53 +01:00

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 a wasi_snapshot_preview1 (or preview2) implementation built from the PermissionPolicy. Returns a WasmSandboxHandle.
  • ADDED: buildWasiPreopens(policy) — maps permissions.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) — maps permissions.fetch[] to a sockets allowlist consulted before each outbound socket connect; non-matching destinations raise PERMISSION_DENIED and emit plugin.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 .wasm file 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 reflect permissions.fs[], assert outbound allowlist reflects permissions.fetch[].
  • src/preopens/preopens.test.ts — read-only vs rw: rule prefix matrix.
  • src/outbound/outbound.test.ts — allowed / denied destination matrix; denied connect raises SAND_WASM_OUTBOUND_DENIED and never opens a socket.
  • src/sandbox/sandbox.spec-conformance.test.ts — observability: denials emit plugin.permission_denied_total with the right labels; PERMISSION_DENIED reaches the guest.
  • src/testing/run-wasm-conformance.test.ts — self-test of the harness against the fixture.
  • tests/dep-graph.test.ts — runtime deps include api-types, api-sandbox-permissions, error, logger.

Migration impact

None — additive only. api-host registers this when host.json defaultSandbox = "wasm".