This document is the complete instruction set for an implementation model
(Codex / Opus / etc.) to execute a safety-first refactoring of this repository.
It was produced from a full evidence-based audit on 2026-06-11 (HEAD 9d5e1dd).
Follow it exactly. When this document says STOP AND ASK, stop and ask the human.
Reduce verified technical debt in the Azazel-Edge codebase without changing any observable behavior, in small reversible steps, each independently verified. Priorities, in order:
- Fix silent-corruption and audit-integrity hazards (thread safety, trace_id).
- Remove duplication and dead weight that is provably safe to remove.
- Clarify module boundaries so the two giant files (
azazel_edge_web/app.py,py/azazel_edge_ai/agent.py) become safely decomposable later. - Make the test suite cheaper to maintain (shared fixtures, no
/tmplitter).
Large design changes are explicitly proposal-only in this effort (see Implementation Phases, Phase 6, and Out-of-scope Items).
Azazel-Edge (AZ-01 / SENTINEL) is a Raspberry Pi-oriented deterministic edge
SOC/NOC gateway. It observes local telemetry (Suricata EVE, NetFlow, SNMP,
WiFi scans, NOC probes), evaluates NOC/SOC state with deterministic evaluators,
selects one of five bounded actions (observe / notify / throttle / redirect /
isolate) via an Action Arbiter, and records operator-visible explanations and a
hash-chained audit trail. Local AI (Ollama) is advisory only and is gated by
py/azazel_edge/ai_governance.py. Raw logs are handled local-first.
Suricata EVE ──► rust/azazel-edge-core (tail, normalize, decide, dry-run plan)
│ Unix socket /run/azazel-edge/ai-bridge.sock
▼
py/azazel_edge_ai/agent.py (socket server, scoring, LLM worker thread)
│ uses py/azazel_edge/* (evaluators, arbiter, explanations, audit)
▼
/run/azazel-edge/*.json snapshots + /var/log/azazel-edge/*.jsonl streams
▲ ▲
py/azazel_edge_control/daemon.py (control socket, │
NOC probes, WiFi, mode mgmt) │
▲ │
azazel_edge_web/app.py (Flask, ~70 routes, gunicorn 2 workers × 4 threads)
py/azazel_edge/cli_unified.py (TUI), azazel_edge_epd.py (e-paper)
azazel_edge_web/app.py— Flask app (appobject; gunicorn targetazazel_edge_web.app:appinsystemd/azazel-edge-web.service)py/azazel_edge_ai/agent.py— AI agent daemon (path hardcoded insystemd/azazel-edge-ai-agent.service)py/azazel_edge_control/daemon.py— control daemonrust/azazel-edge-core/src/main.rs— single-file Rust capture/enforcement corebin/*— launchers; several are installed to/usr/local/binby installer
- The repo is not an installed package: no
pyproject.toml, nosetup.py, noconftest.py, nopytest.ini. CI runsPYTHONPATH=. pytest -q; every test file insertsROOT/pyintosys.pathitself (57 files carry this boilerplate). - All tests are
unittest.TestCasestyle. Test files are namedtest_<subsystem>_v<N>.py;_v2supplements_v1, it does not replace it. - Web API tests pin behavior by monkey-patching module-level names on
azazel_edge_web.app(181 occurrences across test files). Any symbol you move out ofapp.pymust remain importable/patchable at its old name.
These are pinned by tests, consumed across the Rust/Python boundary, or relied on by deployed systemd/installer artifacts. Changing any of them is a behavioral change, not a refactor.
- The five actions and their profiles in
py/azazel_edge/arbiter/action.py(ACTION_PROFILES): names, and thereversible/approval_required/audited/effect/modevalues.approval_required=Trueon throttle/redirect/isolate is a safety guarantee. - Arbiter decision logic and output keys:
action,reason,rejected_alternatives,release_condition,chosen_evidence_ids,action_profile,decision_trace,policy. - The v2 explanation record fields enforced by
py/azazel_edge/explanations/schema.py:ts,trace_id,format_version("v2"),selected_action,reason,rejected_actions,release_condition,policy_profile,config_hash,why_chosen,why_not_others,evidence_ids,next_checks,operator_wording,machine,trust_capsule. - Audit chain format in
py/azazel_edge/audit/logger.py:chain_prev,chain_hash, SHA-256 over canonical JSON withsort_keys=Trueand the existing separators. Do not alter the serialization in any way — it would invalidate every existing on-device log. - AI governance invariants in
py/azazel_edge/ai_governance.py:ALLOWED_INTENTS = {advice, summary, candidate}, theFORBIDDEN_KEYSset, output length caps, and the fact that every invocation (including blocked ones) is audited. Never bypassAIGovernance.invoke().
- The Rust→Python envelope:
{"normalized": ..., "defense": ..., "enforcement": ..., "enforcement_status": ..., "source": "suricata_eve", "pipeline": "rust_event_engine_v1"}and all 14NormalizedEventfield names consumed byagent.py(~lines 1015–1026):ts, src_ip, dst_ip, attack_type, severity, target_port, protocol, sid, category, event_type, action, confidence, risk_score, ingest_epoch. There is NO schema validation at this boundary; a rename fails silently. EnforcementOutcome.modestrings (dry_run,policy_gated,enforced,disabled), thetrace-{hex}trace_id prefix, and the nftables table nameinet azazel_edge.
- All endpoint paths and response shapes listed in
tests/test_dashboard_data_contract.py,test_api_auth_contract.py,test_api_auth_rbac_v1.py,test_triage_api_v1.py,test_demo_api_v1.py,test_handoff_api_v1.py,test_sot_api_v1.py,test_aggregator_api_v1.py,test_stix_taxii_api_v1.py,test_captive_portal_api_v1.py. Notable quirks that are intentional and pinned:/api/state403 body is{"error": "Unauthorized"}but/api/runbooks403 body is{"ok": false, "error": "Unauthorized"}— two shapes, both pinned. Do not unify./api/wifi/connectrejects with 401, other routes with 403.GET /api/wifi/scanandPOST /api/mattermost/commandintentionally do not userequire_token.
- Auth is fail-closed by default (
AUTH_FAIL_OPEN=False); missing token file → 403. mTLS fingerprint check applies to operator-and-above only.
- Service names, socket paths (
/run/azazel-edge/ai-bridge.sock,/run/azazel-edge/control.sock), JSONL/JSON paths under/var/log/azazel-edge/and/run/azazel-edge/, CLI names installed to/usr/local/bin, gunicorn targetazazel_edge_web.app:app, and the hardcoded agent path inazazel-edge-ai-agent.service. verify_runtime_sync.shexit codes (0/1/2) and the file list it checks — renaming/moving any file it tracks breaks deployment verification.- OpenCanary decoy ports (SSH 12222, HTTP 18080) coupled between
security/opencanary/opencanary.confandconfig/redirect_policy.yaml. - The repository tree must stay clean under
tools/claims_discipline.py(tests/test_claims_discipline_v1.pyruns it against the real tree). Do not introduce the forbidden marketing phrases it checks — including in comments, docs, or this kind of instruction file. Refer to them only indirectly, as this document does.
From AGENTS.md and CONTRIBUTING.md (governance; violating these is a
hard failure regardless of test results):
- Never modify anything under
installer/,security/, orsystemd/. These require explicit human approval. If a refactor seems to require it, STOP AND ASK. - Never flip safety defaults:
AZAZEL_AUTH_FAIL_OPENstays"0",AZAZEL_DEFENSE_ENFORCEstaysfalse,AZAZEL_DEFENSE_DRY_RUNstaystrue. Never setrequires_approval: falseon any runbook. - Deterministic First: AI never moves ahead of the Evidence Plane / deterministic path. No autonomous execution of throttle/redirect/isolate.
- Auditability stays intact: adopt/fallback logging and required decision explanation fields must survive every change.
- Docs are English-default. If you change API or configuration surface, update
docs/API_REFERENCE.md/docs/CONFIGURATION.mdin the same change set, and register any new doc indocs/INDEX.md. EN/JA doc pairs must be updated together for behavioral changes. - Never log or relocate secrets: web token, Mattermost tokens/credentials, ntfy token, TLS/CA private keys, aggregator HMAC key, mTLS fingerprint file.
- Do not commit changes to
MEMORY/policy files, and do not add new runtime dependencies (thetests/test_runtime_dependency_contract.pycontract test enforcesrequirements/runtime.txtcoverage; dependency installs happen in CI, not locally).
STOP, do not implement, and ask the human when any of these occur:
- A change would alter any item in Behaviors To Preserve.
- A test contradicts the implementation and you cannot tell which is intended.
- You believe code is dead but cannot prove no runtime/operational consumer
exists (scripts in
bin/may be invoked manually by operators). - A change touches public API shape, on-disk JSONL formats, stored data, auth, notification delivery, or external integrations (Mattermost, ntfy, Wazuh, TAXII, OpenCanary).
- A change would require touching
installer/,security/, orsystemd/. - The fix has multiple defensible designs with product implications.
- Any item in the "Open Questions" file section below is a prerequisite for the change you are about to make and has not been answered yet.
- Q1 (blocks Phase 6a): Is the tactics-engine decision stream
(
/opt/azazel-edge/logs/tactics_engine/decision_explanations.jsonl, written bypy/azazel_edge/tactics_engine/decision_logger.py) consumed by anything external to this repo? May it be marked internal/deprecated in favor of the v2DecisionExplainerstream? - Q2 (blocks Phase 3 item 3.6):
explanations/trust_capsule.pyfalls back to a publicly known default HMAC key whenAZAZEL_TRUST_CAPSULE_HMAC_KEYis unset. Should the fix hard-fail (raise), or warn-and-continue? Hard-fail could break explanation writing on deployed devices that never set the var. - Q3 (blocks any deletion): Which of these are still operationally used:
bin/update-readme-stats,bin/azazel-edge-inject-test-events, theGET /demopage, the rootimages/directory? Default assumption: all are in use; nothing is deleted in this effort. - Q4 (blocks Phase 5 item 5.2):
tests/benchmark/test_pipeline_latency_bench_v1.pyasserts wall-clock p95 thresholds and runs inside the defaultpytest -qgate (flaky on slow runners). May benchmarks be excluded from the default test run and moved to a separate non-gating CI job? This changes the documented definition of "green" in CONTRIBUTING.md/AGENTS.md. - Q5 (blocks touching
path_schema.pyv1 branches): Are any deployed devices still onAZAZEL_PATH_SCHEMA=v1(legacy azazel-gadget paths)? Default assumption: yes; leave all v1/legacy compatibility intact (legacy symlink deprecation date is 2026-12-31).
Run all of these BEFORE any edit and record the output (counts, pass/fail, durations) in your report. Re-run after every phase.
git status # must be clean before you start
git log --oneline -3 # record the base commit
# Python tests (CI-equivalent; use the repo venv or bin/azazel-edge-dev)
bin/azazel-edge-dev bootstrap # once, if .venv is missing
PYTHONPATH=. .venv/bin/pytest -q # the CI gate
# Rust tests
cd rust/azazel-edge-core && cargo test && cd ../..
# Syntax gate
python3 -m compileall -q py azazel_edge_web
# Claims discipline (also runs inside pytest)
python3 tools/claims_discipline.py || true # record outputIf the baseline is already red, STOP AND ASK before changing anything.
Each item: evidence → why → risk → action class. Action classes: [FIX] implement in this effort; [FIX-Q] implement only after its Open Question is answered; [PROPOSE] write a proposal, do not implement.
| # | Debt | Evidence | Why / blast radius | Class |
|---|---|---|---|---|
| A1 | P0AuditLogger is not thread-safe: _last_chain_hash read/update and file append are unguarded; AI_AUDIT is a singleton shared by per-client threads + LLM worker |
py/azazel_edge/audit/logger.py:82-84, py/azazel_edge_ai/agent.py:198 |
Concurrent writes can silently break the hash chain — the core integrity guarantee | [FIX] add threading.Lock around log() |
| A2 | _append_jsonl calls in agent.py outside IO_LOCK (EVENT_LOG_PATH at ~1649; also ~803, 879, 1176, 1199) |
py/azazel_edge_ai/agent.py |
Interleaved/truncated JSONL lines under concurrent clients | [FIX] guard all appends with a lock (IO_LOCK or a dedicated append lock) |
| A3 | EvidenceBus fanout file append unguarded |
py/azazel_edge/evidence_plane/bus.py:21; shared bus in daemon.py:114 |
Same corruption class | [FIX] add lock |
| A4 | trace_id never set in the live advisory: _build_advisory() omits it, so advisory.get("trace_id") is always empty in AI audit records |
py/azazel_edge_ai/agent.py:1012-1122, 920, 939, 1326 |
Production AI audit entries have no trace linkage; contradicts the documented traceability story | [FIX] adopt the incoming Rust enforcement.trace_id when present, else generate one; propagate through _handle_event and _process_llm_task. Add an assertion test. |
| A5 | validate_v2_explanation() is never called by the writer; validation happens only at read time in audit_review.py |
py/azazel_edge/explanations/decision.py:170-175 |
Invalid records written silently | [FIX] validate in explain() before write_jsonl(); on failure log a warning and still write (do NOT raise — preserves runtime behavior) |
| A6 | Web file writes without tmp+rename: _save_captive_registry, _write_topolite_seed_mode; _record_dashboard_trend_point appends outside its own lock |
azazel_edge_web/app.py:858, 2423, 3445 |
Partial-write corruption under gunicorn 2×4 concurrency | [FIX] use the existing _write_json_file atomic pattern; move the trend append inside the lock |
| A7 | SocEvaluator mutable instance state (_incident_store, _seen_*) used via module-level singletons across threads |
py/azazel_edge/evaluators/soc.py:259-263, agent.py:139 |
Race-prone correlation state | [FIX] add an internal lock around evaluate(); do NOT change to per-call instances (the statefulness is intentional) |
| A8 | Trust capsule default HMAC key is a public constant | py/azazel_edge/explanations/trust_capsule.py:27 |
Capsules forgeable when env unset | [FIX-Q: Q2] |
| # | Debt | Evidence | Class |
|---|---|---|---|
| B1 | Two decision-explanation writers with different schemas/paths (DecisionLogger vs DecisionExplainer); only the latter is validated and read by audit_review.py |
py/azazel_edge/tactics_engine/decision_logger.py:91,101 vs py/azazel_edge/explanations/decision.py:13 |
[PROPOSE; consolidation gated on Q1] |
| B2 | Duplicated helpers: _stable_json() (aggregator.py:37 = explanations/trust_capsule.py:15), iso_utc_now() (evidence_plane/schema.py:21 = notify/delivery.py:19), _ensure_parent() (demo_overlay.py:14 = agent.py:221) |
— | [FIX] consolidate into py/azazel_edge/_util.py, re-export from old locations |
| B3 | In-file duplicate correlation logic: agent.py:_evaluate_correlation (~389-443) vs correlation/advanced.py AdvancedCorrelator |
— | [PROPOSE] (agent.py has no direct tests; do not touch its logic in this effort beyond A2/A4) |
| B4 | 57 test files carry identical sys.path boilerplate; no conftest.py; builder helpers (_noc, _soc, _dim) duplicated across arbiter/evaluator tests |
tests/* |
[FIX] root conftest.py + tests/helpers.py; remove boilerplate mechanically |
| B5 | Six test files write to hardcoded Path('/tmp/unused-*.jsonl') |
tests/test_yara_assist_v1.py:71, test_advanced_correlation_v1.py:118, test_decision_explanation_fields_v1.py:80, test_decision_explanation_v2.py:19, test_sigma_assist_v1.py:79, test_attack_defend_visualization_v1.py:29 |
[FIX] use tempfile |
| B6 | Repeated _tail_jsonl(AI_LLM_LOG, limit=20) reads across 5+ dashboard routes, token files re-parsed on every request |
azazel_edge_web/app.py (multiple) |
[PROPOSE] (caching changes observable freshness semantics) |
| B7 | Possibly orphaned scripts: bin/update-readme-stats, bin/azazel-edge-inject-test-events |
report §4 | [no action; Q3 informational only] |
| # | Debt | Evidence | Class |
|---|---|---|---|
| C1 | Layer violation: explanations/decision.py:9 imports azazel_edge.triage (output layer pulling workflow layer + i18n transitively) |
— | [FIX] invert: explain() accepts optional runbook_support param; caller computes it. Keep a deprecation shim so existing callers still work. |
| C2 | path_schema.py exists but is bypassed by many modules: hardcoded paths in control_plane.py:11, daemon.py:60-67, mode_manager.py:20,36, EPD files, tactics_engine/decision_logger.py:101, tactics_engine/config_hash.py:61-62, audit_review.py:21-23; audit log paths absent from path_schema entirely |
— | [FIX, narrow] add env-var overrides where missing (e.g. AZAZEL_CONTROL_SOCKET) and route config_hash.py through path_schema.first_minute_config_candidates(). Defaults must remain byte-identical. Full path-registry unification: [PROPOSE]. |
| C3 | azazel_edge_web/app.py is a 6,593-line monolith (config, auth, ~70 routes, helpers, integrations) |
report §1 | [FIX, staged + PROPOSE]: extract ONLY pure I/O helpers and the auth layer into modules with full re-export from app.py (so the 181 webapp.* monkey-patch sites keep working). Blueprint split: [PROPOSE]. |
| C4 | agent.py (1,781 lines) mixes socket server, LLM worker, correlation, metrics, manual queries; zero direct tests |
— | [PROPOSE] decomposition; in this effort only A2/A4 + characterization tests |
| C5 | cli_unified.py (1,824 lines) holds the shared Snapshot contract used by Textual UI and EPD |
cli_unified.py:52,158 |
[FIX] extract Snapshot + build_snapshot() to py/azazel_edge/snapshot_model.py, re-export from cli_unified.py |
| C6 | i18n.py is 1,923 lines of dict literals parsed at import |
— | [PROPOSE] externalize to data files (startup-cost win on Pi, but migration risk) |
| C7 | PYTHONPATH documented four different ways (CI ., dev script .:py, CONTRIBUTING both, AGENTS another) |
report §1/§6 | [FIX] root conftest.py makes them all work; align docs to one canonical command |
| C8 | Rust main.rs single file; split_whitespace re-parse of built command strings is a latent quoting hazard; DefaultHasher trace ids not stable across Rust versions |
rust/azazel-edge-core/src/main.rs:309,481,404 |
[PROPOSE] (enforcement-sensitive; has tests but the hazard fix needs careful design) |
| # | Debt | Evidence | Class |
|---|---|---|---|
| D1 | Benchmark latency thresholds run in the default CI gate | tests/benchmark/test_pipeline_latency_bench_v1.py |
[FIX-Q: Q4] |
| D2 | No lint, no type check, no coverage in CI | .github/workflows/ci.yml |
[PROPOSE] (adding gates changes contributor workflow) |
| D3 | Web tests monkey-patch 181 module-global names | report §3b | [PROPOSE] config-object migration; in this effort, preserve patchability instead |
| D4 | Manual patching without unittest.mock.patch in test_phase5_operational_hardening.py (leak risk on exception) |
lines ~147-154, 435-448 | [FIX] convert to context-managed patching, behavior identical |
| D5 | No characterization tests for agent.py advisory building or the Rust→Python field contract |
— | [FIX] add: (1) _build_advisory() unit test with a fixed event fixture; (2) a contract test asserting the 14 normalized field names against a sample envelope fixture |
Work strictly in this order. One phase = one commit (or a small series of
commits) on a dedicated branch (e.g. refactor/safety-first-pass). Never start
phase N+1 with phase N unverified.
git status— if not clean, STOP AND ASK (do not mix in others' changes).- Create the working branch.
- Run all Baseline Commands; record results verbatim in the report.
- Add root
conftest.pyinsertingROOT/pyintosys.path(B4, C7). Do not yet remove per-file boilerplate (additive only). - Add
tests/helpers.pywith the shared_noc/_soc/_dimbuilders (mirroring the existing ones; existing tests untouched for now). - Add characterization tests (D5): advisory-building fixture test and the
Rust→Python
NormalizedEventfield-name contract test. - Add a concurrency test for
P0AuditLogger(threads × N appends, thenverify_chain). It is EXPECTED TO FAIL or be flaky against current code — mark it skipped with a comment referencing Phase 3, or write it so it runs after the Phase 3 fix lands (your choice; state it in the report). - Verify: full baseline suite still green (plus the new tests).
- B5: replace the six
/tmp/unused-*test paths withtempfile. - B2: create
py/azazel_edge/_util.py; move_stable_json,iso_utc_now,_ensure_parent; keep thin re-export aliases at the original sites. - D4: convert manual patch/restore in
test_phase5_operational_hardening.pyto context-managed patching. - Mechanically remove the per-file
sys.pathboilerplate from test files (now covered byconftest.py). Touch nothing else in those files. - Verify after each sub-step:
PYTHONPATH=. .venv/bin/pytest -q.
- A1: lock in
P0AuditLogger.log()(andverify_chainif it touches shared state). Enable/un-skip the Phase 1 concurrency test. - A2: bring all
_append_jsonlcall sites inagent.pyunder a lock. - A3: lock in
EvidenceBusfanout append. - A6: atomic writes for captive registry / topolite seed; trend append inside its lock.
- A4: trace_id adoption in
_build_advisory()(prefer incomingenforcement.trace_id, fallback to generatedtrace-{hex}-style id), plus test. Do not change the Rust side. - A8: trust-capsule HMAC key handling — ONLY after Q2 is answered.
- A5: wire
validate_v2_explanationinto the writer (warn-only). - A7: internal lock around
SocEvaluator.evaluate(). - Verify: full suite +
cargo test+ targeted re-run of audit/explanation tests.
- C5: extract
Snapshot/build_snapshot()→py/azazel_edge/snapshot_model.py;cli_unified.pyre-imports them so all existing imports keep working. - C1:
DecisionExplainer.explain()gains optionalrunbook_supportparam; internaltriageimport becomes lazy/fallback so the old call pattern still works; demo runner updated to pass it explicitly. - C3 (stage 1 only): extract pure I/O helpers from
app.pyintoazazel_edge_web/io_helpers.py, and the auth functions intoazazel_edge_web/auth.py. CRITICAL:app.pymust re-export every moved name (from .io_helpers import *plus explicit names) so allwebapp.<name>monkey-patch sites in tests still bind. Run the full web test group after each extraction. - C2 (narrow):
AZAZEL_CONTROL_SOCKETenv override incontrol_plane.py(anddaemon.pyreading the same var);config_hash.pythroughpath_schemacandidates. Defaults byte-identical; add/extend tests. - Verify: full suite; confirm zero changes in any pinned API response by running the web contract tests.
- Migrate arbiter/evaluator tests to use
tests/helpers.pybuilders (delete the in-file duplicates). - D1: benchmark split — ONLY after Q4 is answered. If approved: add pytest
config excluding
tests/benchmarkfrom default discovery, add a separate CI job (non-gating or with relaxed env-configurable thresholds), and update CONTRIBUTING.md/AGENTS.md commands in the same change. - Verify: full suite, and (if Q4 approved) the new benchmark job invocation.
Write docs/ proposal notes (or a single refactor-proposals.md) for:
- 6a. Unifying the two decision-explanation streams (B1; needs Q1).
- 6b.
app.pyblueprint decomposition + web-test config-object migration (C3 stage 2, D3). - 6c.
agent.pydecomposition (LLM worker / correlation / metrics / socket server) and replacing_evaluate_correlationwithAdvancedCorrelator(B3, C4). - 6d.
i18n.pydata externalization (C6). - 6e. Full path-registry unification through
path_schema(C2 full scope). - 6f. Rust
main.rsmodulization + command-string quoting hardening (C8). - 6g. CI additions: ruff/mypy/coverage (D2), auth-token caching (B6). Each proposal must include: motivation, design, migration steps, test impact (especially the monkey-patch sites), and rollback plan.
- Record baseline results BEFORE the first edit; re-run the full gate after
every phase:
PYTHONPATH=. .venv/bin/pytest -q,cargo test(when Rust or the boundary contract is involved),python3 -m compileall -q py azazel_edge_web, and the claims-discipline check. - After Phase 4 web extractions, additionally run the web test group
explicitly:
PYTHONPATH=. .venv/bin/pytest -q tests/test_api_auth_contract.py tests/test_api_auth_rbac_v1.py tests/test_dashboard_data_contract.py tests/test_triage_api_v1.py tests/test_demo_api_v1.py tests/test_sot_api_v1.py tests/test_aggregator_api_v1.py tests/test_stix_taxii_api_v1.py tests/test_captive_portal_api_v1.py tests/test_handoff_api_v1.py. - Any test that fails after your change: do not "fix the test" unless the test itself was asserting the duplicated/internal detail you intentionally moved AND the pinned behavior is unchanged — state this explicitly in the report.
- No new runtime dependency may appear (contract test will catch it; do not
add to
requirements/runtime.txtwithout approval). - Git hygiene:
git statusfirst; never mix pre-existing uncommitted changes with yours; one phase per commit with a message explaining what and why; changes small and revertible; NO drive-by reformatting, renaming, or "while I'm here" edits; behavior changes forbidden unless this document explicitly directs them.
At the end (and at any STOP AND ASK), report:
## Phase Status
Phase 0..6: done / partial / skipped (+reason)
## Baseline vs Final
- pytest: <N passed> → <N passed> (list any new tests)
- cargo test: <result> → <result>
- compileall: <result>
- claims discipline: <result>
## Changes Made
Per phase: files touched, one-line rationale each, commit hashes.
## Behavior Deltas
MUST be "none" outside items explicitly directed here; otherwise list and
justify against this document.
## Questions Raised / Answers Used
Which Open Questions (Q1–Q5) were answered and how they changed the work.
## Commands Run
The exact final verification commands and their results, verbatim.
## Remaining Debt
What was deliberately left (Phase 6 proposals, [PROPOSE] items).
Do NOT do any of the following in this effort:
- Anything under
installer/,security/,systemd/. - Deleting any
bin/script, template, static file, or theimages/dir (Q3 is informational; deletion is a separate, human-approved effort). - Unifying the two web error-response shapes, changing any endpoint path, status code, or response field.
- Changing on-disk formats: audit chain serialization, v2 explanation fields, tactics-engine record fields, NormalizedEvent envelope, snapshot JSON keys.
- Removing legacy azazel-gadget compatibility paths/symlinks (deprecation date
2026-12-31) or
AZAZEL_PATH_SCHEMA=v1branches. - Dependency upgrades, packaging the repo (pyproject/setup), or Python version changes.
- New features, UI redesign, performance optimizations beyond the directed lock/atomic-write fixes.
- Editing BHEU CFP/paper/roadmap content (
docs/cfp/,docs/papers/,docs/roadmaps/,docs/issues/blackhat-europe/) except where a directed change requires a doc cross-reference update. - Rewriting
agent.py,app.pyroutes,i18n.py, or Rust core structure (Phase 6 proposals only).