Files
alknet/docs/architecture/crates/tty
glm-5.2 1ce11717d8 docs(arch): draft alknet-docker architecture specs (ADRs 058-063, OQs 048-051)
Greenfield architecture spec set for the alknet-docker crate — a thin,
single-host bollard wrapper exposing docker container/image operations
as call-protocol ops on the shared alknet/call ALPN, plus a
DockerTtyBackend (impl TtyBackend) behind a tty feature for interactive
terminal sessions into containers over alknet/tty.

Six ADRs:
- 058: docker ops on alknet/call (no separate ALPN; raw-carriage handoff
  dissolved by alknet-tty extraction — interactive attach moved to
  alknet/tty via DockerTtyBackend, no carriage field on call.requested)
- 059: bollard 0.21 (verified current on crates.io) + feature selection
  (http+pipe+time; no ssl/ssh/websocket/buildkit)
- 060: container resource model (ADR-050 application) — alknet.managed/
  alknet.owner labels, list owned_only flag, hosted-services operator
  role via static-resource fallback, handler-driven revoke with
  autonomous-death tolerance, resource-action vocabulary
- 061: DockerTtyBackend in alknet-docker behind tty feature (attach vs
  exec mode; POC drive_attach_raw as reference)
- 062: Docker client + OwnershipStore injection via closure capture
  (not Capabilities, not OperationContext — matches from_openapi pattern)
- 063: exit code on terminal call.responded for non-interactive exec
  (call.completed stays empty, ADR-012 unchanged)

Four spec docs (crates/docker/): README, overview, docker-operations,
docker-tty-backend. Four deferred-scope OQs (048-051): network/volume
ops, buildkit, system events subscription, create options surface.

Updates the tty-backend.md Backend implementations table (DockerTtyBackend
row now specced) and the architecture README (doc table, ADR table,
current-state paragraph, OQ count).

Grounded in the alknet-docker POC (docs/research/alknet-docker/poc-summary.md)
which validated the hard parts; the remaining lifecycle ops are mechanical
bollard wrapping. Reviewed by architecture-reviewer subagent; criticals
(the docker_client injection model conflicting with the Capabilities
contract) resolved via ADR-062/063 before commit.
2026-07-08 09:29:22 +00:00
..

status, last_updated
status last_updated
draft 2026-07-07

alknet-tty

Terminal session protocol handler for the ALPN-as-service architecture: a ProtocolHandler on alknet/tty that pumps a bidirectional byte stream (stdin/stdout/stderr) with a JSON control channel (resize, signal, eof, exit) over a framed bidi stream, decoupled from the backend that allocates the PTY (docker, SSH, local process) via a TtyBackend trait.

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 (stream_type 3, JSON control messages), sentinels
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 alknet/tty): session lifecycle, three-pump bidirectional driver, negotiation errors, exit-chunk ordering (ADR-055), access control
tty-local.md draft alknet-tty-local sibling crate: LocalTtyBackend via portable_pty (PTY) and std::process::Command (pipe/runner). Carries REQ-TTY-02 (signal forwarding to the process group)

Applicable ADRs

ADR Title Relevance
001 ALPN-Based Protocol Dispatch TtyAdapter registers on alknet/tty
002 ProtocolHandler Trait TtyAdapter implements ProtocolHandler
003 Crate Decomposition alknet-tty depends on alknet-core only (no alknet-call — Am. 2); backends depend on alknet-tty for the trait
006 ALPN String Convention and Connection Model alknet/tty is the custom ALPN; one ALPN per connection; new ALPN for incompatible versions
007 BiStream Type Definition TtyAdapter receives a Connection, accepts bidi streams, pumps per-session
009 One-Way Door Decision Framework Wire format is one-way; local backend placement is two-way (decided, not deferred)
012 Call Protocol Stream Model The call protocol's stream model — which tty's raw carriage is not using for the body, by design
032 Forwarded-For Identity forwarded_for for proxied terminal sessions (hub→worker)
040 WebTransport ALPN-Stream-Proxy Parked per ADR-044; the alknet/tty ALPN is reachable over WebTransport's stream proxy when WebTransport revives
044 Defer h3/WebTransport; Browsers Use WebSocket WebTransport deferred; the browser terminal case revives with WebTransport
049 Streaming Handler for Subscriptions The StreamingHandler path tty explicitly does not use for the byte body — raw carriage, not call.responded events
050 Dynamic Resource Ownership Terminal sessions are runtime-spawned resources; AccessControl shape declares against this model
052 alknet-tty Wire Format and Two-Carriage Model The chunk codec, control channel, negotiation frame, fixed channel set
053 TtyBackend Trait and TtyHandle The backend trait, handle shape, blocking-backend accommodation (REQ-TTY-01)
054 Local TTY Backend as a Sibling Crate alknet-tty-local behind a local feature re-export; PTY vs pipe per-session
055 Exit Code on a Control Chunk Exit code on stream_type 3; "exit chunk is last" invariant; adapter owns the ordering
056 Backend Cleanup on Session Cancel Dropping the exit_code future (session cancel) MUST kill the session target; behavioral contract on the TtyBackend trait
057 alknet-tty Does Not Depend on alknet-call Self-contained negotiation framing (~30 lines); format coincides with alknet-call's by convention, not by code reuse

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; confirmed by the POC's concrete PtyControl
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 alknet-tty; 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

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. alknet-tty 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-053.

  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-052.

  3. Fixed channel set, not extensible. Four stream types (0=stdin, 1=stdout, 2=stderr, 3=control), no negotiation. A 5th 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: alknet-tty 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. alknet-tty defines TtyBackend; the backend crates implement it. alknet-tty depends on alknet-core; backends depend on alknet-tty for the trait; alknet-tty does not depend on any backend. This preserves ADR-003's no-handler-depends-on-another-handler rule. alknet-tty does not depend on alknet-call either (the negotiation framing is self-contained — ADR-057). See tty-backend.md and ADR-053.

  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-055). {"type":"exit","code":N} rides on the control channel (stream_type 3) 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 and ADR-055.

  7. 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 alknet's transport instead of HTTP polling. alknet-tty 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-054.

References

  • docs/research/alknet-tty/phase-0-findings.md — Phase 0 research (wire format, backend trait, REQ-TTY-01/02, decision points DP-1 through DP-6, open questions OQ-TTY-01 through OQ-TTY-05)
  • /workspace/alknet-tty-poc/ — Phase 0 local-PTY validation POC (src/raw.rs chunk codec, src/control.rs JSON control schema, src/local_pty.rs blocking→async bridge, src/session.rs session pump, tests/integration.rs + tests/signal.rs round-trip tests)
  • /workspace/alknet-docker-poc/src/raw.rs — the seed codec (stream_type 0/1/2) the tty POC extended with stream_type 3
  • docs/research/alknet-docker/poc-summary.md — the POC that seeded this crate (two-carriage model, raw chunk format, validated targets)
  • docs/research/alknet-ssh/phase-0-findings.md DP-5 — the PTY hedge this crate dissolves (PTY is a tty feature, not an SSH feature)
  • /workspace/@alkdev/dispatch/ — the reverse-runner prior art (currently requires SSH; LocalTtyBackend removes that requirement)
  • portable-pty 0.9 source — the blocking-API constraint that drives REQ-TTY-01 and the signal-delivery contract (REQ-TTY-02)