docs(arch): alknet-tty spec sanity-check fixes — TtyControlHandle newtype (C1), backend cleanup contract (W1, ADR-056), clarifications (S1-S5)

C1: Box<dyn TtyControl + Clone> does not compile (Clone is not object-safe).
Replace with a TtyControlHandle newtype: #[derive(Clone)] wrapping
Arc<dyn TtyControl + Send + Sync>. The trait stays object-safe; the
newtype carries the Clone-ability. Updated across tty-backend.md,
ADR-053, OQ-43, and the tty README.

W1: 'Drop is the cleanup, threads exit on channel close' was false for
the local PTY waiter thread (blocked in Child::wait(), does not observe
channel close) — a child that ignores stdin EOF would be orphaned on
session cancel. New ADR-056 commits a backend cleanup contract: dropping
the exit_code future MUST kill the session target. The local backend
implements it via a ChildKiller held in the exit_code future's Drop
guard (disarmed on resolve). Corrected the lifecycle section in
tty-adapter.md, added the mechanism + constraint to tty-local.md, and
referenced ADR-056 from tty-backend.md, overview.md, both READMEs.

S1: error-frame < 16 MiB stated as a wire-format invariant (not an
assumption) so the 0x00-first-byte disambiguation trick is sound by
construction.
S2: carriage field validation (MUST be "raw" in v1, else
malformed_negotiation) specified.
S3: NegotiateRequest + TerminalParamsWire Rust structs added with
serde defaults and validation rules.
S4: modes field annotated 'backends MUST ignore content in v1' in both
tty-backend.md and ADR-053.
S5: AsyncWrite/Stream qualified with crate origins (tokio::io,
futures_core) in the TtyHandle struct definitions.
This commit is contained in:
glm-5.2 committed 2026-07-07 10:38:49 +00:00
1 parent b90d717ac2
commit 25f0f55592
11 files changed
+594 -92

No files matched your search

@@ -194,17 +194,18 @@ closes after it.
allocation (raw chunks) and a failed allocation (JSON error frame) begin
with bytes the client must read before knowing which framing applies.
The disambiguation is by the first byte: a JSON error frame's 4-byte
big-endian length prefix always starts with `0x00` (error frames are
small — under 16 MiB, so the high byte is zero), 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: error frames use the negotiation
framing (length prefix), success uses the raw chunk framing
(stream_type byte first); the `0x00`-as-length-prefix vs
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.