Implement the on-the-wire frame format and round-trippable codecs for every opcode. After this phase, you can take any byte sequence claiming to be a Brain frame, validate it, and parse it into a typed request — or refuse it cleanly.
- Phase 0 complete (workspace builds, CI green, tag
phase-0-completeexists). crates/brain-coreandcrates/brain-protocolare stubs and inherit workspace deps.
spec/04_wire_protocol/00_purpose.md— what the protocol is for.spec/04_wire_protocol/01_design.md— why binary, why CRCs, why fixed-header.spec/04_wire_protocol/01_design.md— TCP + optional TLS.spec/04_wire_protocol/02_wire_format.md— the 32-byte header layout. Critical.spec/04_wire_protocol/02_wire_format.md— rkyv structured + bytemuck for vector blobs.spec/04_wire_protocol/03_opcodes.md— every opcode and its number.spec/04_wire_protocol/04_handshake.md— initial handshake.spec/04_wire_protocol/05_frame_layouts.md— request bodies.spec/04_wire_protocol/05_frame_layouts.md— response bodies.spec/04_wire_protocol/06_streaming.md— streaming responses.spec/04_wire_protocol/07_error_handling.md— error frame shape, error codes.spec/04_wire_protocol/07_error_handling.md— what counts as malformed.
After reading: every constant, magic byte, length bound, and opcode number should be in your head, not paraphrased.
By end of phase:
crates/brain-coreexports the full type vocabulary used by the protocol (MemoryId,AgentId,ContextId,RequestId,EdgeKind,MemoryKind,Salience,Error, plus any new types the protocol needs).crates/brain-protocolexports:Frame(the parsed envelope)Header(the 32-byte header)Opcode(complete enum)RequestBody,ResponseBody(tagged unions)ProtocolError(error variants from §10)encode(frame) -> Vec<u8>decode(bytes) -> Result<Frame, ProtocolError>
- Property tests covering every opcode round-trip.
- A working fuzz target that exercises
decodeon arbitrary bytes. - Tag:
phase-1-complete.
Each sub-task is a single commit. The "Reads" listed are required reading before writing the code.
Reads:
spec/04_wire_protocol/02_wire_format.md
Writes:
crates/brain-protocol/src/header.rs— new modulecrates/brain-protocol/src/lib.rs— register module, re-export
What to build:
pub const MAGIC: [u8; 4] = *b"BRN0";(already present in stub; verify)pub const VERSION: u8 = 1;pub const HEADER_SIZE: usize = 32;pub const MAX_PAYLOAD_BYTES: usize = (1 << 24) - 1;#[repr(C, packed)] pub struct Header { ... }matching the spec's byte layout exactly. Usebytemuck::Pod+bytemuck::Zeroablefor safe casting.impl Header { pub fn new(opcode, flags, stream_id, payload_len) -> Self }— computes and stores the header CRC32C internally.impl Header { pub fn validate(&self) -> Result<(), ProtocolError> }— checks magic, version, header CRC, length bound. (ProtocolErroris defined in 1.6.)
Tests:
header_has_correct_size:assert_eq!(size_of::<Header>(), 32).header_has_correct_alignment: alignment is 1 (we'rerepr(C, packed)). If a different alignment is required by the spec, assert that.magic_bytes_match:assert_eq!(&MAGIC, b"BRN0").
Done when:
- Module compiles and tests pass.
-
bytemuck::Podderive works (no padding holes — verify by readingmem::size_ofvs sum of fields). -
Header::newcomputes a CRC thatvalidateaccepts.
Pitfalls:
repr(C, packed)makes field access on references unsafe. Always copy out of the struct or useaddr_of!.- Endianness: the spec uses big-endian for multi-byte fields (spec §02/03 §1, §8). Use
u16::to_be_bytesetc. when serializing. (Earlier draft of this doc said "little-endian"; corrected against spec.) - Don't fold the payload CRC into the header CRC — they're separate per spec.
Reads:
spec/04_wire_protocol/02_wire_format.md(CRC sections)
Writes:
crates/brain-protocol/src/crc.rs
What to build:
pub fn header_crc(bytes_before_crc_field: &[u8]) -> u32— computes CRC32C over the header bytes that precede theheader_crc32cfield, per the spec layout.pub fn payload_crc(payload: &[u8]) -> u32— CRC32C over the entire payload.- Both use
crc32c::crc32c(...)from the workspace dep.
Tests:
- Known vector: take the spec's example header bytes (if any) and verify CRC. If no vector, hand-compute one and pin it.
header_crc_excludes_self: hashing the header bytes minus the CRC field gives the value that's stored in the CRC field.
Done when:
- Functions are pure, public, documented.
- Tests pin specific CRC values, not just "round-trips."
Pitfalls:
- CRC32C ≠ CRC32. Confirm
crc32ccrate is the iSCSI variant (it is). crc32c::crc32creturns u32, not bytes. Convert withto_be_bytesfor serialization (spec §02/03 §8 — both CRC fields are big-endian on the wire). (Earlier draft of this doc saidto_le_bytes; corrected against spec.)
Reads:
spec/04_wire_protocol/03_opcodes.md
Writes:
- Update
crates/brain-protocol/src/lib.rs— replace the partial stubOpcodewith the full set. crates/brain-protocol/src/opcode.rs— promote to its own module if the lib.rs is getting busy.
What to build:
#[repr(u8)] enum Opcode { ... }— every opcode from the spec, with the spec's exact numeric values.impl Opcode { pub fn from_u8(b: u8) -> Result<Self, ProtocolError> }— exhaustive match returningUnknownOpcodefor unmapped values.impl Opcode { pub fn is_request(self) -> bool }andis_response/is_adminpredicates.
Tests:
- For each opcode:
Opcode::from_u8(N).unwrap() == Opcode::Foo. - For unknown:
Opcode::from_u8(0xFE).is_err(). - Property test: every value in
0..=255either maps to an opcode or returns the sameUnknownOpcodeerror.
Done when:
- All opcodes from spec §05 are present with matching numbers.
-
from_u8is exhaustive and tested. - Predicate helpers exist if the spec distinguishes request/response/admin.
Pitfalls:
- Don't renumber opcodes. The spec pins them.
- If the spec reserves ranges (e.g.
0x80..=0xEFfor vendor extensions), document that in the module.
Reads:
spec/04_wire_protocol/02_wire_format.mdspec/04_wire_protocol/02_wire_format.md
Writes:
crates/brain-protocol/src/frame.rs
What to build:
pub struct Frame { pub header: Header, pub payload: Vec<u8> }impl Frame { pub fn encode(&self) -> Vec<u8> }— emits header + payload, computes both CRCs, returns the bytes.impl Frame { pub fn decode(bytes: &[u8]) -> Result<(Self, &[u8]), ProtocolError> }— parses one frame, returns(frame, rest)so callers can decode streams.
Tests:
encode_then_decode_roundtrip: with arbitrary opcode and payload bytes.decode_rejects_bad_magic.decode_rejects_bad_version.decode_rejects_bad_header_crc.decode_rejects_bad_payload_crc.decode_rejects_truncated_input.decode_rejects_oversize_payload.
Done when:
- All seven test cases pass.
-
encodeanddecodeare inverses for valid frames. - Errors match the variants in spec §10.
Pitfalls:
- Empty payload is valid. Header still has its CRC; payload CRC is over empty bytes (well-defined CRC of empty input).
- The decoder returns the
restslice for stream consumers — don'tVec::extendand lose the borrow.
Reads:
spec/04_wire_protocol/07_error_handling.md
Writes:
crates/brain-protocol/src/frame.rs— extend thetestsmodule- Or
crates/brain-protocol/tests/frame_proptest.rs
What to build:
proptest!block: arbitrary (opcode, flags, stream_id, payload_bytes) → encode → decode → assert equality.proptest!block: arbitrary bytes → decode → either succeeds (and re-encoding gives back equivalent bytes) or returns a structured error. Must not panic.
Tests:
- The two proptest blocks above.
- Run with at least 1024 cases each (
PROPTEST_CASES=1024 cargo test).
Done when:
- Both proptests pass with default case count.
- No panics on arbitrary input — even malformed.
Pitfalls:
- Bound payload size in the generator (e.g. 0..=8192) so tests don't allocate gigabytes.
- If a test fails, save the seed via
proptest's regression file mechanism.
Reads:
spec/04_wire_protocol/07_error_handling.md
Writes:
crates/brain-protocol/src/error.rs
What to build:
#[derive(thiserror::Error, Debug)] enum ProtocolError { ... }— variants for every error case in §10:BadMagic,UnsupportedVersion(u8),BadHeaderCrc,BadPayloadCrc,Truncated,OversizePayload(usize),UnknownOpcode(u8),MalformedPayload(String),- any others the spec defines.
impl ProtocolError { pub fn code(&self) -> ErrorCode }— maps to the wire-level error code from §10.
Tests:
- For each error variant: it has a
code()matching the spec.
Done when:
- Every variant in spec §10 is represented.
-
From<ProtocolError>forbrain_core::Error(viaInternalorInvalidArgumentas appropriate).
Pitfalls:
- Don't conflate transport errors (TCP reset) with protocol errors. Transport handling is Phase 9.
Reads:
spec/04_wire_protocol/05_frame_layouts.md
Writes:
crates/brain-protocol/src/request.rs
What to build:
enum RequestBody { Encode(EncodeRequest), Recall(RecallRequest), ... }— one variant per request opcode.- For each variant, a struct with the fields per the spec's request schema.
- Encode/decode using
rkyvfor the structured fields andbytemuckfor any vector blobs. impl RequestBody { pub fn encode(&self) -> Vec<u8> }andpub fn decode(opcode: Opcode, bytes: &[u8]) -> Result<Self, ProtocolError>.
Tests:
- For each request variant: round-trip
encode → decode == original.
Done when:
- All request opcodes from §07 have a matching variant and codec.
- Round-trip tests for each.
- Vector blobs (where present) use
bytemuck::cast_slice, not rkyv. (Note: vector-blob composition into the trailing raw section is owned by theFramelayer, not byRequestBody::encode. The struct fieldsvector_offset/vector_dimcarry the placement information; rkyv handles the structured fields only.)
Pitfalls:
rkyvrequires the type to deriveArchive,Serialize,Deserializefrom the rkyv prelude. Add the workspace dep if not already present.- The wire format for vector blobs is little-endian f32 packed. Cross-check with spec §04.
Reads:
spec/04_wire_protocol/05_frame_layouts.mdspec/04_wire_protocol/06_streaming.md
Writes:
crates/brain-protocol/src/response.rs
What to build:
enum ResponseBody { ... }mirroring the request shape — one variant per response.- Streaming variants:
Next,Completeper §09. - Round-trip codecs.
Tests:
- Round-trip every variant.
- Streaming sequence: encode
[Next, Next, Complete], decode, verify ordering preserved.
Done when:
- All response opcodes have variants and codecs.
- Streaming protocol tested (at least encoding/decoding shape; multi-frame transport is Phase 9).
Pitfalls:
- A
Completeresponse can carry a final payload (per §09). Don't assume it's empty.
Reads:
spec/04_wire_protocol/04_handshake.md
Writes:
crates/brain-protocol/src/handshake.rs
What to build:
pub struct ClientHello { ... }andpub struct ServerHello { ... }per §06.- Codecs for both.
pub fn negotiate(client: &ClientHello, server_caps: &ServerCapabilities) -> Result<NegotiatedSession, ProtocolError>.
Tests:
- Round-trip both messages.
- Negotiation: compatible versions succeed; incompatible fail with
UnsupportedVersion.
Done when:
- Hello messages round-trip. (All four — HELLO, WELCOME, AUTH, AUTH_OK — round-trip through rkyv. Phase doc said "ClientHello/ServerHello" but spec §02/06 names the four messages explicitly; spec wins.)
- Negotiation logic matches the spec's compatibility matrix.
Reads:
spec/04_wire_protocol/07_error_handling.md- Phase 0's
fuzz/fuzz_targets/protocol_frame.rsplaceholder.
Writes:
fuzz/fuzz_targets/protocol_frame.rs— replace placeholder with real harness.
What to build:
fuzz_target!(|data: &[u8]| { let _ = brain_protocol::Frame::decode(data); });- Add a second target
protocol_request.rsthat decodes arbitrary bytes as aRequestBodyfor each opcode.
Tests:
cargo +nightly fuzz run protocol_frame -- -max_total_time=60exits cleanly.
Done when:
- Fuzz harness builds.
- 60-second run finds no panics. (Three targets: protocol_frame, protocol_request, protocol_response. Smoked at 60s each — 28M / 19M / 19M runs respectively, zero panics, zero artifacts.)
Pitfalls:
- Fuzzing requires nightly Rust. CI should not fail if nightly is unavailable; gate the fuzz step behind a
nightly-onlyjob.
Reads:
spec/02_data_model/02_memory.mdspec/02_data_model/02_memory.mdspec/02_data_model/05_edges.mdspec/02_data_model/04_salience.md
Writes:
- Update
crates/brain-core/src/*as the protocol reveals new fields.
What to build:
- Anything the protocol's request/response types need that's not yet in
brain-core. - Examples:
EncodeOptions,RecallFilter,PlanDirection.
Tests:
- For each new type: a basic constructor + round-trip via
serdeif it's serializable.
Done when:
-
brain-protocolcompiles without inline duplicates of types that belong in core. (Wire-domain types —WireMemoryId/WireUuid/WireContextIdaliases plusMemoryKindWire/EdgeKindWirerkyv enums — are deliberate per Task 1.7's design and bridge to brain-core viaFrom/Intoimpls inbrain-core::idsandbrain-protocol::convert.) -
brain-corecompiles standalone.
Drift fixes (spec §02/03 wins): MemoryId bit layout corrected (shard 16 + slot 48 + version 32 + reserved 32); ContextId Uuid → u64; ShardId u8 → u16; SlotVersion u16 → u32. Wire-side context_id fields switched to WireContextId = u64 (8 bytes per spec §02/03 §8). Added TxnId (UUIDv7). Updated Edge per spec §02/06: source/target (was from/to), added weight and EdgeOrigin, switched timestamp to unix_nanos.
Pitfalls:
- Resist over-engineering. Only add types that the protocol actively uses.
Before tagging phase-1-complete:
- All sub-tasks 1.1–1.11 marked done in this file.
- Verify suite (fmt-check + build + clippy + test + check-skills) is green on a clean checkout. (122 tests workspace, clippy clean, fmt clean, 23/23 skills valid.)
-
cargo test --workspaceruns ≥ 30 tests, all passing. (122.) - At least one proptest with ≥ 1024 cases per opcode. (
Opcode::from_u8_is_totalcycles every byte;Frame::encode_decode_round_tripanddecode_arbitrary_bytes_is_totalrun 1024 cases each.) - Fuzz target builds and a 60-second run is clean. (Three targets —
protocol_frame,protocol_request,protocol_response— smoked at 60s each, ~67M total runs, zero panics, zero artifacts.) - Public API of
brain-protocolis documented. (Everypubitem carries rustdoc; spec section anchors are inline.) -
cargo doc --workspace --no-depsbuilds without warnings. -
git tag phase-1-completeon the latest green commit.
- One sub-task = one commit, with the message format from
AUTONOMY.md§5. - Larger sub-tasks (1.7, 1.8) may split into 2-3 commits if each commit independently compiles and tests.
- After 1.11, run the full exit checklist, then tag.
Record every non-trivial decision here so subsequent phases (and the user) can find them.
| Date | Decision | Rationale | Sub-task |
|---|---|---|---|
| 2026-05-10 | Header multi-byte fields stored as raw BE byte arrays, not native ints | bytemuck::Pod derive with no padding holes; on-wire layout matches struct 1:1; avoids repr(C, packed) field-ref unsafety |
1.1 |
| 2026-05-10 | decode_with_max(bytes, max) separate from decode(bytes) |
Allocation-amplification defense: peer's claimed payload_len checked before reading payload bytes |
1.4 |
| 2026-05-10 | ErrorCode is #[non_exhaustive]; ErrorCodeWire is closed |
Forward-compat for the canonical type; rkyv needs a closed enum for the wire body. Identity round-trip via From impls |
1.6, 1.8 |
| 2026-05-10 | Wire-domain DTOs (WireMemoryId, WireUuid, MemoryKindWire, EdgeKindWire) live in brain-protocol, not brain-core |
Keeps brain-core rkyv-free; conversion happens at boundary via From/Into |
1.7, 1.11 |
| 2026-05-10 | Vector-blob composition (rkyv structured + trailing raw f32 section) owned by Frame layer, not RequestBody/ResponseBody |
Spec §02/04 separates structured + raw; per-body codec stays single-purpose. End-to-end vector wiring deferred to Phase 2/9 | 1.7, 1.8 |
| 2026-05-10 | Promote to_rkyv_bytes/from_rkyv_bytes to private crate::rkyv_codec |
Both request and response need the HRTB-laden helper; one source of truth |
1.8 |
| 2026-05-10 | negotiate(client, server) does version + capability intersection only; auth-method intersection defers to AUTH-frame handler |
Pure logic testable in isolation; runtime concerns (server picks session_id, populates ServerFeatures) stay in connection layer |
1.9 |
| 2026-05-10 | protocol_request / protocol_response fuzz harnesses dispatch by data[0] mod len(opcodes) rather than Opcode::from_u8 |
Most random bytes are unassigned opcodes; mod-len cycles all variants under coverage guidance | 1.10 |
| 2026-05-10 | Spec §02/03 wins over phase-doc + earlier code: MemoryId layout = shard 16 + slot 48 + version 32 + reserved 32; ContextId = u64; ShardId = u16; SlotVersion = u32 |
Pre-Phase-9, no deployed clients — fix layout drifts now; spec is read-only authoritative | 1.11 |
| 2026-05-10 | Wire context_id fields = WireContextId = u64 (8 bytes) |
Spec §02/03 §8 says ContextId on the wire is 8 bytes; protocol previously used WireUuid (16). Fixed before any deployed client |
1.11 |
| 2026-05-10 | Endianness pitfalls in phase doc corrected against spec §02/03 §8 (header) and §02/03 §2.1 (MemoryId): all multi-byte = big-endian | Phase doc had two LE references that conflicted with the spec | 1.1, 1.2 |
| 2026-05-10 | ClientHello / ServerHello phase-doc names superseded by spec §02/06 names: HELLO / WELCOME / AUTH / AUTH_OK |
Spec wins; codec covers all four messages | 1.9 |