Repository navigation
Engine internals
How ah-engine loads its configuration, why nothing tunable may live in its source, how check logic runs, and how we prove it answers exactly like the Node hooks. Everything here is on the engine-proto branch. Start with Architecture if you have not read it.
Primary sources: docs/AH-ENGINE.md, ah-engine/README.md, ah-engine/DECISIONS.md, docs/DEVELOPMENT.md.
First hit wins (ah-engine/src/bootstrap.rs):
--plugin-root-
AH_ENGINE_PLUGIN_ROOT(set by the thin trigger from its own location; strict: no index means the load fails) -
CLAUDE_PLUGIN_ROOT, thenPLUGIN_ROOT - The root recorded in the engine's snapshot cache
- A development checkout, found by walking up to
plugins/anti-hall/engine/defaults/index.toml
-
plugins/anti-hall/engine/defaults/holds the TOML files.index.tomllists them in load order, and itsextra_diradds one file per batch of ported hooks. - Each setting is its own table:
[section.name]withvalue,doc, and optionallyenv,min,maxandunit. Thedoctext is mandatory: a test checks every key is documented.
plugins/anti-hall/engine/defaults.pristine/ is a read-only copy of the shipped defaults, byte-identical to them (tested in tests/it/defaults_failover.rs). For each file and each setting, the engine tries in order (src/defaults/load.rs):
- the edited
engine/defaults/file - the last-known-good copy in the engine's state dir (
defaults.lkg/) defaults.pristine/- exit 75, so the Node hooks answer
Every fallback is logged and marks the engine degraded. Missing settings can be healed from pristine; in a git checkout that never happens by itself, you run ah-engine config heal.
Highest first (src/cfgstore.rs):
- the setting's own
envvariable, if it has one -
~/.anti-hall/settings.json, the same file the Node side and/anti-hall:settingsuse (a corrupt file is rejected and the last good config stays) - the engine's own
config.tomlin its state dir, strictly validated - the shipped default
-
Start-up. Short-lived clients read one validated snapshot (
defaults.cache) instead of parsing every file. -
File change. The daemon polls a metadata fingerprint (mtime, size, inode) every
config.watch_ms(500 ms) and acts afterconfig.debounce_ms(300 ms).reload()validates everything and swaps the snapshot atomically; an invalid set keeps the old snapshot and logsdefaults_invalidorconfig_invalid. -
Plugin update. A request carrying a plugin root whose
index.tomlis newer is adopted. -
On demand. The
reloadcontrol verb, or SIGHUP. - A few keys (worker count, memory cap, paths, storage) are marked restart-only and wait for the next daemon start.
- Check scripts reload per call when their file fingerprint changes.
Every parameter, setting, table, message text, env-var name, path, limit and schedule lives in shipped config, which user config overrides. Code keeps only structural constants, each commented. Nothing is compiled into the binary. (Decision D17 and its amendment at revision 1.94.)
Tuning a rule means editing plugin files, never rebuilding the engine. Tests that fail the build enforce it:
| Test | What it catches |
|---|---|
ah-engine/tests/it/no_hardcoded_tunables.rs |
A source scan (comments and cfg(test) ignored) for durations, env::var("literal"), const/static values, message-like strings, matches! tables, string arrays, bound literals and file names |
the same file: no_new_hardcoded_literals
|
A shrink-only ratchet against tests/it/hardcoded_baseline.txt: a new line fails, and so does a line that vanished without the baseline being updated |
the same file: no_versioned_model_ids_in_source_or_plugin_config
|
Pinned model versions; routing uses aliases |
ah-engine/tests/it/no_compiled_config.rs |
A scan of the built binary: fails if any doc text or long string value from the shipped defaults, or any rules.json text, appears in it, or if the source embeds a defaults file |
ah-engine/tests/it/defaults_keys.rs |
Every key the source reads is shipped, and every shipped key is read |
ah-engine/tests/it/compiled_logic_gate.rs |
The number of checks with compiled (Rust) decision logic may only go down; the ceiling is now 0 |
The allowlist. no_hardcoded_tunables.rs has one ALLOW table of (file suffix, substring, reason) entries for true structural constants, such as protocol and format versions. The test allowlist_entries_are_justified_and_still_needed fails on an entry without a reason or one that no longer matches anything. Adding an entry is a review decision, not a way around the rule.
The key schema. ah-engine/build_support/keyscan.rs scans the source for typed reader calls (for example defaults::num("a.b")), and build.rs compiles that list of key names in, so a plugin missing a key is rejected at load. It holds the schema, never the values.
To regenerate the ratchet baseline after removing literals: AH_BLESS_BASELINE=1 cargo test --test it -- no_hardcoded_tunables::.
Check decisions are plugin JavaScript, run on an embedded QuickJS-NG (the rquickjs crate; ah-engine/src/script/), decision D88.
-
Where scripts live.
plugins/anti-hall/engine/logic/<check>.js; shared helpers inlogic/lib/NN-*.js(loaded first, in name order); rule sets inlogic/rules/. A user can override a script file by file in~/.anti-hall/logic/. -
Contract. A script defines
decide(payload, opts, event)and returnsnull(nothing to say),'allow','defer',{block},{advisory}or{exact: {code, out, err}}. -
API.
ah.*fromlogic/lib/00-ah.js: config and settings, the request's environment (never the daemon's), bounded file reads, path helpers, linear-time regex, transcript readers, scoped writes under~/.anti-hall(no.., no symlink below the root, size-capped), a small number of locks, andexeclimited to the programs inscript.exec_programs(onlygittoday, with caps on calls, timeout and output). -
Limits (
plugins/anti-hall/engine/defaults/script.toml): CPU time per call (time_limit_ms, 50 ms, with per-check overrides intime_limit_by_check), a wall-clock factor, per-call memory (call_memory_bytes, 16 MiB), stack size, read size, directory-listing size, and a p95 latency budget per check. - Failure. An exception, interrupt, out-of-memory or malformed verdict means defer: the Node hook answers. A check's own failure never blocks the agent.
Run the latency test with cargo test --release --test script_latency -- --nocapture.
The engine must answer exactly like the Node hook it replaces (decision D31). Three layers prove it:
| Layer | Where | How to run |
|---|---|---|
| Check goldens |
ah-engine/tests/golden/<check>.jsonl: frozen cases per scripted check, replayed at a pinned clock with placeholders such as {HOME} and {PLUGIN} (src/script/golden.rs) |
Part of the normal test run. Against the live Node hook: node parity/run-golden.js <check> hooks/<check>.js
|
| Node parity lanes |
ah-engine/tests/it/node_parity/, with recorded Node answers in ah-engine/tests/goldens/<lane>/ (a MANIFEST.json fingerprinting the Node code, plus cases*.jsonl); see tests/goldens/README.md
|
Replay is the default. Re-record: AH_RECORD_NODE=1 cargo nextest run --release --profile record -E 'test(<test>)' (refused in CI). Against live Node: AH_LIVE_NODE=1 cargo nextest run --release --profile soak
|
| Parity harnesses and corpora |
ah-engine/parity/: run-git.js, run-command.js, run-dispatch.js, and others, with corpora and fuzz generators |
For example node run-git.js --engine ../target/release/ah-engine --hooks ../../plugins/anti-hall/hooks --corpus corpus.jsonl --mode both
|
Replay also means re-running recorded real hook payloads through both the engine and the same-version Node hooks and comparing the answers. It is the pre-release check: the go-live measurement in the engine-proto CHANGELOG comes from one replay of 2,113 recorded payloads (0 of 68 blocks weaker than Node). See Development workflow for when it runs.
When a recorded Node answer is stale because Node changed on purpose, re-record that lane in the same change and say so in the commit.
From ah-engine/, as written in its README and in DEVELOPMENT.md:
| Task | Command |
|---|---|
| Full engine suite |
./test.sh (clippy with -D warnings, nextest, doc tests, the wrapper and bootstrap shell tests; fails if a daemon survives) |
| Format and lint | cargo fmt --check && cargo clippy --all-targets -- -D warnings |
| One module | cargo nextest run -E 'test(/^<name>::/)' |
| Regenerate the reference | cargo run -q -- docs --format md > REFERENCE.md |
| Mutation testing | scripts/mutants.sh <checks-module> |
| Fuzzing |
cargo +nightly fuzz run <target> (targets: heredoc, json, glob) |
| Dependencies |
cargo deny check, cargo audit
|
| Debugging |
ah-engine serve in the foreground, ah-engine check <name> with a payload on stdin, ah-engine status --json
|
On a shared machine, run cargo at low priority (nice -n 19, CARGO_BUILD_JOBS=2), as the README recommends.
anti-hall by Mohammed Talas (@talas9) · Repository · Docs site · Discussions · Contributor wiki: the repository is the source of truth; fix this wiki when they disagree.
🗺️ How it works
- 🏗️ Architecture
- ⚙️ Engine internals
- 🌐 DevSwarm
- 🤖 Repo automation
🛠️ How we work
📄 Templates
🔗 Elsewhere