WASM data format
Everything untrusted — the JavaScript interpreter, a compiled skill, a plugin — runs as a WebAssembly guest on wazero. A guest cannot open a socket or a file itself; it reaches a capability only by calling a host function the host handed it. This page documents the exact data that crosses that boundary.
The one import a guest sees
Section titled “The one import a guest sees”The host mounts a single import module, nocturn. Its members are exactly the granted host
functions and nothing else — a capability the guest was not granted is structurally absent,
so it cannot even be named, let alone called (“unforgeable by absence”).
The JavaScript interpreter, for example, declares exactly one import of its own: nocturn.call.
Its other imports are WASI’s — stdio, the clock, random_get — which the sandbox answers itself and
which reach nothing outside the instance.
The (ptr, len) ABI
Section titled “The (ptr, len) ABI”Data crosses as raw bytes in the guest’s linear memory. The call convention is the standard host↔wasm contract (the same one QuickJS, Extism and friends use):
guest calls nocturn.<name>(reqPtr: u32, reqLen: u32) -> u64-
The guest writes its request bytes into its own memory and passes
(reqPtr, reqLen). -
The host reads
reqLenbytes atreqPtr(wazero bounds-checks the read) and copies them out immediately — a memory view is only valid for the duration of the call. -
The host runs the capability and produces response bytes.
-
The host allocates space for the response inside the guest via the guest’s exported
malloc, writes the bytes there, and returns a packed pointer:return value (u64) = (addr << 32) | size -
The guest unpacks
addrandsize, readssizebytes ataddr, thenfreesaddr.
A return value of 0 means an empty response. The guest must therefore export malloc and
free for the host to place the response.
The JSON envelope (for code_run)
Section titled “The JSON envelope (for code_run)”On top of the raw byte ABI, the script dispatcher speaks JSON. When a script calls
nocturn.call(tool, args), the request bytes are:
{ "tool": "http_read", "args": { "url": "https://example.com" } }The host looks tool up in the same tool set the model dispatches through and calls it — which
means the tool’s own gate.Check runs, exactly as it would for the model. The result string comes
back as the response bytes. A script therefore calls a tool through the identical path as the
model, and the interpreter is never rebuilt to add a tool.
Errors come back as a response prefixed with error: . The guest’s binding turns that into
a JavaScript exception, so a denied or failed call raises in the script instead of crashing the
host. Pure computation that never calls a tool asks for nothing at all.
What this buys
Section titled “What this buys”The byte boundary is deliberately tiny and uniform: one import module, one (ptr,len) calling
convention, one JSON envelope. Every tool is just a Go function on the host side of that boundary —
which is exactly why each one is auditable, wrappable and revocable in one place.