Files
alktty/docs/architecture
glm-5.2 da0d395ab3 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)
2026-08-17 11:20:23 +00:00
..

alktty — Architecture

Terminal session protocol for the alk/tty ALPN: a producer/consumer protocol crate on top of alkcall channels. This directory holds the architecture spec docs, the BAST (Binary Abstract Syntax Tree) document for the wire format, and the ADRs.

Documents

Document Status Description
overview.md draft Crate purpose, the two-carriage model in brief, dependencies, ALPN, backend location map, feature gates
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 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 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 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 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)

Applicable ADRs

Ported from the alknet mono-repo and renumbered into alktty's ADR range (001..008). The alknet originals at /workspace/@alkdev/alknet/docs/architecture/decisions/ remain the authoritative source for any ADR not yet ported, and for the alknet ADRs referenced by alknet number in the docs below (which are not tty-specific and therefore not ported into alktty's ADR range).

ADR Title Origin Status
001 alktty Wire Format and Two-Carriage Model alknet ADR-052 Accepted (amended 2026-07-18 — Phase 7 control-channel split)
002 TtyBackend Trait and TtyHandle — the Backend Inversion Point alknet ADR-053 Accepted
003 Local TTY Backend Placement (Single Crate with local Feature) alknet ADR-054 Accepted (records both the alknet sibling-crate decision and the alktty single-crate consolidation)
004 Exit Code on a Control Chunk (the Last Chunk Before Stream Close) alknet ADR-055 Accepted
005 Backend Cleanup on Session Cancel (Drop of exit_code Kills) alknet ADR-056 Accepted
006 Self-Contained Negotiation Framing (No alkcall-Internal-Wire-Types Dependency) alknet ADR-057 Accepted
007 TTY Inside Channels — Sub-Streams, Not Wire Format alknet ADR-077 Accepted (reversed by ADR-008 — kept for historical context)
008 Channels Pure Channel Multiplexing (8-Byte Header, No stream_type) alknet ADR-093 Accepted (amends alknet ADR-071/074; reverses ADR-007)

