refactor(tty): split control channel into STREAM_CTRL_IN/STREAM_CTRL_OUT halves (Phase 7)
The single STREAM_CONTROL = 3 was documented as bidirectional but the
adapter had to ignore Exit from the client because the two directions
were indistinguishable on the same stream_type — half-duplex in
disguise. Phase 7 splits it into two halves so the bidirectionality is
literal on the wire.
Changes:
- wire.rs: STREAM_CTRL_IN = 3 (client→server), STREAM_CTRL_OUT = 4
(server→client); InvalidStreamType bound > 3 → > 4; Chunk::control
→ Chunk::ctrl_in/ctrl_out; ChunkWriter::write_control_json →
write_ctrl_in_json/write_ctrl_out_json; tests split accordingly.
- control.rs: ControlMessage doc updated with the stream_type column;
JSON shape unchanged.
- adapter.rs: pump_client_to_backend dispatches on STREAM_CTRL_IN
(Resize/Signal/Eof; Exit on ctrl_in is a protocol violation,
ignored); send_exit_chunk emits on STREAM_CTRL_OUT; STREAM_CTRL_OUT
from the client is a protocol violation, ignored. 3 new tests for
the direction enforcement; existing tests updated to the new
stream_types.
- negotiation.rs: framing-disambiguation doc updated (server-sent
stream_type set is {1, 2, 4}).
- alknet-tty-local/tests: common/mod.rs, pty.rs, pipe.rs updated to
the new constants.
Specs:
- ADR-052 amended (§4a 'Control channel split (Phase 7 amendment)').
- tty-wire.md + tty-adapter.md updated (last_updated 2026-07-18).
Verification:
- cargo test -p alknet-tty: 65 passed (was 61; +4 new tests).
- cargo test -p alknet-tty-local: 19 passed.
- cargo test --workspace --all-features: 1017 passed, 0 failed.
- cargo clippy --workspace --all-features: clean.
- cargo fmt --all: clean.
This commit is contained in:
1 parent
859ad35896
commit
c2b7055a64
11 files changed
+571
-142
No files matched your search
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-07-07
|
||||
last_updated: 2026-07-18
|
||||
---
|
||||
|
||||
# alknet-tty — TtyAdapter and Session Lifecycle
|
||||
@@ -100,14 +100,18 @@ A `alknet/tty` session on one bidi stream proceeds in three phases:
|
||||
`Some`, a concurrent stderr pump emits stderr chunks (stream_type 2).
|
||||
On backend stdout EOF, emit a zero-length stdout sentinel.
|
||||
- **B. client → backend**: client chunks → backend. stdin chunks
|
||||
(stream_type 0) → `TtyHandle.stdin` (via `AsyncWrite`). Control
|
||||
chunks (stream_type 3) → `ControlMessage` dispatch: `Resize` →
|
||||
`TtyControl::resize`, `Signal` → `TtyControl::signal`, `Eof` →
|
||||
close stdin. `Exit` from the client is ignored (server→client only).
|
||||
(stream_type 0) → `TtyHandle.stdin` (via `AsyncWrite`).
|
||||
Client→server control chunks (`STREAM_CTRL_IN`, stream_type 3) →
|
||||
`ControlMessage` dispatch: `Resize` → `TtyControl::resize`, `Signal`
|
||||
→ `TtyControl::signal`, `Eof` → close stdin. `Exit` on
|
||||
`STREAM_CTRL_IN` is a protocol violation (it's server→client only)
|
||||
and is ignored. `STREAM_CTRL_OUT` (stream_type 4) from the client is
|
||||
a protocol violation (it's the server→client half) and is ignored.
|
||||
On client read-half close or a zero-length stdin chunk, signal EOF
|
||||
to the backend's stdin.
|
||||
- **C. exit → exit chunk**: await `TtyHandle.exit_code`; on resolve,
|
||||
enqueue `{"type":"exit","code":N}` as a control chunk (stream_type 3).
|
||||
enqueue `{"type":"exit","code":N}` as a server→client control
|
||||
chunk (`STREAM_CTRL_OUT`, stream_type 4).
|
||||
|
||||
A drainer task writes chunks to the client in arrival order. After the
|
||||
exit chunk is written (task C resolves and the exit chunk drains),
|
||||
@@ -118,6 +122,36 @@ hardcoded the local PTY backend; the adapter dispatches to any
|
||||
`TtyBackend`. See `/workspace/alknet-tty-poc/src/session.rs` for the
|
||||
reference implementation of the three-pump driver.
|
||||
|
||||
### Bidirectional Control Channel (Phase 7)
|
||||
|
||||
The control channel is split into two halves so it is genuinely
|
||||
bidirectional on the wire:
|
||||
|
||||
- **`STREAM_CTRL_IN = 3`** — client→server control (`Resize`, `Signal`,
|
||||
`Eof`).
|
||||
- **`STREAM_CTRL_OUT = 4`** — server→client control (`Exit`).
|
||||
|
||||
The adapter enforces the direction:
|
||||
|
||||
- An `Exit` arriving on `STREAM_CTRL_IN` is a protocol violation
|
||||
(server→client message on the client→server half) — the adapter
|
||||
ignores it (the previous single `STREAM_CONTROL = 3` could not
|
||||
distinguish the two directions, so `Exit` from the client was always
|
||||
ignored; the split makes the rejection explicit).
|
||||
- A `Resize`/`Signal`/`Eof` arriving on `STREAM_CTRL_OUT` is a protocol
|
||||
violation (client→server message on the server→client half) — the
|
||||
adapter ignores it (the server never dispatches control messages it
|
||||
receives on the server→client half).
|
||||
- `STREAM_CTRL_OUT` (stream_type 4) chunks written by the client are a
|
||||
protocol violation (the client should not write on the server→client
|
||||
half) — the adapter ignores them.
|
||||
|
||||
The exit chunk (`Exit`) is emitted on `STREAM_CTRL_OUT` (stream_type
|
||||
4), not on the previous `STREAM_CONTROL = 3`. A client distinguishing
|
||||
the two halves can route exit vs. control without parsing the JSON
|
||||
`type` tag first. See `docs/research/alknet-crate-extraction/findings.md`
|
||||
Phase 7 and `tty-wire.md` §"Control Channel".
|
||||
|
||||
### Negotiation Errors
|
||||
|
||||
If the server cannot allocate the session, it sends a JSON error response
|
||||
@@ -146,17 +180,18 @@ The disambiguation is by the first byte: a JSON error frame's 4-byte
|
||||
big-endian length prefix always starts with `0x00` (error frames MUST
|
||||
be under 16 MiB — `MAX_CHUNK_LEN` — so the high byte is zero; this is
|
||||
a wire-format invariant, not an assumption), while a raw chunk's first
|
||||
byte is a `stream_type` in `{0, 1, 2, 3}`. A stream_type of `0` (stdin
|
||||
from server) is invalid — the server never sends stdin chunks — so the
|
||||
client distinguishes: read the first byte; if it is `0x00`, interpret
|
||||
the next 4 bytes as a big-endian length prefix and read that many bytes
|
||||
as a JSON error frame; otherwise interpret it as a `stream_type` byte
|
||||
and continue reading the raw chunk header. This is a one-way-door
|
||||
wire-format invariant (ADR-052): error frames use the negotiation
|
||||
framing (length prefix) and MUST be under 16 MiB; success uses the raw
|
||||
chunk framing (stream_type byte first); the `0x00`-as-length-prefix vs
|
||||
`0x00`-as-invalid-stream_type disambiguation is what makes the two
|
||||
distinguishable on the wire.
|
||||
byte is a `stream_type`. The server never sends `0` (stdin —
|
||||
client→server only) or `3` (`STREAM_CTRL_IN` — client→server only), so
|
||||
the server-sent set is `{1, 2, 4}` (stdout, stderr, `STREAM_CTRL_OUT`);
|
||||
`0x00` is unambiguous. The client distinguishes: read the first byte; if
|
||||
it is `0x00`, interpret the next 4 bytes as a big-endian length prefix
|
||||
and read that many bytes as a JSON error frame; otherwise interpret it
|
||||
as a `stream_type` byte and continue reading the raw chunk header. This
|
||||
is a one-way-door wire-format invariant (ADR-052): error frames use the
|
||||
negotiation framing (length prefix) and MUST be under 16 MiB; success
|
||||
uses the raw chunk framing (stream_type byte first); the
|
||||
`0x00`-as-length-prefix vs `0x00`-as-invalid-stream_type disambiguation
|
||||
is what makes the two distinguishable on the wire.
|
||||
|
||||
### Exit-Chunk Ordering (ADR-055)
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-07-07
|
||||
last_updated: 2026-07-18
|
||||
---
|
||||
|
||||
# alknet-tty — Wire Format
|
||||
|
||||
The wire protocol for `alknet/tty`: the negotiation frame (JSON
|
||||
carriage), the raw chunk codec, the control channel, and the sentinels.
|
||||
The two-carriage model is decided in
|
||||
carriage), the raw chunk codec, the control channel (split into
|
||||
`STREAM_CTRL_IN` / `STREAM_CTRL_OUT` halves — Phase 7), and the
|
||||
sentinels. The two-carriage model is decided in
|
||||
[ADR-052](../../decisions/052-alknet-tty-wire-format-and-two-carriage.md);
|
||||
this document specifies what an implementer builds.
|
||||
|
||||
@@ -164,19 +165,34 @@ no `call.responded`/`call.completed` — this is not the call protocol.
|
||||
|
||||
- **`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 | 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 | ctrl-in | client→server | JSON control message (`Resize`, `Signal`, `Eof`) |
|
||||
| 4 | ctrl-out | server→client | JSON control message (`Exit`) |
|
||||
|
||||
`stream_type > 3` is a protocol error (`InvalidStreamType`). There is
|
||||
no extension escape hatch in the byte — a 5th channel is a wire-format
|
||||
`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 (`alknet/tty/v2` per ADR-006), not a
|
||||
negotiated addition to this format. See ADR-052 §"Fixed channel set,
|
||||
not extensible."
|
||||
|
||||
**Bidirectional control channel (Phase 7).** The control channel is
|
||||
split into two halves so it is genuinely bidirectional on the wire:
|
||||
`STREAM_CTRL_IN = 3` carries client→server control (`Resize`,
|
||||
`Signal`, `Eof`); `STREAM_CTRL_OUT = 4` carries server→client control
|
||||
(`Exit`). The previous single `STREAM_CONTROL = 3` was documented as
|
||||
"bidirectional" but the adapter ignored `Exit` from the client
|
||||
because the two directions were indistinguishable on the same
|
||||
stream_type — see `docs/research/alknet-crate-extraction/findings.md`
|
||||
Phase 7. The split makes the bidirectionality explicit: each
|
||||
direction has its own stream_type, and the adapter enforces the
|
||||
direction (an `Exit` arriving on `STREAM_CTRL_IN` is a protocol
|
||||
violation and is ignored; a `Resize` arriving on `STREAM_CTRL_OUT` is
|
||||
likewise a protocol violation and is ignored).
|
||||
|
||||
- **`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`).
|
||||
@@ -208,9 +224,14 @@ Zero-length data chunks are sentinels:
|
||||
Control chunks are never zero-length (the JSON payload is at least
|
||||
`{}`).
|
||||
|
||||
### Control Channel (stream_type 3)
|
||||
### Control Channel
|
||||
|
||||
Control chunks carry a JSON payload tagged by `type`. The schema is the
|
||||
The control channel is split into two halves (Phase 7):
|
||||
|
||||
- **`STREAM_CTRL_IN` (stream_type 3)** — client→server control.
|
||||
- **`STREAM_CTRL_OUT` (stream_type 4)** — server→client control.
|
||||
|
||||
Each half carries JSON payloads tagged by `type`. The schema is the
|
||||
POC's `ControlMessage` (`/workspace/alknet-tty-poc/src/control.rs`):
|
||||
|
||||
```rust
|
||||
@@ -231,12 +252,22 @@ pub enum ControlMessage {
|
||||
}
|
||||
```
|
||||
|
||||
| Direction | Message | Shape | Maps to |
|
||||
|---|---|---|---|
|
||||
| client→server | resize | `{"type":"resize","cols":80,"rows":24,"pixel_width":0,"pixel_height":0}` | SSH `window-change`, docker exec resize, `ioctl(TIOCSWINSZ)` |
|
||||
| client→server | signal | `{"type":"signal","name":"INT"}` | SSH `signal`, docker exec signal, `kill(-pgid, sig)` (REQ-TTY-02) |
|
||||
| client→server | eof | `{"type":"eof"}` | SSH channel EOF, docker stdin close, `ChildStdin::drop` |
|
||||
| server→client | exit | `{"type":"exit","code":0}` | the terminal/completion signal (ADR-055) |
|
||||
| stream_type | direction | Message | Shape | Maps to |
|
||||
|-------------|----------------|---------|-------|---------|
|
||||
| 3 (ctrl_in) | client→server | resize | `{"type":"resize","cols":80,"rows":24,"pixel_width":0,"pixel_height":0}` | SSH `window-change`, docker exec resize, `ioctl(TIOCSWINSZ)` |
|
||||
| 3 (ctrl_in) | client→server | signal | `{"type":"signal","name":"INT"}` | SSH `signal`, docker exec signal, `kill(-pgid, sig)` (REQ-TTY-02) |
|
||||
| 3 (ctrl_in) | client→server | eof | `{"type":"eof"}` | SSH channel EOF, docker stdin close, `ChildStdin::drop` |
|
||||
| 4 (ctrl_out) | server→client | exit | `{"type":"exit","code":0}` | the terminal/completion signal (ADR-055) |
|
||||
|
||||
The adapter enforces the direction: an `Exit` arriving on
|
||||
`STREAM_CTRL_IN` is a protocol violation (the adapter ignores it); a
|
||||
`Resize`/`Signal`/`Eof` arriving on `STREAM_CTRL_OUT` is likewise a
|
||||
protocol violation (the adapter ignores it). The split makes the
|
||||
control channel genuinely bidirectional on the wire — the previous
|
||||
single `STREAM_CONTROL = 3` was documented as "bidirectional" but the
|
||||
adapter had to ignore `Exit` from the client because the two directions
|
||||
were indistinguishable on the same stream_type. See
|
||||
`docs/research/alknet-crate-extraction/findings.md` Phase 7.
|
||||
|
||||
**Signal names.** `name` is an uppercase string. The supported set (per
|
||||
the POC's `signal_from_name`): `HUP`, `INT`, `QUIT`, `TERM`, `KILL`,
|
||||
@@ -261,12 +292,12 @@ type is not.
|
||||
|
||||
Two signals both close the client's stdin:
|
||||
|
||||
1. **`{"type":"eof"}` control chunk** (stream_type 3) — explicit,
|
||||
recommended. Tells the server to close the backend's stdin
|
||||
(`ChildStdin::drop` / PTY writer close). The client may still want to
|
||||
receive remaining stdout + the exit code, so the server does not tear
|
||||
down the session on eof — it just closes stdin and keeps pumping
|
||||
output.
|
||||
1. **`{"type":"eof"}` control chunk** (stream_type 3, `STREAM_CTRL_IN`)
|
||||
— explicit, recommended. Tells the server to close the backend's
|
||||
stdin (`ChildStdin::drop` / PTY writer close). The client may still
|
||||
want to receive remaining stdout + the exit code, so the server does
|
||||
not tear down the session on eof — it just closes stdin and keeps
|
||||
pumping output.
|
||||
2. **Zero-length stdin chunk** (stream_type 0, length 0) — the docker
|
||||
POC's sentinel. Accepted for compatibility with that pattern.
|
||||
|
||||
@@ -288,17 +319,26 @@ a session — see [tty-adapter.md](tty-adapter.md).
|
||||
## Constraints
|
||||
|
||||
- **The wire format is one-way (ADR-052).** The 5-byte header, the fixed
|
||||
stream_type set (0-3), and the two-carriage sequence are bytes clients
|
||||
and servers parse. A 5th channel type requires a new ALPN
|
||||
stream_type set (0-4), and the two-carriage sequence are bytes clients
|
||||
and servers parse. A 6th channel type requires a new ALPN
|
||||
(`alknet/tty/v2` per ADR-006), not a negotiated addition.
|
||||
- **The control channel is split into two halves (Phase 7).**
|
||||
`STREAM_CTRL_IN = 3` is client→server (`Resize`, `Signal`, `Eof`);
|
||||
`STREAM_CTRL_OUT = 4` is server→client (`Exit`). The adapter enforces
|
||||
the direction: an `Exit` on `STREAM_CTRL_IN` is ignored; a `Resize` on
|
||||
`STREAM_CTRL_OUT` is ignored. The split is what makes the control
|
||||
channel genuinely bidirectional on the wire — the previous single
|
||||
`STREAM_CONTROL = 3` was documented as "bidirectional" but the adapter
|
||||
had to ignore `Exit` from the client because the two directions were
|
||||
indistinguishable on the same stream_type.
|
||||
- **No windowing.** The chunk format has no flow-control window; QUIC's
|
||||
per-stream flow control is the backpressure mechanism (OQ-45 resolved:
|
||||
the backpressure chain is complete by construction — QUIC flow control
|
||||
→ bounded drainer channel → bounded stdout channel → OS pipe/PTY
|
||||
buffer → process `write()` blocks; no unbounded buffer breaks the
|
||||
chain). The reversal path, if ever needed, is an additive
|
||||
`ControlMessage` variant on stream_type 3, not a wire-format header
|
||||
change.
|
||||
`ControlMessage` variant on `STREAM_CTRL_IN`/`STREAM_CTRL_OUT`, not a
|
||||
wire-format header change.
|
||||
- **No negotiation round-trip.** The client writes the negotiation frame
|
||||
and starts sending chunks; the server reads the frame and starts
|
||||
pumping. There is no "the server acknowledges the negotiation before
|
||||
@@ -312,18 +352,21 @@ a session — see [tty-adapter.md](tty-adapter.md).
|
||||
without entering raw mode. The error response MUST be under 16 MiB
|
||||
(`MAX_CHUNK_LEN`) so the 4-byte big-endian length prefix's high byte
|
||||
is `0x00` — this is what makes the framing-disambiguation trick
|
||||
(first byte `0x00` = error frame, first byte `1`/`2`/`3` = raw chunk)
|
||||
sound; it is a wire-format invariant, not an empirical observation.
|
||||
See [tty-adapter.md](tty-adapter.md) §"Negotiation errors".
|
||||
(first byte `0x00` = error frame, first byte `1`/`2`/`4` = raw chunk;
|
||||
the server never sends `0` (stdin, client→server) or `3`
|
||||
(`STREAM_CTRL_IN`, client→server), so `0x00` is unambiguous) sound; it
|
||||
is a wire-format invariant, not an empirical observation. See
|
||||
[tty-adapter.md](tty-adapter.md) §"Negotiation errors".
|
||||
|
||||
## Design Decisions
|
||||
|
||||
| Decision | ADR | Summary |
|
||||
|----------|-----|---------|
|
||||
| Wire format and two-carriage model | [ADR-052](../../decisions/052-alknet-tty-wire-format-and-two-carriage.md) | `alknet/tty` ALPN; JSON negotiation frame then raw chunks; fixed channel set 0-3; control as JSON |
|
||||
| Wire format and two-carriage model | [ADR-052](../../decisions/052-alknet-tty-wire-format-and-two-carriage.md) | `alknet/tty` ALPN; JSON negotiation frame then raw chunks; fixed channel set 0-4; control as JSON |
|
||||
| Bidirectional control channel split | Phase 7 (this doc, amended) | `STREAM_CTRL_IN = 3` (client→server) and `STREAM_CTRL_OUT = 4` (server→client) replace the single `STREAM_CONTROL = 3`; the adapter enforces the direction |
|
||||
| No alknet-call dependency (self-contained framing) | [ADR-057](../../decisions/057-alknet-tty-no-alknet-call-dep.md) | alknet-tty implements its own length-prefixed framing; format coincides with alknet-call's by convention, not by code reuse |
|
||||
| Exit code on a control chunk | [ADR-055](../../decisions/055-exit-code-on-control-chunk.md) | `{"type":"exit","code":N}` on stream_type 3; "exit chunk is last" invariant |
|
||||
| Stdin closure canonical signal | OQ-47 | Either `eof` control chunk or zero-length stdin chunk; `eof` recommended |
|
||||
| Exit code on a control chunk | [ADR-055](../../decisions/055-exit-code-on-control-chunk.md) | `{"type":"exit","code":N}` on `STREAM_CTRL_OUT` (stream_type 4); "exit chunk is last" invariant |
|
||||
| Stdin closure canonical signal | OQ-47 | Either `eof` control chunk (`STREAM_CTRL_IN`) or zero-length stdin chunk; `eof` recommended |
|
||||
|
||||
## Open Questions
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
Accepted (amended 2026-07-18 — Phase 7: the control channel is split
|
||||
into `STREAM_CTRL_IN = 3` (client→server) and `STREAM_CTRL_OUT = 4`
|
||||
(server→client) halves; see §"Control channel split" below)
|
||||
|
||||
## Context
|
||||
|
||||
@@ -123,12 +125,15 @@ The bidi stream has two phases:
|
||||
| 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 |
|
||||
| 3 | ctrl-in | client→server | JSON control message (`Resize`, `Signal`, `Eof` — see §4a) |
|
||||
| 4 | ctrl-out | server→client | JSON control message (`Exit` — see §4a) |
|
||||
|
||||
`stream_type > 3` is 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/v2` per ADR-006), not a
|
||||
negotiated addition to this format.
|
||||
`stream_type > 4` is a protocol error (`InvalidStreamType`). (Phase 7
|
||||
amendment, §4a: the original `3 = control (bidirectional)` is split
|
||||
into `3 = ctrl-in` and `4 = ctrl-out`; the original bound was
|
||||
`> 3`.) There is no extension escape hatch in the byte — a 6th
|
||||
channel is a wire-format change requiring a new ALPN (`alknet/tty/v2`
|
||||
per ADR-006), not a negotiated addition to this format.
|
||||
|
||||
- `length` — payload length in bytes, u32 big-endian, max 16 MiB. A
|
||||
chunk larger than 16 MiB is a protocol error (`ChunkTooLarge`). The
|
||||
@@ -176,6 +181,53 @@ tearing down the session. This is a two-way-door extension point within
|
||||
the one-way-door wire format — adding a control message type is
|
||||
additive; changing the chunk header is not.
|
||||
|
||||
### 4a. Control channel split (Phase 7 amendment, 2026-07-18)
|
||||
|
||||
The single `stream_type 3 = control (bidirectional)` in §3 and §4 above
|
||||
is **split into two halves** so the control channel is genuinely
|
||||
bidirectional on the wire:
|
||||
|
||||
| stream_type | channel | direction | payload |
|
||||
|-------------|-------------|----------------|------------------------------------------------------|
|
||||
| 3 | `STREAM_CTRL_IN` | client→server | JSON control message (`Resize`, `Signal`, `Eof`) |
|
||||
| 4 | `STREAM_CTRL_OUT` | server→client | JSON control message (`Exit`) |
|
||||
|
||||
The `stream_type > 3` protocol-error bound becomes `stream_type > 4`.
|
||||
The chunk header is otherwise unchanged (5 bytes: 1 type + 4 length).
|
||||
|
||||
**Why the split.** The original §4 documented stream_type 3 as
|
||||
"bidirectional" and listed the four control messages with their
|
||||
directions. But the adapter had no way to distinguish the two
|
||||
directions on the same stream_type — `Exit` from the client was always
|
||||
ignored (the adapter's `pump_client_to_backend` matched `Exit` and
|
||||
logged "ignoring Exit control from client (server→client only)"). The
|
||||
spec said "bidirectional"; the code was half-duplex. The split makes
|
||||
the bidirectionality literal: each direction has its own stream_type,
|
||||
the adapter enforces the direction (an `Exit` arriving on
|
||||
`STREAM_CTRL_IN` is a protocol violation; a `Resize` arriving on
|
||||
`STREAM_CTRL_OUT` is a protocol violation), and a client can route
|
||||
exit vs. control without parsing the JSON `type` tag first.
|
||||
|
||||
**Door type.** One-way, same as the original §3 / §4. The stream_type
|
||||
set is bytes clients and servers parse. A client written against the
|
||||
old single-`STREAM_CONTROL` shape will misread `STREAM_CTRL_OUT = 4` as
|
||||
`InvalidStreamType (> 3)` and tear down the session — the split is a
|
||||
wire-format change, not an additive extension. The reversal path is
|
||||
the same as the original: a new ALPN (`alknet/tty/v2`), which coexists
|
||||
rather than replaces. The trade is one new stream_type byte now vs.
|
||||
the half-duplex-in-disguise flaw forever.
|
||||
|
||||
**What changes in the spec.** §3's stream_type table gains a 5th row
|
||||
(`4 = ctrl_out, server→client`); the bound becomes `> 4`. §4's
|
||||
direction table is unchanged in content (the four messages keep their
|
||||
directions and shapes) but the direction is now encoded in the
|
||||
stream_type, not just in the adapter's behavior. ADR-055's "exit chunk
|
||||
is last" invariant is unchanged — the exit chunk still rides the
|
||||
control channel, just on `STREAM_CTRL_OUT` (stream_type 4) instead of
|
||||
the old single `STREAM_CONTROL` (stream_type 3). See
|
||||
`docs/research/alknet-crate-extraction/findings.md` Phase 7 for the
|
||||
full migration notes.
|
||||
|
||||
### 5. Negotiation errors use the JSON framing, not the raw chunk format
|
||||
|
||||
If the server cannot allocate the session (unknown backend, PTY
|
||||
|
||||
Reference in new issue
Block a user