first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,140 @@
# `wasi-stdio-runner`
## Purpose
Synchronous "one-envelope-per-`_start`" driver for WASI-stdio
sandboxed plugins. Owns the WASM instantiation against a host-supplied
WASI snapshot extended with stdin/stdout/stderr bindings, then exposes
a single `exchange(envelope, phase)` primitive the bridge calls once
per protocol step. Each invocation queues exactly one outgoing frame,
invokes `_start`, and pops exactly one reply frame.
The runner intentionally does NOT model a long-running guest loop.
WebAssembly's JS API is strictly synchronous and would force a busy-
spin if the guest tried to block on empty stdin. The one-shot model
keeps every step bounded and re-entrant for the next request within
the same instance.
## Features
- Self-instantiates the WASM module — the runner is the source of
truth for the WASI snapshot it wires.
- Stdin/stdout backed by `createWasiStdioStreams`; stderr lines
forward to a `WasiStdioLoggerSink`.
- `proc_exit` ignored (not treated as a trap) — Javy-compiled plugins
call it after every `_start`.
- Per-step deadline via `stdioTimeoutMs` (default 5s); a missing reply
frame surfaces as `SAND_WASM_BRIDGE.STDIO_TIMEOUT` with `phase` and
elapsed milliseconds in the error context.
- Trap propagation honours the seven-kind classification from
`classifyTrap` — `unreachable` mid-handler surfaces as
`HANDLER_TRAPPED` with `trapKind: "unreachable"`.
- `dispose()` is idempotent and chains through to the optional
`WasmSandboxHandle` it was constructed with.
- `extraImports` lets the bridge wire `suaveplan_caps.*` capability
imports from the sandbox into the runner's parallel instance so
`SAND_PERM.DENIED` flows continue working under wasi-stdio.
## Usage: Basic
```ts
import { createWasiStdioRunner } from
"@suaveplan/api-sandbox-wasm-host-bridge/wasi-stdio";
declare const module: WebAssembly.Module;
const runner = createWasiStdioRunner({
pluginName: "example",
wasmModule: module,
});
const reply = runner.exchange({ kind: "routes" }, "routes");
if (reply.kind === "routes") {
for (const route of reply.routes) {
void route;
}
}
runner.dispose();
```
## Usage: Advanced
```ts
import { createWasiStdioRunner } from
"@suaveplan/api-sandbox-wasm-host-bridge/wasi-stdio";
import type { LoggerLike } from "@suaveplan/types";
declare const module: WebAssembly.Module;
declare const logger: LoggerLike;
declare const capabilityImports: WebAssembly.ModuleImports;
const runner = createWasiStdioRunner({
pluginName: "ex",
wasmModule: module,
logger,
stdioTimeoutMs: 30_000,
extraImports: { suaveplan_caps: capabilityImports },
});
try {
const ok = runner.exchange({ kind: "start" }, "start");
if (ok.kind !== "ok") {
throw new Error(`unexpected reply ${ok.kind}`);
}
declare function buildRequest(): never;
const response = runner.exchange(
{ kind: "request", request: buildRequest() },
"request",
);
void response;
} finally {
runner.dispose();
}
```
## API Reference
### `createWasiStdioRunner(deps): WasiStdioRunner`
- `deps.pluginName: string` — plugin name (error context label).
- `deps.wasmModule: WebAssembly.Module` — pre-compiled module.
- `deps.logger?: WasiStdioLoggerSink` — stderr line sink.
- `deps.stdioTimeoutMs?: number` — per-step deadline (default 5000).
- `deps.sandbox?: { dispose; disposed }` — sandbox dispose chain.
- `deps.extraImports?: Record<string, ModuleImports>` — composed with
the runner's WASI snapshot at instantiate time.
### `WasiStdioRunner.exchange(envelope, phase): PluginEnvelopeOut`
Sends one envelope, invokes `_start`, pops one reply frame. Throws:
- `SAND_WASM_BRIDGE.HANDLER_TRAPPED` (with `trapKind` + `phase`) on
WASM traps during `_start`.
- `SAND_WASM_BRIDGE.STDIO_TIMEOUT` when no reply frame is buffered
after `_start` returns.
- `SAND_WASM_BRIDGE.LIFECYCLE_FAILED` if called after `dispose()`.
- `SAND_WASM_BRIDGE.FRAME_OVERSIZE` / `MARSHAL_FAILED` / `PROTOCOL_MISMATCH`
from the framer when encoding/decoding fails.
### `WasiStdioRunner.dispose(): void`
Idempotent; chains through to `deps.sandbox.dispose()`.
## Implementation Notes
- The runner wires a minimal WASI snapshot in-process — it does NOT
delegate fs/path/preopens to `@suaveplan/api-sandbox-wasm`'s
`createWasiSnapshot`, because those imports are not part of the
WASI-stdio variant's contract (Javy-compiled plugins use stdin/
stdout exclusively). When permission-policy preopens DO matter for
a hybrid plugin, the consumer composes the additional imports via
`extraImports`.
- `proc_exit` is treated as a normal return. Most WASI runtimes throw
when `_start` calls `proc_exit` non-zero, but Javy's pattern is to
call `proc_exit(0)` after writing the reply; treating it as a
no-op keeps the guest's intent intact.
- `STDIO_TIMEOUT` triggers when `_start` returns without writing any
frame to fd 1. It is not driven by a wall-clock timer — WebAssembly
is synchronous, so there is no thread to interrupt. The `elapsedMs`
context field carries the observed `_start` duration so dashboards
can spot near-misses.
- `dispose()` resets every buffer in addition to chaining the sandbox
dispose. Resetting first guarantees that a future GC cycle on the
runner does not keep the guest's memory pinned through a closure.