diff --git a/.claude/hooks/content-lint.sh b/.claude/hooks/content-lint.sh new file mode 100755 index 0000000..1c3405e --- /dev/null +++ b/.claude/hooks/content-lint.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# PostToolUse(Write|Edit): block an edit that ADDS a banned token to a .rs or +# .md file. Counts are compared against the committed version, so a file that +# already carries a banned token stays editable and only a net increase blocks. +set -u +f=$(jq -r '.tool_input.file_path // .tool_response.filePath // empty' 2>/dev/null) || exit 0 +case "$f" in *.rs|*.md) ;; *) exit 0 ;; esac +[ -f "$f" ] || exit 0 + +block() { printf '{"decision":"block","reason":%s}\n' "$(jq -Rn --arg r "$1" '$r')"; exit 0; } + +# $1 = rg flags, $2 = pattern, $3 = message +check() { + local now head root rel + now=$(rg -o $1 -- "$2" "$f" 2>/dev/null | wc -l | tr -d ' '); now=${now:-0} + [ "$now" -eq 0 ] && return 0 + head=0 + if root=$(git -C "$(dirname "$f")" rev-parse --show-toplevel 2>/dev/null); then + rel=${f#"$root"/} + head=$(git -C "$root" show "HEAD:$rel" 2>/dev/null | rg -o $1 -- "$2" 2>/dev/null | wc -l | tr -d ' ') + head=${head:-0} + fi + [ "$now" -gt "$head" ] && block "$3 File: $f (was $head, now $now)." + return 0 +} + +check -F $'\xe2\x80\x94' "This edit adds an em-dash. House style bans em-dashes: use an ASCII hyphen, a colon, or split the sentence." +exit 0 diff --git a/.claude/hooks/nextest-on-stop.sh b/.claude/hooks/nextest-on-stop.sh new file mode 100755 index 0000000..33837e7 --- /dev/null +++ b/.claude/hooks/nextest-on-stop.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Stop hook: run `cargo nextest run` for the workspace crates that have +# uncommitted .rs changes. Non-blocking; reports a pass/fail summary. No-op when +# nothing relevant changed or cargo/nextest are unavailable. +set -u +root=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0 +cd "$root" || exit 0 +command -v cargo >/dev/null 2>&1 || exit 0 +cargo nextest --version >/dev/null 2>&1 || exit 0 + +changed=$(git status --porcelain=v1 2>/dev/null | awk '{print $NF}' | rg '\.rs$' || true) +[ -z "$changed" ] && exit 0 + +meta=$(cargo metadata --no-deps --format-version 1 2>/dev/null) || exit 0 +pkgs=$(printf '%s\n' "$changed" | while IFS= read -r f; do + [ -n "$f" ] || continue + printf '%s' "$meta" | jq -r --arg f "$root/$f" \ + '.packages[] | (.manifest_path | rtrimstr("Cargo.toml")) as $d | select($f | startswith($d)) | .name' +done | sort -u) +[ -z "$pkgs" ] && exit 0 + +args=(); while IFS= read -r p; do [ -n "$p" ] && args+=(-p "$p"); done <<< "$pkgs" +list=$(printf '%s' "$pkgs" | tr '\n' ' ') +if out=$(cargo nextest run --no-tests=warn "${args[@]}" 2>&1); then + printf '{"systemMessage":%s,"suppressOutput":true}\n' "$(jq -Rn --arg m "nextest OK for touched crates: $list" '$m')" +else + fails=$(printf '%s' "$out" | rg -N 'FAIL|error\[|test result: FAILED|panicked' | tail -15) + printf '{"systemMessage":%s}\n' "$(jq -Rn --arg m "nextest FAILED for touched crates ($list): +$fails" '$m')" +fi +exit 0 diff --git a/.claude/hooks/rustfmt-on-edit.sh b/.claude/hooks/rustfmt-on-edit.sh new file mode 100755 index 0000000..1592703 --- /dev/null +++ b/.claude/hooks/rustfmt-on-edit.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# PostToolUse(Write|Edit): format the edited Rust file with rustfmt (edition 2024). +# Fast per-file format; no-op for non-.rs paths or when rustfmt is unavailable. +set -u +f=$(jq -r '.tool_input.file_path // .tool_response.filePath // empty' 2>/dev/null) || exit 0 +case "$f" in *.rs) ;; *) exit 0 ;; esac +[ -f "$f" ] || exit 0 +command -v rustfmt >/dev/null 2>&1 || exit 0 +rustfmt --edition 2024 "$f" 2>/dev/null || true +exit 0 diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..c257406 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,32 @@ +{ + "hooks": { + "PostToolUse": [ + { + "matcher": "Write|Edit", + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/rustfmt-on-edit.sh", + "timeout": 30 + }, + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/content-lint.sh", + "timeout": 15 + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/nextest-on-stop.sh", + "timeout": 600 + } + ] + } + ] + } +} diff --git a/.config/nextest.toml b/.config/nextest.toml new file mode 100644 index 0000000..18cb55d --- /dev/null +++ b/.config/nextest.toml @@ -0,0 +1,3 @@ +[profile.default] +# Fail a hung test by name after 5 minutes instead of letting it eat the CI timeout. +slow-timeout = { period = "60s", terminate-after = 5 } diff --git a/.gitignore b/.gitignore index 35532e0..d330db8 100644 --- a/.gitignore +++ b/.gitignore @@ -25,9 +25,14 @@ Thumbs.db # which is then caught by the rule above). !.env.example -# Agent skills / AI tooling — installed locally, never committed. +# Agent skills / AI tooling. Shared repo config is tracked; anything personal +# or per-session is not. .agents/ -.claude/ +.claude/* +!.claude/settings.json +!.claude/hooks/ +.claude/settings.local.json +.claude/worktrees/ skills-lock.json # Engine runtime state (default state_dir from engine.toml). diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..453efea --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,75 @@ +`CLAUDE.md` is a symlink to this file. + +## What nexum-runtime is + +nexum-runtime is a WASM Component Model host runtime for web3 modules, and it supervises guest components built against the `nexum:host` WIT world. +Each module gets a capability-gated view of the host: chain access over JSON-RPC, an allowlisted `wasi:http` outbound gate, a local key-value store, clocks, and structured logging. +The host enforces fuel, memory, and epoch limits per module. + +The runtime is generic and venue-agnostic: it ships no venue and no domain payload, and `crates/nexum-cli` composes the core lattice and nothing else. +A downstream layer adds its own capabilities through the extension seam in `crates/nexum-world`. +A composition root declares `[extensions.]` rows in an `extensions.toml`, and `synthesize` emits those rows after the core capability rows in the module's WIT world. + +This repository is the leaf of the Nullis runtime stack and carries no cross-repo dependencies. +The videre and shepherd repositories build on the SDK and the runtime published here. + +## Layout + +- `crates/nexum-runtime` - the engine host: wasmtime embedding, supervisor, capability providers, and metrics. +- `crates/nexum-cli` - the bare `nexum` engine binary, composed over `nexum-launch` with the `CoreRuntime` preset. +- `crates/nexum-launch` - the generic launcher: CLI parsing, config loading, tracing setup, and the run loop for a preset. +- `crates/nexum-sdk` - the guest-side SDK that modules build against, host-neutral and domain-free. +- `crates/nexum-sdk-test` - in-memory host mocks and assertion helpers for module unit tests. +- `crates/nexum-module-macros` - the `#[module]` proc-macro, reached through `nexum_sdk::module`. +- `crates/nexum-tasks` - task lifecycle and graceful shutdown, and the only crate that spawns raw `tokio` tasks. +- `crates/nexum-world` - per-module WIT world synthesis, the core capability table, and the extension registry. +- `modules/example` - the minimal reference module, with balance-tracker, http-probe, and price-alert under `modules/examples/`. +- `modules/fixtures/` - adversarial fixtures: clock-reader, flaky-bomb, fuel-bomb, memory-bomb, panic-bomb, and slow-host. +- `tools/load-gen` - the load generator for soak runs. +- `wit/nexum-host` - the `nexum:host` WIT package. + +## Build, test, lint + +The workspace uses Rust edition 2024. +The flake pins the toolchain to Rust 1.94.0, which matches the toolchain CI installs. +Run `nix develop` to enter the dev shell, or run `direnv allow` once. +The dev shell supplies the toolchain, the `wasm32-wasip2` target, `cargo-nextest`, `just`, `ripgrep`, and `ast-grep`. + +Use the justfile recipes: + +```sh +just build # the engine plus every guest wasm +just test # host engine unit tests through nextest +just fmt # cargo fmt --all +just lint # cargo clippy --workspace --all-targets --all-features -- -D warnings +just ci # the full CI series: fmt, clippy, doc, wasms, nextest, doctests +``` + +Run tests with `cargo nextest run`, not `cargo test`. +nextest does not run doctests, so run `cargo test --doc --workspace --all-features` as well. +Run `just fmt` and `just lint` before each commit, because CI fails on any rustfmt or clippy warning. + +The hooks in `.claude/hooks/` support this loop. +`rustfmt-on-edit.sh` formats each edited `.rs` file. +`nextest-on-stop.sh` runs nextest for the crates with uncommitted `.rs` changes at the end of a turn. +Each hook exits without work when `rustfmt`, `cargo`, or `cargo-nextest` is absent, so both stay silent outside the dev shell. + +## House rules + +Do not use em-dashes in source, rustdoc, markdown, commit messages, or PR and issue bodies. +Use an ASCII hyphen, use a colon, or write two sentences. +`.claude/hooks/content-lint.sh` blocks an edit that adds an em-dash to a `.rs` or `.md` file. + +Write each commit message as a Conventional Commit with an imperative subject. +Disclose AI assistance with an honest `AI Assistance: used for ` line in the commit message and in the PR body. +Never add a `Co-Authored-By: Claude Code` footer or a `Generated with Claude Code` line. + +Keep one logical line per paragraph in a PR or issue body. +GitHub renders a single newline in a comment as a line break, so a sentence-per-line body wraps too early. + +## Documentation + +Write all documentation in ASD-STE100 Simplified Technical English. +Use short sentences, the active voice, and one idea per sentence. +In a markdown file, put each sentence on its own line and do not wrap within a sentence. +GitHub reflows the file when it displays it, and the diff then shows one changed line per changed sentence. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/flake.nix b/flake.nix index a861afc..a4a5a6b 100644 --- a/flake.nix +++ b/flake.nix @@ -47,6 +47,10 @@ wasm-tools wabt just + # Search tooling the agent workflow leans on: ripgrep for text, + # ast-grep for syntax-aware Rust queries and rewrites. + ripgrep + ast-grep pkg-config openssl ] ++ lib.optionals stdenv.isLinux [ mold ];