6.9 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | |
|---|---|---|---|---|---|---|---|---|
| tty-local/review-tty-local | Review alknet-tty-local for REQ-TTY-01/02 and ADR-056 contract conformance | completed |
|
moderate | low | phase | review |
Description
Review the alknet-tty-local implementation for spec conformance, with
particular attention to the three load-bearing requirements: REQ-TTY-01
(backends need not be natively async), REQ-TTY-02 (signal forwarding to the
process group), and ADR-056 (kill-on-Drop cancel-cleanup). This is the
quality checkpoint at the end of the local backend phase, before the
integration tests and the feature re-export wire the two crates together.
Review Checklist
-
PTY mode conformance (tty-local.md §"PTY Mode"):
allocate_ptyusesportable_pty::native_pty_system().openpty()CommandBuilder::set_controlling_tty(true)(the default; session leader)- Three std threads (reader, writer, waiter) feeding tokio mpsc/oneshot
TtyHandle.stdoutis the reader mpsc asPin<Box<dyn Stream<Item = Bytes> + Send>>TtyHandle.stdinis the writer mpsc asBox<dyn AsyncWrite + Send + Unpin>TtyHandle.stderrisNone(PTY merges stdout/stderr)TtyHandle.exit_codeisLocalExitFuture(oneshot + kill guard)TtyHandle.controlisSome(TtyControlHandle::new(Arc::new(PtyControl)))- Reader thread sends zero-length
Bytessentinel on EOF - Writer thread drops the writer on
StdinCmd::Eof AsyncWritewrapper over the mpsc sender (poll_write, poll_flush, poll_close)
-
REQ-TTY-02: Signal forwarding to the process group (tty-local.md §"REQ-TTY-02"):
PtyControl::signaluseskill(-pgid, sig)(negative pid = process group)- Fallback to
kill(pid, sig)if the group signal fails portable_ptychild is a session leader (controlling_tty = true)signal_from_namefromalknet-ttyfor the 9 supported names- Unknown names fall back to
ChildKiller::kill(SIGHUP) - Contract is "best-effort delivery to the foreground process group"
- Integration test validates process-group targeting (a child of the shell receives the signal)
-
Pipe mode conformance (tty-local.md §"Pipe Mode"):
allocate_pipeusestokio::process::CommandwithStdio::piped()TtyHandle.stdinisChildStdinasBox<dyn AsyncWrite + Send + Unpin>TtyHandle.stdout/stderrwrapped asPin<Box<dyn Stream<Item = Bytes> + Send>>TtyHandle.stderrisSome(separate streams)TtyHandle.exit_codeisPipeExitFuture(wait + kill guard)TtyHandle.controlisSome(TtyControlHandle::new(Arc::new(PipeControl)))PipeControl::resizeis a no-opPipeControl::signaluseskill(pid, sig)(NOT process group; documented limitation)- Unknown signal names fall back to
start_kill()(SIGKILL)
-
ADR-056: kill-on-Drop cancel-cleanup (tty-local.md §"Cancel-Cleanup"):
LocalExitFuture::DropcallsChildKiller::kill(SIGHUP)when dropped without resolvingLocalExitFuturedisarms the guard on resolve (no-op Drop on happy path)PipeExitFuture::DropcallsChild::start_kill()when dropped without resolvingPipeExitFuturedisarms the guard on resolve- Neither future is a bare
oneshot::Receiver/ bareChild::wait()(the POC's shape — MUST have the guard) - Integration test: drop the
TtyHandlemid-session, assert the child is killed (no orphan)
-
LocalTtyBackend conformance (tty-local.md, tty-backend.md):
LocalTtyBackend::new()takes no depsallocate()branches onparams.terminal(Some → PTY, None → pipe)resource_id()returnsNone- Empty
cmd→TtyError::AllocFailed backend_paramsignored (no backend-specific selector fields)
-
Dependency constraints:
portable_ptyandlibcdeps present (the heavy deps, here not in alknet-tty)alknet-ttydependency (workspace path) for the trait and types- No
alknet-coredirect dependency (accessed via alknet-tty if needed) - No
bollard/russh(those are future backend crates)
-
Pattern consistency:
TtyControlimpls usealknet_tty::TtyControltraitTtyControlHandle::new(Arc::new(...))wrapping (OQ-43 pattern)TtyErrorfor errors (notanyhow— the POC usedanyhow; the crate usesTtyError)tracingfor structured logging#[cfg(unix)]gates onlibc::killpaths
-
Test coverage:
- PTY: happy path, stdin round-trip, resize, signal (process group), cancel cleanup, unknown signal
- Pipe: happy path, stdin round-trip, separate stderr, signal, cancel cleanup, resize no-op, unknown signal
- Backend: PTY dispatch, pipe dispatch, empty cmd, resource_id
Acceptance Criteria
- PTY mode matches tty-local.md §"PTY Mode" (three-thread bridge, PtyControl)
- REQ-TTY-02 satisfied:
kill(-pgid, sig)with fallback, session leader, process-group test passes - Pipe mode matches tty-local.md §"Pipe Mode" (tokio::process, PipeControl, stderr Some)
- ADR-056 satisfied: both
LocalExitFutureandPipeExitFuturehave kill-on-Drop guards - Neither exit future is a bare
oneshot::Receiver/ bareChild::wait()(the POC's shape) LocalTtyBackendbranches onterminal,resource_idreturnsNoneportable_pty/libcdeps present; nobollard/russh/alknet-coredirect depTtyControlimpls use thealknet_tty::TtyControltrait +TtyControlHandle::newwrapping#[cfg(unix)]gates onlibc::killpathscargo fmt --check -p alknet-tty-localpassescargo clippy -p alknet-tty-localpasses with no warnings- All tests pass (PTY + pipe + backend)
- Cancel-cleanup integration tests confirm no orphaned processes
References
- docs/architecture/crates/tty/tty-local.md — the authoritative spec
- docs/architecture/crates/tty/tty-backend.md — the trait this implements
- docs/architecture/decisions/053-ttybackend-trait-and-ttyhandle.md — ADR-053 (REQ-TTY-01)
- docs/architecture/decisions/054-local-tty-backend-sibling-crate.md — ADR-054
- docs/architecture/decisions/056-backend-cleanup-on-session-cancel.md — ADR-056
- /workspace/alknet-tty-poc/src/local_pty.rs — the reference (note: POC lacks the kill guard)
- /workspace/alknet-tty-poc/tests/signal.rs — the SIGINT-forwarding test (REQ-TTY-02)
Notes
This review focuses on the three load-bearing requirements. The ADR-056 kill guard is the most likely deviation — the POC's
exit_codewas a bareoneshot::Receiver<i32>without a guard, and an implementer who copies the POC's shape verbatim violates the contract. Verify bothLocalExitFutureandPipeExitFuturehave the guard and the cancel-cleanup tests pass (no orphaned processes). REQ-TTY-02's process-group targeting is the other subtle requirement — verify the integration test spawns a shell with a child and confirms the child receives the signal.
Summary
To be filled on completion