Key Design Principles

  1. A terminal session is a terminal concern, not an SSH or Docker concern. SSH and Docker are two backends that can allocate a PTY. alktty owns the terminal session lifecycle; the backends (DockerTtyBackend, SshTtyBackend, LocalTtyBackend) implement a TtyBackend trait. This dissolves the PTY hedge in the alknet-ssh research (DP-5): PTY is not an SSH feature delegated to a separate crate, it's a tty feature that SSH happens to be able to provide. See overview.md and ADR-002.

  2. Two-carriage model: JSON negotiation, then raw chunks. The bidi stream opens with a single length-prefixed JSON negotiation frame (terminal params, backend selector, command), then switches to a raw chunk format ([stream_type: u8][length: u32 be][payload]) for the life of the session. The call protocol's JSON-RPC shape handles the structured request; raw bytes handle the body, which is what a terminal actually is. No per-chunk EventEnvelope overhead, no base64. See tty-wire.md and ADR-001.

  3. Fixed channel set, not extensible. Five stream types (0=stdin, 1=stdout, 2=stderr, 3=ctrl-in, 4=ctrl-out), no negotiation. A 6th channel type is a wire-format change (one-way door); the ALPN model handles extensibility at the protocol level (a new ALPN is cheap, a wire-format change is not). The impoverishment vs SSH channels is the feature: alktty multiplexes one service (a terminal session) with a fixed channel structure, not arbitrary services. See tty-wire.md.

  4. The backend trait is the inversion point. alktty defines TtyBackend; the backend crates implement it. alktty depends on alkcall; backends depend on alktty for the trait; alktty does not depend on any backend. This preserves alknet ADR-003's no-handler-depends-on-another-handler rule. alktty does not depend on alkcall's internal wire types either (the negotiation framing is self-contained — ADR-006). See tty-backend.md and ADR-002.

  5. Backends need not be natively async (REQ-TTY-01). The trait's adapter-facing types (AsyncWrite, Stream<Item = Bytes>, BoxFuture, TtyControl) are the adapter's contract. A backend may expose blocking handles internally and bridge them via std threads + tokio mpsc/oneshot (the pattern portable_pty requires, and the local-PTY POC validated). The bridging pattern is a documented, supported implementation strategy. See tty-backend.md and tty-local.md.

  6. Exit code on a control chunk, last before stream close (ADR-004). {"type":"exit","code":N} rides on STREAM_CTRL_OUT (stream_type 4) and is the last chunk before the server closes the write half. This gives coordinators deterministic completion notification — no polling, no plugin state. The adapter owns the ordering; backends resolve exit_code and the adapter awaits, sends the chunk, closes. See tty-adapter.md.

  7. Drop of exit_code future kills the session target (ADR-005). On session cancel (connection drop, stream reset), the adapter drops the TtyHandle, which drops the exit_code future without driving it to completion. The backend's exit_code future's Drop-on-cancel MUST kill the child/container/SSH process. This is a behavioral contract on the TtyBackend trait — the adapter has no separate kill method; the cleanup is wired into the exit_code future's Drop by the backend. See tty-adapter.md and tty-local.md.

  8. The runner pattern is preserved, not specialized. The local backend in pipe mode (terminal: None) is a process-streaming endpoint — the same shape as GitHub/Gitea Actions runners, just over alk's transport instead of HTTP polling. alktty provides the mechanism (framed byte stream + exit code); runner policy (job management, log persistence, task graph) is a downstream crate's job. See tty-local.md and ADR-003.

  9. TTY always uses its 5-byte format, including inside channels (ADR-008). The same wire.rs code runs in both direct alk/tty and channels alk/channels modes; only the BiStream source differs. The channels layer strips its 8-byte header and hands TTY the payload transparently. This reverses the earlier two-mode design (ADR-007, kept for historical context). See tty-adapter.md.

Relevant Open Questions

OQ Title Status Relevance
OQ-43 TtyControl trait object Clone constraint resolved control: Option<TtyControlHandle> via a #[derive(Clone)] newtype wrapping Arc<dyn TtyControl + Send + Sync>; the trait is NOT Clone (not object-safe) — the newtype carries Clone-ability
OQ-44 Terminal modes (TTY modes) deferred(scope) TerminalParams.modes reserved; default terminal modes suffice for current scope; blocked on a concrete mode-control use case
OQ-45 Flow control for high-throughput stdout resolved QUIC per-stream flow control is the backpressure mechanism (chain complete by construction); no application-level windowing. Reversal is an additive ControlMessage variant, not a wire-format change
OQ-46 Runner API surface deferred(scope) The runner mechanism (pipe mode) is in alktty; runner policy (job management, log persistence, task graph) is a downstream crate, not in scope here
OQ-47 Stdin closure canonical signal resolved Either a zero-length stdin chunk or a {"type":"eof"} control chunk; both are accepted; the spec recommends eof for explicitness

References

  • docs/plans/project-setup.md — the current plan (phases 05). Phases 03 are landed; Phase 4 (this directory: architecture docs + BAST schema + renumbered ADRs) is landed by this commit; Phase 5 (tests, including integration tests in tests/ at the crate root) is not yet done.
  • alknet originals of the ported ADRs (alknet ADR-052, 053, 054, 055, 056, 057, 077, 093) at /workspace/@alkdev/alknet/docs/architecture/decisions/ — the authoritative source for any ADR not yet ported, and for the alknet ADRs referenced by alknet number in the docs above (which are not tty-specific and therefore not ported into alktty's ADR range).
  • alktype BAST format spec — the normative format spec for BAST documents (the meta-schema tty-bast.md conforms to); see also /workspace/@alkdev/alktype/docs/architecture/bast-format.md.
  • alkcall architecture README at /workspace/@alkdev/alkcall/docs/architecture/README.md — the producer/consumer protocol-crate pattern alktty follows.