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
Resolve with a typed error.
- STOP/
RevokeAll stays 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; Resolve accepted only there. Portable Linux+macOS (research §26).
SCM_RIGHTS fd-pass after connect — daemon passes back a dedicated control fd; Resolve only on it (research §25, 27).
- Fallback (weaker, document if used): per-launch
0600 cookie 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])
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.
Part of #44 (ADR 0001). Phase 1a. Closes
THREAT-MODEL.mdresidual-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.rsauthorizes onsame_uid()alone, soResolve{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
Resolve.Resolvewith a typed error.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.rsalready 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).0600cookie read by the app.Daemon (
deckard-signerd): keepsame_uid()for the public socket (defense-in-depth/logging); routeResolveonly from the control channel; aResolveon 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 whereResolveis sent after a completed hold-to-confirm (shell.rs:142area). 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; macOSLOCAL_PEERCRED(no pid; pid viaLOCAL_PEERPID) — fd-capability is the portable primitive. Confineunsafefd handling to the app crate with a scoped, documented#[allow(unsafe_code)](match theeth.rspattern); keep fd plumbing out ofdeckard-core. Use already-presentnix/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).evaluateparity unaffected.DoD
Shared DoD (see #44) plus:
THREAT-MODEL.mdresidual #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::spawnis platform-fiddly → SCM_RIGHTS, then cookie. Handle the supervise respawn path so a restarted app re-handshakes with no window whereResolveis ungated.Sources: research §22–29;
THREAT-MODEL.md(residual #1); man7 unix(7); cloudflare SCM_RIGHTS; capability-myths-demolished.