The previous BAST document modeled the JSON payloads (NegotiateRequest, ControlMessage and its resize/signal/eof/exit variants, TerminalParams) as BAST struct/union definitions with uint16/int32 fields. That was a category error: BAST describes binary data layouts, and per the BAST format spec itself, "a BAST document cannot validate a JSON payload." The control and negotiation payloads on the wire are UTF-8 JSON text serialized via serde_json, not struct-encoded binary — the uint16/int32 field widths implied a binary encoding that does not exist on the wire and would have misled any generated validator. The rewrite keeps only the genuinely-binary framing layer: - ChunkHeader (5-byte: stream_type u8 + length u32 BE) - StreamType enum (name->index table; documented deviation: on-wire is uint8, not BAST's standard u32 enum index) - Chunk (header + length-prefixed bytes payload) - NegotiationFrame (4-byte BE length prefix + UTF-8 JSON body, modeled as bytes since the body's JSON interpretation is above the BAST layer) The JSON shapes (NegotiateRequest, ControlMessage, TerminalParams) remain specified in tty-wire.md and implemented by the Rust source (src/negotiation.rs, src/control.rs), which are the source of truth for those payloads. Cross-references in tty-wire.md, overview.md, and README.md updated to reflect the simplified scope. The drift-detection test (project plan "Risk: BAST schema drift") still works unchanged — it asserts the StreamType enum values match wire.rs's STREAM_* constants, and that enum is retained. Docs-only change; no Rust source changes. Verification: - cargo test --all-features -> 122 tests pass (unchanged) - cargo clippy --all-targets --all-features -- -D warnings -> clean - cargo fmt --check -> clean - cargo doc --no-deps -> no new warnings (9 pre-existing rustdoc link warnings in src/, unchanged) - BAST JSON block parses as valid JSON (4 : ChunkHeader, StreamType, Chunk, NegotiationFrame)
12 KiB
status, last_updated
| status | last_updated |
|---|---|
| draft | 2026-08-17 |
alktty — BAST Document for the alk/tty Wire Format
This document is the BAST (Binary Abstract Syntax Tree) specification
for the binary portions of the alk/tty wire format. It is a normative
JSON document conforming to the BAST meta-schema at
https://alk.dev/bast/v1/schema (a standard JSON Schema Draft 2020-12
document); any JSON Schema Draft 2020-12 validator can check whether the
BAST below is well-formed.
alktty does not depend on alktype. The hand-rolled ChunkReader /
ChunkWriter in src/wire.rs is the runtime codec; this BAST document
is the human-readable contract that describes what those types
round-trip. If runtime validation against the BAST becomes desirable
later, alktype becomes an optional dep and this document is already
there to feed it. See ADR-001
and ADR-006.
Scope
The alk/tty wire format mixes a small binary framing layer with
UTF-8 JSON payloads for the structured parts. Per the BAST format
spec, a BAST document describes binary data layouts — it is not a
format for validating JSON payloads (validate_bytes vs
validate_json). This document
therefore specifies only the binary framing; the JSON shapes are
specified in prose in tty-wire.md and implemented by the
Rust source (src/negotiation.rs, src/control.rs), which are the
source of truth for those payloads.
The binary portions this BAST covers (both one-way doors per ADR-001):
- The 5-byte chunk header (
[stream_type: u8][length: u32 BE] [payload]) — the raw chunk codec insrc/wire.rs. Five stream types:STREAM_STDIN=0,STREAM_STDOUT=1,STREAM_STDERR=2,STREAM_CTRL_IN=3,STREAM_CTRL_OUT=4. - The negotiation frame length prefix (4-byte BE
u32followed bylengthbytes of UTF-8 JSONNegotiateRequestbody) — self-contained per ADR-006.
What this BAST deliberately does not cover:
NegotiateRequestJSON shape — a UTF-8 JSON object, not a binary layout. Specified in tty-wire.md §"Phase 1" and implemented byNegotiateRequestinsrc/negotiation.rs.ControlMessageJSON shape — the UTF-8 JSON payloads ofSTREAM_CTRL_IN/STREAM_CTRL_OUTchunks (theresize/signal/eof/exitvariants). Specified in tty-wire.md §"Control Channel" and implemented byControlMessageinsrc/control.rs.TerminalParamsJSON shape — thettyfield ofNegotiateRequest. Specified in tty-wire.md §"Phase 1" and implemented byTerminalParamsWireinsrc/negotiation.rs.
A previous revision of this document modeled the JSON payloads as
BAST struct/union definitions with uint16/int32 fields. That
was a category error: BAST describes binary layouts, and the control
and negotiation payloads on the wire are UTF-8 JSON text, not
struct-encoded binary. The JSON field types (u16, i32, String)
are JSON value types produced and consumed by serde_json, not binary
field widths. Describing them as BAST structs implied a binary
encoding that does not exist on the wire and would have misled any
generated validator. The simpler model — BAST for the binary framing,
prose + Rust source for the JSON payloads — matches what the wire
format actually is.
The BAST document
{
"$schema": "https://alk.dev/bast/v1/schema",
"$defs": {
"ChunkHeader": {
"kind": "struct",
"endian": "big",
"fields": [
{ "name": "stream_type", "kind": "uint8" },
{ "name": "length", "kind": "uint32" }
]
},
"StreamType": {
"kind": "enum",
"values": ["Stdin", "Stdout", "Stderr", "CtrlIn", "CtrlOut"]
},
"Chunk": {
"kind": "struct",
"endian": "big",
"fields": [
{ "name": "header", "kind": { "$ref": "#/$defs/ChunkHeader" } },
{ "name": "payload", "kind": "bytes", "encoding": "length-prefixed", "maxLength": 16777216 }
]
},
"NegotiationFrame": {
"kind": "struct",
"endian": "big",
"fields": [
{ "name": "length", "kind": "uint32" },
{ "name": "body", "kind": "bytes", "encoding": "length-prefixed", "maxLength": 16777216 }
]
}
}
}
Annotations
ChunkHeader — binary, exact
Maps 1:1 to the on-wire bytes. 5 bytes: stream_type as uint8 (1
byte), length as uint32 big-endian (4 bytes). This is the 5-byte
chunk header committed by ADR-001.
The Rust types are STREAM_STDIN/STREAM_STDOUT/STREAM_STDERR/
STREAM_CTRL_IN/STREAM_CTRL_OUT and CHUNK_HEADER_LEN = 5 in
src/wire.rs.
StreamType — enum, documented deviation: on-wire encoding is uint8, not BAST's standard u32 enum index
The BAST meta-schema specifies that an enum's binary representation
is a u32 index into values (0-based). The alk/tty wire format
encodes stream_type as a uint8 (1 byte), not a u32 (4
bytes) — the chunk header is 5 bytes, not 8. This is a deliberate
deviation: the chunk header needs a 1-byte discriminator, and 4 bytes
of padding would double the per-chunk overhead.
The StreamType enum is therefore normative for the name→value
mapping (the index of a name in values is the integer that appears
on the wire as the uint8 stream_type), but a generated validator
must NOT emit a u32 read for stream_type. The on-wire encoding is
the uint8 declared in ChunkHeader; the enum provides the
integer→name table for validation and diagnostics.
| index | name | on-wire byte | direction | payload |
|---|---|---|---|---|
| 0 | Stdin |
0x00 |
client→server | raw bytes |
| 1 | Stdout |
0x01 |
server→client | raw bytes |
| 2 | Stderr |
0x02 |
server→client | raw bytes |
| 3 | CtrlIn |
0x03 |
client→server | UTF-8 JSON control |
| 4 | CtrlOut |
0x04 |
server→client | UTF-8 JSON control |
stream_type > 4 is a protocol error (InvalidStreamType); there is
no extension escape hatch in the byte (a 6th channel is a wire-format
change requiring a new ALPN — ADR-001).
Chunk — binary, exact
A chunk is the 5-byte ChunkHeader followed by length bytes of
payload. The payload field is bytes with encoding: "length-prefixed" and maxLength: 16777216 (16 MiB = MAX_CHUNK_LEN
in src/wire.rs). The payload's interpretation depends on
stream_type:
Stdin/Stdout/Stderr(0/1/2): raw bytes. Zero-length payloads on these channels are sentinels (zero-length stdin = EOF from client; zero-length stdout = "drained" from server); see tty-wire.md §"Sentinels".CtrlIn/CtrlOut(3/4): UTF-8 JSON text (a serializedControlMessage). Control chunks are never zero-length (the JSON payload is at least{}). The JSON shape is not specified by this BAST — see tty-wire.md §"Control Channel" andsrc/control.rs.
NegotiationFrame — binary, exact (out-of-band for the chunk codec)
The negotiation frame is a 4-byte big-endian length prefix + a UTF-8
JSON body. This maps 1:1 to the on-wire bytes: length as uint32
big-endian (4 bytes), body as a length-prefixed byte string
(length bytes of UTF-8 JSON). The maxLength: 16777216 (16 MiB)
constraint is the same as MAX_CHUNK_LEN — error frames MUST be under
16 MiB so the 4-byte length prefix's high byte is 0x00, which is
what makes the framing-disambiguation trick sound (first byte 0x00
= error/negotiation frame; first byte 1/2/4 = raw chunk; see
tty-wire.md §"Constraints").
The body is modeled as bytes (not string) because the BAST
describes the binary framing layer: the body is length bytes whose
interpretation (UTF-8 JSON, parseable as NegotiateRequest) is a
contract above this BAST layer, specified in
tty-wire.md §"Phase 1" and implemented in
src/negotiation.rs. The NegotiateRequest JSON shape is not
specified by this BAST.
The negotiation frame is out-of-band for the chunk codec: it is
read once at session start (Phase 1, JSON carriage), then the stream
switches to raw chunks (Phase 2). The ChunkReader/ChunkWriter in
src/wire.rs does not read or write negotiation frames; the
NegotiationReader/NegotiationWriter in src/negotiation.rs does.
See ADR-006.
Validation
The BAST document above is validatable in two ways:
- Structural validation — run any JSON Schema Draft 2020-12
validator against the BAST meta-schema at
https://alk.dev/bast/v1/schema. This checks whether the BAST is well-formed (correctkindstrings, required fields present,$reftargets exist). It does NOT check whether a binary payload conforms to the layout — that is the BAST-native validator's job (see the alktype BAST format spec,/workspace/@alkdev/alktype/docs/architecture/bast-format.md). - Drift detection — a cheap test that parses the BAST document
with
serde_jsonand asserts theStreamTypeenum values matchwire.rs'sSTREAM_STDIN/STREAM_STDOUT/STREAM_STDERR/STREAM_CTRL_IN/STREAM_CTRL_OUTconstants (the index of each name invaluesis the integer constant). This catches the common drift case (a new stream_type added towire.rsbut not the BAST, or vice versa) without requiring alktype as a dep — justserde_json, which alktty already has. See the project setup plan's "Risk: BAST schema drift" mitigation.
Design Decisions
| Decision | ADR | Summary |
|---|---|---|
| Wire format and two-carriage model | ADR-001 | The 5-byte chunk header + negotiation frame this BAST describes |
| Self-contained negotiation framing | ADR-006 | The NegotiationFrame is self-contained in alktty (not reused from alkcall's EventEnvelope framing) |
| Exit code on a control chunk | ADR-004 | The exit JSON variant carried on CtrlOut (specified in tty-wire.md, not this BAST) |
References
- ADR-001 — the wire format decision this BAST specifies
- ADR-006 — the
negotiation framing is self-contained (the
NegotiationFrameis not reused from alkcall) - tty-wire.md — the prose wire format spec this BAST formalizes (covers both the binary framing and the JSON payloads)
src/wire.rs— the runtime chunk codec (ChunkReader/ChunkWriter,STREAM_*constants,MAX_CHUNK_LEN) this BAST describessrc/negotiation.rs— theNegotiationReader/NegotiationWriterfor theNegotiationFrame, andNegotiateRequest(JSON shape, out of BAST scope)src/control.rs—ControlMessage(JSON shape, out of BAST scope)- alktype BAST format spec — the
normative format spec for BAST documents (the meta-schema this
document conforms to); see also
/workspace/@alkdev/alktype/docs/architecture/bast-format.md - Project setup plan, "Risk: BAST schema drift" — the drift-detection
test mitigation (
docs/plans/project-setup.md)