# 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.