Files
boxmaker/docs/plans/M3a/02-proto-admin-wire.md
T
kyleandClaude Opus 5 e3f37da232 Hand over the M3a plan: 22 tasks, their files, and the check record
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>
2026-09-18 23:45:43 -07:00

161 lines
5.9 KiB
Markdown

# M3a task 02: the admin messages and the other wire additions
**Branch:** `m3a` (run `git switch m3a`; `git status --short` must be empty, otherwise stop)
**Commit subject:** `Add the admin messages, approval ids as numbers, and two turn events`
## Goal
Everything `proto::wire` needs for M3a: four more deny reasons, two more error codes, the eight
admin messages of `admin.sock` with their body types, approval ids as `u64`, and two new turn
events. This is a format we define, so **every new struct and every new enum variant rejects
unknown fields**.
## Files
- Copy: `crates/proto/tests/wire.rs`, `turn_wire.rs`, `strict.rs`, `admin_wire.rs`, and the
seventeen files in `crates/proto/tests/fixtures/wire/` (sixteen new, and a changed
`tool_response_pending.json`)
- Modify: `crates/proto/src/wire.rs`, `crates/proto/src/lib.rs`, `crates/bxctl/src/chat.rs`,
`docs/implementer-log.md`
## Check first
Task 01 made `DecisionRecord::Allowed` and `Ask` empty struct variants (`Allowed {}`, `Ask {}`),
because serde does not apply `deny_unknown_fields` to the unit variants of an internally tagged
enum. Run `grep -n 'Allowed {},' crates/proto/src/audit.rs`. If it prints nothing, stop and
report: the test `an_outcome_rejects_unknown_and_misplaced_fields` cannot pass without it. Write
the braces wherever you name these two variants, as the tests do: `DecisionRecord::Allowed {}`.
## Interfaces
Keep the order of variants and fields exactly as given. The order is the wire format.
In `wire.rs`, change the import to
`use crate::{CallId, DataClass, DecisionRecord, SessionId, Timestamp, Usage};` and then:
```rust
pub enum Message {
// … the six existing variants, unchanged, then:
Approvals(Empty),
ApprovalList(ApprovalList),
Approve(Approve),
ApproveResult(ApproveResult),
Refuse(Refuse),
Ok(Empty),
CheckGrants(Empty),
GrantsReport(GrantsReport),
}
pub enum ErrorCode {
// … the ten existing variants, then:
Forbidden,
NoSuchApproval,
}
pub enum ToolResponse {
PendingApproval { approval: u64, expires: Timestamp }, // was `approval: String`
// … the rest unchanged
}
pub enum DenyReason {
// … the six existing variants, then:
GrantsInvalid,
AuditUnavailable,
InvalidArguments,
StateUnreadable,
}
pub enum TurnEvent {
// … the ten existing variants, then:
ApprovalPending { approval: u64, tool: String, expires: Timestamp },
ToolDenied { name: String, reason: DenyReason },
}
```
New types, at the end of `wire.rs`. Every one of the eight gets
`#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]` and
`#[serde(deny_unknown_fields)]`; `Empty` also derives `Default`.
```rust
// The admin messages on admin.sock. JSON: {"kind":"approvals","body":{}}
pub struct Empty {}
pub struct PendingApproval {
pub approval: u64,
pub session: SessionId,
pub call: CallId,
pub tool: String,
pub arguments: String,
pub grant: String,
pub taint: DataClass,
pub created: Timestamp,
pub expires: Timestamp,
}
pub struct ApprovalList { pub items: Vec<PendingApproval> }
pub struct Approve { pub approval: u64 }
pub struct ApproveResult { pub outcome: DecisionRecord }
pub struct Refuse { pub approval: u64, pub reason: Option<String> }
pub struct GrantProblem { pub file: String, pub line: Option<u64>, pub problem: String }
pub struct GrantsReport { pub problems: Vec<GrantProblem> }
```
`Empty {}` is written with braces, not as `struct Empty;`: a unit struct would encode as `null`,
and the body must be `{}`.
In `lib.rs` the `pub use wire::{…}` line becomes:
```rust
pub use wire::{
ApprovalList, Approve, ApproveResult, DenyReason, Empty, Envelope, ErrorCode, GrantProblem,
GrantsReport, Message, PROTOCOL_VERSION, PendingApproval, Refuse, ToolRequest, ToolResponse,
Turn, TurnDone, TurnEvent, WireError,
};
```
## `bxctl` must still compile
`crates/bxctl/src/chat.rs` has two `match`es with no catch-all arm. Add arms; do not add `_ =>`.
1. In `code_name`, after the `Inference` arm:
```rust
ErrorCode::Forbidden => "forbidden",
ErrorCode::NoSuchApproval => "no such approval",
```
2. In `Printer::event`, after the `Queued | Progress` arm:
```rust
// Printed from task 20 on; until then these events are not sent.
TurnEvent::ApprovalPending { .. } | TurnEvent::ToolDenied { .. } => {}
```
`loopd` needs no change: its `match` on `ToolResponse::PendingApproval` uses `{ .. }`.
## Steps
- [ ] **1. Copy.** `git switch m3a`, then
`cp docs/plans/M3a/files/crates/proto/tests/*.rs crates/proto/tests/` **only for the four files
named above** (`wire.rs turn_wire.rs strict.rs admin_wire.rs`), and
`cp docs/plans/M3a/files/crates/proto/tests/fixtures/wire/*.json crates/proto/tests/fixtures/wire/`
- [ ] **2. See the tests fail.** `cargo test -p proto --test admin_wire`. Expected: it does not
compile.
- [ ] **3. Edit `wire.rs` and `lib.rs`**, then the two arms in `chat.rs`. Run `cargo fmt --all`.
- [ ] **4. Go through every new type one by one** (eight structs, and the variants added to five
enums) and check each has `deny_unknown_fields`, either its own or its enum's. The enums already
have it; the eight structs each need their own.
- [ ] **5. See the tests pass.**
`cargo test -p proto --test wire --test turn_wire --test admin_wire --test strict`.
Expected: `wire` 10 passed, `turn_wire` 5 passed, `admin_wire` 10 passed, `strict` 5 passed.
- [ ] **6. Run the gate.** `make gate`. Expected last line: `gate: ok`.
- [ ] **7. Log and commit.**
`git add crates/proto crates/bxctl/src/chat.rs docs/implementer-log.md && git commit`
## Done when
- The four test files report the counts in step 5, and `make gate` prints `gate: ok`.
## Stop and report if
- `Allowed {},` is missing from `audit.rs` ("Check first").
- A fixture cannot be matched byte for byte with the field order given here.
- Any file outside `proto` other than `bxctl/src/chat.rs` stops compiling.