feat(connections): #199 · list live dapp sessions in the app, revocable per site#223
Merged
Conversation
…le per site The ending beat of the browser demo: connect → attribute → revoke → the site is locked out. Revoke is a session-scoped DISCONNECT, not a ban (ADR 0006): the revoked site's next request fails 4100 Unauthorized and it may reconnect via eth_requestAccounts for a fresh session. Bridge: sessions stay in bridge RAM (DappSessionStore gains list/revoke_all; revoke() wakes from dead code). New POST /admin on the existing loopback server (list | revoke | clear — every op returns the post-op live list). Gate: /admin requires the x-deckard-admin header, which is deliberately NEVER CORS-allowed — a web page can't clear the preflight, so only local processes (the app) can enumerate or disconnect sessions. No daemon/wire/contract changes at all — the daemon-relay alternative would pay the #31 wire tax for zero alpha benefit. App: hand-rolled blocking loopback client (bridge_admin.rs, no new deps), a 5s whole-session connections poll mirroring the #200 pending poll (epoch- fenced + in-flight-guarded, plus a connections_epoch so a stale poll reply can't resurrect a just-revoked row), sidebar Connections rows (Globe + host + ghost ✕), the rail's Connections count goes live, and ⌘K gains revoke-site with a live label (1 site → its host; N → "all N") whose action always matches the label. Lock and Lockdown best-effort-clear all bridge sessions. Tests: revoke→4100→reconnect-fresh round-trip, admin dispatch semantics, the admin gate, a const-level CORS invariant, response-parser + origin_host units, registry order, and a new reflective registry↔handler test (every ⌘K id must have a run_palette_command arm). Closes #199
…e session clear Codex adversarial review (gpt-5.5 xhigh) finding: Lock and Lockdown fired the daemon key-zeroize and the bridge clear_sites as two unordered detached tasks. If the bridge clear won the race, a dapp reconnecting via eth_requestAccounts in the window before the daemon locked would be granted a fresh session that survives the lock. Sequenced in one background task — daemon authority down first, then the bridge disconnect — that reconnect now hits a locked daemon and grants nothing. Also documents the accepted low-severity edge codex flagged: two OVERLAPPING per-site revokes whose loopback replies reorder can leave the slower row on screen until the next 5s poll tick reconciles (every revoke IS applied on the bridge; only the interim display can lag).
…ocation cargo-deny-action v2.1.0 (released 2026-07-13 09:32 UTC, between main's last green run and this PR's first run) removed the use-git-cli positional from action.yml without updating the entrypoint's positional indexing, so the log-level value lands in a command slot and every run dies with `unrecognized subcommand 'warn'` — on both the advisories and supply-chain jobs, and it would take the nightly audit down too. Pin all three uses to the last-good v2.0.20; unpin to @v2 once upstream ships a fix.
hellno
force-pushed
the
hellno/browser-session-revoke
branch
from
July 13, 2026 10:01
7f737e7 to
9d27ef6
Compare
Owner
Author
|
Post-review updates on this branch:
DoD re-run after all of the above: fmt clean, |
…est error semantics, loopback guard, shared revoke plan Findings from the 10-angle xhigh review (3 angles independently confirmed the first one): - Lockdown (stop_revoke_all) now cancels the connections poll exactly like lock() does: with the bridge clear sequenced AFTER the daemon revoke, a still-running tick could refetch the not-yet-cleared sessions, pass the post-bump epoch fence, and flash "N connected" onto a locked-down wallet. - Honest error semantics: only ECONNREFUSED proves no bridge (→ empty list); every other failure — connect timeout, read timeout, non-200 — keeps the current list instead of falsely blanking live sessions. A failed single-site revoke can no longer wipe sibling rows or read as "all disconnected". - resolve_loopback_addr refuses a non-loopback DECKARD_BRIDGE_ADDR: the x-deckard-admin marker and disconnect ops must never leave this machine, and a remote endpoint must never feed the Connections trust surface (+ tests). - apply_disconnect: one shared helper for per-site revoke and revoke-all — the epoch fence, the NEW in-flight guard (a poll can no longer start under the post-bump epoch and land a pre-revoke snapshot), and the error handling can no longer drift between the two paths. - RevokeTarget: the ⌘K revoke-site label and its Enter action now read ONE computed target (Nothing/One/All) instead of hand-maintaining the same 0/1/N partition in two files — the label structurally cannot promise a different disconnect than Enter performs. Accepted, documented: DESIGN's favicon+trust-dot stays #48's (never fabricate a trust signal); admin DTO sharing via deckard-contract deferred (that crate is the frozen daemon wire, this channel is the alpha's swappable one); the per-op session-list clone (tiny by design).
Owner
Author
|
/code-review (xhigh, 10 finder angles + adversarial verify + gap sweep) — findings addressed in Fixed (5 correctness/security, 2 structure):
Accepted, documented: DESIGN's favicon+trust-dot stays #48's (a fabricated trust dot would break never-fabricate-a-trust-signal); admin DTO sharing via DoD after fixes: fmt clean · |
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.
Closes #199. The ending beat of the browser demo: connect → attribute (#198) → revoke → the site is locked out.
Per the issue's 2026-07-10 scope decision and ADR 0006: revoke is a session-scoped disconnect, not a ban. The revoked site's next request fails
4100 Unauthorized; it may reconnect viaeth_requestAccountsfor a fresh session. No blocklist, nothing persisted.The channel decision the issue left to this PR
App ↔ bridge = a minimal
POST /adminon the bridge's existing loopback HTTP server. Sessions stay where they live (bridge RAM,DappSessionStore); the daemon, wire contract, MockDaemon, freeze fixtures andcapabilities.rsare untouched — the daemon-relay alternative would pay that four-place #31 wire tax for zero alpha benefit, and a daemon-side copy of bridge-RAM sessions could drift. Consequence: the issue's "wire round-trips for the three new variants" test bullet doesn't apply (there are no new wire variants); it collapses into bridge-level admin-dispatch tests instead.Ops are
list/revoke/clear, and every op replies with the post-op live session list, so the app refreshes its whole view in one round-trip.Why a web page can't call
/admin/adminrequires thex-deckard-adminheader (any value). That header is deliberately never inaccess-control-allow-headers: a custom request header forces a CORS preflight, the preflight refuses it, so a malicious page can neither enumerate your connected origins nor disconnect sessions — only a local, preflight-free process (the app) can. A const-level test pins the invariant. This stays inside ADR 0006's "minimal loopback check, no token auth" posture.App side
bridge_admin.rs— hand-rolled blocking loopback HTTP client (no new dependencies;Cargo.toml/Cargo.lockuntouched), called only fromcx.background_spawn.connections_epochso a poll reply already in flight when you revoke can't land last and resurrect the just-revoked row ("the row is gone" is the promise — it must not flicker back).Connectionsrow goes live (1 connected) — it was hardcoded"none"and would have lied the moment a site connected.revoke-site(inserted beforerevoke-all, which stays the registry's last entry) with a live label: one site →Revoke connected site — <host>; several →Revoke all N connected sites; the action always matches the displayed label (per-site precision at N>1 lives on the row's ✕; revoking more only reduces authority — the fail-safe direction).Tests
Bridge: revoke → 4100 → reconnect-fresh round-trip; admin dispatch (list excludes revoked / unknown-origin revoke is a no-op / clear empties / missing-origin + unknown-op error); the header gate; the CORS const invariant. App: response-parser +
origin_hostunits; registry order; and a new reflective registry↔handler test — every ⌘K id must have arun_palette_commandarm (include_str!+ exact"<id>" =>token).Out of scope, on purpose: #203 / #218 (the issue suggested folding them only on the daemon path — this PR has zero
daemon.rschurn), connect-time approval + per-origin permissions (#48), session persistence.Definition of done
cargo fmt --all --check— cleanjust check— green on default and--features trayTMPDIR=/tmp cargo test --workspace— 461 passed, 2 ignored (39 suites)Live acceptance run (QA vault +
just qa-bridge+ anvil)Connect grants a session; the admin gate refuses a header-less request:
After ⌘K → Revoke connected site (Enter):
After ⌘K → STOP (Lockdown):
eth_accounts → []— every session cleared.Before — no connections
Connected — sidebar row + live rail count
⌘K — the live label names exactly what Enter disconnects
After revoke — the row is gone
Lockdown — sessions cleared with the key