Skip to content

The first agent demo — see-and-stop Activity feed (#60) + headless runner (#61) + collapse/fold redesign - #67

Merged
hellno merged 16 commits into
mainfrom
hellno/wallet-gap-analysis
Jun 19, 2026
Merged

The first agent demo — see-and-stop Activity feed (#60) + headless runner (#61) + collapse/fold redesign#67
hellno merged 16 commits into
mainfrom
hellno/wallet-gap-analysis

Conversation

@hellno

@hellno hellno commented Jun 16, 2026

Copy link
Copy Markdown
Owner

The first agent demo — see-and-stop, and a demo you can actually run

Closes #60 and #61. Builds on the prior Tier-0 approval-queue work already on this branch.

#60 — the Activity feed (the see-and-stop surface)

  • Daemon: a new ActivityRecord ledger (additive + byte-stable wire; PendingRecord/ApprovalStatus untouched) that retains auto-allowed + executed agent actions — with tx_hash, a daemon-stamped timestamp, an auto_allowed flag (hands-free vs human-approved), and the actual breached cap (PerTxCap/DailyCap, recomputed off the frozen verdict path — kills the hardcoded "over per-tx cap" lie). Expire-on-read.
  • App: a Superhuman-style inbox + log — a "NEEDS YOU" triage band you clear to "All clear", then a settled log. Inline clear-signing review (with the real cite). A header STOP control. Two-signal fidelity restored (muted outcome label, the small glyph carries color, amber on rows you acted on). The shield payoff is now visible: "moved 0.02 ETH to your private balance".

#61 — a self-contained headless runner

scripts/demo-agent.sh + just demo-vault / just demo-deposit / just demo-agent: a poll → shield → execute loop over the same six key-less MCP tools (idempotent ratchet, over-cap re-poll, STOP/locked detection, mainnet refusal). No new authority, no new wire. The 60-second demo is three terminal commands + the app window. The Claude-Desktop prompt doc stays as the narrative version.

Redesign (from the design/product review — challenged the spec)

  • Collapsed the redundant Approvals surface into the feed and deleted it (the feed already inline-approves).
  • Deleted the fake "agent acting" demo toggle + breathing pulse — the real runner replaces them.
  • Folded the "Agent" entity into the Wallet — Atlas is key-less automation on the same EOA, not a separate account, so the policy fence now lives in the wallet cockpit and the "Agents" sidebar entity is gone.

Invariants held

Agent stays key-less; one approval path (Resolve over the control channel — no Execute on approve, no blind-approve from a list row); STOP zeroizes + denies in-flight + stays reachable; the verdict path (evaluate/propose/execute re-check/stop/mainnet guardrail) is frozen; wire additive + byte-stable; never signs chain_id = 0.

Verification

  • cargo fmt --all --check clean · just check (clippy -D warnings, default and --features tray) clean · 245 workspace tests · shellcheck clean.
  • Reviewed by a multi-agent workflow and codex (static, after its CLI hit an auth wall). All confirmed findings fixed — notably a CRITICAL blind-approve path and a policy-fence honesty bug after STOP.
  • Visually verified by driving the real app + the runner against an anvil Sepolia fork.

Screenshots (driving the real app — please attach via the web UI)

Captured at /tmp/deckard-qa-shots/final/:

  • 01-feed-inbox-log.png — NEEDS YOU (over-cap shield, selected, ⌘⏎ approve · x deny, the real per-tx-cap cite) + the executed-shield LOG with the privacy payoff + tx hashes.
  • 03-clear-signing-review.png — the inline review card citing the actual cap ("Per-transaction cap", not a hardcoded string).
  • 04-feed-log-payoff.png — the settled log: "moved X ETH to your private balance" (muted), a denied row in amber ("declined / not approved"), no Agents entity.
  • 05-wallet-no-agents.png — the wallet cockpit; the "Agents" sidebar entity is gone.

(CLI can't embed local PNGs — drag-drop these into the PR.)

Notes / minor follow-ups

  • The feed header (H1 + STOP) sits in a scroll container and can open slightly scrolled; STOP is always reachable via ⌘K (revoke-all). Worth a one-line scroll-to-top on open.
  • Codex's real cross-model pass needs an interactive codex login (its refresh token expired); the prompt is preserved at /tmp/codex-review/ for a re-run.

hellno added 5 commits June 15, 2026 17:58
Extend the wire contract + daemon so the GUI approval queue can render
agent-initiated over-cap proposals, ordered and counted down by expiry.

- contract: add ProposalOrigin{App,Agent}; carry it on SignerRequest::Propose;
  append remaining_ms + origin to PendingRecord (the inbox wire shape).
- daemon: stamp each PendingReq with its origin; PendingList now calls
  expire_stale() BEFORE building records (expiry is daemon-authoritative, never
  a bypassable UI timer) and computes remaining_ms from expires_at (panic-free).
- client: thread origin through propose / propose_blocking.
- tests: new pending_list suite (origin round-trip, expire-before-list,
  remaining_ms==0 on terminal); update propose callers across the e2e suites.
A top-level Approvals inbox and Activity ledger built on the daemon's
PendingList. Money-safety: approval ALWAYS routes through the clear-signing
review (no blind-approve from a list row); agent proposals are Resolve-only —
the app never broadcasts, the agent executes its own write once Allowed.

- approvals_view: dense soonest-to-expire queue (loading / empty / fail-loud /
  expired states) + the clear-signing review (agent band, danger-early,
  ⌘Enter approve / Esc cancel).
- activity_view: terminal records as the two-actor "Atlas → You" chain,
  honestly labelled session-scoped (in-memory).
- shell: Approvals + Activity surfaces, ⌘⇧A, a PendingList poller, and
  resolve-only approve/deny that is non-optimistic (re-fetch authoritative
  status on success, fail loud on error).
- shell_chrome: sidebar Approvals entry with an amber pending-count badge,
  plus an Activity entry.
- palette: Approvals / Activity / Approve / Deny commands. signer: resolve_blocking.
- sidecar: propose with ProposalOrigin::Agent so the queue attributes the
  agent (Atlas) versus a human-composed action.
- copy: the needs-approval guidance (sidecar / server / failure) no longer
  says "the approval UI is not in this alpha" — it now sends the human to the
  Deckard app's Approvals queue (⌘⇧A) + clear-signing review card.
- acceptance: assert the shield tool docs carry the "Approvals queue" keyword.
- common: ignore origin when destructuring Propose in the mock.
…ess runner (#61) + Superhuman collapse/fold redesign

#60 — the Activity FEED (not just an inbox): a daemon-side ActivityRecord ledger
(additive, byte-stable wire) that retains auto-allowed + executed agent actions
(tx_hash, timestamp, the ACTUAL breached cap — per-tx vs daily, daemon-computed,
no hardcoded lie), an honest auto_allowed flag (hands-free vs human-approved), and
expire-on-read. App: a Superhuman-style inbox — a "NEEDS YOU" triage band you clear
to "All clear" + a settled LOG, inline clear-signing review, a STOP control, two-signal
fidelity (muted label / glyph carries color / amber on rows you acted on), and the
shield payoff made visible ("moved X ETH to your private balance").

#61 — a self-contained headless runner (scripts/demo-agent.sh + just demo-vault/
demo-deposit/demo-agent) that drives the same 6 key-less MCP tools in a
poll -> shield -> execute loop (idempotent ratchet, over-cap re-poll, STOP/locked
detection, mainnet refusal). No new authority, no new wire. The Claude-Desktop prompt
doc stays as the narrative version.

Redesign (post-review): collapsed the redundant Approvals surface into the feed and
deleted it; deleted the demo "agent acting" toggle + breathing pulse (the real runner
replaces them); folded the "Agent" sidebar entity INTO the Wallet (Atlas is key-less
automation on the same EOA, not a separate account) — the policy fence now lives in
the wallet cockpit.

Invariants held: agent stays key-less; ONE approval path (Resolve over the control
channel, no Execute on approve, no blind-approve); STOP zeroizes + denies in-flight;
the verdict path is frozen; wire is additive + byte-stable; never signs chain_id=0.

Verification: cargo fmt clean; clippy -D warnings clean (default + tray); 245 workspace
tests; shellcheck clean. Reviewed by a multi-agent workflow + codex (static); all
confirmed findings fixed (notably the CRITICAL blind-approve path and a policy-fence
honesty bug after STOP).
@hellno

hellno commented Jun 16, 2026

Copy link
Copy Markdown
Owner Author

Screenshots — driving the real app (anvil Sepolia fork, runner-populated feed)

The feed — inbox + log. The over-cap shield sits in NEEDS YOU (selected, ⌘⏎ approve · x deny, the real over per-tx cap cite); the executed shields fall to the LOG with the privacy payoff + tx hashes. Two-signal reads again (muted labels, glyph carries color). The Activity nav shows the amber needs-you badge, and there's no "Agents" entity.
feed inbox + log

Clear-signing review — opened inline from the feed, citing the actual cap ("Per-transaction cap"), not a hardcoded string.
clear-signing review

Settled log — "moved 0.0x ETH to your private balance" (the payoff, muted), a denied row in amber ("declined / not approved" — you acted on it), real tx hashes + relative time.
feed log + payoff

The wallet cockpit — the "Agents" sidebar entity is gone; Atlas folded into the wallet (its policy fence lives here now).
wallet, no agents


Verified: fmt clean · just check clean (clippy -D warnings, default and tray) · 245 workspace tests · shellcheck clean. Multi-agent review + codex (static) — all confirmed findings fixed (incl. a CRITICAL blind-approve path + a policy-fence honesty bug after STOP).

Minor follow-up: the feed header can open slightly scrolled, clipping the in-header STOP (always reachable via ⌘K → "STOP"); a one-line scroll-to-top-on-open would polish it. Codex's real cross-model pass needs an interactive codex login (token expired) — prompt preserved for a re-run.

…inherited EIP-7702 delegation)

UX (#2): the feed heading + STOP control are now PINNED outside the scroll region, so the
panic brake can never scroll off screen — only the body (needs-you + log) scrolls beneath it.
(The header was inside the scroll container before; in a normal-width window STOP showed, but it
could scroll away — and a safety control shouldn't.)

Demo (#4 root cause): `just demo-deposit` now clears any inherited EIP-7702 delegation on the
demo wallet before funding it. The demo wallet is anvil's PUBLIC account-0 key, and on the
forked Sepolia someone had 7702-delegated that address (its on-chain code = 0xef0100<delegate>).
A plain ETH transfer to a 7702-delegated account runs the DELEGATE contract instead of crediting
the EOA, so the balance never rose and the agent's deposit-delta detector never fired (shields,
which only SIGN, still worked — which is why it looked half-broken). Clearing it back to a plain
EOA makes the loop fully hands-free: deposit arrives -> Atlas notices -> shields -> broadcasts,
no manual MCP commands. Verified end-to-end against the live app + runner.

Plus the new STOP-flow screenshots (pinned STOP, armed "Confirm STOP", the revoked banner).

fmt + clippy (default + tray) + 245 workspace tests + shellcheck all green.
@hellno

hellno commented Jun 16, 2026

Copy link
Copy Markdown
Owner Author

Follow-ups: the demo is now truly hands-free, and STOP is pinned

#4 — why the agent never noticed deposits (root cause). The demo wallet is anvil's public account-0 (0xf39F…2266). On real Sepolia, someone used that public key to set an EIP-7702 delegation on it (its on-chain code is 0xef0100 + a contract address). just demo forks Sepolia, so it inherits that delegation — and a plain ETH transfer to a 7702-delegated account runs the delegate contract instead of crediting the EOA. The balance never rose, so the agent's "did a deposit arrive?" detector never fired (shields still worked because they only sign). just demo-deposit now clears the delegation back to a plain EOA. Result — fully hands-free, zero manual commands:

Atlas: noticed +0.02 ETH · proposing shield of 0.02 ETH…
Atlas: auto-approved within cap · broadcast ✓ 0xbe0e34…

(Delightful irony: EIP-7702 is exactly Deckard's deferred "agent wallet" model.)

#2 — STOP is now pinned. The heading + STOP control moved outside the scroll region, so the panic brake can never scroll off screen. The full flow, driven live (the shield 0.02 row below is the runner acting on its own):

Pinned STOP + hands-free shield Armed Fired
stop pinned armed revoked

STOP arms on the first click ("Confirm STOP — revoke & lock signing · Esc to cancel"), fires on the second → the "Stopped — key zeroized" banner, and the runner reports the kill cleanly (STOP — key zeroized, in-flight denied; unlock to re-arm).

All green: fmt · clippy (default + tray) · 245 workspace tests · shellcheck.

hellno added 3 commits June 16, 2026 17:28
Resolved one conflict in crates/deckard-mcp/src/sidecar.rs: main refactored the sidecar to
delegate chain-probing + the low-level socket request to the new deckard-wallet-client crate
(PR #56 browser-bridge-out-of-mcp). Took main's structure (self.wallet.ensure_chain() + the
trimmed deckard_contract imports), keeping ProposalOrigin since shield()/execute() still tag
their proposes Agent-origin (the #60 feed shows those as "Atlas").

Carried this branch's additive wire change (SignerRequest::Propose gained an `origin` field)
into the merged-in deckard-wallet-client: its connect-time chain probe now passes
`origin: ProposalOrigin::Agent` (was failing to compile without it).

Verified green on the merged tree: cargo fmt --check, clippy -D warnings (default + tray),
full workspace tests, shellcheck.
…lock #66)

Resolved conflicts so the Swap surface (main) and the Activity feed (this branch) coexist:
- Surface enum + view_label + render dispatch: keep BOTH Surface::Activity and Surface::Swap.
- lock() reset: take main's shield/send/swap sub-struct resets (the CommitFlow refactor moved
  shield/send state into self.shield/self.send and added self.swap) AND keep this branch's
  activity STOP-banner/arming/review clears.
- Send/Shield render dispatch: take main's generic render_commit(&{SEND,SHIELD}_VIEW); insert the
  Activity arm; keep main's Swap arm.
- Shield/Send review propose: take main's recipient-snapshot return tuple AND keep this branch's
  `origin: ProposalOrigin::App` arg (the wire's Propose requires origin).
- palette_commands: keep main's `swap` command + this branch's activity/approvals/STOP commands;
  dropped main's brittle `assert_eq!(COMMANDS.len(), 11)` (the merged registry is larger) in favor
  of membership asserts.
- swap.rs: main's swap shaped-approve propose_blocking now passes ProposalOrigin::App (App-origin —
  the user's foreground swap from the GUI), required by this branch's wire change.

Verified green on the merged tree: cargo fmt --check, clippy -D warnings (default + tray), full
workspace tests (0 failures), shellcheck.
… human swap "Atlas"

Codex/two-signal finding: a user-initiated GUI swap produced two feed rows with MISMATCHED
actors — the order row hardcoded ProposalOrigin::Agent (cyan "Atlas wants to swap…") while its
companion exact-gross approve was correctly App (amber "You…"). A foreground human action narrated
as the agent's action is exactly the trust-signal confusion the two-signal model exists to prevent.
propose_order is only ever called from the app, so every order in the feed was mislabeled; the
Swap surface is unflagged + user-reachable, so it's user-visible on Sepolia/mainnet.

Fix (symmetric to Propose, which already carries origin): SignerRequest::ProposeOrder gains an
additive `origin` field, threaded through Daemon::propose_order + SignerClient::propose_order[_blocking];
the daemon stores the caller's origin instead of a hardcoded Agent; the app's GUI swap (swap.rs +
shell.rs) passes App. Reserves Agent for a future sidecar swap path. Display-only — the verdict path
(evaluate_order) is untouched. Round-trip test extended for both origins; all call sites updated.

Verified: fmt --check, clippy -D warnings (default + tray), full workspace tests (0 failures).
@hellno

hellno commented Jun 16, 2026

Copy link
Copy Markdown
Owner Author

Merged latest main (Swap v1 GUI #25 + CommitFlow refactor + #66) + codex review

Brought the branch up to date with main (which landed Swap + a surface refactor in parallel). Resolved 8 conflict regions across shell.rs/shell_chrome.rs/palette_commands.rs/swap.rs so the Swap surface (main) and the Activity feed (this branch) coexist; threaded the branch's additive origin wire field through main's new swap/CommitFlow call sites.

Codex (GPT-5 xhigh, read-only) reviewed the merge integration + hunted P0s. It (and an independent source cross-check) confirmed every conflict resolution is semantically correct — exhaustive surface match, no dropped flow state on re-unlock, the right propose tuple + origin, the palette in sync, the earlier cancel→Executed bug still closed, and no invariant regression (key-less agent, single private-control approval path, no blind-approve, STOP zeroize/reachability, frozen verdict path, byte-stable wire).

It found one real bug — a two-signal mislabel: a user's GUI swap rendered the order row as "Atlas wants to swap…" (cyan/agent) because the daemon hardcoded swap orders Agent and ProposeOrder carried no origin, while the companion approve was correctly "You" (amber). Pre-existing in main, but the actor-colored feed makes it wrong + visible (Swap is unflagged + user-reachable). Fixed in 25625d2: ProposeOrder gains an additive origin (symmetric to Propose), the GUI swap passes App, Agent reserved for a future sidecar — display-only, verdict path untouched.

Merged tree is green: fmt · clippy -D warnings (default + tray) · full workspace tests (0 failures) · shellcheck.

hellno added 7 commits June 17, 2026 15:35
…dows so the STOP control + row trailing cluster never clip off the right edge)
…o-deposit chain guard

Codex full-pass must-fix: refresh_activity reconciled the feed selection by RAW NUMERIC INDEX, not
by request_id. The ~2s poller can insert/remove proposed rows between renders, so a stale index
could point at a DIFFERENT pending record than the operator sees highlighted — and `x`/deny (and
the palette deny-selected) resolve the SELECTED record. Result: a deny could fire against the wrong
pending card under background churn. (Wrong-APPROVE was never reachable — approve self-guards by
opening the request_id-keyed review first.) Fix: preserve the highlight by request_id across every
refresh — capture the selected id from the old snapshot, re-key to it in the new pending subset, and
clamp only if it's gone. Corrected the now-accurate approve/deny comment (deny is one-key by design:
it only REFUSES a spend — the fail-safe direction — and now targets the highlighted record by id).

demo-deposit HIGH footgun: the chain guard only refused chain 1, so any OTHER real chain fronted at
:8545 (a forwarded Sepolia/Holesky node, a tunnel) would pass and `cast send` with a PUBLIC dev key.
Switched to an allowlist — refuse unless the RPC reports the exact demo chain id (matches the `demo`
recipe's own enforcement).

Verified: fmt --check, clippy -D warnings (default + tray), full workspace tests (0 failures), shellcheck.
…mo runner + amber-honesty

Three codex-pass follow-ups, all approved as fix-now.

1. STOP-integrity hole (HIGH, swap path). `stop()` skipped the on-chain `invalidateOrder` for any
   order whose LOCAL approval record had lapsed to `ApprovalStatus::Expired` (the 120s approval
   TTL). But a signed order is loose on the CoW orderbook and settleable until its REAL `valid_to`
   (up to 24h) — those are different clocks. So a signed order could still settle AFTER STOP. Fix:
   gate STOP's cancel on the order's `valid_to`, never the local `Expired` status; skip only
   genuinely-unsettleable (`valid_to` in the past) orders. Extracted the selection into a pure
   `select_orders_to_cancel(requests, now)` with unit tests (a locally-Expired-but-settleable order
   IS selected; unsigned / already-cancelled / past-valid_to / plain-Tx are skipped).

2. Demo runner redesign (HIGH+LOW, scripts/demo-agent.sh). The loop tracked deposits with a single
   high-water mark it ratcheted BOTH up (on deposit) and down (after a shield settled). That
   dual-direction scalar raced the settlement: a deposit landing in the ~2s window between a
   broadcast and the balance dropping got absorbed by the downward rebaseline and lost (and an
   unexpected shield reply orphaned a deposit). Replaced with a FIXED baseline set once at startup
   — work each poll is just (public - baseline), new proposals gated on "nothing in flight", and the
   daemon's deterministic request_id is the idempotency authority (re-propose returns the same
   record as-is). No ratchet, no race, less code. Aligned docs/build/32-agent-loop-prompt.md to the
   same model.

3. Amber-honesty (MEDIUM, the feed). A lapsed card mapped to `Decided{approved:false}`, so the feed
   tinted it amber "you acted" / "not approved" even though NOBODY acted. Split out a new additive
   `ActivityLifecycle::Expired` (roundtrip-tested): a human denial / STOP revoke stay
   `Decided{false}` (a human DID act → amber is honest); a lapsed window renders neutral (muted
   "expired" label + a neutral dash glyph, never amber). The two-signal "amber = a human acted"
   promise now holds.

Verified: cargo fmt --check, just check (clippy -D warnings, default + tray), cargo test --workspace
(272 passed, 0 failed), shellcheck + bash -n on demo-agent.sh.
A high-recall /code-review pass (7 angles → verify) over our authored #60/#61 + fixes diff surfaced
five real, reachable bugs — every one in code added this session. All fixed, with regression tests.

1. Runner wedges forever (HIGH, scripts/demo-agent.sh). try_execute() only dropped a saved id on
   broadcast / STOP / user_denied; an `expired` (120s approval-TTL lapse) fell through to "keep
   waiting", so the id stuck in PENDING_IDS — and the redesign's `pending_count == 0` gate then
   blocked EVERY future shield. Same for a re-proposed user_denied card (propose path treated it as
   transient → infinite "retry" spam). Fix: classify the daemon's terminal verdicts
   (mcp_is_terminal_refusal = expired | a human denied | user_denied) in BOTH try_execute and the
   propose-failure handler — drop the id AND advance the baseline past the refused funds (recorded
   per-id in a new PENDING_BASE array), so the loop gives up on dead funds and keeps watching for
   new ones. Aligned docs/build/32-agent-loop-prompt.md (it had the same expired-handling gap).

2. `--once` smoke shields the whole wallet (MED, justfile). Pinning baseline=0 against the prefunded
   demo wallet (anvil acct 0 holds ~10 ETH on the fork) treated the whole balance as surplus → over
   the per-tx cap → no broadcast → exit 1. Added `just demo-smoke`: captures the PRE-deposit
   balance, deposits, then runs --once pinned to that baseline so it shields exactly the deposit.
   The bare --once no longer defaults baseline to 0 (reads live balance = safe no-op).

3. Approved-then-lapsed erased the amber signal (MED, daemon.rs). A human-approved over-cap card
   that lapsed before execute mapped to ActivityLifecycle::Expired (neutral) — erasing that a human
   acted. Fix: activity_lifecycle now maps `Expired if req.approved` → Decided{approved:true}.

4. STOP-revoke of an auto-allow showed muted, not amber (MED, activity_view.rs). The amber tint
   keyed on bare `!auto_allowed`, but lock() leaves auto_allowed=true on a STOP-revoked row. A STOP
   is a human action → must read amber. Extracted a tested `human_acted` helper (any
   Decided{approved:false} OR !auto_allowed) and keyed the tint on it.

5. Stale activity_reviewing → dropped refusal (MED, shell.rs). resolve_activity_target preferred a
   set `activity_reviewing` even after a poll settled that record and the render fell through to the
   feed, so the first `x` resolved a stale off-screen id (a silent no-op) instead of the highlighted
   row. Fix: honor `activity_reviewing` only while its id is still in the pending subset, else fall
   back to the highlighted row.

Verified: fmt --check, just check (clippy -D, default + tray), cargo test --workspace (274 pass, 0
fail — +2 regression tests), shellcheck + bash -n.
…a blind-approve regression)

A second adversarial pass over the prior fix commit (5734602) caught that two of those fixes
introduced NEW bugs — exactly why fixes get the same scrutiny as features.

1. BLIND-APPROVE regression (HIGH, shell.rs). Fix #5's filter fallback turned a safe no-op into a
   real wrong-row resolve: when a review for card X is open and a background poll expires X, the
   render falls through to the feed (it's &self, can't clear activity_reviewing) with a DIFFERENT
   over-cap card Y highlighted. A one-key ⌘Enter then approved Y — whose clear-signing card was
   never shown — violating "no blind-approve of a spend" (the agent would then execute Y's overage).
   Real fix: clear `activity_reviewing` IN refresh_activity, atomically with the snapshot swap, when
   its id leaves the pending set. Now approve sees None → re-opens the highlighted row's review
   (no blind-approve), and deny targets the visible highlighted row (fail-safe, not the old no-op).

2. False-positive `expired` match (HIGH, demo-agent.sh). mcp_is_terminal_refusal grepped a BARE
   "expired", which also matches transient RPC errors ("connection expired", "session expired")
   embedded in broadcast_failed/signer_error prose — mis-classifying a transient failure as terminal
   and abandoning a still-valid card's funds. Anchored to the exact daemon copy ("this request
   expired", "a human denied this request"). Verified: terminal copies match, RPC errors don't.

3. Two-signal mismatch (MED, activity_view.rs). The amber LABEL was re-keyed to `human_acted` but
   the "→ You" actor-chain link still gated on bare `!auto_allowed`, so a STOP-revoked auto-allow
   showed the amber label with NO human in the chain. Re-keyed `needed_human` to `human_acted` too.

4. Propose-path baseline clarity (demo-agent.sh). `BASELINE_WEI=public_wei` is numerically the
   just-now propose-time balance (symmetric with try_execute's PENDING_BASE), but read as "absorb
   the whole wallet". Rewrote as `wei_add BASELINE_WEI surplus` (identical value) so the intent —
   "skip exactly the funds the daemon just refused" — is plain.

Verified: fmt --check, just check (clippy -D, default + tray), cargo test --workspace (274 pass, 0
fail), shellcheck + bash -n, and a matcher-precision simulation.
…e verify loop found)

Round-3 verification found a THIRD path to the same blind-approve hazard the round-2 fix only
partially closed: `review_activity_row` (the proposed-row click handler) set `activity_reviewing`
UNCONDITIONALLY, so a click landing one frame after a background poll settled that row left a stale
review id with no refresh before the next ⌘Enter — and `approve_activity`'s `.or_else(highlighted)`
fallback then resolved a DIFFERENT highlighted over-cap row whose clear-signing card was never shown.

Whack-a-mole on entry points won't converge, so this is the DEEP fix: approve now resolves ONLY the
actively-reviewed, STILL-PENDING record — it has NO highlighted-row fallback. The decision is a pure
`approve_target(reviewing, pending)` helper (returns the id to resolve, or None = "open a review
first, resolve nothing"), so a blind-approve of an unreviewed spend is structurally impossible no
matter how `activity_reviewing` went stale (poll settle, raced click, anything). Deny keeps its
one-key highlighted-row fallback (it only REFUSES — the fail-safe direction).

Also hardened `review_activity_row` to set `activity_reviewing` only when the clicked row is still
pending (no phantom review for a just-settled row). The round-2 refresh-time clear stays as state
hygiene but is no longer load-bearing for the invariant.

New unit test `approve_target_never_falls_back_to_the_highlighted_row` pins the guarantee: a
reviewed id that has left the pending set resolves NOTHING (never the other highlighted row).

Verified: fmt --check, just check (clippy -D, default + tray), cargo test --workspace (275 pass, 0
fail), shellcheck.
Brings in main's browser-bridge provider events, wallet balance refresh, swap UX fixes
(one-hold/repeat-amount/hardening), and release/docs work.

Conflicts resolved:
- palette_commands.rs: kept ours — our branch added activity/approvals/STOP commands beyond main's
  swap, so COMMANDS.len() is >12; dropped main's brittle hardcoded `== 12` for the membership asserts.
- anvil_e2e.rs: kept BOTH new tests (ours `activity_feed_executed_carries_tx_hash` #60 + main's
  `repeat_same_amount_swap_approve_is_not_replay_blocked`) as separate fns + main's helpers; unioned
  the imports (ActivityLifecycle/BreachedLimit/ProposalOrigin + SwapOrder).

Semantic merge fix (git didn't flag — clean auto-merge, wouldn't compile): main added new
propose/propose_order calls with the OLD single-arg signature in shaped_approve.rs
(`shaped_approve_requires_a_live_pending_order`); adapted them to our `(intent, ProposalOrigin)`
signature (App for the order, Agent for the shaped approve), matching the file's existing calls.

Verified: cargo check --workspace --all-targets, fmt --check, just check (clippy -D, default + tray),
cargo test --workspace (277 pass, 0 fail), shellcheck.
@hellno
hellno merged commit f2ef34f into main Jun 19, 2026
5 checks passed
@hellno
hellno deleted the hellno/wallet-gap-analysis branch June 19, 2026 14:16
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