Shipping a compiled module
wasm/handlers.js is a convenience, not the contract. The contract is the module’s ABI, and any language that can target it — Rust, TinyGo, AssemblyScript, Zig, hand-written WAT — can ship a self-contained realm. Two conventions are accepted, and the host tells them apart by export probe: a module exporting embabel_dispatch is a reactor; anything else runs as a WASI command. Reactor wins if a module fits both.
Reactor. The module exports memory, embabel_alloc(len: i32) -> i32, and embabel_dispatch(functionPtr: i32, functionLen: i32, argsPtr: i32, argsLen: i32) -> i64, plus optional WASI _initialize (invoked before dispatch when present; _start is not called). All strings are UTF-8. The returned i64 packs a pointer to the response in its high 32 bits and the response length in its low 32; each half is read as a non-negative 32-bit value (so bounded by 2³¹−1) and must lie inside the module’s memory, or the dispatch fails. Allocation is one-way: the host writes the function and args into guest memory through embabel_alloc; the guest allocates its own response buffer; nothing is ever freed, because every dispatch runs in a fresh instance discarded afterwards — which is also what enforces statelessness. The response must be exactly one JSON envelope, {"result": ...} or {"error": "..."}: anything else fails the dispatch, an error envelope fails it with that message, and a guest trap fails it with the trap’s.
Reactor host imports live under module "embabel": call(reqPtr: i32, reqLen: i32) -> i64 invokes a gateway tool — request {"tool": "<gateway name>", "args": {...}}, reply an envelope the host writes into guest memory through embabel_alloc and returns with the same pointer/length packing — and log(level: i32, ptr: i32, len: i32) logs UTF-8 text (level 0 = debug, 1 = info, 2 = warn, anything else = error).
WASI command stdio. For interpreter-in-wasm bundles. The host writes exactly one request line to stdin — {"function": "<namespace>.<name>", "args": {...}} plus a newline — and runs _start. Stdout is line-oriented: a line whose first byte is NUL is a protocol frame; every other line is guest logging. A frame is a NUL-delimited marker — \0embabel:call\0 or \0embabel:result\0 — followed on the same line by the frame’s JSON. A call frame carries {"tool": ..., "args": ...}; the host invokes the gateway and appends the reply envelope to stdin as a new line before the guest’s write returns, so the guest reads the reply by blocking on stdin. There is no correlation id — calls are strictly one at a time, request then reply, in order. The result frame carries the dispatch’s response envelope and must appear exactly once: no result frame, a second result frame, or a nonzero exit status each fail the dispatch. A NUL-prefixed line matching neither marker is treated as logging.
Both conventions speak the same { result } / { error } envelope at every hop. The guest gets byte-array stdio only — no inherited descriptors, no preopened filesystem.

