The strict.rs in files/ is the merged one and walks task 02's new wire fixtures, so task 01's gate could not pass; the first run stopped on it. Task 01 now copies area A's version, checked to give 3 and 5 passed at task 01's end state. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
M3a implementation plan: the decision path
For the implementing model: do not work from this file. The owner gives you one task file at a time (
01-…to22-…). This file is the index for the owner and the reviewer.
Goal: every tool call loopd makes is decided by brokerd against owner-written grants and
the session's taint, recorded in a hash-chained audit log before anything happens, and, for ask
grants, approved or refused by the owner through bxctl. Tools do not run yet: the runner is a
trait whose production implementation refuses (M3b adds Podman).
Architecture: proto gains the audit records, the chain verifier (shared by brokerd and
bxctl) and the admin messages. brokerd is built bottom up: config, arguments, grants, policy,
session state and the audit writer (tasks 04 to 09), then the runner seam, the pending table, the
ledger (the audit writer and the state files behind one lock), the two socket handlers and
brokerd serve (10 to 15). loopd gets the real ToolPort over broker.sock (16, 17). bxctl
gets the admin commands, audit verify and approvals inside chat (18 to 20). A gate script
checks every runbook pointer (21), and one test runs loopd against the real brokerd binary (22).
Tech stack: as M2b. brokerd gains serde, serde_json and toml, all already vetted.
Spec: docs/specs/2026-09-18-m3a-decision-path.md. Brief: docs/design.md. Every fail-closed
message ends with a pointer into docs/runbook.md.
Global constraints
- Everything in
AGENTS.md, including "Lessons from earlier reviews". - No new dependency beyond the three above. Formats we define reject unknown fields, at every depth
(
proto/tests/strict.rswalks every object of every fixture). DecisionRecord::Allowed {}andAsk {}are written with braces everywhere (task 01 says why).- A
Decisionis built only inpolicy, aRunSpeconly inrunner;compile_faildoctests prove both. - Branch
m3a. One task, one fresh OpenCode session, one commit. Runcargo fmt --allbefore the gate. Review happens once, after task 22.
Tasks
The last column is how the given tests were checked before hand-over (decision of 2026-09-18,
tip T17) and what that check exposed. The detail is in checks-a.md to checks-e.md.
| # | File | Delivers | Tests that define it | Check | What the check exposed |
|---|---|---|---|---|---|
| 01 | 01-proto-audit-types.md |
AuditRecord, AuditEvent, DecisionRecord and friends |
proto/tests/records.rs, strict.rs, fixtures/records/audit.jsonl |
reference (the types) | DecisionRecord's unit variants accepted unknown fields (serde ignores deny_unknown_fields there); found by strict.rs. After hand-over: the merged strict.rs named task 02's fixtures, so task 01's gate could not pass (fixed with strict.rs-task01) |
| 02 | 02-proto-admin-wire.md |
the admin messages, u64 approval ids, approval_pending, tool_denied |
wire.rs, turn_wire.rs, admin_wire.rs, strict.rs, 17 wire fixtures |
reference (the types) | the same serde hole, found independently; serde's array form for structs (left open in decisions.md) |
| 03 | 03-proto-chain-verifier.md |
ChainVerifier, ChainReport |
proto/tests/chain.rs, 30 fixture logs with real hashes |
reference, generated fixtures, 8 mutants | four spec gaps: which seq the lists report, the resumed verifier inside a failed region, a Recovery describing nothing, the report fields a writer needs |
| 04 | 04-brokerd-config.md |
brokerd.toml into Config |
brokerd/tests/config.rs, 6 fixtures |
minimal reference | socket paths when only home is set; stale "Used by" column in dependencies.md |
| 05 | 05-brokerd-args.md |
typed tool arguments; path, host and URL checks | brokerd/tests/args.rs |
minimal reference | IPv4 literals passed the host grammar; found by writing the test table |
| 06 | 06-brokerd-grants.md |
GrantSet loading, every rule |
brokerd/tests/grants.rs, grant fixtures |
minimal reference | a missing grants directory; a fixture name swallowed by a global gitignore |
| 07 | 07-brokerd-policy.md |
decide, redecide, Denial |
policy.rs, policy_matching.rs, policy_redecide.rs, policy_property.rs (oracle) |
oracle, 6 mutants; plus a minimal reference | the write_file overlap case; redecide's signature; the M1 compile_fail doctests had lost their teeth. The reference found nothing the oracle did not |
| 08 | 08-brokerd-state.md |
session state files | brokerd/tests/state.rs |
minimal reference | a file saying public is damaged |
| 09 | 09-brokerd-audit-writer.md |
the audit writer, startup, recovery, --accept-break |
brokerd/tests/audit.rs, audit_startup.rs |
reference, mutants | two startup defects that would have stopped brokerd starting (the resume deadlock; tampering only visible in the latest file) |
| 10 | 10-brokerd-runner.md |
Runtime, RunSpec, run, Refusing |
brokerd/tests/runner.rs, support/runtime.rs |
reference | none |
| 11 | 11-brokerd-approvals.md |
the pending table | brokerd/tests/approvals.rs |
reference | Verdict::Run must box the Decision (clippy) |
| 12 | 12-brokerd-ledger.md |
the three locked steps, AuditSink |
ledger.rs, ledger_answer.rs, support/{rig,sink}.rs |
reference | unreadable state recorded as secret; no Result when state fails; the rig broke task order (fixed) |
| 13 | 13-brokerd-broker.md |
one request on broker.sock |
broker.rs, broker_pending.rs, broker_sequence.rs, support/client.rs |
reference | every request is recorded, grants_invalid included; an unsendable pending frame |
| 14 | 14-brokerd-admin.md |
one request on admin.sock; expiry |
brokerd/tests/admin.rs |
reference | a refusal that cannot be recorded must not answer ok; the re-decision's mode |
| 15 | 15-brokerd-serve.md |
brokerd serve |
brokerd/tests/serve.rs (drives the binary) |
reference | no read timeout on a request frame (accepted for M3a, section 14) |
| 16 | 16-loopd-tools.md |
ToolPort with the pending callback, Registry::m3a(), denial texts |
tools.rs, turn.rs, turn_broker.rs |
real code (a skeleton was impossible) | echo must stay in the test registry; which tool name the events carry |
| 17 | 17-loopd-broker-port.md |
BrokerPort, [broker], three runbook pointers |
broker_port.rs, broker_port_bad.rs, pointers.rs, config.rs, device.rs |
skeleton; later a body as a measurement | the timeout must be a deadline; five silent cases. The body found nothing more |
| 18 | 18-bxctl-admin.md |
approvals, approve, refuse, grants check; escaping |
escape.rs, cli.rs, admin.rs |
skeleton; later bodies as a measurement | the spec's example contradicted section 6; "+41" parses as a u64. The bodies found nothing more |
| 19 | 19-bxctl-audit-verify.md |
bxctl audit verify |
bxctl/tests/verify.rs |
reference | the output lines, torn tail included; a clash with task 18 over verify.rs (fixed) |
| 20 | 20-bxctl-chat-approvals.md |
the approval block and question in chat |
chat_print.rs, chat_approvals.rs |
skeleton; later a body as a measurement | --say/--json ambiguous; tool names were printed raw; one stdin reader |
| 21 | 21-runbook-check.md |
scripts/check-runbook.sh in the gate |
scripts/test-gate-scripts.sh |
the real script, 5 mutants | a computed anchor cannot be checked; no pointer at all must fail. It then caught such a pointer in task 17's test |
| 22 | 22-end-to-end.md |
the two-process test; the straylight script | loopd/tests/end_to_end.rs |
reference (the real binary) | the recorded model calls read_file directly, not through call_tool |
files/ holds everything the tasks copy in, byte-identical to the reference tree
(~/src/boxmaker-ref, branch m3a-ref, unpushed). files/Makefile-task21 is task 21's
Makefile; files/Makefile is task 22's, with the end-to-end line. Likewise
files/crates/proto/tests/strict.rs-task01 is task 01's strict.rs; files/.../strict.rs is task
02's, which also walks task 02's new wire fixtures.
State of the checks at hand-over. In the reference tree make gate passes in full, the
end-to-end line included, so every given test has passed against some implementation. Area E's
suites (with broker_port and end_to_end) ran ten times idle and ten under load; area A's
startup suite five times, area B's three times and once under load. The merged tree then ran all
suites five times under sixteen busy loops on eight cores with no failure. For tasks 10 to 15 the tree was cut back to each task's
starting point and the tests compiled at each step. Mutation checks: audit 8 of 8, policy oracle
6 of 6, check-runbook.sh 5 of 5, broker 14 of 14. make verify-device with task 17's device.rs
passed twice on straylight (Ornith, slot 0).
What the record says for M3b (the decision on references): the references found real defects
only where the logic was stateful and security-bearing (the audit chain and startup, the broker's
ledger and approvals). For plumbing (config, args, grants, state) and for the clients
(loopd's port, bxctl), the tests, the exit lists and a skeleton found everything; the bodies
written afterwards as measurements found nothing more. For policy the mutation-tested oracle was
enough. Most spec defects were found by writing test tables and exit lists, not by running code.
For the owner: running a task
tools/run-plan.sh docs/plans/M3a, or one fresh OpenCode session per task with
"Read docs/plans/M3a/01-proto-audit-types.md and do exactly that task."
- Do not run
make verify-devicebetween tasks 16 and 17: task 16 removesechofrom the registryloopd serveuses, and task 17 brings thedevice.rsthat expects that. - After task 22, run
tools/check-m3a-device.shonce on this machine against straylight. It checks/slotsfirst and uses Ornith slot 0 only; it has not been run yet by anyone.
For the reviewer: after task 22
git log --oneline master..m3a: twenty-two commits with the trailer.- Copied files unchanged:
for f in $(cd docs/plans/M3a/files && find . -type f ! -name Makefile-task21 ! -name strict.rs-task01); do cmp "docs/plans/M3a/files/$f" "$f"; done git diff master..m3a --stat -- docs/design.md docs/specs docs/plans docs/runbook.md AGENTS.md CLAUDE.md deny.tomlis empty.make gate,make audit,make verify-device,tools/check-m3a-device.sh.- Read every
brokerdsource file against its task and the spec, the ledger and the approval handoff first. Probe from outside: a grant file edited while a call waits for approval, twobxctl approveat once,brokerdkilled mid-call then restarted, an audit file edited by hand,loopdkilled while its call is pending. - Run
brokerd's suites,broker_portandend_to_endrepeatedly under CPU load. - Findings to
docs/implementer-log.md; lessons todocs/implementer-lessons.md.