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:
@@ -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) |
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user