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>
161 lines
5.9 KiB
Markdown
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.
|