Repository navigation
Description
Re-scoped 2026-07-10 → ADR 0006 (via #202). The shipped
127.0.0.1:8765loopback bridge (#62/#63/#139/#141/#146/#148/#198, operator docdocs/browser-bridge.md) is the accepted interim alpha transport — temporary and experimental, not this PRD's end state. This PRD (native messaging, post-audit) is what replaces that wire. Related: the connect-with-no-approval gap is assigned to #48; per-site revoke is a session-scoped disconnect, not a ban (#199).
Part of #44 (ADR 0001). Phase 2. "Connect to any dapp" through a transport Deckard fully owns — our wire, our UX, local-first, no third-party relay, no embedded browser, no store as a trust anchor. Deliberate replacement for WalletConnect (shelved; rationale in ADR 0001). Gated on PRD-01, PRD-02, PRD-05, and an external audit (SECURITY.md). Starts with the spike (#49).
Why
Universal reach without the "external bad stuff": no bundled engine (rejected), no WalletConnect (centralized relay/metadata leak/Project-ID/bad UX/no Rust SDK — research §10–14). Dapps speak EIP-1193, so reach all of them by injecting a standard EIP-1193/EIP-6963 provider (§7, 21–22) via a first-party connector, carried over a Deckard-owned local wire to a key-less proposer mirroring deckard-mcp. Payoff = the vision: no relay → no third-party metadata leak (privacy win); native instant approval cards (the UX we control); universal reach without becoming a browser.
Goals
- A key-less proposer process (
deckard-bridged) mirroringdeckard-mcp: speaks the owned wire to the connector +deckard-contractto the daemon. No key, cannotresolve(PRD-01), typed intents/messages only via PRD-02 builders. - A first-party connector injecting EIP-1193 + EIP-6963 (
rdnse.g.sh.deckard). Target Chromium AND Firefox from the start (separate manifests; MV3 quirks). - Distribution: self-distributed + signed first — trust = our signature + key-less design, not a store review. Firefox self-hosts signed XPIs; Chromium self/sideload path documented. Store listings are convenience only.
- Owned wire = spike decision (Spike: Deckard-native dapp bridge (PRD-04 #1) #49): leading candidate native messaging (not web-reachable) over a localhost port (DNS-rebinding/CSWSH, §2–4).
- Map methods:
eth_sendTransaction→Intent;personal_sign/eth_signTypedData_v4→SignMessage(PRD-02);wallet_switch/addEthereumChain→guarded (PRD-02). Refuseeth_sign+ (v1) EIP-7702 delegation. Per-origin scope via PRD-05.
Non-goals
WalletConnect (shelved), embedded webview (rejected). Mobile — a browser connector is desktop-only; mobile universal-reach is an explicit open problem (keep deckard-bridged core logic platform-agnostic, but don't build for mobile now). Generic ABI decoding (PRD-02 scope).
Design
Topology mirrors deckard-mcp: dapp ──EIP-1193──► connector ──owned wire──► deckard-bridged ──contract wire──► deckard-signerd; native clear-signing card via the GPUI app over PRD-01's control channel.
Owned wire (settled by spike #49): native messaging (connectNative) gated by manifest, not web-reachable — the decisive security edge; exercise MV3 SW reconnect + user-writable host-manifest registration (we install it). Hardened localhost only if a target browser blocks native messaging (then origin-allowlist + token + Host checks).
Connector (trust bounded, not store-based): minimal, open-source, key-less; self-distributed + signed first (Firefox direct; Chromium self/sideload documented); a fully compromised connector can only propose (§6, 22, 29). EIP-6963 announce + window.ethereum legacy fallback.
UX (anti-WalletConnect): native connect card showing unverified origin + requested scope (PRD-05); local instant approval, no QR/relay. Every signing request → PRD-02 card. Sessions listable + revocable from ⌘K + governance.
Acceptance tests
Spike #49 first (round-trip on both browsers, settle wire + distribution). Then: bridged_is_keyless (memory/fd scan, deckard-mcp red-team harness); bridged_cannot_resolve (PRD-01 control-channel gate); wire_not_web_reachable; method_out_of_scope_refused (PRD-05); eth_sign_refused/delegation_refused; eip6963_announced (coexists with another wallet); transcript hygiene.
DoD
Shared DoD (#44) plus: ⌘K connect/disconnect/list-sessions/revoke/STOP-all; owned-wire choice + connector distribution/trust model documented in SECURITY.md+THREAT-MODEL.md (new "dapp bridge" surface); desktop-only/mobile-open-question recorded; off by default (never installs a host / opens a connection silently); new deps approved + deny.toml/Cargo.lock reviewed; external audit sign-off recorded before shipping beyond testnet.
Risks
First-party connector is a standing supply-chain target — contained by design; keep minimal/open-source; consider reproducible builds + signature pinning connector↔host. User-writable native-messaging manifest (§4) — same uid boundary; key-less host can't self-approve. MV3 flakiness (§5) — reconnect + heartbeat, covered by spike. We're inventing a wire — keep it a thin typed envelope over deckard-contract. Mobile unsolved — revisit if/when funded.
Sources: research §1–7, 22–29, 30–37; docs/build/30-mcp-shape.md; EIP-1193, EIP-6963; chrome native-messaging docs; github.blog localhost-dangers; shelved WalletConnect rationale (ADR 0001).
Activity
hellno commentedon Jun 19, 2026
⛓ Blocked by #46, #48, #49 — the native bridge is gated on PRD-02 (#46) + PRD-05 (#48), starts with spike #49 (and an external audit).
hellno commentedon Jul 5, 2026
Dependency surfaced by E5 (#185): the owned wire must carry the dapp origin
E5 shipped the shared Review's consuming
Origin::Dapprail (renders<domain> requests, neutral, no third signal color). For it — and for #186's "origin attribution across you / dapp / agent" — to work on dapp transactions, the owned wire (this PRD) needs to thread the origin domain:ProposalOriginis only{ App, Agent }(crates/deckard-contract/src/rpc.rs), and the bridge proposes a dappsend_transactionasProposalOrigin::Appwith no domain (Intenthas no origin field,deckard-browser-bridge/src/lib.rs). So a dapp tx is currently indistinguishable from a user's own send.SignMessage.origin, so E5's rail renders<domain> requestsfor those for free — no plumbing needed there.ProposalOrigin::Dapp { domain }(or an origin field on the proposal), thread it bridge →Propose{intent, origin}wire (CBOR+JSON) → daemonActivityRecord→ feed. Note:Stringmakes the enum non-Copy, so every existing== ProposalOrigin::Agentcomparison ripples.Consumers ready and waiting: the E5 review rail (#185, shipped) and the E6 feed attribution (#186).