Skip to content

WASM Modules

WASM sandboxes are created from a module reference (module_ref). The daemon resolves the ref to a content-addressed file under SB_WASM_MODULES_DIR and records it in the wasm_modules catalogue.

A module_ref is resolved in this precedence order:

  1. Standard-module keyword — a short alias your operator staged on every node (e.g. python, javascript). Recommended: no host paths, identical fleet-wide. See Standard modules.
  2. oci://host/repo:tag — a registry artifact, pulled under your own credentials and cached content-addressed. See Bring your own module.
  3. Catalogue id — a digest id returned by createWasmModule.
  4. file:///path/to/module.wasm / bare filename under SB_WASM_MODULES_DIR — for self-managed hosts.

Whatever the form, the daemon validates the artifact is a core wasip1 module (WASI components are rejected), records the resolved digest, and pins it to the sandbox so a restart or failover boots the exact same bytes. Remote oci:// pulls are restricted to the hosts in SB_WASM_REGISTRY_ALLOWLIST.

Operators pre-stage a curated set of language modules on every node (via the stage-wasm-modules playbook) and expose them as reserved keywords through SB_WASM_STANDARD_MODULES. You reference one by name — no upload, no path:

await client.create({ module_ref: 'python', runtime: 'wasm' })

A standard-module keyword always wins over a same-named file in the modules dir, so the curated runtime can never be shadowed.

Compile your program to a core wasip1 module (GOOS=wasip1 for Go, --target wasm32-wasip1 for Rust), then push it to the registry under your own credentials. The daemon validates and forwards the bytes — it never stores them or acts as a registry — and returns the oci:// ref to use on create.

import { readFile } from 'node:fs/promises'
const wasm = await readFile('app.wasm')
const pushed = await client.pushWasmModule({
name: 'tenant/app',
tag: 'v1',
module: new Uint8Array(wasm),
registryUsername: process.env.AOCR_USER,
registryToken: process.env.AOCR_TOKEN!,
})
await client.create({ module_ref: pushed.moduleRef, runtime: 'wasm' })

You can also oras push your module to the registry directly and skip the daemon — then create with the oci:// ref plus your registry credentials. Either way the registry is the source of truth and the daemon only pulls to run.

Register modules on the host before create, or let create resolve inline. Catalogue rows are per-node (not cluster-forwarded), like Firecracker templates.

const mod = await client.createWasmModule({
moduleRef: 'file:///opt/agent.wasm',
entrypoint: '_start',
})
const rows = await client.listWasmModules()
await client.deleteWasmModule(mod.id)

When SB_WASM_POOL_ENABLED=true, the daemon keeps pre-spawned workers per module digest. Creates against a warm module skip cold worker boot.

const sandbox = await client.create({
module_ref: 'https://example.com/builds/agent.wasm',
runtime: 'wasm',
memoryMB: 512,
})

Peers gossip local_wasm_module_ids via /v1/capacity. Placement prefers nodes that already cache the requested module_ref.

When SB_WASM_MODULE_GC_ENABLED=true, the wasm module janitor runs two sweeps each SB_WASM_MODULE_GC_INTERVAL:

  • Catalogue rows. Unreferenced rows older than SB_WASM_MODULE_GC_TTL are removed.
  • oci:// cache bytes. Content-addressed files under SB_WASM_CACHE_DIR (<digest>.wasm) are evicted when older than SB_WASM_CACHE_GC_TTL (default 24h), or oldest-first when the cache exceeds SB_WASM_CACHE_MAX_BYTES (default unlimited). A digest still referenced by a sandbox or a catalogue row is never evicted, regardless of age or disk pressure — eviction only reclaims pull-on-create modules that nothing depends on.