M3a spec: a ledger module and a two-process end-to-end test

Both came out of planning the tasks. The three locked steps need a home
that is neither the connection handler nor the pending table. The
one-process end-to-end test would have made loopd dev-depend on
brokerd, which the crate-dependency gate forbids.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-18 22:36:55 -07:00
co-authored by Claude Fable 5.1
parent 1008dce351
commit 66fa143524
+12 -5
View File
@@ -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