# 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 } pub struct Approve { pub approval: u64 } pub struct ApproveResult { pub outcome: DecisionRecord } pub struct Refuse { pub approval: u64, pub reason: Option } pub struct GrantProblem { pub file: String, pub line: Option, pub problem: String } pub struct GrantsReport { pub problems: Vec } ``` `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.