Repository navigation
Description
Why
The wire contract (crates/deckard-contract: Intent / Decision / Policy / SignerRequest, CBOR over UDS) is frozen by convention with no version field, no capability discovery, and no written rule for what an addition may look like. Upcoming features (7702 session keys, x402) both add request kinds — without evolution rules each one is an ad-hoc breaking change to a contract we call frozen.
Grounded in verified prior art: RFC 9987 (ssh-agent — 20 years unversioned, formalized late with named extensions + a generic-failure valve; https://datatracker.ietf.org/doc/rfc9987/), MCP date-versioning, protobuf never-reuse discipline (https://protobuf.dev/programming-guides/proto3/), EIP-5792 capability maps (https://eips.ethereum.org/EIPS/eip-5792), varlink GetInfo.
Decision (and tradeoff)
Capability discovery instead of a version handshake. One additive request:
SignerRequest::Hello // -> HelloInfo { spec_version: "YYYY-MM-DD", capabilities: Vec<String>, impl_name: String }An old daemon receiving Hello fails to decode the unknown enum variant and returns the existing frame-decode error — that failure is the backward-compat valve, and it already ships. No existing frame changes; the round-trip tests prove it. Tradeoff: no negotiated handshake (à la MCP ≤2025-11) — accepted because MCP itself is moving away from the handshake, and the valve needs zero migration.
The five evolution rules (normative; documenting them in docs/build/ is part of this issue)
- Capability discovery, not version negotiation —
Helloas above; old peers answer with the existing decode error. - Date-version (
YYYY-MM-DD), bump only on breaking change — additions ship under a new capability name, never a version bump. Capability names are never reused. - Protobuf discipline on the CBOR — map-key names are forever (never reuse, rename, or retype; removed keys go to a
reservedlist); decoders MUST ignore unknown struct keys and MUST reject unknown enum variants. - Two distinguishable failures — "unsupported message/capability" (frame-decode error) vs "supported but failed" (
Decision::Denyfrom the frozen vocabulary). - In-repo home — rules + registry live in this repo; publication artifacts are deferred (below).
In scope
-
Hellorequest +HelloInforesponse indeckard-contract; answered in every daemon state includingLocked; reveals capability names only — no state, no policy contents, no key material. - Baseline capability registry table in the new build doc:
core(the shipped socket API),mcp.v0.1(the 6-tool profile) — columns: name · status · since · defining doc. - The five rules above written into the build doc.
- Tests: E1
Helloshape (spec_versionmatches^\d{4}-\d{2}-\d{2}$, capabilities ⊇ {core, mcp.v0.1}); E2 old-peer replay via the existing round-trip fixtures (byte-identical — freeze holds); E3 unknown-variant frame → decode error, connection survives, no panic, nothing signed; E4 extra unknown map key on a struct → decodes, key ignored (pins serde behavior; nodeny_unknown_fieldson wire types). - No code path branches on
impl_name(feature detection = capabilities only).
Not in scope (deferred until a second implementer appears)
- Extractable standalone spec document, CDDL schema (RFC 8610), golden-vector files, crates.io publication.
- Standalone spec repo; ERC draft.
- Any change to existing frames or to the Deny-reason vocabulary (that's Freeze the Deny-reason vocabulary in deckard-contract #28).
Definition of done
- All in-scope checkboxes; existing
deckard-contractround-trip tests untouched and green. -
cargo fmt --all --checkclean;just checkgreen (both feature configs);cargo test --workspacegreen. - No new or changed dependencies.
Open question
Should HelloInfo also carry the daemon's active chain id, so a wrong-chain client fails at first contact instead of at the first Propose (Deny{chain_mismatch} today fires late)? Leaning yes.
Activity
hellno commentedon Jun 24, 2026
Reframed by ADR-0005 (#135). Scope narrows to protocol message-kind discovery only — "does this daemon understand request kind X" — explicitly not authorization (that is the Rules engine, #135). Decoupled from agent-send (Send is an existing
Intent, needs no wire change). Deferred until a genuinely new request kind lands (e.g. #33 / #34).feat(wire): #31 · Hello capability discovery + the five evolution rul…
hellno commentedon Jul 7, 2026
Done — implemented and merged via #212 (squash to
main).All in-scope items shipped:
SignerRequest::Hello→HelloInfo{spec_version, capabilities, impl_name}, single-source capability registry (core,mcp.v0.1) indeckard-contract/src/capabilities.rs, answered in every daemon state incl. Locked, the five evolution rules indocs/build/40-wire-evolution.md, and E1–E4 tests (shape, byte-identity, unknown-variant rejection valve, unknown-key tolerance) plus a daemon-socket test.Open question resolved: no —
HelloInfodoes not carry the active chain id. Capability discovery stays state-free ("capability names only"); a wrong-chain client is already refused at firstProposewithDeny{chain_mismatch}. If that ever changes, it can be added additively under these same rules.feat(wire): #198 · Dapp origin on the wire — attribute browser transa…
feat(connections): #199 · list live dapp sessions in the app, revocab…
feat(connections): #199 · list live dapp sessions in the app, revocab…