Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,101 @@
<!-- markdownlint-disable MD024 -->
# 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"`