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>
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user