docs(protocol): reconcile APPROVER-PROTOCOL with shipped server (I1-I5, N1-N14)#1
Merged
Merged
Conversation
…5, N1-N14) Field-by-field reconciliation of the canonical wire protocol against the shipped server (core #86 + T0a #87). 19 discrepancies resolved; none required a server change (T0a's raw_data closed the only such gap). Key corrections: risk safe|warning (not low|med|high); server 0.1.0 (not 0.2.0, informational — proto is the only compat gate); already_resolved.state includes pending (deny-vs-approve race); no error:expired code; decoded_call may be null; simulation removed (A' deferred to v0.2). Adds a normative field wire-format table (top-level to/amount_wei via Display = EIP-55/decimal vs decoded_call sub-fields via serde = lowercase-hex/0x-hex) and the full error-code vocabulary. get/list allowed pre-auth (perimeter = socket fs 0700, not the handshake). oversize non-close documented as shipped + core follow-up #27. Lockout audit gap noted vs carry-over #26. AGENTS.md invariant #1: renders DecodedCall + raw calldata verbatim; SimulationResult on the card deferred to v0.2 (A').
temrjan
added a commit
that referenced
this pull request
Jul 9, 2026
…#3) * build(deps): add ratatui/crossterm/serde/zeroize for the approver client 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. * feat(protocol): wire types + codec mirroring APPROVER-PROTOCOL 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). * feat(transport): socket worker thread + request/response + errors 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). * feat(app): MVU model + one-in-flight intents + PIN hygiene 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. * feat(ui): watch/auth/card render (verbatim) + edge states 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. * feat(console): wire the event loop, key mapping, exit codes 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. * fix(console): Gate-2 — UI→stderr, PIN capacity, Drop shutdown + minors 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.
temrjan
added a commit
that referenced
this pull request
Jul 10, 2026
First minor: C-PR-1 complete (transport + auth + watch + approve/deny, invariants #1-#7 covered, 130 tests). The v0.1.0 tag publishes the binary-carrier image ghcr.io/rustok-org/rustok-console consumed by mcp/Dockerfile.wallet via ARG CONSOLE_IMAGE.
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 T0b — field-by-field reconciliation of the canonical wire protocol against the shipped server (core #86 + T0a #87).
What
docs/APPROVER-PROTOCOL.md(I1–I5 from the C-PR-1 spec + N1–N14 found by the audit). None required a server change — T0a'sraw_dataclosed the only such gap.to/amount_weivia Display = EIP-55/decimal vs nesteddecoded_call.*via serde = lowercase-hex/0x-hex) and §3.8 the full error-code vocabulary — the contract the T1a serde types mirror.AGENTS.mdinvariant docs(protocol): reconcile APPROVER-PROTOCOL with shipped server (I1-I5, N1-N14) #1: rendersDecodedCall+ raw calldata verbatim;SimulationResulton the card deferred to v0.2 (decision A′).Highlights
risk=safe|warning;server=0.1.0(informational — proto is the only compat gate);already_resolved.stateincludespending(deny-vs-approve race); noerror:expired;decoded_callmay benull;get/listallowed pre-auth (perimeter = socket fs 0700, not the handshake).oversizenon-close documented as shipped + core follow-up #27; lockout audit gap noted vs carry-over #26.Verification
Doc-only. Every claim verified against
approver.rs/approval.rs/pin.rs/typesand the dependency sources (alloy-primitives 1.6.0, ruint 1.17.2); field formats proved with an isolated scratch crate. Reviewer verified the final text field-by-field: Gate-2 clean, 0 discrepancies.