Reference Phase B (WASM) community extension for the CueCrux Crux daemon.
Single tool — ext.summarise.prefix(prefix, top_sentences?) — that:
- Reads every fact under
args.prefixvia the host'scrux::query_facts. - Computes a frequency-based extractive summary.
- Writes the summary back to the fact store under
summarise::{prefix}, keysummary.
Use it as a starting template for community-contributed Wasm extensions.
- A
kind: wasmmanifest withwasm_module_path+wasm_module_sha256pinning. - The full Wasm host ABI surface (
log,now_unix_ms,read_fact,store_fact,query_facts) called from Rust via#[link(wasm_import_module = "crux")]extern bindings. - The
extension_call(req_ptr, req_len, resp_ptr, resp_cap) -> i32entry point and how to round-trip JSON through linear memory. - An end-to-end build pipeline:
cargo build→ SHA-256 → Ed25519 manifest signing → install via the daemon'sPOST /v1/extensions/register.
.
├── Cargo.toml # workspace root
├── module/ # the .wasm crate
│ ├── Cargo.toml # crate-type = ["cdylib"]
│ └── src/lib.rs # extension_call + summariser logic
├── signer/ # native signing helper
│ ├── Cargo.toml
│ └── src/main.rs # builds + signs manifest.json
├── dev-keypair.json # PUBLIC dev seed (safe to commit)
├── manifest.json # signed manifest (regenerated by signer)
├── extension.wasm # built artefact (regenerated by signer)
├── LICENSE # MIT
└── README.md
rustup target add wasm32-unknown-unknowncargo build --release -p summarise-module --target wasm32-unknown-unknown
cargo run -p summarise-signerThe signer:
- reads the freshly-built
target/wasm32-unknown-unknown/release/summarise_module.wasm, - copies it to
extension.wasm, - computes SHA-256 of the bytes,
- builds the manifest with
kind: wasm,wasm_module_path: extension.wasm, and the computed sha, - signs it with the dev keypair from
dev-keypair.json, - writes the result to
manifest.json.
The daemon must be built with the wasm feature:
cargo build --release -p corecruxd --features wasm-extensionsThen through the Console (http://127.0.0.1:14800/console → Extensions):
a. Trusted keys → add the dev fingerprint + public key from the
signer output (p_… and the 64-char hex). Trust tier LocallySigned.
b. + Install → paste the contents of manifest.json.
c. Place the wasm bytes where the daemon expects them. By default
that's <data_dir>/extensions/ext.summarise/extension.wasm. Either:
- Copy
extension.wasmfrom this repo into that directory yourself. - Or use the URL form: edit
manifest.jsonto swapwasm_module_pathforwasm_module_url: "https://your-host/extension.wasm", re-sign, and the daemon will download + verify at install time (M6.4 download path).
d. + Issue grant to a passport with these scopes:
| Field | Value |
|---|---|
| Allowed tools | ext.summarise.prefix |
| Read prefixes | personal::notes:: (or whatever you want to summarise) |
| Write prefixes | summarise::personal::notes:: |
| Rate (per min) | 30 |
e. Test call (or use raw curl):
DAEMON=http://127.0.0.1:14800
curl -s -X POST "$DAEMON/v1/extensions/ext.summarise/tools/ext.summarise.prefix/invoke" \
-H 'content-type: application/json' \
-d '{"args":{"prefix":"personal::notes::","top_sentences":3},"passport_fpr":"<your-passport-fpr>"}'Response shape:
{
"result": {
"summary": "<top-3 sentences joined by '. '>",
"fact_count": 12,
"stored_under": "summarise::personal::notes",
"stored_fact_id": "<host-assigned>",
"at_unix_ms": 1714938000000,
"top_sentences": 3
},
"elapsed_ms": 3,
"fuel_consumed": 84120,
"log": [{"level": "info", "message": "summarising prefix=personal::notes:: top=3", "at_unix_ms": …}],
"request_id": "req-…"
}The Wasm module exports:
(extension_call (param i32 i32 i32 i32) (result i32))
(memory (export "memory") 1)
The daemon writes the request JSON at req_ptr..req_ptr+req_len. The
module reads it, does its work, writes a JSON response at
resp_ptr..resp_ptr+resp_cap, and returns bytes-written (or -1 on
overflow).
Imports (from module "crux"):
log(level_ptr, level_len, msg_ptr, msg_len) -> ()
now_unix_ms() -> u64
current_passport_json(ptr, cap) -> i32
read_fact(entity_ptr, entity_len, key_ptr, key_len, resp_ptr, resp_cap) -> i32
store_fact(entity_ptr, entity_len, key_ptr, key_len, value_ptr, value_len,
confidence_thousandths, resp_ptr, resp_cap) -> i32
query_facts(prefix_ptr, prefix_len, query_ptr, query_len, top_k,
resp_ptr, resp_cap) -> i32
get_secret_decrypted(...) -> i32 [stub: -6 NOT_IMPLEMENTED]
emit_receipt(...) -> i32 [stub: -6 NOT_IMPLEMENTED]
Negative return codes (stable wire contract):
| rc | meaning |
|---|---|
| -1 | not found |
| -2 | no grant |
| -3 | scope violation |
| -4 | response buffer too small |
| -5 | fact_store unavailable |
| -6 | host fn not implemented |
| -10 | host internal error |
| -11 | bad input (utf-8, OOB pointer) |
| -12 | serialise error |
| Limit | Default | Env override |
|---|---|---|
| Fuel | 1,000,000 instructions | CORECRUXD_WASM_FUEL_DEFAULT |
| Linear memory | 16 MiB | CORECRUXD_WASM_MEMORY_BYTES_DEFAULT |
| Wall clock | 1 second | CORECRUXD_WASM_WALL_MS_DEFAULT |
| Epoch tick | 10 ms | CORECRUXD_WASM_EPOCH_TICK_MS |
- Change
dev-keypair.json'ssigning_key_seed_hexto a new random 32-byte hex string (head -c 32 /dev/urandom | xxd -p -c 64) and updatepassport_fpr_labelaccordingly. - Edit
module/src/lib.rsto implement your tool. Theextension_call,host_*wrappers, andwrite_responseboilerplate should remain stable; everything insiderun(...)is your tool's logic. - Update the manifest fields in
signer/src/main.rs(id,name,summary,tools[].name,tools[].input_schema). cargo build --release -p summarise-module --target wasm32-unknown-unknown && cargo run -p summarise-signer.
MIT — see LICENSE. The dev keypair seed in
dev-keypair.json is public and reproducible from a label; do not
use it to sign production manifests.