Skip to content

Engine internals

talas9 edited this page Oct 9, 2026 · 1 revision

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.

Config loading

Where the engine finds the plugin

First hit wins (ah-engine/src/bootstrap.rs):

  1. --plugin-root
  2. AH_ENGINE_PLUGIN_ROOT (set by the thin trigger from its own location; strict: no index means the load fails)
  3. CLAUDE_PLUGIN_ROOT, then PLUGIN_ROOT
  4. The root recorded in the engine's snapshot cache
  5. A development checkout, found by walking up to plugins/anti-hall/engine/defaults/index.toml

The defaults files

  • plugins/anti-hall/engine/defaults/ holds the TOML files. index.toml lists them in load order, and its extra_dir adds one file per batch of ported hooks.
  • Each setting is its own table: [section.name] with value, doc, and optionally env, min, max and unit. The doc text is mandatory: a test checks every key is documented.

Failover: defaults.pristine

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):

  1. the edited engine/defaults/ file
  2. the last-known-good copy in the engine's state dir (defaults.lkg/)
  3. defaults.pristine/
  4. 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.

User overlay

Highest first (src/cfgstore.rs):

  1. the setting's own env variable, if it has one
  2. ~/.anti-hall/settings.json, the same file the Node side and /anti-hall:settings use (a corrupt file is rejected and the last good config stays)
  3. the engine's own config.toml in its state dir, strictly validated
  4. the shipped default

When config is re-read

  • 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 after config.debounce_ms (300 ms). reload() validates everything and swaps the snapshot atomically; an invalid set keeps the old snapshot and logs defaults_invalid or config_invalid.
  • Plugin update. A request carrying a plugin root whose index.toml is newer is adopted.
  • On demand. The reload control 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.

The no-hardcoding rule

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::.

The script sandbox

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 in logic/lib/NN-*.js (loaded first, in name order); rule sets in logic/rules/. A user can override a script file by file in ~/.anti-hall/logic/.
  • Contract. A script defines decide(payload, opts, event) and returns null (nothing to say), 'allow', 'defer', {block}, {advisory} or {exact: {code, out, err}}.
  • API. ah.* from logic/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, and exec limited to the programs in script.exec_programs (only git today, 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 in time_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.

Goldens, parity and replay

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.

Contributor commands

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.

Home

🗺️ How it works

🛠️ How we work

📄 Templates

🔗 Elsewhere

Clone this wiki locally