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,78 @@
# Bridge factory
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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
```ts
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
```ts
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.