# M3a task 19: `bxctl audit verify` **Branch:** `m3a` (run `git switch m3a`; `git status --short` must be empty, otherwise stop) **Commit subject:** `Add bxctl audit verify` ## Goal `bxctl audit verify [--home ]` reads `/audit/` itself and verifies the whole hash chain with `proto::ChainVerifier`. It asks no daemon, so it works when `brokerd` refuses to start, which is exactly when the owner needs it. ## Files - Copy: `crates/bxctl/tests/verify.rs` - Modify: `crates/bxctl/src/verify.rs` (task 18 made it with a placeholder body; replace the body), `docs/implementer-log.md` Do not touch `lib.rs` or `main.rs`: task 18 already added `pub mod verify;` and the command. The tests read the fixture logs of task 03 from `crates/proto/tests/fixtures/audit/`. ## Interfaces ```rust /// Verifies the whole audit log under `home` and prints the report to `out`. /// `Ok(true)`: the chain verifies. `Ok(false)`: it does not. `Err`: the log cannot be read. pub fn run(home: &Path, out: &mut dyn Write) -> std::io::Result; ``` ## What `run` does 1. List `/audit/`. A log file is a name of the form `YYYY-MM-DD.jsonl`: ten characters, digits with `-` at positions 4 and 7, then `.jsonl`. Ignore every other name (`.lock` is always there). Sort the names. 2. `ChainVerifier::new()`, then `feed(name, &bytes)` for each file in order, then `finish()`. 3. **If the report has a failure**, print exactly two lines and return `Ok(false)`: ``` 2026-09-17.jsonl:4: prev is not the hash of the line before see docs/runbook.md#audit-chain-broken ``` That is `{file}:{line}: {what}` from the failure. Print nothing else. 4. **Otherwise** print the first line below, then one line for each entry of each list, in this order, and return `Ok(true)`. A list that is empty prints nothing. | From | Line | |---|---| | always | `audit: ok, {records} records, head {hex}` (`head none` when `head` is `None`) | | `recoveries` | `recovered line: {file}:{line}` | | `accepted_breaks` | `accepted break: {file}:{line}` | | `abandoned` | `pending or abandoned: approval {seq}` | | `unfinished` | `running or unfinished: decision {seq}` | | `clock_warnings` | `clock went backwards: {file}:{line}` | | `torn_tail` | `torn final line: {file}:{line} (brokerd recovers it at its next start)` | The double names are deliberate. `bxctl` reads the files without asking `brokerd`, so an approval still waiting and a call still running look the same as ones a crash cut off. A torn tail is not a failure: it is also what a `brokerd` in the middle of a write looks like. 5. **Errors.** If `/audit` cannot be listed, or a log file cannot be read, return the `io::Error` (use `?`) and print nothing. A missing directory is an error, not an empty log. An existing directory with no log files is an empty log: `audit: ok, 0 records, head none`. A `write` to `out` that fails is returned with `?` as well. ## The command (already in `main.rs` from task 18) `bxctl audit verify [--home ]` calls `run`. `Ok(true)` is exit status 0, `Ok(false)` is 1, and `Err(e)` prints `bxctl: cannot read the audit log under {home}: {e}` and is 1. Check that `main.rs` does this (`grep -n 'cannot read the audit log' crates/bxctl/src/main.rs`). If it does not, stop and report; do not edit `main.rs`. ## Steps - [ ] **1. Copy.** `cp docs/plans/M3a/files/crates/bxctl/tests/verify.rs crates/bxctl/tests/` - [ ] **2. See the test fail.** `cargo test -p bxctl --test verify`. Expected: it compiles, and the tests fail against the placeholder. - [ ] **3. Replace the placeholder body in `verify.rs`.** Run `cargo fmt --all`. - [ ] **4. See the tests pass.** `cargo test -p bxctl --test verify`. Expected: `6 passed`. - [ ] **5. Try it.** `cargo run -q -p bxctl -- audit verify --home /nonexistent; echo $?` Expected: one line on stderr starting `bxctl: cannot read the audit log under /nonexistent`, then `1`. And `cargo run -q -p bxctl -- audit; echo $?` prints the usage and `2`. - [ ] **6. Run the gate.** `make gate`. Expected last line: `gate: ok`. - [ ] **7. Log and commit.** `git add crates/bxctl docs/implementer-log.md && git commit` ## Done when - `cargo test -p bxctl --test verify` reports 6 passed; step 5 printed what it says; `make gate` prints `gate: ok`. ## Stop and report if - `bxctl` would need to depend on `brokerd`. It must not: the directory walk here is a second, small copy of the one in `brokerd::audit`, on purpose.