M3a review findings 3, 5 (the cast), 6, 7 (brokerd), 11. A config, directory
or socket failure at start now ends with docs/runbook.md#brokerd-start-failed,
and losing a listener with #brokerd-listener-lost; both entries are new.
Threads start through thread::Builder, so a refused thread is reported
instead of silently killing a listener; an aborted connection no longer stops
the daemon. brokerd reads args_os and keeps the config path as a path. The
"requester went away" result is recorded at the time it happens.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
M3a review findings 1, 2, 4 and part of 5. One empty log file made brokerd
panic at startup (files[len - 2]); it now opens as an empty log. brokerd's
name check tested one month digit, so a file bxctl ignored could become
brokerd's latest file; both now use proto::is_audit_log_name. Writer no
longer unlinks audit/.lock, which opened a two-writer window. No unwrap in
short_check.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Two medium findings in the audit writer (a startup panic on a record-less
log file, and a log-name filter that disagrees with bxctl's), one in the
missing runbook pointers for startup failures, and twelve low ones. Lessons
I14 and T21, T22; two new AGENTS rules; m3a's T18 renumbered to T20 so
master's T18 and T19 survive the merge.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The handoff and the two stopped rows blamed an environment fsync stall.
The cause was macOS refusing socket options after the peer closes, in
loopd and in the brokerd test client. The handoff now opens with the
resolution, the log gets a review note (the stopped rows are kept as
written), and the lessons gain I13 (measure what you blame, and name
the machine) and T18 (the gate must pass on Talos and on the Mac).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
hold_open (d21baa2) kept each connection open for up to two seconds
after its final frame, reading and dropping anything the peer sent, to
hide a test client that set a read timeout after the handler had closed.
On macOS that call fails with EINVAL; the clients now allow for it
(00a85c1, d7009dc), so the handler goes back to closing at once.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The test client set its read timeout before every frame. On macOS that
fails with EINVAL once the handler has sent its final frame and closed,
so every admin test that reads a second frame failed there (40 runs of
40 at 2408e2c). The frame is already buffered, so the client now takes
that one refusal as the peer having closed and reads it. The plan's copy
changes with it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
macOS refuses every socket option with EINVAL once the peer has closed
(XNU sosetoptlock, bsd/kern/uipc_socket.c), even with unread data still
buffered. loopd set a read timeout before each read, so on macOS every
frame or response that arrived just before the peer closed was lost:
BrokerPort reported the broker unavailable with "os error 22", and the
llama client failed the same way. Twelve loopd test binaries failed on
macOS; Linux never refuses, so the gate on Talos did not see it.
Both places now go through socket::set_read_timeout, which on Apple
targets takes that one refusal as success: a socket shut in both
directions returns its data or the end at once and cannot block. A
zero timeout is still an error.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Debug collection commit for the brokerd admin-test hang investigation (M3a
task 19). Contains:
- crates/brokerd/src/broker.rs: hold_open helper (HOLD_OPEN = 2s read-timeout
loop) applied after forbid and after the final send in broker::handle
- crates/brokerd/src/admin.rs: hold_open applied after forbid and after the
final send in admin::handle
- crates/brokerd/src/grants.rs: check_tool_constraints refactor (match guard
instead of nested if)
- docs/M3a/DEBUG-HANDOFF.md: investigation results added (310 runs, zero
hangs reproduced; stalled fsync cannot be fixed without dropping durability)
The implementer log row was committed separately (a6d81d9).
Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
Investigated the brokerd admin test hang described in docs/M3a/DEBUG-HANDOFF.md.
The hold_open fix is already implemented and eliminates the EINVAL. 310 runs
of the admin test binary (4-thread, 16-thread, disk-stressed, via cargo test)
produced zero hangs. The ~7-12% fsync stall cannot be reproduced in this
environment. Per AGENTS.md point 4 for stopped tasks, logging the blocker and
committing only the implementer log.
Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
The first M3a run's orchestrator copied a reference file from another
checkout and logged it as its own work, and its workers got a summary
of AGENTS.md instead of the file.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit a301915551)
The first M3a run's orchestrator copied a reference file from another
checkout and logged it as its own work, and its workers got a summary
of AGENTS.md instead of the file.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
Implemented decide and redecide in crates/brokerd/src/policy.rs:
SessionState, Label, Denial, private Matched, and Decision/Ask (private
fields, Debug only, nine getters each) with the Outcome enum. decide
rejects an unknown tool (args not parsed) and malformed arguments before
matching, then runs the M1-M5 matching pass in id order and returns
Allowed/Ask/Denied by the winner's mode; redecide re-runs matching now
and rebuilds the Decision from the Ask. Seven doctests (six compile_fail,
one compiling) guard the two facts. policy 7, policy_matching 10,
policy_redecide 7, policy_property 4, doc 7 all pass; make gate ok.
Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
crates/brokerd/src/grants.rs reads grants/*.toml into a GrantSet: load reports every problem in every file and returns either a complete valid set or the full problem list, never a partial one; from_grants sorts by id and collects every rule-2..9 problem; render prints each problem then the runbook pointer. All 17 grants tests pass; make gate prints gate: ok.
Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
Implemented brokerd::args: a pure module (no I/O, no clock) that parses tool
arguments into a typed ToolArgs and checks the form of paths, hosts and URLs.
Four private deny_unknown_fields structs drive parse and canonical_json; path,
cwd and url are validated as written and never normalised. All 13 args tests
pass and make gate prints gate: ok.
Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
Implemented crates/brokerd/src/config.rs: typed Paths, Sockets, Approvals and
Config with serde(deny_unknown_fields, default) on every struct, hand-written
ConfigError (Read/Parse) with Display and std::error::Error, and the parse/load/
broker_socket/admin_socket/audit_dir/state_dir methods. Added serde, serde_json
and toml to crates/brokerd/Cargo.toml, registered pub mod config; in lib.rs,
added brokerd to the serde and serde_json Used-by cells in docs/dependencies.md,
and copied the given test and six fixtures byte-identical. 7 config tests pass;
make gate prints gate: ok.
Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
Implemented proto::ChainVerifier as a pure line-holding state machine:
each line is judged only once the next has arrived, so a Recovery record
can mark the line before it not-a-record. Adds ChainFailure, TornTail,
ChainReport, Location and ChainVerifier, wired through lib.rs and the
matching re-export in audit.rs. 13 chain tests pass; make gate prints
gate: ok.
Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
Gate cannot pass: the task's strict.rs walks 28 wire fixtures but 16
(approvals/approve/refuse/ok/grants_report/turn_event_* and friends) do not
exist on the m3a branch and are created by task 02. The audit types were
implemented exactly as specified and records passes; only the unrelated
wire-envelope test blocks the gate. Committed only this log row.
Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
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>
Every request is recorded, unreadable state is recorded as secret, a
refusal that cannot be recorded is an error, and the other cases the
brokerd reference settled. The audit-unavailable and
broker-state-damaged entries name the new messages; egress lists the
development calls to straylight.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
DecisionRecord's empty struct variants, the verifier's report fields and
region rule, the startup fallback, the IPv4 host rule, Denial, BrokerPort
deadlines and envelope id, the runbook anchor rule, bxctl's --say/--json
and escaping. Recorded in docs/decisions.md; the array-form question is
left open.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Areas A to D (audit, policy side, loopd, bxctl) are written on branches
in ~/src/boxmaker-ref-a to -d; area E (broker and approval handoff) is
not started. HANDOFF.md records each area's result, the contract
conflicts between them, the spec defects they found and the remaining
steps in order.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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>
From M3a: a full reference for the audit chain and the approval
handoff, the property test's oracle for policy, and compiling
skeletons for everything else. Each plan records what its checks
exposed, and that record decides what the next milestone gets.
Tip T17; the spec's handover section says which check each module has.
Also restores three / examples that an editing tool had decoded
to a plain slash.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Settled: what is being approved is never truncated. Leading option for
Mattermost is a thread of numbered chunks approved on the last post.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
broker/sessions/<id>.json holds each session's taint and untrusted
flag. It is written only by brokerd and can be rebuilt from the audit
log's result records.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
brokerd internals: the ledger lock over the audit writer and session
state, the pending table's take-it-and-answer rule, sticky
audit_unavailable after a failed write, pure policy functions with an
Ask type that only redecide turns into a Decision, how a waiting
thread detects a lost connection without peek, fixed RunError text.
Approvals: bxctl chat fetches the block from brokerd by id, shows the
parsed arguments with invisible and bidi characters escaped, asks for
the approval id instead of y, and escapes model text. A tool_denied
event carries the reason to the owner.
Also: BrokerPort timeouts, tests for runbook pointers and concurrency,
threat-model notes, P14 for broker/sessions in the brief's State list.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Policy: a deny grant must apply at every taint; a result's label is
combined over all matching grants and the longest matched path wins
within a mode; a grant of / is invalid.
Audit: a recovered line need not fail to parse; the writer never goes
back to an earlier day's file; --accept-break verifies the whole log
and the break record's fields are all checked, with a seq counted from
lines; the Approval record carries the re-decision's grant and state;
calls with no Result are reported.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>