Port the alknet-tty architecture docs into alktty and add the BAST document for the alk/tty wire format. Docs-only; no Rust source changes. Spec docs (docs/architecture/, flat layout — single-crate repo): - overview.md — crate purpose, two-carriage model, deps, ALPN, backend location map, feature gates - tty-wire.md — 5-byte chunk codec, control channel split (STREAM_CTRL_IN=3 / STREAM_CTRL_OUT=4), sentinels - tty-backend.md — TtyBackend trait, TtyHandle, TtyControl, REQ-TTY-01 (backends need not be natively async) - tty-adapter.md — TtyAdapter, three-pump driver, exit-chunk ordering (ADR-004), cancel cleanup (ADR-005), access control - tty-local.md — LocalTtyBackend (local feature module), PTY + pipe modes, REQ-TTY-02 (signal forwarding to process group) - README.md — architecture index ADRs (docs/architecture/decisions/, renumbered 001..008 from alknet 052,053,054,055,056,057,077,093 in order): - 001 wire format + two-carriage model (incl. Phase 7 control- channel split amendment) - 002 TtyBackend trait + TtyHandle - 003 local backend placement (records both the alknet sibling- crate decision and the alktty single-crate consolidation behind a local feature) - 004 exit code on a control chunk - 005 backend cleanup on session cancel - 006 self-contained negotiation framing - 007 tty inside channels (reversed by 008; kept for historical context with reversal notice) - 008 channels pure channel multiplexing (reverses 007; TTY always uses its 5-byte format) BAST document (docs/architecture/tty-bast.md): - Normative JSON spec for the alk/tty wire format, conforming to the BAST meta-schema at https://alk.dev/bast/v1/schema - 5-byte chunk header (struct, big-endian: stream_type uint8, length uint32) + StreamType enum (Stdin=0..CtrlOut=4) - ControlMessage union (field-name discriminator on type: resize/signal/eof/exit) with documented deviation that on-wire control payloads are UTF-8 JSON, not BAST's binary union encoding - NegotiationFrame (4-byte BE length + UTF-8 JSON body) + NegotiateRequest / TerminalParams JSON shapes - StreamType enum deviation noted: on-wire uint8, not BAST's standard u32 enum index (chunk header is 5 bytes, not 8) - alktty does not depend on alktype; the hand-rolled wire.rs is the runtime codec, the BAST is the human-readable contract AGENTS.md: fixed the ADR mapping table to match the plan's 8-to-8 mapping (the previous table substituted ADR-050 for 054, relabeled 056 as control-message split, dropped 077, and added a new control-split ADR at 006 — inconsistent with both the plan and the prose). ADR-050 (dynamic resource ownership) is an alkcall/alknet- core ADR, not tty-specific, and is not ported; the Phase 7 control split stays as an amendment inside ADR-001, mirroring alknet. Verification (all pass, no Rust source changed): - cargo test (80 passed) - cargo test --all-features (103 passed) - cargo clippy --all-targets -- -D warnings (clean) - cargo fmt --check (clean) - cargo check --target wasm32-unknown-unknown (clean) - cargo clippy --target wasm32-unknown-unknown -- -D warnings (clean) - cargo doc --no-deps: 9 pre-existing intra-doc-link warnings in src/session.rs and src/channels.rs (untouched by this commit; not introduced here) - BAST JSON parses; StreamType indices match wire.rs constants (0=Stdin..4=CtrlOut) - all markdown cross-reference links resolve
11 KiB
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 alk/tty wire format; a normative JSON spec conforming to the BAST meta-schema at https://alk.dev/bast/v1/schema |
| 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
-
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 aTtyBackendtrait. 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. -
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-chunkEventEnvelopeoverhead, no base64. See tty-wire.md and ADR-001. -
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.
-
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. -
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 patternportable_ptyrequires, and the local-PTY POC validated). The bridging pattern is a documented, supported implementation strategy. See tty-backend.md and tty-local.md. -
Exit code on a control chunk, last before stream close (ADR-004).
{"type":"exit","code":N}rides onSTREAM_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 resolveexit_codeand the adapter awaits, sends the chunk, closes. See tty-adapter.md. -
Drop of
exit_codefuture kills the session target (ADR-005). On session cancel (connection drop, stream reset), the adapter drops theTtyHandle, which drops theexit_codefuture without driving it to completion. The backend'sexit_codefuture'sDrop-on-cancel MUST kill the child/container/SSH process. This is a behavioral contract on theTtyBackendtrait — the adapter has no separate kill method; the cleanup is wired into theexit_codefuture'sDropby the backend. See tty-adapter.md and tty-local.md. -
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. -
TTY always uses its 5-byte format, including inside channels (ADR-008). The same
wire.rscode runs in both directalk/ttyand channelsalk/channelsmodes; only theBiStreamsource 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 0–5). Phases 0–3 are landed; Phase 4 (this directory: architecture docs + BAST schema + renumbered ADRs) is landed by this commit; Phase 5 (tests, including integration tests intests/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.mdconforms 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.