feat(console): approver client — transport + auth + watch (read-only)#3
Merged
Conversation
T1a foundation. ratatui 0.30 (crossterm backend, immediate-mode, no async runtime) + crossterm 0.29 pinned to ratatui's re-export; serde/serde_json for the JSON-lines wire types; zeroize for PIN hygiene; insta (dev) for TUI render snapshots. cargo deny check is green on the new tree (advisories/bans/licenses/ sources ok) — the WTFPL/Unicode-DFS-2016 crates are transitive to the unused termwiz/termina backends and are not in the crossterm build graph.
Pure protocol layer (no I/O): Request encode + parse_hello/auth/list/get, and the domain types (Summary, Card, DecodedCall, Kind, Risk). Faithful to the canon: numeric wire strings (amount_wei decimal, decoded amount 0x-hex, raw_data) are kept as String and rendered verbatim (AGENTS.md #1) — a decoded amount is bignum-safe text, never a truncating integer parse; decoded_call is Option (null for bare/unknown); unknown fields ignored (§6 additive). auth request deferred to the transport layer (PIN must serialize into a Zeroizing buffer, not through the general Serialize path). console is now lib+bin so the layer is unit-tested in isolation. 18 tests; red proven by mutating the enum rename_all (kind/risk wire mapping).
The client's only owner of the UNIX stream: a dedicated worker thread holds a blocking std UnixStream (no async runtime — AGENTS.md) and does request→response; the MVU layer talks to it over two mpsc channels and never blocks on the slow approve path. hello handshake on connect; one request in flight (protocol §1). Every failure surfaces as Reply::Fatal without a panic — refused connect (NotConnected), mid-session EOF / read-side close (ConnectionLost), garbled or oversize reply line (Protocol), fatal hello mismatch (UnsupportedProto). PIN never travels as a plain String: auth carries a pre-serialized Zeroizing line. 8 fake-socket tests, deterministic (recv-block, no sleep); the read-side EOF guard is isolated from a write-side broken pipe (mutation-proven).
Pure update logic over messages (no render, no I/O): Connecting → Authing → Watching → Fatal. One request in flight (protocol §1) — the periodic list poll is suppressed while busy (never queued), and a user action taken meanwhile is parked in a single latest-wins slot so it is not lost. Watch behaviour: the selection follows an item's id across a refresh (not its index); a vanished selection on open shows a transient note, not a dead card; open/back toggles the card. PIN hygiene: a dedicated Pin type wraps Zeroizing<String> (zeroed on drop), redacts its digits in Debug (so a derived Model Debug cannot leak the PIN — note Zeroizing itself derives Debug and Serialize, hence the wrapper), accepts digits only (keeping the hand-built auth JSON injection-free), and builds the auth line by hand into a Zeroizing buffer rather than via serde (which would allocate an un-zeroized String copy). Request stays non-Debug so the PIN cannot surface even in a test failure. 11 tests; red proven by mutating the in-flight guard and the Pin Debug redaction.
Immediate-mode render as a pure function of the Model (ratatui 0.30). Screens: connecting, unlock (PIN shown only as dots — render_auth receives just the digit count, never the digits, so it structurally cannot leak them), watch (queue list + verbatim card), and fatal (the transport reason). The card renders the core's fields as received — address, decimal amount_wei, 0x-hex decoded amounts, and raw_data all verbatim (AGENTS.md #1), with no re-derivation; an UNLIMITED flag and high-risk reasons are surfaced from the core's own flags. Edge states: empty queue shows a waiting message; a vanished selection shows the transient note, not a dead card. 6 TestBackend tests; red proven by dropping raw_data from the card render.
Replace the stub with the real binary: ratatui::try_init (no-TTY → clear message + exit 3), the socket worker via Transport, and the MVU event loop — draw, read a key (Press only), map it to a Msg by phase, fold it into the Model, send any resulting request, drain worker replies, and tick the list poll every 2.5s. A fatal phase is shown until a keypress, then exits. ratatui installs the panic hook that restores the terminal. Key map and exit codes are pure functions with unit tests (Ctrl-C quits anywhere; digits/backspace/enter on the PIN screen; ↑↓/enter/esc/q in watch; connecting & fatal ignore ordinary keys; unsupported-proto → upgrade code). 5 tests. approve/deny + full TTY gate: C-PR-1b.
Three blockers (all in this branch's new code): - B1 main.rs: render to STDERR, not stdout (invariant #7). ratatui::init uses stdout; replaced with hand-rolled try_init/restore/set_panic_hook on a CrosstermBackend<Stderr>. enable/disable_raw_mode open /dev/tty directly, so only the alternate-screen escape + backend target stderr. run() unchanged (backend-agnostic via &mut Frame). Keeps stdout clean for C-PR-1b's decision. - B2 app.rs Pin::auth_line: reserved 21 bytes for a 22+len line → String realloc freed the old buffer (with the PIN) WITHOUT zeroizing. Now reserves PREFIX.len()+len+SUFFIX.len() via named constants (format changes stay correct). Test asserts capacity == len (no realloc) for pin_len 1..=12. - B3 transport.rs Transport::drop could hang forever: closing the request channel does not interrupt a worker parked in read_line, so join() blocked on a stalled server (Ctrl-C during a long wait → SIGKILL needed). Connect is now synchronous so a stream clone is kept; Drop shutdown(Both)s it to unblock the read before join. Test proves drop returns (recv_timeout) against a stall server. Minors folded in: render_detail wraps (never truncates a card value); impl Error for TransportError; drop unused crossterm/insta deps (all via ratatui::crossterm; no insta snapshots); verbatim render tests now assert label+value on one line (catches a field swap). Nits: pop-on-empty + nav-edge tests. Gates green: fmt/clippy-0/test 52/deny.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Console step T1a (C-PR-1a): the terminal approver client — connect → hello → PIN → watch the queue read-only.
approve/deny+ the full TTY gate land in C-PR-1b.Layers (console is now lib+bin)
docs/APPROVER-PROTOCOL.md. Numeric wire strings (amount_wei, decodedamount,raw_data) are kept asStringand rendered verbatim (invariant docs(protocol): reconcile APPROVER-PROTOCOL with shipped server (I1-I5, N1-N14) #1) — no parse-to-number at all;decoded_callisOption(null for bare/unknown); unknown fields ignored (§6).UnixStream(no async runtime — AGENTS.md); MVU ↔ worker over two mpsc channels, one request in flight (§1). Every failure surfaces asReply::Fatalwithout a panic.Dropshuts the socket down before join so a stalled server can't hang exit.Pintype wrappingZeroizing<String>with redacted Debug (Zeroizing itself derives Debug+Serialize), auth line hand-built into a Zeroizing buffer (not serde), digits-only (injection-safe);Requestis non-Debug so the PIN can't surface even in a test failure.Gates
fmt ✓ · clippy
-D warnings0 ✓ · test 52/0 ✓ ·cargo deny(advisories/bans/licenses/sources) ✓. Fake-UnixListenertests deterministic (recv-block, no sleep); TestBackend render; red→green proven by mutation across all layers. ratatui 0.30 API verified via context7.Review
Two-stage Gate-2: fleet (5 lenses) +
/security-review+/rust-review, fresh context — APPROVED after a BLOCKED round (3 blockers fixed: UI→stderr, PIN capacity off-by-one, Drop hang; + 4 minors). Delta re-reviewed line-by-line.approve/deny + TTY gate + default-reject: C-PR-1b.