Files
alktty/docs/architecture/README.md
T
glm-5.2 b3f50d1836 phase 4: architecture docs + BAST schema + renumbered ADRs
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
2026-08-17 10:52:26 +00:00

155 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](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-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-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-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) |
## 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](decisions/001-wire-format-and-two-carriage.md) | alktty Wire Format and Two-Carriage Model | alknet ADR-052 | Accepted (amended 2026-07-18 — Phase 7 control-channel split) |
| [002](decisions/002-ttybackend-trait-and-ttyhandle.md) | TtyBackend Trait and TtyHandle — the Backend Inversion Point | alknet ADR-053 | Accepted |
| [003](decisions/003-local-backend-placement.md) | 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](decisions/004-exit-code-on-control-chunk.md) | Exit Code on a Control Chunk (the Last Chunk Before Stream Close) | alknet ADR-055 | Accepted |
| [005](decisions/005-backend-cleanup-on-session-cancel.md) | Backend Cleanup on Session Cancel (Drop of `exit_code` Kills) | alknet ADR-056 | Accepted |
| [006](decisions/006-negotiation-framing-self-contained.md) | Self-Contained Negotiation Framing (No alkcall-Internal-Wire-Types Dependency) | alknet ADR-057 | Accepted |
| [007](decisions/007-tty-inside-channels.md) | TTY Inside Channels — Sub-Streams, Not Wire Format | alknet ADR-077 | Accepted (**reversed by ADR-008** — kept for historical context) |
| [008](decisions/008-channels-pure-channel-multiplexing.md) | 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](overview.md) and [ADR-002](decisions/002-ttybackend-trait-and-ttyhandle.md).
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](tty-wire.md) and
[ADR-001](decisions/001-wire-format-and-two-carriage.md).
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](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](decisions/006-negotiation-framing-self-contained.md)).
See [tty-backend.md](tty-backend.md) and
[ADR-002](decisions/002-ttybackend-trait-and-ttyhandle.md).
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](tty-backend.md) and [tty-local.md](tty-local.md).
6. **Exit code on a control chunk, last before stream close
([ADR-004](decisions/004-exit-code-on-control-chunk.md)).**
`{"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](tty-adapter.md).
7. **Drop of `exit_code` future kills the session target
([ADR-005](decisions/005-backend-cleanup-on-session-cancel.md)).**
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](tty-adapter.md) and
[tty-local.md](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](tty-local.md) and
[ADR-003](decisions/003-local-backend-placement.md).
9. **TTY always uses its 5-byte format, including inside channels
([ADR-008](decisions/008-channels-pure-channel-multiplexing.md)).**
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](decisions/007-tty-inside-channels.md), kept for
historical context). See [tty-adapter.md](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](https://alk.dev/bast/v1/schema) — 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.