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>
5.9 KiB
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 incrates/proto/tests/fixtures/wire/(sixteen new, and a changedtool_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:
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.
// 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:
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 matches with no catch-all arm. Add arms; do not add _ =>.
- In
code_name, after theInferencearm:ErrorCode::Forbidden => "forbidden", ErrorCode::NoSuchApproval => "no such approval", - In
Printer::event, after theQueued | Progressarm:// 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, thencp 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), andcp 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.rsandlib.rs, then the two arms inchat.rs. Runcargo 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:wire10 passed,turn_wire5 passed,admin_wire10 passed,strict5 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 gateprintsgate: ok.
Stop and report if
Allowed {},is missing fromaudit.rs("Check first").- A fixture cannot be matched byte for byte with the field order given here.
- Any file outside
protoother thanbxctl/src/chat.rsstops compiling.