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

3.8 KiB

Sandbox WASM Host Bridge

Package: @suaveplan/api-sandbox-wasm-host-bridge Status: PROPOSED Type: new-package (host-integration layer)

Why

Same shape as api-sandbox-vm-host-bridge but for the WASM tier. @suaveplan/api-sandbox-wasm ships a WasmSandboxHandle — a WebAssembly.Instance running under WASI snapshot_preview1 with permission-gated suaveplan_caps.* imports. @suaveplan/api-host.installPlugin(spec) takes a PluginModule. The bridge between them does not exist, so gate 2.8's "deploy under wasm tier" requirement is unreachable.

Concrete consumer

  • @suaveplan/api-host.installPlugin(spec) — consumes the PluginModule produced by this bridge.
  • @suaveplan/e2e-foundation-stack-full Stage-8 closure smoke — deploys the WASM artifact produced by @suaveplan/wasm-compiler-javy (sibling change) through this bridge under the wasm tier.

Host bridge

This package IS the host-integration for api-sandbox-wasm. Required by §19.

End-to-end test

  • e2e/foundation-stack-full/tests/sandbox-wasm-deployment.spec.ts — deploys a WASM-compiled api-plugin-example via this bridge, drives an HTTP request through the host, asserts response was produced by the WASM-sandboxed plugin.

What changes

Package surface

import { createWasmSandbox } from "@suaveplan/api-sandbox-wasm";
import { createSandboxedWasmPluginModule } from "@suaveplan/api-sandbox-wasm-host-bridge";

const sandbox = createWasmSandbox({
    pluginName: "example",
    policy,
    capabilities,
    checker,
    wasmModule: await WebAssembly.compile(wasmBytes),
});

const module = await createSandboxedWasmPluginModule({
    sandbox,
    manifest: pluginManifest,
});

await host.installPlugin({ kind: "wasm-sandboxed", module, manifest });

Marshalling

Same SerializedRequest / SerializedResponse shapes from @suaveplan/api-types/sandbox-bridge (authored by the api-sandbox-vm-host-bridge change as the shared contract). Both bridges use the same wire format.

WASM-specific marshalling uses linear-memory buffer passing:

  1. Bridge allocates a buffer via the WASM __suaveplan_alloc(len) export.
  2. Writes the JSON-encoded SerializedRequest bytes into linear memory at the allocated offset.
  3. Calls the WASM __suaveplan_handle_request(ptr, len) export, which returns (responsePtr, responseLen).
  4. Reads the response bytes from linear memory.
  5. Frees both buffers via __suaveplan_free(ptr).
  6. Parses the SerializedResponse and returns the host Response.

The WASM plugin must export __suaveplan_alloc, __suaveplan_free, __suaveplan_handle_request, __suaveplan_routes (returns a JSON-encoded routes array at init time), __suaveplan_start, __suaveplan_dispose. The @suaveplan/wasm-compiler-javy sibling produces artifacts that conform to this ABI.

Error codes

SAND_WASM_BRIDGE.* registry:

  • INVALID_PLUGIN_SURFACE — WASM module didn't export required functions.
  • MEMORY_ALLOC_FAILED — __suaveplan_alloc returned 0 or trapped.
  • MARSHAL_FAILED — JSON-encoding of the request failed.
  • UNMARSHAL_FAILED — WASM returned non-conformant response bytes.
  • HANDLER_TRAPPED — WASM execution trapped.
  • CAPABILITY_DENIED — capability import returned WASI ACCES.
  • LIFECYCLE_FAILED — start/dispose export call trapped.

Conformance harness

runWasmHostBridgeConformance({ buildBridge }) with five invariants mirroring the vm-bridge harness: route-registration, request-round-trip, lifecycle, error-propagation, capability-gate.

Out of scope

  • The WASM compilation pipeline itself (sibling change wasm-compiler-javy).
  • Component-model-based WASM plugins (this bridge targets WASI snapshot_preview1; component-model is a follow-up).
  • Cross-instance shared memory (WASM linear-memory isolation is by design).