Files
boxmaker/docs/plans/M3a/19-bxctl-audit-verify.md
kyleandClaude Opus 5 e3f37da232 Hand over the M3a plan: 22 tasks, their files, and the check record
Task files, the files they copy in (byte-identical to the reference on
m3a-ref), each area's check record, and a README with the per-task
table of what each check exposed. The handoff note is done with.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 23:45:43 -07:00

4.4 KiB

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 <path>] reads <home>/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

/// 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<bool>;

What run does

  1. List <home>/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 <home>/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 <path>] 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.