Skip to content

PRD-04: Deckard-native bridge (universal reach, owned wire) #50

Open

Description

@hellno

Re-scoped 2026-07-10 → ADR 0006 (via #202). The shipped 127.0.0.1:8765 loopback bridge (#62/#63/#139/#141/#146/#148/#198, operator doc docs/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) mirroring deckard-mcp: speaks the owned wire to the connector + deckard-contract to the daemon. No key, cannot resolve (PRD-01), typed intents/messages only via PRD-02 builders.
  • A first-party connector injecting EIP-1193 + EIP-6963 (rdns e.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). Refuse eth_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

  1. hellno commented on Jun 19, 2026

    @hellno
    OwnerAuthor

    ⛓ 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).

  2. hellno commented on Jul 5, 2026

    @hellno
    OwnerAuthor

    Dependency surfaced by E5 (#185): the owned wire must carry the dapp origin

    E5 shipped the shared Review's consuming Origin::Dapp rail (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:

    • Today ProposalOrigin is only { App, Agent } (crates/deckard-contract/src/rpc.rs), and the bridge proposes a dapp send_transaction as ProposalOrigin::App with no domain (Intent has no origin field, deckard-browser-bridge/src/lib.rs). So a dapp tx is currently indistinguishable from a user's own send.
    • Dapp messages already carry their domain in SignMessage.origin, so E5's rail renders <domain> requests for those for free — no plumbing needed there.
    • The plumbing this PRD owns: add ProposalOrigin::Dapp { domain } (or an origin field on the proposal), thread it bridge → Propose{intent, origin} wire (CBOR+JSON) → daemon ActivityRecord → feed. Note: String makes the enum non-Copy, so every existing == ProposalOrigin::Agent comparison ripples.

    Consumers ready and waiting: the E5 review rail (#185, shipped) and the E6 feed attribution (#186).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions