docs: fix tty-bast.md to cover only binary framing, not JSON payloads

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)
This commit is contained in:
2026-08-17 11:20:23 +00:00
parent b84c67bd8a
commit da0d395ab3
4 changed files with 113 additions and 239 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ Syntax Tree) document for the wire format, and the ADRs.
|----------|--------|-------------| |----------|--------|-------------|
| [overview.md](overview.md) | draft | Crate purpose, the two-carriage model in brief, dependencies, ALPN, backend location map, feature gates | | [overview.md](overview.md) | draft | Crate purpose, the two-carriage model in brief, dependencies, ALPN, backend location map, feature gates |
| [tty-wire.md](tty-wire.md) | draft | The wire format: negotiation frame (JSON carriage), raw chunk codec (`[stream_type: u8][length: u32 be][payload]`), control channel split into `STREAM_CTRL_IN` / `STREAM_CTRL_OUT` halves, sentinels | | [tty-wire.md](tty-wire.md) | draft | The wire format: negotiation frame (JSON carriage), raw chunk codec (`[stream_type: u8][length: u32 be][payload]`), control channel split into `STREAM_CTRL_IN` / `STREAM_CTRL_OUT` halves, sentinels |
| [tty-bast.md](tty-bast.md) | draft | The BAST (Binary Abstract Syntax Tree) document for the `alk/tty` wire format; a normative JSON spec conforming to the BAST meta-schema at `https://alk.dev/bast/v1/schema` | | [tty-bast.md](tty-bast.md) | draft | The BAST (Binary Abstract Syntax Tree) document for the binary framing layer of the `alk/tty` wire format (5-byte chunk header + negotiation length prefix); a normative JSON spec conforming to the BAST meta-schema at `https://alk.dev/bast/v1/schema`. JSON payloads are specified in `tty-wire.md` and the Rust source, not in the BAST |
| [tty-backend.md](tty-backend.md) | draft | `TtyBackend` trait, `TtyParams`, `TtyHandle`, `TtyControl` — the inversion point between the wire-format adapter and the backends. Carries REQ-TTY-01 (backends need not be natively async) | | [tty-backend.md](tty-backend.md) | draft | `TtyBackend` trait, `TtyParams`, `TtyHandle`, `TtyControl` — the inversion point between the wire-format adapter and the backends. Carries REQ-TTY-01 (backends need not be natively async) |
| [tty-adapter.md](tty-adapter.md) | draft | `TtyAdapter` (`ProtocolHandler` on `alk/tty`): session lifecycle, three-pump bidirectional driver, negotiation errors, exit-chunk ordering (ADR-004), access control, session-cancel cleanup (ADR-005) | | [tty-adapter.md](tty-adapter.md) | draft | `TtyAdapter` (`ProtocolHandler` on `alk/tty`): session lifecycle, three-pump bidirectional driver, negotiation errors, exit-chunk ordering (ADR-004), access control, session-cancel cleanup (ADR-005) |
| [tty-local.md](tty-local.md) | draft | `LocalTtyBackend` (the `local` feature module): `portable_pty` (PTY) and `tokio::process::Command` (pipe/runner). Carries REQ-TTY-02 (signal forwarding to the process group) | | [tty-local.md](tty-local.md) | draft | `LocalTtyBackend` (the `local` feature module): `portable_pty` (PTY) and `tokio::process::Command` (pipe/runner). Carries REQ-TTY-02 (signal forwarding to the process group) |
+8 -4
View File
@@ -246,10 +246,14 @@ PTY allocation code. See [ADR-003](decisions/003-local-backend-placement.md).
/ `STREAM_CTRL_OUT` halves, JSON control messages), sentinels, and / `STREAM_CTRL_OUT` halves, JSON control messages), sentinels, and
the fixed-channel-set rationale. the fixed-channel-set rationale.
- **[tty-bast.md](tty-bast.md)** — the BAST (Binary Abstract Syntax - **[tty-bast.md](tty-bast.md)** — the BAST (Binary Abstract Syntax
Tree) document for the `alk/tty` wire format; a normative JSON spec Tree) document for the binary framing layer of the `alk/tty` wire
conforming to the BAST meta-schema at format (the 5-byte chunk header and the negotiation frame's 4-byte
`https://alk.dev/bast/v1/schema`, validatable by any JSON Schema length prefix); a normative JSON spec conforming to the BAST
Draft 2020-12 validator. meta-schema at `https://alk.dev/bast/v1/schema`, validatable by any
JSON Schema Draft 2020-12 validator. The JSON payloads
(`NegotiateRequest`, `ControlMessage`, `TerminalParams`) are
specified in `tty-wire.md` and the Rust source, not in the BAST —
BAST describes binary layouts, not JSON shapes.
- **[tty-backend.md](tty-backend.md)** — the `TtyBackend` trait, - **[tty-backend.md](tty-backend.md)** — the `TtyBackend` trait,
`TtyParams`, `TtyHandle`, `TtyControl`. The inversion point between `TtyParams`, `TtyHandle`, `TtyControl`. The inversion point between
the wire-format adapter and the backends. Carries REQ-TTY-01 the wire-format adapter and the backends. Carries REQ-TTY-01
+97 -232
View File
@@ -6,42 +6,70 @@ last_updated: 2026-08-17
# alktty — BAST Document for the `alk/tty` Wire Format # alktty — BAST Document for the `alk/tty` Wire Format
This document is the **BAST (Binary Abstract Syntax Tree)** specification This document is the **BAST (Binary Abstract Syntax Tree)** specification
for the `alk/tty` wire format. It is a normative JSON document conforming for the binary portions of the `alk/tty` wire format. It is a normative
to the BAST meta-schema at `https://alk.dev/bast/v1/schema` (a standard JSON document conforming to the BAST meta-schema at
JSON Schema Draft 2020-12 document); any JSON Schema Draft 2020-12 `https://alk.dev/bast/v1/schema` (a standard JSON Schema Draft 2020-12
validator can check whether the BAST below is well-formed. 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` / **alktty does not depend on alktype.** The hand-rolled `ChunkReader` /
`ChunkWriter` in `src/wire.rs` is the runtime codec; this BAST document `ChunkWriter` in `src/wire.rs` is the runtime codec; this BAST document
is the human-readable contract that describes what those types is the human-readable contract that describes what those types
round-trip. If runtime validation against the BAST becomes desirable round-trip. If runtime validation against the BAST becomes desirable
later (e.g., to reject malformed chunks at the framing boundary with a later, alktype becomes an optional dep and this document is already
generated validator), alktype becomes an optional dep and this document there to feed it. See [ADR-001](decisions/001-wire-format-and-two-carriage.md)
is already there to feed it. See [ADR-001](decisions/001-wire-format-and-two-carriage.md)
and [ADR-006](decisions/006-negotiation-framing-self-contained.md). and [ADR-006](decisions/006-negotiation-framing-self-contained.md).
## Scope ## Scope
The BAST below describes the two wire formats that live in this crate The `alk/tty` wire format mixes a small binary framing layer with
(both one-way doors per [ADR-001](decisions/001-wire-format-and-two-carriage.md)): 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`](https://alk.dev/bast/v1/spec)). This document
therefore specifies only the binary framing; the JSON shapes are
specified in prose in [tty-wire.md](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](decisions/001-wire-format-and-two-carriage.md)):
1. **The 5-byte chunk header** (`[stream_type: u8][length: u32 BE] 1. **The 5-byte chunk header** (`[stream_type: u8][length: u32 BE]
[payload]`) — the raw chunk codec in `src/wire.rs`. Five stream types: [payload]`) — the raw chunk codec in `src/wire.rs`. Five stream
`STREAM_STDIN=0`, `STREAM_STDOUT=1`, `STREAM_STDERR=2`, types: `STREAM_STDIN=0`, `STREAM_STDOUT=1`, `STREAM_STDERR=2`,
`STREAM_CTRL_IN=3`, `STREAM_CTRL_OUT=4`. `STREAM_CTRL_IN=3`, `STREAM_CTRL_OUT=4`.
2. **The negotiation frame** (4-byte BE length prefix + UTF-8 JSON 2. **The negotiation frame length prefix** (4-byte BE `u32` followed
`NegotiateRequest` body) — self-contained per by `length` bytes of UTF-8 JSON `NegotiateRequest` body) —
[ADR-006](decisions/006-negotiation-framing-self-contained.md), not self-contained per
reused from alkcall's `EventEnvelope` framing. The `NegotiateRequest` [ADR-006](decisions/006-negotiation-framing-self-contained.md).
JSON shape is wire-stable once consumers exist.
The control-message `union` (field-name discriminator on `type`: What this BAST deliberately does **not** cover:
`resize`, `signal`, `eof`, `exit`) describes the JSON shape of the
control channel payloads; the on-wire encoding of a control chunk is a - **`NegotiateRequest` JSON shape** — a UTF-8 JSON object, not a
5-byte chunk header with `stream_type ∈ {3, 4}` and a UTF-8 JSON payload binary layout. Specified in [tty-wire.md](tty-wire.md) §"Phase 1"
(the `ControlMessage` serialized via `serde_json`). See "Annotations" and implemented by `NegotiateRequest` in `src/negotiation.rs`.
below for where the JSON carriage does not map 1:1 to BAST's - **`ControlMessage` JSON shape** — the UTF-8 JSON payloads of
binary-native vocabulary. `STREAM_CTRL_IN`/`STREAM_CTRL_OUT` chunks (the `resize`/`signal`/
`eof`/`exit` variants). Specified in [tty-wire.md](tty-wire.md)
§"Control Channel" and implemented by `ControlMessage` in
`src/control.rs`.
- **`TerminalParams` JSON shape** — the `tty` field of
`NegotiateRequest`. Specified in [tty-wire.md](tty-wire.md) §"Phase
1" and implemented by `TerminalParamsWire` in
`src/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 ## The BAST document
@@ -54,7 +82,7 @@ binary-native vocabulary.
"endian": "big", "endian": "big",
"fields": [ "fields": [
{ "name": "stream_type", "kind": "uint8" }, { "name": "stream_type", "kind": "uint8" },
{ "name": "length", "kind": "uint32" } { "name": "length", "kind": "uint32" }
] ]
}, },
@@ -67,90 +95,17 @@ binary-native vocabulary.
"kind": "struct", "kind": "struct",
"endian": "big", "endian": "big",
"fields": [ "fields": [
{ "name": "header", "kind": { "$ref": "#/$defs/ChunkHeader" } }, { "name": "header", "kind": { "$ref": "#/$defs/ChunkHeader" } },
{ "name": "payload", "kind": "bytes", "encoding": "length-prefixed", "maxLength": 16777216 } { "name": "payload", "kind": "bytes", "encoding": "length-prefixed", "maxLength": 16777216 }
] ]
}, },
"ControlMessage": {
"kind": "union",
"discriminator": { "kind": "field", "name": "type" },
"fields": [
{ "name": "type", "kind": "string" }
],
"mapping": {
"resize": { "$ref": "#/$defs/ResizeMessage" },
"signal": { "$ref": "#/$defs/SignalMessage" },
"eof": { "$ref": "#/$defs/EofMessage" },
"exit": { "$ref": "#/$defs/ExitMessage" }
}
},
"ResizeMessage": {
"kind": "struct",
"fields": [
{ "name": "type", "kind": "string" },
{ "name": "cols", "kind": "uint16" },
{ "name": "rows", "kind": "uint16" },
{ "name": "pixel_width", "kind": "uint16" },
{ "name": "pixel_height", "kind": "uint16" }
]
},
"SignalMessage": {
"kind": "struct",
"fields": [
{ "name": "type", "kind": "string" },
{ "name": "name", "kind": "string", "maxLength": 16 }
]
},
"EofMessage": {
"kind": "struct",
"fields": [
{ "name": "type", "kind": "string" }
]
},
"ExitMessage": {
"kind": "struct",
"fields": [
{ "name": "type", "kind": "string" },
{ "name": "code", "kind": "int32" }
]
},
"NegotiationFrame": { "NegotiationFrame": {
"kind": "struct", "kind": "struct",
"endian": "big", "endian": "big",
"fields": [ "fields": [
{ "name": "length", "kind": "uint32" }, { "name": "length", "kind": "uint32" },
{ "name": "body", "kind": "string", "encoding": "length-prefixed", "maxLength": 16777216 } { "name": "body", "kind": "bytes", "encoding": "length-prefixed", "maxLength": 16777216 }
]
},
"NegotiateRequest": {
"kind": "struct",
"fields": [
{ "name": "carriage", "kind": "string", "maxLength": 16 },
{ "name": "backend", "kind": "string", "maxLength": 64 },
{ "name": "tty", "kind": { "$ref": "#/$defs/TerminalParams" } },
{ "name": "cmd", "kind": { "kind": "array", "element": "string", "count": 0 } },
{ "name": "cwd", "kind": "string" },
{ "name": "env", "kind": { "kind": "record", "values": "string" } },
{ "name": "backend_params", "kind": { "kind": "record", "values": {} } }
]
},
"TerminalParams": {
"kind": "struct",
"fields": [
{ "name": "term", "kind": "string" },
{ "name": "cols", "kind": "uint16" },
{ "name": "rows", "kind": "uint16" },
{ "name": "pixel_width", "kind": "uint16" },
{ "name": "pixel_height", "kind": "uint16" },
{ "name": "modes", "kind": {} }
] ]
} }
} }
@@ -159,20 +114,11 @@ binary-native vocabulary.
## Annotations ## Annotations
The BAST above is well-formed (it conforms to the BAST meta-schema).
Some definitions map 1:1 to the on-wire bytes; others describe the
logical shape of a JSON carriage whose on-wire encoding is UTF-8 text,
not the BAST-native binary encoding. The annotations below record
which is which and note the two deliberate deviations from BAST's
binary-native vocabulary. A generated validator should consult these
annotations alongside the BAST; the runtime codec (`src/wire.rs`) is
the source of truth for the bytes.
### `ChunkHeader` — binary, exact ### `ChunkHeader` — binary, exact
Maps 1:1 to the on-wire bytes. 5 bytes: `stream_type` as `uint8` (1 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 byte), `length` as `uint32` big-endian (4 bytes). This is the 5-byte
5-byte chunk header committed by [ADR-001](decisions/001-wire-format-and-two-carriage.md). chunk header committed by [ADR-001](decisions/001-wire-format-and-two-carriage.md).
The Rust types are `STREAM_STDIN`/`STREAM_STDOUT`/`STREAM_STDERR`/ The Rust types are `STREAM_STDIN`/`STREAM_STDOUT`/`STREAM_STDERR`/
`STREAM_CTRL_IN`/`STREAM_CTRL_OUT` and `CHUNK_HEADER_LEN = 5` in `STREAM_CTRL_IN`/`STREAM_CTRL_OUT` and `CHUNK_HEADER_LEN = 5` in
`src/wire.rs`. `src/wire.rs`.
@@ -181,10 +127,10 @@ The Rust types are `STREAM_STDIN`/`STREAM_STDOUT`/`STREAM_STDERR`/
The BAST meta-schema specifies that an `enum`'s binary representation The BAST meta-schema specifies that an `enum`'s binary representation
is a `u32` index into `values` (0-based). The `alk/tty` wire format 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) — encodes `stream_type` as a **`uint8`** (1 byte), not a `u32` (4
the chunk header is 5 bytes, not 8. This is a deliberate deviation: bytes) — the chunk header is 5 bytes, not 8. This is a deliberate
the chunk header needs a 1-byte discriminator, and 4 bytes of padding deviation: the chunk header needs a 1-byte discriminator, and 4 bytes
would double the per-chunk overhead. of padding would double the per-chunk overhead.
The `StreamType` enum is therefore **normative for the name→value The `StreamType` enum is therefore **normative for the name→value
mapping** (the index of a name in `values` is the integer that appears mapping** (the index of a name in `values` is the integer that appears
@@ -210,73 +156,39 @@ change requiring a new ALPN — [ADR-001](decisions/001-wire-format-and-two-carr
A chunk is the 5-byte `ChunkHeader` followed by `length` bytes of A chunk is the 5-byte `ChunkHeader` followed by `length` bytes of
payload. The `payload` field is `bytes` with `encoding: payload. The `payload` field is `bytes` with `encoding:
"length-prefixed"` and `maxLength: 16777216` (16 MiB = `MAX_CHUNK_LEN` "length-prefixed"` and `maxLength: 16777216` (16 MiB = `MAX_CHUNK_LEN`
in `src/wire.rs`). Zero-length payloads are sentinels on the data in `src/wire.rs`). The `payload`'s interpretation depends on
channels (zero-length stdin = EOF from client; zero-length stdout = `stream_type`:
"drained" from server); control chunks are never zero-length (the JSON
payload is at least `{}`). See [tty-wire.md](tty-wire.md) §"Sentinels".
### `ControlMessage` — union, documented deviation: on-wire encoding is UTF-8 JSON, not BAST's binary union encoding - `Stdin`/`Stdout`/`Stderr` (0/1/2): raw bytes. Zero-length payloads on
these channels are sentinels (zero-length stdin = EOF from client;
The BAST `union` with a field-name discriminator describes a binary zero-length stdout = "drained" from server); see
layout whose discriminator is a length-prefixed string field and whose [tty-wire.md](tty-wire.md) §"Sentinels".
variant is a binary struct. The `alk/tty` control channel does **not** - `CtrlIn`/`CtrlOut` (3/4): UTF-8 JSON text (a serialized
use that binary encoding: a control chunk's payload is **UTF-8 JSON `ControlMessage`). Control chunks are never zero-length (the JSON
text** (`serde_json`-serialized), and the `type` tag is a JSON string payload is at least `{}`). The JSON shape is **not** specified by
field, not a BAST length-prefixed string. this BAST — see [tty-wire.md](tty-wire.md) §"Control Channel" and
`src/control.rs`.
The `ControlMessage` union here is **normative for the JSON variant
shapes** (the `mapping` keys are the JSON `type` values; the variant
structs are the JSON field sets a peer must accept/produce), but a
generated validator must NOT emit a binary union reader for control
chunks. The on-wire encoding is: read the `ChunkHeader`, read
`length` bytes as UTF-8, parse the result as JSON, dispatch on the
`type` field. The Rust type is `ControlMessage` in `src/control.rs`
(`#[serde(tag = "type", rename_all = "snake_case")]`).
The `type`-tagged enum is the extension seam per
[ADR-001](decisions/001-wire-format-and-two-carriage.md): unknown
`type` values are **ignored** (not a protocol error) so a newer client
sending a control message an older server doesn't recognize degrades
gracefully. Adding a control message type is additive (two-way-door
within the one-way wire format); changing the meaning of an existing
type is not.
### `ResizeMessage`, `SignalMessage`, `EofMessage`, `ExitMessage` — JSON shapes
These are the four control message variants. Their field types
(`uint16`, `int32`, `string`) describe the **JSON value types** a peer
must accept/produce, not binary layouts — the on-wire encoding is
UTF-8 JSON text inside a control chunk's payload (see
`ControlMessage` above). The `maxLength` on `SignalMessage.name` is a
validation constraint (signal names are short uppercase strings:
`HUP`, `INT`, `QUIT`, `TERM`, `KILL`, `USR1`, `USR2`, `TSTP`, `CONT`);
it bounds the accepted JSON string length, not a binary reservation.
| variant | direction | stream_type | JSON shape |
|---------|----------------|-----------------|----------------------------------------------------------------------------------|
| resize | client→server | `CtrlIn` (3) | `{"type":"resize","cols":80,"rows":24,"pixel_width":0,"pixel_height":0}` |
| signal | client→server | `CtrlIn` (3) | `{"type":"signal","name":"INT"}` |
| eof | client→server | `CtrlIn` (3) | `{"type":"eof"}` |
| exit | server→client | `CtrlOut` (4) | `{"type":"exit","code":0}` |
The `exit` chunk is the last control chunk before stream close
([ADR-004](decisions/004-exit-code-on-control-chunk.md)); `code` is
`int32` (matches `std::process::ExitStatus::code()`; negative values
are signal-terminated, e.g., `-9` for SIGKILL on Unix; `-1` is the
"backend couldn't determine the exit code" sentinel).
### `NegotiationFrame` — binary, exact (out-of-band for the chunk codec) ### `NegotiationFrame` — binary, exact (out-of-band for the chunk codec)
The negotiation frame is a 4-byte big-endian length prefix + UTF-8 JSON The negotiation frame is a 4-byte big-endian length prefix + a UTF-8
body. This maps 1:1 to the on-wire bytes: `length` as `uint32` JSON body. This maps 1:1 to the on-wire bytes: `length` as `uint32`
big-endian (4 bytes), `body` as a length-prefixed UTF-8 string big-endian (4 bytes), `body` as a length-prefixed byte string
(`length` bytes). The `maxLength: 16777216` (16 MiB) constraint is the (`length` bytes of UTF-8 JSON). The `maxLength: 16777216` (16 MiB)
same as `MAX_CHUNK_LEN` — error frames MUST be under 16 MiB so the constraint is the same as `MAX_CHUNK_LEN` — error frames MUST be under
4-byte length prefix's high byte is `0x00`, which is what makes the 16 MiB so the 4-byte length prefix's high byte is `0x00`, which is
framing-disambiguation trick sound (first byte `0x00` = error/negotiation what makes the framing-disambiguation trick sound (first byte `0x00`
frame; first byte `1`/`2`/`4` = raw chunk; see = error/negotiation frame; first byte `1`/`2`/`4` = raw chunk; see
[tty-wire.md](tty-wire.md) §"Constraints"). [tty-wire.md](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](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 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 read once at session start (Phase 1, JSON carriage), then the stream
switches to raw chunks (Phase 2). The `ChunkReader`/`ChunkWriter` in switches to raw chunks (Phase 2). The `ChunkReader`/`ChunkWriter` in
@@ -284,52 +196,6 @@ switches to raw chunks (Phase 2). The `ChunkReader`/`ChunkWriter` in
`NegotiationReader`/`NegotiationWriter` in `src/negotiation.rs` does. `NegotiationReader`/`NegotiationWriter` in `src/negotiation.rs` does.
See [ADR-006](decisions/006-negotiation-framing-self-contained.md). See [ADR-006](decisions/006-negotiation-framing-self-contained.md).
### `NegotiateRequest` — JSON shape
The `NegotiateRequest` struct describes the **JSON shape** of the
`NegotiationFrame.body`, not a binary layout. The on-wire encoding is
UTF-8 JSON text inside the negotiation frame's `body` field (see
`NegotiationFrame` above). The Rust type is `NegotiateRequest` in
`src/negotiation.rs` (`#[derive(Serialize, Deserialize)]` with
`#[serde(flatten)]` on `backend_params`).
Field notes:
- `carriage` — `"raw"` in v1 (the only carriage); any other value →
`malformed_negotiation`. `maxLength: 16` bounds the accepted JSON
string length.
- `backend` — the backend selector key (`"local"`, `"docker"`,
`"ssh"`); `maxLength: 64`.
- `tty` — `null` for pipe/runner mode (no PTY —
[ADR-003](decisions/003-local-backend-placement.md)); `Some` for PTY
mode. The BAST uses a `$ref` to `TerminalParams` for the non-null
case; the on-wire JSON `null` is the pipe-mode sentinel.
- `cmd` — command vector (argv[0] + args); non-empty (checked by the
adapter). The BAST uses `array` with `count: 0` as a placeholder —
**BAST v1 requires `count` for arrays** (D-BAST-004), and a
variable-length command vector does not have a schema-known count.
This is a third documented deviation: the `cmd` field is a
JSON array of strings of arbitrary length, not a fixed-count BAST
array. A generated validator should treat `cmd` as a JSON array
(variable length, non-empty), not a BAST fixed-count array.
- `cwd` — working directory (`null` = inherit/default).
- `env` — environment variables (empty = inherit); a `record` from
string to string.
- `backend_params` — backend-specific selector fields, opaque to
alktty. The BAST uses `record` with an empty value type (`{}`) as a
placeholder for "arbitrary JSON value"; the adapter passes this map
through verbatim and each backend deserializes its own
strongly-typed params struct from it. See
[ADR-002](decisions/002-ttybackend-trait-and-ttyhandle.md) §"Backend
params are opaque."
### `TerminalParams` — JSON shape
The terminal parameters carried in `NegotiateRequest.tty` when non-null.
JSON shape, not binary layout. `modes` is reserved (OQ-44 — default
terminal modes suffice for the current scope); backends MUST ignore
its content in v1. The empty value type (`{}`) is a placeholder for
"arbitrary JSON value."
## Validation ## Validation
The BAST document above is validatable in two ways: The BAST document above is validatable in two ways:
@@ -337,10 +203,10 @@ The BAST document above is validatable in two ways:
1. **Structural validation** — run any JSON Schema Draft 2020-12 1. **Structural validation** — run any JSON Schema Draft 2020-12
validator against the BAST meta-schema at validator against the BAST meta-schema at
`https://alk.dev/bast/v1/schema`. This checks whether the BAST is `https://alk.dev/bast/v1/schema`. This checks whether the BAST is
well-formed (correct `kind` strings, required fields present, `$ref` well-formed (correct `kind` strings, required fields present,
targets exist). It does NOT check whether a binary payload conforms `$ref` targets exist). It does NOT check whether a binary payload
to the layout — that is the BAST-native validator's job (see the conforms to the layout — that is the BAST-native validator's job
alktype BAST format spec, (see the alktype BAST format spec,
`/workspace/@alkdev/alktype/docs/architecture/bast-format.md`). `/workspace/@alkdev/alktype/docs/architecture/bast-format.md`).
2. **Drift detection** — a cheap test that parses the BAST document 2. **Drift detection** — a cheap test that parses the BAST document
with `serde_json` and asserts the `StreamType` enum values match with `serde_json` and asserts the `StreamType` enum values match
@@ -358,8 +224,7 @@ The BAST document above is validatable in two ways:
|----------|-----|---------| |----------|-----|---------|
| Wire format and two-carriage model | [ADR-001](decisions/001-wire-format-and-two-carriage.md) | The 5-byte chunk header + negotiation frame this BAST describes | | Wire format and two-carriage model | [ADR-001](decisions/001-wire-format-and-two-carriage.md) | The 5-byte chunk header + negotiation frame this BAST describes |
| Self-contained negotiation framing | [ADR-006](decisions/006-negotiation-framing-self-contained.md) | The `NegotiationFrame` is self-contained in alktty (not reused from alkcall's `EventEnvelope` framing) | | Self-contained negotiation framing | [ADR-006](decisions/006-negotiation-framing-self-contained.md) | The `NegotiationFrame` is self-contained in alktty (not reused from alkcall's `EventEnvelope` framing) |
| Backend params are opaque | [ADR-002](decisions/002-ttybackend-trait-and-ttyhandle.md) | `NegotiateRequest.backend_params` is an opaque JSON object; the adapter does not interpret it | | Exit code on a control chunk | [ADR-004](decisions/004-exit-code-on-control-chunk.md) | The `exit` JSON variant carried on `CtrlOut` (specified in `tty-wire.md`, not this BAST) |
| Exit code on a control chunk | [ADR-004](decisions/004-exit-code-on-control-chunk.md) | The `ExitMessage` variant and the "exit chunk is last" invariant |
## References ## References
@@ -369,13 +234,13 @@ The BAST document above is validatable in two ways:
negotiation framing is self-contained (the `NegotiationFrame` is not negotiation framing is self-contained (the `NegotiationFrame` is not
reused from alkcall) reused from alkcall)
- [tty-wire.md](tty-wire.md) — the prose wire format spec this BAST - [tty-wire.md](tty-wire.md) — the prose wire format spec this BAST
formalizes formalizes (covers both the binary framing and the JSON payloads)
- `src/wire.rs` — the runtime chunk codec (`ChunkReader`/`ChunkWriter`, - `src/wire.rs` — the runtime chunk codec (`ChunkReader`/`ChunkWriter`,
`STREAM_*` constants, `MAX_CHUNK_LEN`) this BAST describes `STREAM_*` constants, `MAX_CHUNK_LEN`) this BAST describes
- `src/control.rs` — the `ControlMessage` tagged enum this BAST - `src/negotiation.rs` — the `NegotiationReader`/`NegotiationWriter`
describes for the `NegotiationFrame`, and `NegotiateRequest` (JSON shape, out
- `src/negotiation.rs` — the `NegotiateRequest` / `NegotiationReader` / of BAST scope)
`NegotiationWriter` this BAST describes - `src/control.rs` — `ControlMessage` (JSON shape, out of BAST scope)
- [alktype BAST format spec](https://alk.dev/bast/v1/schema) — the - [alktype BAST format spec](https://alk.dev/bast/v1/schema) — the
normative format spec for BAST documents (the meta-schema this normative format spec for BAST documents (the meta-schema this
document conforms to); see also document conforms to); see also
+7 -2
View File
@@ -388,7 +388,12 @@ a session — see [tty-adapter.md](tty-adapter.md).
- `src/control.rs` — the JSON control schema (`ControlMessage` tagged - `src/control.rs` — the JSON control schema (`ControlMessage` tagged
enum) this spec documents enum) this spec documents
- [tty-bast.md](tty-bast.md) — the BAST (Binary Abstract Syntax Tree) - [tty-bast.md](tty-bast.md) — the BAST (Binary Abstract Syntax Tree)
document for this wire format; a normative JSON spec downstream document for the binary framing layer of this wire format (the
consumers can validate against 5-byte chunk header and the negotiation frame's 4-byte length
prefix); a normative JSON spec downstream consumers can validate
against. The JSON payloads (`NegotiateRequest`, `ControlMessage`,
`TerminalParams`) are specified in this document and the Rust
source, not in the BAST — BAST describes binary layouts, not JSON
shapes
- [tty-adapter.md](tty-adapter.md) — the session lifecycle that consumes - [tty-adapter.md](tty-adapter.md) — the session lifecycle that consumes
this wire format this wire format