Files
alknet/tasks/tty/review-tty.md
T

135 lines
7.1 KiB
Markdown

---
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<dyn AsyncWrite + Send + Unpin>), `stdout` (Pin<Box<dyn Stream<Item=Bytes> + Send>>), `stderr` (Option), `exit_code` (BoxFuture<Result<i32, TtyError>>), `control` (Option<TtyControlHandle>)
- `TtyControl` trait: `resize`, `signal` (object-safe, NOT `Clone`)
- `TtyControlHandle`: `#[derive(Clone)]`, `Arc<dyn TtyControl + Send + Sync>` newtype (OQ-43)
- `From<NegotiateRequest> 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<HashMap<String, Arc<dyn TtyBackend>>>`), `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