Record the second review of task 23 and the findings first left out

The review table gains findings 16 to 20, which the two review agents
reported and the first write-up dropped. The independent review of the fix
commits, and what was changed for it, is recorded; task 23's claims about
its tests are corrected. The spec and decisions record the day-long cap, the
ttl_ms bound, the socket-directory rule and the listener's retry.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-22 21:54:28 -07:00
co-authored by Claude Opus 5.5
parent fc8befaf5b
commit f0b39a4766
4 changed files with 39 additions and 7 deletions
+1
View File
@@ -6,6 +6,7 @@ Newest first. A decision that changes `docs/design.md` lands in the same commit
| Date | Decision | Reason |
|---|---|---|
| 2026-09-22 | M3a review fixes. `loopd` waits at most a day after a pending frame and `brokerd` refuses `[approvals] ttl_ms` over a day. A socket whose directory is `/` or a symbolic link is refused at start. A listener out of file descriptors or memory pauses and retries instead of stopping `brokerd`. `brokerd` and `bxctl` share one rule for audit log file names (`proto::is_audit_log_name`, real months and days only). The fixes were made by the design model, and the fix commits were reviewed by a separate agent before the merge. | A far `expires` parked a turn for ever; `chmod` through a link changed its target; an idle-connection flood could stop the daemon; the two components disagreed about which files were the log. Ornith was under heavy contention. |
| 2026-09-18 | M3a plan checks, `brokerd`. Every tool request gets a `Decision` record, including those denied `grants_invalid` or `state_unreadable`; forbidden kinds get none. An unreadable session state is recorded as `secret`, untrusted. A refusal that cannot be recorded is `error internal`, not `ok`. The re-decision's outcome is the matched grant's mode. A pending frame that cannot be sent is handled like a lost connection. Expiry lives in `admin`. A request frame without a read timeout is accepted for M3a. | Found while writing the reference for tasks 10 to 15: the spec left each case open, and `bxctl refuse` would have reported success for a refusal that was not on disk. |
| 2026-09-18 | M3a plan checks, audit. `DecisionRecord::Allowed {}` and `Ask {}` are empty struct variants. The resumed verifier accepts a break naming an older file inside a failed region too. When the previous file's last line does not parse, an ordinary start verifies the whole log. A `Recovery` that describes no line is a failure. `abandoned` and `unfinished` both name the decision's `seq`. The report carries what a `Recovery` or `AcceptedBreak` must hold. `--accept-break` with nothing to accept writes nothing. | serde ignores `deny_unknown_fields` on unit variants of an internally tagged enum, so `{"outcome":"allowed","x":1}` decoded; found by `strict.rs` in two areas, and the struct-variant fix was kept over a hand-written `try_from` as the smaller one that also refuses `"reason":null`. As specified, a correctly accepted break could stop every later start while `--accept-break` said "nothing to accept". |
| 2026-09-18 | M3a plan checks, policy and `loopd`. A host name's last label starts with a letter. For `write_file` a grant path equal to the argument does not count toward the match. `redecide` returns `Result<Decision, Denial>`, and `Denial` carries the `deny` grant and its hash. `BrokerPort`'s waits are deadlines, not per-read timeouts; the envelope id is `request.call.0`. `approval_pending.tool` and `tool_denied.name` are the target tool, not `call_tool`. `echo` stays in the test registry. | `127.0.0.1` and `127.1` fitted the host grammar, so "no IP literals" was false. A per-read timeout let a trickling peer hold a turn for ever. `call_tool` tells the owner nothing. |
+26 -1
View File
@@ -6,7 +6,7 @@ reviewer adds findings under "Reviews" once per milestone.
| Task | Date | Status | Gate runs | First gate | Deviations | Notes | Model |
|---|---|---|---|---|---|---|---|
| M3a/23-review-fixes | 2026-09-22 | done | 1 | pass | none | Done by the design model, not an implementer run: Ornith was under heavy contention. Findings 1 to 13 fixed, each with a test that failed first except 6, 11 and 13 (checked by reading); 14 moves to M3b's first task; of 15, the log-name rule, `MAX_PATH`'s doc and the unused push are fixed, and the two that change the baseline wait for an epoch change. The server expectations in the device tests were updated to the new layout. | Claude Opus 5.5 |
| M3a/23-review-fixes | 2026-09-22 | done | 1 | pass | none | Done by the design model, not an implementer run: Ornith was under heavy contention. Findings 1 to 13 and 16 fixed. Tests that failed without their fix cover 1, 2, 3, 4, 7, 8, 9, 10 and 16; two more (a torn first record, an empty later file) passed before the fix and are regression guards; 6, 11 and 13 were checked by reading. 14 moves to M3b's first task; of 15, the log-name rule, `MAX_PATH`'s doc and the unused push are fixed, and the two that change the baseline wait for an epoch change. The server expectations in the device tests were updated to the new layout. | Claude Opus 5.5 |
| M3a/22-end-to-end | 2026-09-22 | done | 1 | pass | none | Copied `end_to_end.rs`, `Makefile` (workspace build then ignored e2e with `BOXMAKER_BROKERD`) and `tools/check-m3a-device.sh`. Without the variable: 0 passed, 1 ignored. With it: 1 passed five runs. `--ignored` without the variable fails with "set BOXMAKER_BROKERD". `make gate` ran the e2e line (`1 passed`) and prints `gate: ok`. Did not run the straylight script. | Grok 4.6 |
| M3a/21-runbook-check | 2026-09-22 | done | 1 | pass | none | Wrote `scripts/check-runbook.sh`: find `*.rs` under crates (prune `target/`), awk out every `docs/runbook.md#` pointer, empty anchors fail, each remaining anchor must match a whole `## <anchor>` line, every missing one is reported with its files, then one exit. Step 5: dropping `-x` from grep failed with "the entry is the whole line, at level two"; `exit 1` at a missing-anchor report failed with "both missing entries and their files are reported". Real tree exits 0. `make gate` prints `gate: ok`. | Grok 4.6 |
| M3a/20-bxctl-chat-approvals | 2026-09-22 | done | 1 | pass | `AdminError` has no `Io` variant (task 18 maps write failures to `Protocol` via `From<io::Error>`), so `handle_pending` exit 8 reports every `cmd_approve`/`cmd_refuse` `Err` as `approval {id}: {e}` rather than returning `Err(AdminError::Io(e))`. Writes inside `handle_pending` itself still use `?`. | Moved `stream_turn` into `chat.rs` with `OnPending`/`Approvals`/`TurnIo`/`handle_pending`. The owner is shown `brokerd`'s list item, never the event's tool/args. Only the id typed in full (after stripping one trailing `\n` then one `\r`) approves; anything else refuses. `run` holds one `BufReader` on stdin for both modes. `Printer::event` escapes model text and tool names, prints the three fail-closed runbook lines as whole literals, and prints nothing for `ApprovalPending`. 21/12/20/9/12/8 tests five runs; `make gate` prints `gate: ok`. | Grok 4.6 |
@@ -358,6 +358,11 @@ refusing runner, the model reported the failure in its own words, and `bxctl aud
| 13 | low | implementer (20) | `AdminError` lacks the `Io` variant task 18 gives, so a failed write is downgraded to `Protocol` and answered by writing another line to the same failed writer. Disclosed in the row's Deviations column. | Task 23 |
| 14 | low | spec | `ToolArgs::HttpFetch` has public `url` and `host`, so "the host is the URL's host" is not enforced by the type. Nothing breaks it today (`args::parse` is the only producer), but M3b fetches `url` while policy matched `host`. | Before M3b |
| 15 | nit | implementer | `bxctl`'s `verify.rs` skips a `.jsonl` file whose name is not a date without a word (related to 2); `MAX_PATH`'s doc mentions URLs; `grants.rs:95` pushes an empty name it never uses; `find_tool` matches core tools, so `find_tool("time")` offers `clock` and `call_tool` then refuses it; the four tool schemas omit `"additionalProperties": false`. | When next touched |
| 16 | low | spec | `serve` set 0700 on whatever directory held a socket, following a symbolic link (the link's target changed) and falling back to `/` for a socket with no parent. Omitted from this table when first written. | Task 23 (`ba369f8`) |
| 17 | low | implementer (17) | `BrokerPort`'s deadline does not cover `connect` or the request write; a peer that accepts a few bytes at a time can stretch the send. The request is small and fits a socket buffer. Omitted when first written. | Open; M3b |
| 18 | nit | plan (17) | A pending frame marked `final` returns the generic "the tool broker is unavailable", so the spec's "the tool broker gave no final answer" cannot be reached over the wire; task 17 said to do it this way. Omitted when first written. | Open; spec to match |
| 19 | low | tests | `--accept-break` on a real break was tested only at library level, and `serve`'s directory and socket failures had no tests. Omitted when first written. | Task 23 (`cfa0247`, `serve_pointers.rs`); the listener-lost path is still untested |
| 20 | nit | spec | A relative or empty `home` gives paths relative to the working directory; an `approvals` insert with an id already present drops the earlier waiter. Neither can happen today (ids are audit `seq`s; the deployed config is absolute). Omitted when first written. | Open |
What was good: the argument checks are exactly the spec's tables at every boundary I could
construct, including the IPv4 spellings the spec review added; `RunSpec` and `Decision` are both
@@ -382,3 +387,23 @@ Ornith on straylight now runs with `parallel = 4` over one unified 262,144-token
slots that is no longer true, and the cache reasoning behind P9 (another client evicting our
slot) needs re-measuring before M3b leans on it.
### M3a, task 23 — reviewed 2026-09-22 by a separate agent, then by the design model
The design model wrote the spec, the reference, the review and these fixes, so the six fix commits
(`eed0a22` to `f6841f1`) were given to an agent that saw only the code, the spec and the finding
descriptions. It found no serious new defect, confirmed the audit resume logic and the lock change,
and ran every crate's tests. It found five problems, all fixed in `e08deb3` and `bb4d7c0`:
| # | Severity | Finding | Fix |
|---|---|---|---|
| R1 | low | `ChatError::Frame` and `AdminError::Frame` printed serde's message, which quotes a bad frame's text after decoding: escape sequences from a compromised peer reached the terminal. Confirmed by running. | Escaped |
| R2 | low | Pre-existing: a torn last line followed by an empty later file had its recovery written into the later file, breaking the chain for good. Confirmed by running. | The line is ended in its own file |
| R3 | low | The shared name rule accepted month 13 and day 99. | Real months and days only |
| R4 | low | The 24-hour cap departed from spec section 8 without a record, and a `ttl_ms` over a day would have been given up by `loopd` while listed. | Spec amended; `brokerd` refuses `ttl_ms` over a day |
| R5 | nit | The `MAX_PATH` doc edit added a line instead of replacing one. | Fixed |
It also showed that "an aborted connection no longer stops the daemon" meant nothing on Linux: the
errors skipped there do not occur, and running out of file descriptors still stopped `brokerd`. It
now pauses and retries instead. A timed-out admin request now says whether `brokerd` acted is
unknown. And two of task 23's tests passed before their fix; the record above says so.
+3 -3
View File
@@ -11,7 +11,7 @@ test that fails without it; the rest are checked by reading, as the last column
| 3 | `brokerd`'s config, directory and socket failures end with `#brokerd-start-failed`; a lost listener or a refused thread with `#brokerd-listener-lost`; both entries are new | `brokerd/tests/serve_pointers.rs` (the listener path by reading) |
| 4 | `Writer` no longer unlinks `audit/.lock` | `brokerd/tests/audit_edges.rs` |
| 5 | No `unwrap` in `audit.rs`, no `as` in `grants.rs` | gate |
| 6 | `serve` starts threads with `thread::Builder`; a refused per-connection thread closes that connection and prints a line; an aborted connection no longer stops the daemon | by reading |
| 6 | `serve` starts threads with `thread::Builder`; a refused per-connection thread closes that connection and prints a line; running out of file descriptors or memory pauses the listener (second round) | by reading |
| 7 | Every role's `main` reads `args_os`. `brokerd` and `loopd` keep the config path as a path, so one that is not UTF-8 works; `bxctl` and `inferproxy` take text and answer such an argument with their usage | `brokerd/tests/serve_pointers.rs`, `loopd/tests/args_os.rs`, `bxctl/tests/args_os.rs`, `inferproxy/tests/args_os.rs` |
| 8 | `BrokerPort` waits at most 24 hours after a pending frame, whatever `expires` says | `loopd/tests/broker_port_cap.rs` |
| 9 | `bxctl`'s admin requests time out after 30 s | `bxctl/tests/admin_timeout.rs` |
@@ -20,12 +20,12 @@ test that fails without it; the rest are checked by reading, as the last column
| 12 | `bxctl`'s usage says what `audit verify` does | `bxctl/tests/cli.rs` (unchanged) |
| 13 | `AdminError::Io`, and a failed write is returned at once | by reading |
| 14 | Not in this task: `ToolArgs::HttpFetch`'s `url` and `host` become one sealed type in M3b's first task, where the runtime starts reading `url` | — |
| 15 | `verify.rs` shares the name rule (2); `MAX_PATH`'s doc; the unused push in `grants.rs`. `find_tool` and `additionalProperties` change the baseline and wait for the next epoch change | — |
| 15 | `verify.rs` shares the name rule (2) and names the log-like files it did not check; `MAX_PATH`'s doc; the unused push in `grants.rs`. `find_tool` and `additionalProperties` change the baseline and wait for the next epoch change | — |
Also, because the server changed: `crates/loopd/tests/device.rs` keeps the expected server in one
constant, `EXPECT`, now four slots over one 262,144-token pool, and `tools/check-m3a-device.sh`
matches it. They stay recorded expectations rather than values read from `/props`, which would
make the self-test's own check pass by definition.
Result: six code commits (`eed0a22` to `f6841f1`) and this record; every new test failed before its fix; `make gate`
Result: six code commits (`eed0a22` to `f6841f1`), then a second round after an independent review of those (`ba369f8` to `fc8befa`; see the log). Most new tests failed before their fix; two are regression guards that passed before it; `make gate`
ok; `make verify-device` 6 of 6 and `tools/check-m3a-device.sh` ok on straylight.
+9 -3
View File
@@ -98,7 +98,7 @@ broker = "/var/lib/boxmaker/run/loop-broker/broker.sock"
admin = "/var/lib/boxmaker/run/owner-broker/admin.sock"
[approvals]
ttl_ms = 900000 # 15 min
ttl_ms = 900000 # 15 min; 1 to 86400000 (a day), else a config error
```
A socket path that is absent or empty means the default under `home`, as in `loopd`.
@@ -553,7 +553,10 @@ Nothing is truncated, rewritten or deleted, ever.
`brokerd` creates each directory if it is missing and sets its mode to 0700 whether it made it or
found it (a failure to do so is a startup error), removes a stale socket file, binds, and sets the
socket to 0600. All of this comes after the audit lock is taken (section 5, "Startup"): the lock
socket to 0600. It refuses a socket whose directory is `/` or a symbolic link, since the mode
change would land on `/` or on the link's target (added after the M3a review). Once serving, a
listener that runs out of file descriptors or memory pauses and retries; any other `accept`
failure stops `brokerd` (`see docs/runbook.md#brokerd-listener-lost`). All of this comes after the audit lock is taken (section 5, "Startup"): the lock
is what proves the socket file is stale and not another `brokerd`'s.
Any other message kind on a socket is answered with `error` `forbidden`, and the connection is
@@ -692,7 +695,10 @@ processes, output size) are M3b's.
`fn call(&self, req: &ToolRequest, on_pending: &mut dyn FnMut(&Pending)) -> ToolResponse`.
`BrokerPort` waits for the first frame until `[broker] timeout_ms` (default 120,000) after the
call began, and after a pending frame until the frame's `expires` plus `timeout_ms`, which
leaves a call approved at the last moment the same time to run as any other. These are
leaves a call approved at the last moment the same time to run as any other. The wait before
that `timeout_ms` is never more than a day (`MAX_PENDING_WAIT`), whatever `expires` says, and
`brokerd` refuses a `ttl_ms` over a day, so the two agree (added after the M3a review: a far
`expires` parked a turn for ever). These are
deadlines, not per-read socket timeouts: a peer that trickles bytes must not hold a turn for
ever. (Unlike the inference path's liveness rule, this is a total limit.) M3b must keep its tool
time limit under `timeout_ms`. If the socket cannot be reached, closes early or times out, the