--- id: tty/review-tty name: Review alknet-tty core crate for spec conformance before local backend begins status: completed depends_on: [tty/adapter] scope: moderate risk: low impact: phase level: review --- ## Description Review the alknet-tty core crate implementation for spec conformance, pattern consistency, and correctness before `alknet-tty-local` begins implementation against the `TtyBackend` trait. This is the quality checkpoint at the end of the core crate phase — the trait shape is a one-way door (ADR-053), and the local backend builds against it, so any issues here propagate to the sibling crate. ### Review Checklist 1. **Wire codec conformance** (tty-wire.md §"Phase 2"): - `ChunkReader`/`ChunkWriter` with 5-byte header (1 stream_type + 4 BE length) - `STREAM_STDIN`=0, `STREAM_STDOUT`=1, `STREAM_STDERR`=2, `STREAM_CONTROL`=3 - `MAX_CHUNK_LEN` = 16 MiB; `ChunkTooLarge` on overflow - `InvalidStreamType` on stream_type > 3 - `ConnectionClosed` on clean EOF (not `Io`) - Zero-length chunks are sentinels (codec doesn't special-case; adapter interprets) 2. **Control messages conformance** (tty-wire.md §"Control Channel"): - `ControlMessage` enum: `Resize`, `Signal`, `Eof`, `Exit` - `#[serde(tag = "type", rename_all = "snake_case")]` - `Exit.code` is `i32` (negative = signal-terminated) - `signal_from_name` for 9 names, `#[cfg(unix)]` gated - Unknown `type` values: `from_slice` returns error; adapter ignores (not a schema catch-all) 3. **Negotiation conformance** (tty-wire.md §"Phase 1", §"Constraints"): - `NegotiateRequest` with `carriage`, `backend`, `tty`, `cmd`, `cwd`, `env`, `backend_params` - `serde(flatten)` captures backend-specific fields into `backend_params` - `TerminalParamsWire` with `term`, `cols`, `rows`, `pixel_width`, `pixel_height`, `modes` - Length-prefixed framing (4-byte BE + JSON body), self-contained (no alknet-call dep, ADR-057) - `NegotiationReader` bounds-checks against `MAX_CHUNK_LEN` - Error response shape: `{"error":"...","field":"..."}` - Framing disambiguation: error frame first byte is `0x00` (under 16 MiB invariant) 4. **Backend trait conformance** (tty-backend.md, ADR-053): - `TtyBackend` trait: `allocate()` (async), `resource_id()` (default `None`) - `TtyError` `#[non_exhaustive]`: `AllocFailed`, `WaitFailed`, `Io`, `Backend` - `TtyParams`: `terminal`, `cmd`, `cwd`, `env`, `backend_params` (opaque `serde_json::Map`) - `TerminalParams`: `term`, `cols`, `rows`, `pixel_width`, `pixel_height`, `modes` - `TtyHandle`: `stdin` (Box), `stdout` (Pin + Send>>), `stderr` (Option), `exit_code` (BoxFuture>), `control` (Option) - `TtyControl` trait: `resize`, `signal` (object-safe, NOT `Clone`) - `TtyControlHandle`: `#[derive(Clone)]`, `Arc` newtype (OQ-43) - `From for TtyParams` (or constructor) - REQ-TTY-01 documented (backends need not be natively async) - ADR-056 kill-on-Drop contract documented on `exit_code` field 5. **Adapter conformance** (tty-adapter.md): - `TtyAdapter` struct with `backends` (`Arc>>`), `ownership` (Option) - `impl ProtocolHandler` with `alpn()` = `b"alknet/tty"` - `handle()` loops `accept_bi`, spawns `drive_session` per stream - Negotiation errors: `unknown_backend`, `malformed_negotiation`, `allocate_failed` as JSON in negotiation framing - Three-pump driver: stdout→client, client→backend (stdin + control), exit→exit-chunk - stderr pump concurrent when `stderr` is `Some` - **Exit-chunk-is-last** (ADR-055): stdout/stderr pumps complete AND exit_code resolves before exit chunk - Exit error → `{"type":"exit","code":-1}` - Unknown control `type` ignored; `Exit` from client ignored - Zero-length stdin chunk AND `eof` control both close backend stdin - Scope-gate (`tty:open`) at negotiation - Ownership check via `resource_id()` + `OwnershipProvider::owns()` when wired - Connection drop / stream reset drops `TtyHandle` (ADR-056 cancel-cleanup) - Client write-half close does NOT trigger cancel-cleanup 6. **Dependency constraints**: - No `portable_pty`, `bollard`, `russh` dependency (lean core) - No `alknet-call` dependency (ADR-057 — self-contained framing) - `alknet-core` is the only alknet dependency - `local` feature gate declared; `alknet-tty-local` not yet wired (later task) 7. **Pattern consistency**: - `thiserror` for error enums, `Result` propagation - `tracing` for structured logging (debug/warn) - `async-trait` for `TtyBackend` and `ProtocolHandler` - `tokio` runtime idioms (mpsc, oneshot, spawn) 8. **Test coverage**: - Wire codec round-trip + error cases - Control message round-trip + `signal_from_name` - Negotiation round-trip + `serde(flatten)` + error framing - `MockBackend` + `TtyControlHandle` delegation - Adapter: happy path, exit-chunk-is-last, stdin EOF, control dispatch, unknown control, all 3 negotiation errors, exit error, cancel cleanup, scope gate, ownership check ## Acceptance Criteria - [ ] All wire codec types match tty-wire.md §"Phase 2" - [ ] All control message types match tty-wire.md §"Control Channel" - [ ] All negotiation types match tty-wire.md §"Phase 1" + §"Constraints" - [ ] All backend trait types match tty-backend.md (ADR-053) - [ ] Adapter matches tty-adapter.md (session lifecycle, exit ordering, access control) - [ ] Exit-chunk-is-last invariant (ADR-055) enforced in the adapter, not the backend - [ ] ADR-056 kill-on-Drop contract documented on `TtyHandle.exit_code` - [ ] No `portable_pty`/`bollard`/`russh`/`alknet-call` dependency - [ ] `local` feature gate declared, `alknet-tty-local` not yet wired - [ ] `cargo fmt --check -p alknet-tty` passes - [ ] `cargo clippy -p alknet-tty` passes with no warnings - [ ] All tests pass - [ ] `MockBackend` is reusable for the local backend's integration tests ## References - docs/architecture/crates/tty/README.md - docs/architecture/crates/tty/overview.md - docs/architecture/crates/tty/tty-wire.md - docs/architecture/crates/tty/tty-backend.md - docs/architecture/crates/tty/tty-adapter.md - docs/architecture/decisions/052-alknet-tty-wire-format-and-two-carriage.md - docs/architecture/decisions/053-ttybackend-trait-and-ttyhandle.md - docs/architecture/decisions/055-exit-code-on-control-chunk.md - docs/architecture/decisions/056-backend-cleanup-on-session-cancel.md - docs/architecture/decisions/057-alknet-tty-no-alknet-call-dep.md ## Notes > This review verifies the core crate is spec-conformant before the local > backend builds against the `TtyBackend` trait. The trait shape is one-way > (ADR-053) — any issues here propagate to `alknet-tty-local` and the future > docker/SSH backend crates. Pay special attention to the exit-chunk-is-last > ordering (ADR-055) and the kill-on-Drop contract (ADR-056) — these are the > subtle invariants the POC did not fully enforce. If deviations are found, > document and fix before proceeding to the local backend tasks. ## Summary > To be filled on completion