Repository navigation
Description
Part of #44 (ADR 0001). Phase 1a. Closes THREAT-MODEL.md residual-risk #1. Foundational and independent — has standalone security value with zero dapps, and is a hard prerequisite for PRD-04.
Why
crates/deckard-signerd/src/auth.rs authorizes on same_uid() alone, so Resolve{approved:true} is honored from any same-uid process — the textbook confused-deputy gap (research §22–24: uid is ambient authority; peer-cred proves who connected, not which role). Every hardware/companion signer (Trezor, Ledger, GridPlus, Frame) separates request (promiscuous endpoint) from approval (a surface the requester can't drive). Deckard must manufacture that split in software before any browser-reachable proposer exists.
Goals
- The GPUI app is the sole principal that can send
Resolve. - Approval authority is an unforgeable capability the daemon hands only to the app it controls.
- The public proposer socket keeps accepting proposals/reads but rejects
Resolvewith a typed error. - STOP/
RevokeAllstays reachable on every channel (it only reduces authority).
Non-goals
Multi-principal daemon; salted request-ids (residual #6); keystore/guardrail/STOP-latency changes; dapp-origin auth (that's PRD-05).
Design
Capability channel (supervise.rs already has the app spawn+supervise the daemon — the app is the parent):
socketpair()inherited by the daemon child (recommended) — app creates an AF_UNIX pair pre-spawn, passes one end to the child, keeps the other;Resolveaccepted only there. Portable Linux+macOS (research §26).SCM_RIGHTSfd-pass after connect — daemon passes back a dedicated control fd;Resolveonly on it (research §25, 27).
- Fallback (weaker, document if used): per-launch
0600cookie read by the app.
Daemon (deckard-signerd): keep same_uid() for the public socket (defense-in-depth/logging); route Resolve only from the control channel; a Resolve on the public socket → typed denial (deny_reasons::RESOLVE_NOT_AUTHORIZED, URL-redacted, no payload echo). Keep STOP on every channel. Don't re-trust peer creds across the connection lifetime (staleness, research §24).
App (deckard-app): establish the control channel at spawn (supervise.rs/shell.rs); thread its handle to where Resolve is sent after a completed hold-to-confirm (shell.rs:142 area). Hold-to-confirm contract unchanged — only the channel changes.
Contract: prefer no wire change (channel, not frame, carries authority). If a cookie fallback is used, add a typed field with redacted Debug.
Cross-platform: Linux SO_PEERCRED/socketpair/SCM_RIGHTS; macOS LOCAL_PEERCRED (no pid; pid via LOCAL_PEERPID) — fd-capability is the portable primitive. Confine unsafe fd handling to the app crate with a scoped, documented #[allow(unsafe_code)] (match the eth.rs pattern); keep fd plumbing out of deckard-core. Use already-present nix/tokio — no new deps.
Acceptance tests (default cargo test, not #[ignore])
resolve_rejected_on_public_socket— typed denial; record stays Pending.resolve_accepted_on_control_channel— flips Pending→Allowed.stop_still_works_on_public_socket.second_proposer_cannot_self_approve— end-to-end red-team (maps to residual build(gpui): default to the git GPUI pair (Zed + gpui-component) over crates.io #1).- Mock⇄daemon
evaluateparity unaffected.
DoD
Shared DoD (see #44) plus: THREAT-MODEL.md residual #1 moves to "Mitigated" with the mechanism named; a decision note records which capability mechanism was chosen; the four tests run by default.
Risks
fd inheritance across Command::spawn is platform-fiddly → SCM_RIGHTS, then cookie. Handle the supervise respawn path so a restarted app re-handshakes with no window where Resolve is ungated.
Sources: research §22–29; THREAT-MODEL.md (residual #1); man7 unix(7); cloudflare SCM_RIGHTS; capability-myths-demolished.
Activity
docs: move PRD/spike work items to GitHub issues; keep ADR + research
docs(walletbeat): reconcile with ADR 0001 + issue tracking convention
hellno commentedon Jun 14, 2026
Delivered in PR #55 (merged 2026-06-14).
Capability-gated Resolve via an AF_UNIX socketpair: the GPUI app (parent) mints the pair before spawning the daemon and hands the child one end by fd inheritance (
DECKARD_RESOLVE_FD), keeping the other as a ControlChannel. The daemon honorsResolveONLY on the inherited control end (Channel::Control); a Resolve on the public proposer socket is refused with the typedresolve_not_authorizeddenial; STOP (Lock/RevokeAll) stays reachable on every channel; each respawn re-handshakes and fails closed mid-restart.THREAT-MODEL.md residual-risk #1 → Mitigated; ADR 0001 PRD-01 open question resolved (socketpair fd-inheritance). Tests green:
resolver_auth(public-socket Resolve rejected, control-channel accepted, STOP-on-public still works, red-team second-proposer-cannot-self-approve) — re-verified 2026-06-14. Closing PRD-01.