Files
kyleandClaude Fable 5.1 3e26c2e3c0 Add M1 plan, given tests and fixtures, AGENTS.md and implementer log
Seven task files for the implementing model under docs/plans/M1/, with
the test files, byte-exact fixtures, Makefile, deny.toml and gate-script
self-test they copy into place. All of it was verified against a private
reference implementation: the gate passes after every task in order.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 01:22:43 -07:00

113 lines
4.5 KiB
Markdown

# M1 task 04: `proto` frame codec
**Branch:** `m1` (run `git switch m1`; `git status --short` must be empty, otherwise stop)
**Commit subject:** `Add length-prefixed frame codec to proto`
## Goal
Read and write one envelope on a byte stream. This is the only I/O in `proto`. It is the first code
that touches bytes from another process, so it must never trust a length it has not checked.
## Context
From the spec: "A 4-byte big-endian unsigned length `N`, then `N` bytes of UTF-8 JSON holding one
envelope. `MAX_FRAME` is 1,048,576. A reader that sees `N == 0` or `N > MAX_FRAME` returns an error
without reading or allocating the body." And: "`v` is the protocol version, 1. A reader that sees
another value" rejects the frame.
## Files
- Copy: `crates/proto/tests/frame.rs`, `crates/proto/tests/fixtures/frame/tool_request.bin`
- Create: `crates/proto/src/frame.rs`
- Modify: `crates/proto/src/lib.rs`, `docs/implementer-log.md`
## Interfaces
Consumes from task 03: `proto::Envelope`, `proto::PROTOCOL_VERSION`.
Produces, in `crates/proto/src/frame.rs`, re-exported from the crate root:
```rust
pub const MAX_FRAME: usize = 1_048_576;
#[derive(Debug)]
pub enum FrameError {
Closed, // the stream ended before the first byte of a frame
Empty, // the length prefix was 0
TooLarge(usize), // the length prefix, or an outgoing body, was larger than MAX_FRAME
BadVersion(u32), // the envelope's v was not PROTOCOL_VERSION
Json(serde_json::Error), // the body was not a valid envelope, or could not be encoded
Io(std::io::Error), // any other I/O failure, including a stream that ends mid-frame
}
// FrameError implements Display (a short message per variant) and std::error::Error.
pub fn write_frame<W: std::io::Write>(w: &mut W, env: &Envelope) -> Result<(), FrameError>;
pub fn read_frame<R: std::io::Read>(r: &mut R) -> Result<Envelope, FrameError>;
```
Rules the tests check:
- `write_frame` encodes first. If the body is larger than `MAX_FRAME` it returns `TooLarge(len)`
and writes **nothing**. Otherwise it writes the 4-byte length, the body, and flushes.
- `read_frame` reads exactly 4 length bytes. `Read::read` may return fewer bytes than asked for,
even one at a time; keep reading until you have all 4. If the stream ends before the first of
them, return `Closed`. If it ends after 1 to 3 of them, return `Io`.
- It checks `N == 0` (`Empty`) and `N > MAX_FRAME` (`TooLarge(N)`) **before** it allocates a
buffer or reads the body. One test uses a reader that panics if the body is read.
- A body of exactly `MAX_FRAME` bytes is allowed.
- A stream that ends inside the body is `Io`. A body that is not a valid envelope is `Json`.
- After decoding, `v != PROTOCOL_VERSION` is `BadVersion(v)`.
## API notes
- `u32::from_be_bytes([u8; 4])` and `u32::to_be_bytes()`.
- `serde_json::to_vec(&T) -> Result<Vec<u8>, serde_json::Error>` and
`serde_json::from_slice::<T>(&[u8])`.
- `Read::read_exact` returns `ErrorKind::UnexpectedEof` when the stream ends early, which is what
you want for the body. It cannot tell a clean close from a cut-off header, so do not use it for
the 4 length bytes.
- A `read` that fails with `ErrorKind::Interrupted` should be retried.
- Build an `io::Error` from a kind with `std::io::Error::from(std::io::ErrorKind::UnexpectedEof)`.
## Steps
- [ ] **1. Copy the test and fixture.**
```sh
git switch m1
cp docs/plans/M1/files/crates/proto/tests/frame.rs crates/proto/tests/
cp -r docs/plans/M1/files/crates/proto/tests/fixtures/frame crates/proto/tests/fixtures/
```
- [ ] **2. See the test fail.** `cargo test -p proto --test frame`. Expected: it does not compile.
- [ ] **3. Write `frame.rs`,** and add to `lib.rs` (alphabetical order, as before):
```rust
pub mod frame;
pub use frame::{FrameError, MAX_FRAME, read_frame, write_frame};
```
- [ ] **4. See the test pass.** `cargo test -p proto --test frame`. Expected: `13 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 frame` reports 13 passed.
- `make gate` prints `gate: ok`.
- `cmp crates/proto/tests/frame.rs docs/plans/M1/files/crates/proto/tests/frame.rs` prints nothing.
- `frame.rs` contains no `unwrap`, `expect`, `panic!` or unchecked indexing.
## Stop and report if
- `write_matches_the_fixture_byte_for_byte` fails although `cargo test -p proto --test wire` passes.