diff --git a/docs/specs/2026-09-18-m3a-decision-path.md b/docs/specs/2026-09-18-m3a-decision-path.md index a464524..a706880 100644 --- a/docs/specs/2026-09-18-m3a-decision-path.md +++ b/docs/specs/2026-09-18-m3a-decision-path.md @@ -19,7 +19,7 @@ in `docs/specs/2026-09-17-pre-m1-design.md`; section 6 of this document replaces | Any edit, deletion, reordering or gap in the audit log is found, with its file and line | Tampering suite, through `bxctl audit verify` and `brokerd`'s startup | | `loopd` cannot approve: the tool socket refuses every admin message | Socket tests | | An approval only lets through a call that is still `ask` when approved | Approval tests | -| A denial reaches the model as a plain tool result and the turn goes on | End-to-end test in one process; on straylight with Ornith | +| A denial reaches the model as a plain tool result and the turn goes on | End-to-end test with the real `brokerd` binary; on straylight with Ornith | | Every fail-closed state's message names a runbook entry, and every entry named exists | A test per state for the pointer; the gate script for the entry | Out of scope: running tools, containers, the egress proxy, tool timeouts (all M3b); secrets @@ -49,6 +49,7 @@ Each is one file under 500 lines with one purpose. Only `serve` starts threads. | `state` | Each session's taint and untrusted flag, read and written as files. | | `audit` | The audit writer: lock, chain, sync, rollover, startup check, recovery, accepted breaks. | | `approvals` | The table of pending approvals and their expiry. In memory only. | +| `ledger` | The audit writer and the session state behind one lock, and the three steps that hold it (section 2, "Threads and locks"). | | `runner` | `Runtime`, `RunSpec`, and `run(decision, runtime)`. The only place a `RunSpec` is built. | | `broker` | Handles one `broker.sock` connection: one tool request from decision to answer. | | `admin` | Handles one `admin.sock` connection: one admin request. | @@ -821,10 +822,16 @@ implementation, the oracle, or a compiling skeleton) is in section 15. `y` refuses and the id approves; a line already waiting in stdin refuses; an id missing from the list prints "no longer pending" and asks nothing; model content holding `ESC[8m` comes out escaped. -- **End to end, in one process.** The fake llama server, `loopd`'s turn loop, and the `brokerd` - library with the recording runtime. The scripted model calls `call_tool` for `read_file` with no - grant; its next request contains "Denied: no grant allows this call."; the audit log holds one - `Decision` record with `no_grant`. +- **End to end, in two processes.** A test in `loopd` starts the real `brokerd` binary (its path + comes from `BOXMAKER_BROKERD`; the test is `#[ignore]`d without it, and `make gate` builds the + workspace and then runs it with the variable set) on a temporary home, and runs `loopd`'s turn + loop against the fake llama server with a `BrokerPort` on that `brokerd`'s socket. The scripted + model calls `call_tool` for `read_file` with no grant; its next request contains "Denied: no + grant allows this call."; the audit directory, read with `proto::ChainVerifier`, verifies and + holds one record, a `Decision` with `no_grant`. It cannot be one process: that would make `loopd` depend on + `brokerd`, even as a dev-dependency, and no crate may depend on another role's crate + (`scripts/check-crate-deps.sh` checks dev-dependencies too). Two processes over the real socket + is also the better test. - **The runbook script**, with its self-test. **On straylight** (a scripted check, not part of `verify-device`): the script first reads