Skip to content

docs(protocol): reconcile APPROVER-PROTOCOL with shipped server (I1-I5, N1-N14)#1

Merged
temrjan merged 1 commit into
mainfrom
docs/t0b-reconcile-protocol
Jul 9, 2026
Merged

docs(protocol): reconcile APPROVER-PROTOCOL with shipped server (I1-I5, N1-N14)#1
temrjan merged 1 commit into
mainfrom
docs/t0b-reconcile-protocol

Conversation

@temrjan

@temrjan temrjan commented Jul 9, 2026

Copy link
Copy Markdown
Member

Console step T0b — field-by-field reconciliation of the canonical wire protocol against the shipped server (core #86 + T0a #87).

What

  • 19 discrepancies resolved in docs/APPROVER-PROTOCOL.md (I1–I5 from the C-PR-1 spec + N1–N14 found by the audit). None required a server change — T0a's raw_data closed the only such gap.
  • Adds two normative tables: §3.7 field wire-formats (top-level to/amount_wei via Display = EIP-55/decimal vs nested decoded_call.* via serde = lowercase-hex/0x-hex) and §3.8 the full error-code vocabulary — the contract the T1a serde types mirror.
  • AGENTS.md invariant docs(protocol): reconcile APPROVER-PROTOCOL with shipped server (I1-I5, N1-N14) #1: renders DecodedCall + raw calldata verbatim; SimulationResult on 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.state includes pending (deny-vs-approve race); no error:expired; decoded_call may be null; 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.

Verification

Doc-only. Every claim verified against approver.rs/approval.rs/pin.rs/types and 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.

…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 temrjan merged commit fdf59a8 into main Jul 9, 2026
4 checks passed
@temrjan temrjan deleted the docs/t0b-reconcile-protocol branch July 9, 2026 04:40
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant