Files
2026-05-27 23:29:53 +01:00

89 lines
6.2 KiB
Markdown

# WASI-stdio ABI variant for the WASM host bridge
**Package**: `@suaveplan/api-sandbox-wasm-host-bridge` (extension) + `@suaveplan/api-types/sandbox-bridge` (additive)
**Status**: PROPOSED
**Type**: extension + types-additive
## Why
The shipped wasm host-bridge (commit `a9fa31db95`) accepts WASM modules exporting six custom-shaped functions (`__suaveplan_alloc(i32) → i32`, `__suaveplan_handle_request(i32,i32) → i64`, etc.) plus `memory`. Producing such a module by compiling TypeScript is the job of `@suaveplan/wasm-compiler-javy`.
When the build agent investigated against Javy v8.1.1 (Bytecode Alliance, current latest), it surfaced an architectural blocker: **Javy cannot emit custom-shaped WASM exports**. Its `-C wit=...` flag restricts WIT-exposed exports to "function exports with no arguments and no return values" (verified via `javy build -C help`). Javy's host-interaction model is fixed: the host writes input to fd 0 (stdin), reads output from fd 1 (stdout). This is the WASI standard interface and is the only ABI Javy honors.
Three resolutions were considered (full transcript in the agent's stop report); the chosen path is to **extend the bridge to accept either the existing direct-export ABI OR a WASI-stdio ABI variant**. The compiler then becomes a thin Javy wrapper that emits Javy's native artifact — no outer-WASM authorship, no Component Model migration, no bridge-spec rewrite for the existing variant.
## Concrete consumer
- `@suaveplan/wasm-compiler-javy` (rewritten in a sibling proposal) — every TS→WASM artifact it emits speaks the WASI-stdio variant.
- Plugin authors who choose Javy as their toolchain (the recommended TS→WASM path).
- `e2e/foundation-stack-full` Stage-8 closure smoke — once both proposals ship, `api-plugin-example` deploys under the wasm tier via Javy + WASI-stdio ABI.
## Host bridge
The bridge surface gains one additive option. The default remains the current direct-export ABI so already-shipped consumers see no behaviour change.
```ts
type WasmAbiVariant = "direct-i32-i64" | "wasi-stdio";
createSandboxedWasmPluginModule({
sandbox,
manifest,
abiVariant: "wasi-stdio", // default: "direct-i32-i64"
});
```
When `abiVariant === "wasi-stdio"`, the bridge:
1. Skips the six-export ABI validation (those exports won't exist).
2. Routes every request through the WASI-stdio framing protocol defined below.
3. Treats the WASM module as a long-running guest process: a single `_start` call (the WASI entry point) launches the plugin, which then enters a request loop reading from fd 0.
## End-to-end test
- `packages/api-stack/api/api-sandbox-wasm-host-bridge/tests/integration/wasi-stdio-flow.test.ts` — instantiates a hand-encoded WASM fixture that speaks the WASI-stdio protocol, runs the conformance harness against it, and asserts: route discovery via `{kind:"routes"}` envelope; request round-trip via `{kind:"request"}` / `{kind:"response"}`; error envelope surfaces as SuaveplanError host-side; lifecycle commands (`{kind:"start"}` / `{kind:"dispose"}`) idempotent.
## What changes
### Shared protocol contract (additive to `@suaveplan/api-types/sandbox-bridge`)
New types alongside the existing `SerializedRequest`/`SerializedResponse`/`SerializedRoute`. Wire format is length-prefixed JSON frames so the protocol survives any WASI implementation's buffering semantics.
```ts
// Each envelope is a single WASI-stdio frame:
// [u32 BE length][JSON bytes of length 'length']
export type PluginEnvelopeIn =
| { readonly kind: "request"; readonly request: SerializedRequest }
| { readonly kind: "routes" }
| { readonly kind: "start" }
| { readonly kind: "dispose" };
export type PluginEnvelopeOut =
| { readonly kind: "response"; readonly response: SerializedResponse }
| { readonly kind: "routes"; readonly routes: readonly SerializedRoute[] }
| { readonly kind: "ok" }
| { readonly kind: "error"; readonly code: string; readonly message: string };
export const WASI_STDIO_PROTOCOL_VERSION: 1;
```
The contract lives in `api-types` because both bridges (host-side framer) and Javy-compiled plugins (sandbox-side framer, via the suaveplan ABI shim) MUST agree on the wire shape down to the byte. Putting it in either runtime package would re-introduce the SerializedRequest/Response divergence the orchestrator just resolved.
### Bridge runtime (extends `@suaveplan/api-sandbox-wasm-host-bridge`)
1. `src/wasi-stdio/wasi-stdio.ts` — the framer: length-prefix encode/decode, Zod-validated envelope schemas, frame-too-large guard (`SAND_WASM_BRIDGE.FRAME_OVERSIZE`).
2. `src/wasi-stdio/wasi-stdio-runner.ts` — wraps a `WasmSandboxHandle` configured for WASI-stdio: writes envelopes to fd 0, reads from fd 1, surfaces stderr lines as logger events.
3. `src/bridge/bridge.ts` — `createSandboxedWasmPluginModule` becomes a `switch (abiVariant)`. The existing direct-i32-i64 path is renamed `src/abi-direct/` and the new path is `src/abi-wasi-stdio/`. Routes returned by `{kind:"routes"}` envelope feed into the same `BridgedPlugin` shape.
4. New error codes: `SAND_WASM_BRIDGE.FRAME_OVERSIZE`, `SAND_WASM_BRIDGE.PROTOCOL_MISMATCH`, `SAND_WASM_BRIDGE.STDIO_TIMEOUT`. The shipped 7 codes are preserved.
5. ABI validation: under `abiVariant: "wasi-stdio"`, validate `memory` + `_start` + WASI imports (`wasi_snapshot_preview1.{fd_read, fd_write, environ_get, environ_sizes_get, ...}`). Missing → `SAND_WASM_BRIDGE.INVALID_PLUGIN_SURFACE` with `context.missing`.
6. Trap classification, capability-gate routing, idempotent lifecycle: all preserved — but lifecycle commands now go through `{kind:"start"}` / `{kind:"dispose"}` envelopes, not direct export calls.
### Conformance harness (extends)
`runWasmHostBridgeConformance({ buildBridge, abiVariant })` gains an `abiVariant` parameter. Both variants must pass all five existing invariants; the harness picks the right fixture WASM based on the variant.
## Out of scope
- No change to `@suaveplan/api-types/sandbox-bridge`'s `SerializedRequest`/`SerializedResponse`/`SerializedRoute`. They round-trip unchanged through both variants.
- No change to `@suaveplan/api-sandbox-vm-host-bridge`. The vm tier uses structuredClone, not stdio.
- No deprecation of the direct-i32-i64 variant. Both stay first-class.