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.
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
-
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 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-053. -
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-052. -
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.
-
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. -
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-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 resolveexit_codeand the adapter awaits, sends the chunk, closes. See tty-adapter.md and ADR-055. -
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.rschunk codec,src/control.rsJSON control schema,src/local_pty.rsblocking→async bridge,src/session.rssession pump,tests/integration.rs+tests/signal.rsround-trip tests)/workspace/alknet-docker-poc/src/raw.rs— the seed codec (stream_type 0/1/2) the tty POC extended with stream_type 3docs/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.mdDP-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;LocalTtyBackendremoves that requirement)portable-pty0.9 source — the blocking-API constraint that drives REQ-TTY-01 and the signal-delivery contract (REQ-TTY-02)