A compute module is a WebAssembly module that settles deterministic
narrative steps — combat resolution, economy ticks, rule checks — inside a
sandboxed wasmtime host. It is a stateless pure function: each invocation
receives a ComputeInput envelope and returns a 4-part ComputeOutput
envelope; no state carries between calls. It runs host-local: the operator
installs it under ~/.nexus42/modules/<id>/, and a Connect peer can only name
it (module bytes are never peer-supplied). Over the shipped Connect surface
compute is read-only — the module never commits state itself (see
Read-only compute).
The normative ABI contract is
.mstar/specs/active/compute/compute-module-abi.md —
this doc is the authoring reference: the contract at a glance, the
manifest.json contract (incl. wasm_sha256), the allowlist gate, and the
operator install. The module-authoring walkthrough lives in
modules/README.md.
New modules should be authored on the official nexus-module-sdk
(modules/nexus-module-sdk/): nexus_entry!
generates the three ABI exports (alloc / init / compute) and wires the
global allocator, and the typed envelope skeleton + accessor helpers cover the
V1 surface — zero hand-copied marshalling. Scaffold from the in-repo template
(modules/_template/) and drive the authoring loop
with nexus42 compute build|validate|install (see the walkthrough in
modules/README.md) — the loop is service-host-free;
execution happens in a host: the Control Room's Run Studio, the Connect
compute op for admitted peers, or the core seam
ExecutionHandle::compute_run. For a guided authoring
reference, use the compute-module-author skill in the external
42ch-dev/agent-toolkit
repository (no agent skill ships in this repo). The ABI contract below stays
normative — the SDK is a generator for it, not a replacement.
Target wasm32-unknown-unknown (no WASI required). std is available on
this target — only I/O, threads, and the wall clock are absent (emit a
placeholder created_at; the host stamps authoritative timestamps).
| Export | Signature | Required | Purpose |
|---|---|---|---|
memory |
exported linear memory | yes | The host reads input JSON and writes output JSON into this memory. |
alloc |
(len: u32) -> u32 |
yes | Allocate len bytes in linear memory; return the pointer. |
compute |
(in_ptr: u32, in_len: u32, out_ptr: u32, out_cap: u32) -> i64 |
yes | Read ComputeInput JSON from [in_ptr, in_ptr+in_len), write ComputeOutput JSON to [out_ptr, out_ptr+written), return written. Negative returns are error sentinels (below). |
init |
() -> () |
no | One-shot setup, called once after instantiation when the manifest declares init_export. |
The compute export name is configurable — the manifest's compute_export
field declares it; the signatures are fixed. If init traps, the invocation
fails immediately (no retry). Full semantics: spec §2–§2.1.
A module may import up to two host functions from the nexus namespace. The
host registers only the functions the manifest's host_functions list
names; importing anything else fails instantiation.
| Import | Signature | Behavior |
|---|---|---|
nexus::kb_read |
(id_ptr: u32, id_len: u32, out_ptr: u32, out_cap: u32) -> i64 |
Look up a KnowledgeEntry by ID in the invocation's key_blocks snapshot; write its JSON to out. Returns bytes written, -1 if not found, -2 if out_cap too small. |
nexus::narrative_query |
(q_ptr: u32, q_len: u32, out_ptr: u32, out_cap: u32) -> i64 |
Return narrative context JSON (V1 passes narrative_state through verbatim). Same return convention. |
Canonical data path: the host always bundles the relevant KnowledgeEntries
into ComputeInput.key_blocks (selected by the manifest's
required_key_block_types). Most modules — including basic-combat — read
everything they need from that inline snapshot and declare host_functions: []. Use the host imports only to look up additional blocks or context
beyond what the host pre-selected. Spec §3.
Each compute() call runs in a fresh wasmtime Store + Instance —
limits start from a clean slate and nothing leaks across calls (this is why
alloc may leak intentionally; see below). Three independent limits; a breach
traps and surfaces as a ComputeError, never a host crash. Spec §8.
| Limit | Default | Manifest override |
|---|---|---|
| Fuel (instruction count) | 10,000,000 | max_fuel |
| Memory cap | 64 MiB | max_memory_mib |
| Wall-time | 30 s | max_wall_time_ms |
All exchange goes through the module's linear memory as UTF-8 JSON, with a pointer+length convention for input and pointer+capacity for output (spec §6):
- The host calls
alloc(in_len)to reserve the input buffer and writes theComputeInputJSON into[in_ptr, in_ptr+in_len). - The host calls
alloc(out_cap)to reserve the output buffer (out_capis typically 64 KiB — enough for a 4-part combat output). - The module writes the
ComputeOutputJSON into[out_ptr, out_ptr+written)and returnswritten:
| Return | Meaning |
|---|---|
>= 0 |
Success — bytes written to the output buffer |
-1 |
Generic module error |
-2 |
Output buffer too small (out_cap < needed) |
Host functions use the same convention (-1 = not found / unsupported query).
basic-combat/src/lib.rs (now SDK-based)
documents the wire mechanics the SDK's shim implements — module authors do not
copy this by hand; nexus_entry! generates the exports for them (see
Author with the SDK). The convention itself is
normative:
allocbuilds aVecandmem::forgets it — the leak is intentional (the per-invocation instance is discarded right after the call, so there is no long-lived leak).computereturns-2when the serialized output would exceedout_cap, copies the output withptr::copy_nonoverlapping(input and output buffers are separate allocations), and returns-1on any compute error (malformed input / missing combatants).- A
#[global_allocator](dlmalloc) is required:stdprovides none onwasm32-unknown-unknown. The host's memory cap bounds growth.
Defined in
schemas/daemon-api/compute/compute-input.schema.json
(spec §4):
| Field | Type | Meaning |
|---|---|---|
schema_version |
integer (1) |
Envelope version. |
world_ref |
object | World + timeline locator (world_id, branch_id, timeline_head_event_id). |
key_blocks |
array of KnowledgeEntry |
Snapshot of relevant entries (full wire shape, incl. body with state). Selected by required_key_block_types. |
narrative_state |
object | Optional narrative position context; module-declared shape. |
invocation |
object | Optional module-defined freeform params (declared in schemas.invocation), passed through verbatim. |
Defined in
schemas/daemon-api/compute/compute-output.schema.json.
Exactly four top-level keys (spec §5):
| Key | Meaning |
|---|---|
state_delta |
Ordered add / sub / set ops on dotted nested state paths of computable KnowledgeEntry bodies. |
timeline_events |
Events to append (event_type: "state_update", status: "canon" for compute outcomes; placeholder created_at). |
new_key_blocks |
New KnowledgeEntries the module creates; the host upserts them. |
battle_report |
Module-declared freeform report; kind discriminates the payload. |
The host applies the output in this order: state_delta → new_key_blocks →
timeline_events → battle_report (events may reference freshly upserted
entries; the report reflects post-delta state). All deltas apply atomically —
no partial application on error.
Every module ships a manifest.json next to its .wasm. Full contract:
spec §7 and modules/README.md.
| Field | Type | Meaning |
|---|---|---|
module_id |
string | Unique module id. Must match the directory name and the install directory (~/.nexus42/modules/<id>/). |
name |
string | Human-readable name. |
version |
string | Module SemVer (independent of the Nexus ABI version). |
nexus_abi_version |
integer | Compute envelope ABI version. Unrecognized versions are rejected. |
required_key_block_types |
array of string | BlockTypes the module reads (e.g. ["character"]); the host bundles matching entries into key_blocks. |
compute_export |
string | Name of the WASM export implementing compute. |
init_export |
string | Name of the WASM export implementing init; empty string if none. |
| Field | Type | Default | Meaning |
|---|---|---|---|
description |
string | — | Free-form description. |
author |
string | — | Author attribution. |
host_functions |
array of string | [] |
Subset of ["kb_read", "narrative_query"] the module may call. |
battle_report_kind |
string | — | Discriminator the module emits in battle_report.kind. |
max_fuel |
integer | host default (10M) | Per-invocation fuel override. |
max_memory_mib |
integer | host default (64) | Per-invocation memory-cap override (MiB). |
max_wall_time_ms |
integer | host default (30000) | Per-invocation wall-time override (ms). |
wasm_sha256 |
string | — | SHA-256 of the compiled .wasm bytes this manifest pairs with (see below). |
schemas |
object | — | inline JSON-Schema fragments — key_block_attributes, key_block_state, invocation, battle_report — validated by the host before/after invocation. Omitting the block disables validation (existing modules keep working unchanged). Spec §7.3. |
wasm_sha256 is the SHA-256 of the exact .wasm bytes the manifest ships
with (64 lowercase hex chars). It is optional but recommended: when
present, the loader hashes the loaded bytes and rejects a mismatch before
the pair is compiled or cached — an old manifest + new .wasm always
mismatches, so a mixed pair never enters the module cache and every invocation
fails as a host fault (internal_error, "wasm does not match manifest
wasm_sha256").
When the field is absent (legacy manifests), the loader falls back to the stat fence: a size + mtime re-stat around the read that detects a mid-load swap. The fence cannot detect a same-size swap landing outside its observation windows — another reason to set the hash.
Compute it from the installed artifact:
shasum -a 256 basic-combat.wasm # macOS (prints "<hash> <filename>")
sha256sum basic-combat.wasm # Linux (prints "<hash> <filename>")Embedded modules are auto-paired. crates/nexus-wasm-host/build.rs
compiles the embedded copy and injects wasm_sha256 computed from the actual
compiled bytes into the staged manifest — the embedded pair is always
content-consistent, and the source modules/<id>/manifest.json is left
untouched. (Embedded modules are not reachable over Connect — see
Operator install.)
Compute is gated by the peer allowlist. A peer may invoke a module only when
its allowlist entry's module_scope contains the module id — missing or
empty module_scope denies ALL compute with module_not_scoped, fail-closed,
before any WASM execution:
{
"peer_ids": [
{
"peer_id": "12D3KooW…",
"world_scope": ["wld_…"],
"op_scope": ["upsert", "promote", "relate", "check", "assemble", "compute"],
"module_scope": ["basic-combat"]
}
]
}module_scope ids are host-local module names (~/.nexus42/modules/),
never peer-supplied bytes — the peer names only what the operator installed.
world_scope / op_scope / module_scope are per-peer; denials happen before
any orchestrator call, with zero side effects. The module id for an invocation
resolves from the staged compute session state first, then the entry's
body.computable.module_id. Allowlist mechanics and the full scoped-entry
contract: nexus-runtime.md and
the integrator walkthrough.
Modules live under ~/.nexus42/modules/<id>/ (the home the runtime resolved:
--home > NEXUS42_HOME > $HOME):
~/.nexus42/modules/<id>/<id>.wasm
~/.nexus42/modules/<id>/manifest.json
<id> must match both the directory name and the manifest's module_id. An
absent or incomplete pair is module_not_found at invoke time.
Build and install (mirrors strategy-samples/README.md §5; basic-combat is
a standalone crate — not a member of the nexus workspace, with its own
Cargo.lock) with the repo's own daemon-free module tools. nexus42 compute build runs cargo for you (resolving CARGO_TARGET_DIR itself — the repo's
.envrc relocates the target dir), injects wasm_sha256 into the staged
manifest, and stages the pair under dist/<module_id>/; nexus42 compute install re-verifies the pairing and copies the pair into the CLI home's
module store:
rustup target add wasm32-unknown-unknown # once per toolchain
nexus42 compute build --manifest modules/basic-combat/manifest.json --release
# → staged pair: modules/basic-combat/dist/basic-combat/basic-combat.wasm
# + modules/basic-combat/dist/basic-combat/manifest.json (wasm_sha256 injected)
nexus42 compute install --module-id basic-combat \
--manifest modules/basic-combat/dist/basic-combat/manifest.json \
--wasm modules/basic-combat/dist/basic-combat/basic-combat.wasmcompute install targets the CLI home ($HOME/.nexus42 — the nexus42 CLI
resolves its home from $HOME only). When the runtime resolves a different
home (--home <path> or NEXUS42_HOME=<path> over a differing $HOME),
copy the staged pair into <home>/.nexus42/modules/basic-combat/ by hand
instead — the staged manifest already carries the matching wasm_sha256.
On the Connect surface the embedded module set is not reachable: the
runtime serves only operator-installed modules under ~/.nexus42/modules/.
The full end-to-end compute walkthrough (stage a session, invoke, receipts):
strategy-samples/README.md §5.
Over the shipped Connect surface compute is read-only by design:
settle: trueis rejected withsettle_not_enabled— the module never commits state itself.- A
settle: falseresponse carries no settledstatemap; it surfaces the merged computable state view (the confirmed receipt: session state merged with the request'scomputablemap and the module'sstate_delta). - Committing the confirmed result is the caller's job: persist the receipt
in your turn ledger and apply world-state changes through the write path
(
upsertwith world-aware CAS / structured-failure rules — never a forced overwrite).
The module's ComputeOutput (state deltas, timeline events, new blocks) is
what the host would apply in a settling surface; the read-only lane hands the
receipt back to the caller instead. The settle → receipt → narrate discipline
is documented in the
TRPG turn strategy.
- Integrator walkthrough — the E2 loop, incl. the compute op.
- Runtime usage — install/run, allowlist +
module_scopesetup, home layout. - Strategy authoring — the strategy side of the loop.
- Docs index — all docs.
- Module guide:
modules/README.md— authoring walkthrough + embedding procedure. - Reference implementation:
modules/basic-combat/. - ABI spec (normative):
.mstar/specs/active/compute/compute-module-abi.md.