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>
4.5 KiB
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:
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_frameencodes first. If the body is larger thanMAX_FRAMEit returnsTooLarge(len)and writes nothing. Otherwise it writes the 4-byte length, the body, and flushes.read_framereads exactly 4 length bytes.Read::readmay 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, returnClosed. If it ends after 1 to 3 of them, returnIo.- It checks
N == 0(Empty) andN > 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_FRAMEbytes is allowed. - A stream that ends inside the body is
Io. A body that is not a valid envelope isJson. - After decoding,
v != PROTOCOL_VERSIONisBadVersion(v).
API notes
u32::from_be_bytes([u8; 4])andu32::to_be_bytes().serde_json::to_vec(&T) -> Result<Vec<u8>, serde_json::Error>andserde_json::from_slice::<T>(&[u8]).Read::read_exactreturnsErrorKind::UnexpectedEofwhen 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
readthat fails withErrorKind::Interruptedshould be retried. - Build an
io::Errorfrom a kind withstd::io::Error::from(std::io::ErrorKind::UnexpectedEof).
Steps
- 1. Copy the test and fixture.
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 tolib.rs(alphabetical order, as before):
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.
git add crates/proto docs/implementer-log.md
git commit
Done when
cargo test -p proto --test framereports 13 passed.make gateprintsgate: ok.cmp crates/proto/tests/frame.rs docs/plans/M1/files/crates/proto/tests/frame.rsprints nothing.frame.rscontains nounwrap,expect,panic!or unchecked indexing.
Stop and report if
write_matches_the_fixture_byte_for_bytefails althoughcargo test -p proto --test wirepasses.