Repository navigation
Description
Part of #44 — the thin, forward-compatible slice of #48 (registry, anti-phishing, connect-time approval, per-origin policy all stay in #48). This is the ending beat of the browser demo: connect → attribute → revoke → the site is locked out.
Why: a connection you can't see or kill isn't under your control. This makes every connected site visible in the app and revocable in two keystrokes.
Corrected 2026-07-10 (codebase re-verified + scope decision): the structural claims below are confirmed. Decision (2026-07-10): revoke is a session-scoped disconnect, NOT a persistent ban — see "Scope decision". That keeps this light and likely avoids the heavy daemon-held-store move the original design assumed.
Problem
Dapp sessions live only in the bridge process RAM (DappSessionStore, crates/deckard-browser-bridge/src/lib.rs:49-102); its revoke() is dead code (#[allow(dead_code)], lib.rs:91, only caller is the unit test at lib.rs:1395); the app's Connections sidebar is a hardcoded, non-interactive "No connected sites yet" placeholder (shell_chrome.rs:191-199). The app has no channel to the bridge — it speaks only SignerRequest to the daemon, and the bridge is a separate process the app never launches. Sessions are invisible and unkillable.
Scope decision (2026-07-10)
Revoke = disconnect, not ban. Clicking revoke drops the current session so the site is
disconnected; its next request fails 4100 Unauthorized and it must re-run eth_requestAccounts to
get a fresh session. There is NO persistent blocklist and NO long-term "who is banned" memory —
this is the "disconnect" model (like MetaMask), not a permanent block. Rationale: keep it light for the
alpha; the richer per-origin permission/registry model is #48. See ADR 0006 (#202).
Consequence for the build: because a revoked site is simply disconnected (and may reconnect), we do
not need a persistent daemon-held grant store. The lightest thing that gives a real disconnect beat
is enough — expose the bridge's live session list to the app and let the app clear a session:
- Sessions can stay where they already live (bridge RAM,
DappSessionStore) — the missing piece is
an app ↔ bridge channel to list + clear them (the app speaks onlySignerRequestto the daemon
today, and the bridge is a separate process). The PR settles whether that channel is a minimal
app→bridge call over the existing loopback or a small daemon relay; pick the smaller diff. The heavy
daemon-held-store + full Wire-contract evolution: Hello capability discovery + the five evolution rules #31 wire tax the original design assumed is not required for a
disconnect-only revoke. - Wake the dead code:
revoke()is#[allow(dead_code)]today (lib.rs:91) — this issue gives it
a real caller. Keep the4100re-connect semantics (lib.rs:364-367).
App: the Connections sidebar lists live origins (mirror the refresh_activity poll shape,
shell.rs:2585-2638); new ⌘K command revoke-site ("Revoke connected site", aliases: disconnect /
revoke site / unlink) in palette_commands.rs + Shell::run_palette_command. A registry test asserts
revoke-all is the LAST entry (palette_commands.rs:416) — insert the new command before the
revoke-all entry (:178) and that assertion still holds.
Lifecycle: Lockdown (RevokeAll) and Lock clear all sessions — the global stop reduces ALL authority
(#203). Bridge/daemon restart clears them too (in-memory).
Reality check (2026-07-10)
- Size: with the disconnect-only decision, this is back toward the original ~350–450 LOC if the
session store stays in the bridge and we add only a list/clear channel + the sidebar poll + the ⌘K
command. It only balloons to ~500–700 LOC if we choose to move the store into the daemon (adding the
Wire-contract evolution: Hello capability discovery + the five evolution rules #31 wire tax below) — which the decision says we can skip. - If you DO move the store to the daemon (optional), the wire tax hits four places: the daemon
dispatcher (daemon.rs:309-385), the second, fully-exhaustive, no-catch-allMockDaemon
(crates/deckard-mcp/tests/common/mod.rs:155-294), round-trip freeze fixtures
(deckard-contract/src/lib.rs, per Wire-contract evolution: Hello capability discovery + the five evolution rules #31), and acapabilities.rsentry. Prefer the lighter path. - Fold in the neighbours (both touch
daemon.rs, near-free here): Rename the global stop to Lockdown #203 (Lockdown rename — this
issue's lifecycle copy references it) and perf(signerd): #216 follow-up · bound unbounded per-session self.requests growth #218 (prune the unbounded per-session request map). - Connect-approval gap (
eth_requestAccountsgrants with no human approval,lib.rs:228-238) is
assigned to PRD-05: Per-origin permissions, registry & anti-phishing #48, not this issue — consistent with disconnect-not-ban (a reconnect re-grants).
Tests
- Daemon: grant → list → revoke round-trip; revoked origin filtered from list.
- Bridge: revoked origin → 4100 (extend
send_transaction_requires_connected_account,lib.rs:1470-1483). - Wire round-trips for the three new variants (Wire-contract evolution: Hello capability discovery + the five evolution rules #31 pattern).
RevokeAll/Lockclear sessions.- Palette registry↔handler reflective test.
Acceptance
- Connect from the fixture dapp → the origin appears under Connections (screenshot).
- ⌘K → Revoke connected site → the next dapp request fails 4100; the row is gone (screenshot).
- Lockdown clears all sessions.
- DoD green; before/after screenshots.
Activity
feat(connections): #199 · list live dapp sessions in the app, revocab…
fix(connections): #199 · sequence daemon lock/revoke BEFORE the bridg…
fix(connections): #199 · /code-review hardening — poll lifecycle, hon…