# M1 task 03: `proto` IPC messages **Branch:** `m1` (run `git switch m1`; `git status --short` must be empty, otherwise stop) **Commit subject:** `Add IPC envelope and tool messages to proto` ## Goal Add the message types that roles send each other. They are defined by `crates/proto/tests/wire.rs` and the byte-exact JSON files in `crates/proto/tests/fixtures/wire/`. Each fixture must decode to the expected value, and the value must encode back to exactly the fixture's bytes. ## Context From the spec: "Unknown fields are rejected everywhere. Enums are tagged with explicit strings. No floats. No free-form maps." An envelope looks like this: ```json {"v":1,"id":7,"final":true,"msg":{"kind":"tool_request","body":{"session":"mm-thread-42","call":3,"tool":"read_file","arguments":"{\"path\":\"/etc/hosts\"}"}}} ``` `arguments` is the model's JSON object carried as a string. `proto` never parses it. ## Files - Copy: `crates/proto/tests/wire.rs`, `crates/proto/tests/fixtures/wire/` (6 files) - Create: `crates/proto/src/wire.rs` - Modify: `crates/proto/src/lib.rs`, `docs/implementer-log.md` ## Interfaces Consumes from task 02: `proto::SessionId`, `proto::CallId`, `proto::Timestamp`, `proto::DataClass`. Produces, in `crates/proto/src/wire.rs`, re-exported from the crate root. **Keep fields and variants in exactly this order**; serde writes JSON in declaration order and the fixtures depend on it. ```rust pub const PROTOCOL_VERSION: u32 = 1; pub struct Envelope { pub v: u32, pub id: u64, pub r#final: bool, pub msg: Message } // JSON: {"kind":"tool_request","body":{...}} ; kinds are snake_case pub enum Message { ToolRequest(ToolRequest), ToolResponse(ToolResponse), Error(WireError) } pub struct WireError { pub code: ErrorCode, pub detail: String } // JSON: "bad_frame", "bad_version", "bad_message", "internal" pub enum ErrorCode { BadFrame, BadVersion, BadMessage, Internal } pub struct ToolRequest { pub session: SessionId, pub call: CallId, pub tool: String, pub arguments: String } // JSON: {"status":"result","content":...} ; the tag sits beside the fields; statuses are snake_case pub enum ToolResponse { PendingApproval { approval: String, expires: Timestamp }, Result { content: String, class: DataClass, untrusted: bool, truncated: bool }, Failed { message: String }, Denied { reason: DenyReason }, } // JSON: "no_grant", "grant_expired", "taint_too_high", "denied_by_grant", "approval_refused", "approval_expired" pub enum DenyReason { NoGrant, GrantExpired, TaintTooHigh, DeniedByGrant, ApprovalRefused, ApprovalExpired } ``` Derives: every type gets `Debug, Clone, PartialEq, Eq, Serialize, Deserialize`. `ErrorCode` and `DenyReason` also get `Copy`. Rules the tests check: an unknown field is an error in the envelope, beside `kind` and `body`, inside a body, and inside a `ToolResponse` variant. A missing field is an error, including `final`. An unknown `kind` or `status` is an error. ## API notes (serde 1.0, verified 2026-09-17) - `#[serde(deny_unknown_fields)]` works on structs and on enums. - Adjacently tagged enum: `#[serde(tag = "kind", content = "body")]`. - Internally tagged enum (tag beside the variant's own fields): `#[serde(tag = "status")]`. - `#[serde(rename_all = "snake_case")]` renames variants: `ToolRequest` becomes `tool_request`. - Several attributes can share one `#[serde(...)]`, separated by commas. - A field named `r#final` serializes as `final`. No rename is needed. ## Steps - [ ] **1. Copy the test and fixtures.** ```sh git switch m1 mkdir -p crates/proto/tests/fixtures cp docs/plans/M1/files/crates/proto/tests/wire.rs crates/proto/tests/ cp -r docs/plans/M1/files/crates/proto/tests/fixtures/wire crates/proto/tests/fixtures/ ``` - [ ] **2. See the test fail.** `cargo test -p proto --test wire`. Expected: it does not compile. - [ ] **3. Write `wire.rs`,** and add to `lib.rs`: ```rust pub mod wire; pub use wire::{ DenyReason, Envelope, ErrorCode, Message, PROTOCOL_VERSION, ToolRequest, ToolResponse, WireError, }; ``` Keep the `pub mod` lines in `lib.rs` in alphabetical order, and the `pub use` lines after them in alphabetical order by module. - [ ] **4. See the test pass.** `cargo test -p proto --test wire`. Expected: `9 passed; 0 failed`. - [ ] **5. Run the gate.** `make gate`. Expected last line: `gate: ok`. - [ ] **6. Log and commit.** ```sh git add crates/proto docs/implementer-log.md git commit ``` ## Done when - `cargo test -p proto --test wire` reports 9 passed. - `make gate` prints `gate: ok`. - `diff -r crates/proto/tests/fixtures/wire docs/plans/M1/files/crates/proto/tests/fixtures/wire` and `cmp crates/proto/tests/wire.rs docs/plans/M1/files/crates/proto/tests/wire.rs` print nothing. ## Stop and report if - A fixture cannot be matched byte for byte with the field order given above. - `deny_unknown_fields` does not reject one of the cases in the test with the enum representations named above.