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,153 @@
# `wasi-stdio-frame`
## Purpose
Owns the WASI-stdio wire format: a single length-prefixed JSON frame
exchanged on the guest's standard streams. Encodes outgoing
`PluginEnvelopeIn` values into `[u32 BE length][JSON bytes]` byte
buffers and decodes incoming frames back into Zod-validated
`PluginEnvelopeOut` values. Every failure mode surfaces as a stable
`SAND_WASM_BRIDGE.*` code so the caller can branch on `code` without
parsing messages.
The encoder refuses oversize frames BEFORE allocating the output
buffer, so no partial writes ever reach fd 0. The decoder enforces the
same 16 MiB ceiling on incoming frames AND validates the JSON payload
against the schema for `PluginEnvelopeOut`, preventing a malformed
sandbox reply from propagating into the host pipeline.
## Features
- `encodeWasiStdioFrame(envelope, pluginName)` — emit a single frame
ready to write into fd 0 in one syscall.
- `decodeWasiStdioFrame(bytes, pluginName)` — parse one frame and
validate its envelope against the strict Zod schema. Returns a
type-narrowed `PluginEnvelopeOut`.
- `PluginEnvelopeInSchema` / `PluginEnvelopeOutSchema` — Zod schemas
re-exported for callers that need to validate other byte sources
(e.g. integration tests, replay tooling).
- `MAX_WASI_STDIO_FRAME_BYTES` — 16 MiB ceiling per direction.
- Six distinct decoder failure reasons each carry a specific
`context.reason`: `short_header`, `length_mismatch`, `invalid_utf8`,
`invalid_json`, `shape_mismatch`, plus the oversize case under its
own code.
## Usage: Basic
```ts
import {
decodeWasiStdioFrame,
encodeWasiStdioFrame,
} from "@suaveplan/api-sandbox-wasm-host-bridge/wasi-stdio";
const frame = encodeWasiStdioFrame({ kind: "routes" }, "example");
// → write frame to fd 0
declare const incoming: Uint8Array;
const reply = decodeWasiStdioFrame(incoming, "example");
if (reply.kind === "routes") {
for (const route of reply.routes) {
// route.method, route.path
}
}
```
## Usage: Advanced
```ts
import {
decodeWasiStdioFrame,
encodeWasiStdioFrame,
MAX_WASI_STDIO_FRAME_BYTES,
PluginEnvelopeOutSchema,
} from "@suaveplan/api-sandbox-wasm-host-bridge/wasi-stdio";
import { SandboxWasmBridgeError } from "@suaveplan/api-sandbox-wasm-host-bridge/error-codes";
// Catch the oversize-frame case BEFORE the write hits the guest:
try {
const frame = encodeWasiStdioFrame(
{ kind: "request", request: buildHugeRequest() },
"example",
);
void frame;
} catch (err) {
if (
err instanceof SandboxWasmBridgeError &&
err.code === "SAND_WASM_BRIDGE.FRAME_OVERSIZE"
) {
// Surface a 413 to the caller without touching the guest.
}
}
// Use the raw schema to validate replay-buffer bytes outside the
// runtime path:
declare const replayBytes: Uint8Array;
declare function buildHugeRequest(): never;
const view = new DataView(
replayBytes.buffer,
replayBytes.byteOffset,
replayBytes.byteLength,
);
const length = view.getUint32(0, false);
if (length <= MAX_WASI_STDIO_FRAME_BYTES) {
const result = PluginEnvelopeOutSchema.safeParse(
JSON.parse(
new TextDecoder().decode(replayBytes.subarray(4, 4 + length)),
),
);
void result;
}
declare function decodeRoundtrip(): void;
void decodeRoundtrip;
void decodeWasiStdioFrame;
```
## API Reference
### `encodeWasiStdioFrame(envelope, pluginName): Uint8Array`
Throws:
- `SAND_WASM_BRIDGE.MARSHAL_FAILED` — `JSON.stringify` rejected the
envelope (e.g. it carried a `BigInt`).
- `SAND_WASM_BRIDGE.FRAME_OVERSIZE` — payload exceeds
`MAX_WASI_STDIO_FRAME_BYTES`.
### `decodeWasiStdioFrame(bytes, pluginName): PluginEnvelopeOut`
Throws `SAND_WASM_BRIDGE.PROTOCOL_MISMATCH` for `short_header`,
`length_mismatch`, `invalid_utf8`, `invalid_json`, `shape_mismatch`;
`SAND_WASM_BRIDGE.FRAME_OVERSIZE` when the declared length exceeds
the ceiling.
### `MAX_WASI_STDIO_FRAME_BYTES`
`16 * 1024 * 1024` — fixed by the protocol.
### `PluginEnvelopeInSchema` / `PluginEnvelopeOutSchema`
Zod schemas for the two envelope unions. Strict object shapes;
`safeParse` returns the discriminated union after passing.
## Implementation Notes
- Length is encoded big-endian (`DataView.setUint32(..., false)`) to
match every other length-prefix wire format in the Suaveplan stack
(CBOR, drive-template manifests).
- The encoder refuses oversize frames BEFORE allocating the output
buffer — emitting a partial header followed by truncated payload
would leave the guest's fd 0 in an unrecoverable state and force a
sandbox restart.
- The decoder rejects extra bytes implicitly by ignoring anything past
`HEADER_BYTES + length` — the runner is expected to slice exactly
one frame's worth of bytes per call. Extra bytes after a valid
frame are the caller's responsibility (they belong to the NEXT
frame).
- Zod schemas are pinned at module-load time so each
encode/decode is allocation-stable past the first invocation.
- The `PluginEnvelopeIn` schema is exported for callers that need to
validate envelopes they constructed by hand (e.g. test factories
authoring corrupt payloads); the production encode path does NOT
validate outgoing envelopes — type-narrowed callers cannot
construct an invalid `PluginEnvelopeIn`.