Files
skills/docs/api-stack/api/api-sandbox-wasm-host-bridge/src/bridge/bridge.md
2026-05-29 08:20:56 +01:00

4.4 KiB

Bridge factory

(no submodules)

Purpose

createSandboxedWasmPluginModule(options) is the public entry point of @suaveplan/api-sandbox-wasm-host-bridge. It dispatches on the supplied abiVariant: the default ("direct-i32-i64") routes to buildDirectInstall in bridge-direct.ts, which validates the six __suaveplan_* ABI exports + memory and bridges requests through linear memory; the additive variant ("wasi-stdio") routes to buildWasiStdioInstall in bridge-wasi-stdio.ts, which validates the WASI snapshot-preview1 import surface + _start + memory and bridges requests through length-prefixed JSON envelopes on fd 0 / fd 1. Both paths return the same SandboxedWasmPluginInstall shape so the host's installPlugin call is variant-agnostic. The returned bundle carries the module, the bridged PipelineRoute[], the idempotent lifecycle, and the underlying sandbox handle.

Features

  • Single bridge-init validation: the ABI is checked, routes are discovered, and instruments are wired up in one synchronous block; failures surface BEFORE the host sees the install bundle.
  • Per-route closures own the full linear-memory round-trip: alloc → write → handle_request → unpack → read → free → JSON-decode → host Response.
  • Trap classification: __suaveplan_handle_request faults surface as SAND_WASM_BRIDGE.HANDLER_TRAPPED with context.trapKind derived from the runtime error message.
  • Empty-response detection: a (0, 0) packed return value from the guest is treated as a marshal failure (reason: "empty_response") rather than as a silent success.
  • Capability denial passthrough: a SerializedResponse.errorCode === "SAND_PERM.DENIED" is re-thrown as the canonical SandboxPermissionError; other error codes surface as SAND_WASM_BRIDGE.CAPABILITY_DENIED carrying the sandbox's original code in context.sandboxErrorCode.
  • Lifecycle stop chains through to WasmSandboxHandle.dispose() so a host.uninstallPlugin always tears down the underlying sandbox.

Usage: Basic

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

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

const install = createSandboxedWasmPluginModule({ sandbox, manifest });
await host.installPlugin({
    module: install.module,
    manifest: install.manifest,
    manifestHash,
    specifier: "example@0.1.0",
    routes: install.routes,
});

Usage: Advanced

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

const instruments = createSandboxWasmBridgeInstruments({
    pluginName: manifest.name,
    meterProvider: customProvider,
});

const install = createSandboxedWasmPluginModule({
    sandbox,
    manifest,
    instruments,
});

install.module.start();
try {
    await runRequests(install.routes);
} finally {
    install.module.stop();
}

API Reference

  • createSandboxedWasmPluginModule(options): SandboxedWasmPluginInstall — throws SAND_WASM_BRIDGE.INVALID_PLUGIN_SURFACE on ABI mismatch.
  • CreateSandboxedWasmPluginModuleOptions — { sandbox, manifest, instruments? }.
  • SandboxedWasmPluginInstall — { module, manifest, routes, lifecycle, sandbox }.
  • SandboxedWasmPluginModule — plugin-module facade with name, version, requires, isolation: "sandboxed", trustLevel, start(), stop().

Implementation Notes

The per-route closure is async because reading the host's Request.body requires awaiting the arrayBuffer() promise; the guest call itself is synchronous (WASM exports are synchronous unless the module uses the JS Promise Integration proposal, which this bridge does not target). Base64-url-no-pad encoding/decoding is inlined rather than delegated to a foundation utility because the bridge must boot before any optional foundation polyfill has a chance to load — keeping the codec self-contained avoids a circular load order with @suaveplan/api-host. Object.freeze is applied to the routes array AND each route's options / params / statusMap records so the host's downstream code cannot mutate the bridge's view by accident.