5.9 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | |
|---|---|---|---|---|---|---|---|---|
| tty/wire-codec | Implement raw chunk codec (ChunkReader/ChunkWriter, RawError, stream types) | completed |
|
narrow | low | component | implementation |
Description
Implement the raw chunk codec in src/wire.rs. This is the Phase 2 "raw
carriage" format (ADR-052): after the single JSON negotiation frame, the bidi
stream switches to a chunk format for the life of the session.
The codec is a direct port of the POC's /workspace/alknet-tty-poc/src/raw.rs,
generalized into the crate's wire module. The POC code is the reference; this
task ports it verbatim with crate-appropriate doc comments and the RawError
type exposed publicly.
Wire format
[stream_type: u8][length: u32 be][payload bytes]
-
stream_type(1 byte) — the channel:stream_type channel direction payload 0 data-in (stdin) client→server raw bytes 1 data-out (stdout) server→client raw bytes 2 data-err (stderr) server→client raw bytes 3 control bidirectional JSON control message stream_type > 3is a protocol error (InvalidStreamType). There is no extension escape hatch in the byte — a 5th channel is a wire-format change requiring a new ALPN (alknet/tty/v2per ADR-006). -
length(4 bytes, big-endian) — payload length in bytes. Max 16 MiB (MAX_CHUNK_LEN = 16 * 1024 * 1024). A chunk larger than 16 MiB is a protocol error (ChunkTooLarge). -
payload(lengthbytes) — raw bytes (data channels) or UTF-8 JSON (control).
Types to implement
pub const STREAM_STDIN: u8 = 0;
pub const STREAM_STDOUT: u8 = 1;
pub const STREAM_STDERR: u8 = 2;
pub const STREAM_CONTROL: u8 = 3;
pub const CHUNK_HEADER_LEN: usize = 5; // 1 byte type + 4 bytes length
pub const MAX_CHUNK_LEN: u32 = 16 * 1024 * 1024;
#[derive(Debug, thiserror::Error)]
pub enum RawError {
#[error("io: {0}")]
Io(#[from] std::io::Error),
#[error("connection closed")]
ConnectionClosed,
#[error("invalid chunk header: stream type {0}")]
InvalidStreamType(u8),
#[error("chunk too large: {0}")]
ChunkTooLarge(u32),
}
#[derive(Debug, Clone)]
pub struct Chunk {
pub stream_type: u8,
pub bytes: bytes::Bytes,
}
impl Chunk {
pub fn stdin(bytes: bytes::Bytes) -> Self;
pub fn stdout(bytes: bytes::Bytes) -> Self;
pub fn stderr(bytes: bytes::Bytes) -> Self;
pub fn control(bytes: bytes::Bytes) -> Self;
}
pub struct ChunkReader<R: AsyncRead + Unpin> { /* ... */ }
impl<R: AsyncRead + Unpin> ChunkReader<R> {
pub fn new(reader: R) -> Self;
pub fn into_inner(self) -> R;
pub async fn read_chunk(&mut self) -> Result<Chunk, RawError>;
}
pub struct ChunkWriter<W: AsyncWrite + Unpin> { /* ... */ }
impl<W: AsyncWrite + Unpin> ChunkWriter<W> {
pub fn new(writer: W) -> Self;
pub fn into_inner(self) -> W;
pub async fn write_chunk(&mut self, chunk: &Chunk) -> Result<(), RawError>;
pub async fn write_stdin(&mut self, bytes: &[u8]) -> Result<(), RawError>;
pub async fn write_control_json(&mut self, json: &[u8]) -> Result<(), RawError>;
}
Implementation notes
read_chunkreads the 5-byte header, validatesstream_type <= 3(elseInvalidStreamType), validateslength <= MAX_CHUNK_LEN(elseChunkTooLarge), readslengthbytes. OnUnexpectedEofreading the header or payload, returnConnectionClosed(notIo) — the stream ended cleanly.write_chunkwrites the 5-byte header then the payload (if non-empty), then flushes.write_stdinandwrite_control_jsonare convenience helpers.- Zero-length chunks are sentinels (see tty-wire.md §"Sentinels"): a zero-length
stdin chunk is EOF from the client; a zero-length stdout chunk is "drained"
from the server. The codec does not special-case these — they are just chunks
with
length == 0; the adapter interprets them.
Tests
Port the POC's round-trip behavior: write a chunk, read it back, assert
equality. Test all four stream types. Test InvalidStreamType (stream_type 4)
and ChunkTooLarge (length > MAX_CHUNK_LEN). Test ConnectionClosed on
truncated header/payload. Use tokio::io::duplex as the transport stand-in.
Acceptance Criteria
STREAM_STDIN/STREAM_STDOUT/STREAM_STDERR/STREAM_CONTROLconstants defined (0-3)CHUNK_HEADER_LEN(5) andMAX_CHUNK_LEN(16 MiB) constants definedRawErrorenum withIo,ConnectionClosed,InvalidStreamType,ChunkTooLargeChunkstruct withstream_type/bytesfields andstdin/stdout/stderr/controlconstructorsChunkReader::read_chunkvalidates stream_type and length, returnsConnectionClosedon clean EOFChunkWriter::write_chunk/write_stdin/write_control_jsonwrite header + payload + flush- Round-trip unit tests for all four stream types
- Unit test:
InvalidStreamTypeon stream_type > 3 - Unit test:
ChunkTooLargeon length > MAX_CHUNK_LEN - Unit test:
ConnectionClosedon truncated header and truncated payload cargo test -p alknet-ttysucceedscargo clippy -p alknet-ttysucceeds with no warnings
References
- docs/architecture/crates/tty/tty-wire.md — wire format spec (§"Phase 2: Raw Chunk Format", §"Sentinels")
- docs/architecture/decisions/052-alknet-tty-wire-format-and-two-carriage.md — ADR-052
- /workspace/alknet-tty-poc/src/raw.rs — the reference implementation to port
Notes
This is a near-verbatim port of the POC's
raw.rs. The codec is transport-agnostic (works over anyAsyncRead/AsyncWrite); the adapter task wires it to QUIC bidi streams. TheMAX_CHUNK_LENconstant is shared with the negotiation module (the framing-disambiguation trick depends on error frames being under 16 MiB so the high byte of the length prefix is0x00— see tty-adapter.md §"Negotiation errors").
Summary
To be filled on completion