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

53 lines
3.8 KiB
Markdown

# `@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"`.