Files
boxmaker/docs/plans/M3a
kyleandClaude Opus 5 ed8cf49540 M3a task 01: copy a strict.rs of its own
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>
2026-09-19 12:07:59 -07:00
..

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-… to 22-…). 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.rs walks every object of every fixture).
  • DecisionRecord::Allowed {} and Ask {} are written with braces everywhere (task 01 says why).
  • A Decision is built only in policy, a RunSpec only in runner; compile_fail doctests prove both.
  • Branch m3a. One task, one fresh OpenCode session, one commit. Run cargo fmt --all before 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-device between tasks 16 and 17: task 16 removes echo from the registry loopd serve uses, and task 17 brings the device.rs that expects that.
  • After task 22, run tools/check-m3a-device.sh once on this machine against straylight. It checks /slots first and uses Ornith slot 0 only; it has not been run yet by anyone.

For the reviewer: after task 22

  1. git log --oneline master..m3a: twenty-two commits with the trailer.
  2. 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
  3. git diff master..m3a --stat -- docs/design.md docs/specs docs/plans docs/runbook.md AGENTS.md CLAUDE.md deny.toml is empty.
  4. make gate, make audit, make verify-device, tools/check-m3a-device.sh.
  5. Read every brokerd source 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, two bxctl approve at once, brokerd killed mid-call then restarted, an audit file edited by hand, loopd killed while its call is pending.
  6. Run brokerd's suites, broker_port and end_to_end repeatedly under CPU load.
  7. Findings to docs/implementer-log.md; lessons to docs/implementer-lessons.md.