Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions docs/adr/0002-agent-wallet-and-session-keys.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# ADR 0002 — Agent wallet and session keys

- **Status:** Proposed (2026-06-14), then **Deferred (2026-06-15, `/plan-ceo-review`)** — not before the
first agent demo runs and an audit. See [`docs/agent-authorization-map.md`](../agent-authorization-map.md)
→ Current focus. The positioning below stands; the timing does not.
- **Deciders:** @hellno (maintainer)
- **Context inputs:** [`docs/agent-authorization-map.md`](../agent-authorization-map.md),
[`docs/research/05-agentic-wallets.md`](../research/05-agentic-wallets.md), `THREAT-MODEL.md`,
`MANIFESTO.md`, `DESIGN.md`, `docs/build/30-mcp-shape.md`, issue
[`#33`](https://github.com/hellno/deckard/issues/33) (EIP-7702 session keys),
[`#31`](https://github.com/hellno/deckard/issues/31) (wire evolution).
- **Supersedes / relates:** records the positioning behind the "agent wallet" goal. The executable
work lives as GitHub issues (`#33`, `#31`, and a sizing spike drafted alongside this ADR).
Convention: work items live in issues; decisions + research live in `docs/`.

## Decision drivers (agreed with the maintainer)

Captured via a requirements pass (2026-06-14):

1. **A distinct agent-controlled wallet is a real product goal**, not just framing. The motivating
capability is the agent having its **own address** and its **own funded float**, with
**blast-radius isolation**: a runaway or compromised agent can drain only the float, never the
user's main balance.
2. **The mechanism is EIP-7702 session keys** (`#33`): the user's wallet funds and authorizes a
**scoped grant** (per-tx cap, expiry, allowlist) to the agent's key. The agent wallet is not a
second independent EOA the user tops up by hand; it is an authority the user's wallet delegates.
3. **v1 is software-enforced, consistent with the rest of v1.** Per the v1 honesty decision
(2026-06-14), v1 limits are enforced by the daemon's policy gate plus human approval, not by the
chain. Chain-enforcement is the post-audit upgrade, not a v1 claim.
4. **Staging is left open, to be settled by a spike.** Before committing to "software agent wallet
now" vs "jump straight to `#33`", size `#33` (backend selection, audited-contract dependency, the
`#31` prerequisite, effort). The spike decides.

Plus the standing constraint from `SECURITY.md`: Deckard is `0.0.1-alpha`, unaudited, single-maintainer,
testnet-keys-only. Any new signing authority is sequenced behind an audit, and `MANIFESTO.md` already
commits to this ("Hands-off autonomy comes after a security audit ... not before").

## Context

Today Deckard runs **model 1**: a single human key lives in `deckard-signerd`; the agent is a
**key-less proposer** that submits typed `Intent`s over MCP; the daemon's policy gate decides; the
human approves via a native hold-to-confirm card. There is no agent key, no agent address, no float.
The trust spine that makes this safe is built and merged (`#4`, `#45` resolver authentication, `#28`
deny vocabulary, `#27` agent surface).

An "agent wallet" is **model 2**: a second, lesser key with its own scoped authority. This is the
single largest item in the backlog and currently exists only as plan (`#33`). It depends on `#31`
(capability discovery on the frozen wire), an on-chain delegation contract that must be on a hardcoded
allowlist of audited deployments, and a backend choice (MetaMask Delegation Framework vs Porto/Ithaca).

## Decision

1. **Adopt the agent wallet as a goal, delivered through `#33` session keys.** The agent gains a
distinct address and a scoped, revocable float granted by the user's wallet.
2. **v1 enforcement is software.** The daemon holds the agent key (a second keystore class) and
refuses to sign anything outside the granted scope. On mainnet, the existing guardrail
(`THREAT-MODEL.md`: chain-1 auto-Allow downgrades to `NeedsApproval`) still applies, so hands-free
autonomy is real on testnet/fork now and gated on mainnet until chain-enforcement lands.
3. **Chain-enforcement is the post-audit upgrade.** EIP-7702 delegation to an audited contract makes
the scope physically unbreakable. Same agent address; software caps swap for chain-enforced ones.
4. **Staging is decided by a spike, not now.** Run a sizing spike for `#33` (see the drafted issue).
Its report chooses between "software agent wallet first" and "jump to `#33`", and records the
backend selection and the `#31` dependency.

## Invariants this must respect (non-negotiable, from existing locked decisions)

- **The thinking agent stays key-less.** The MCP/proposer layer never holds either key. Only the
daemon holds keys. The agent wallet is a *daemon-held* key the agent can request signatures from
within scope, never a key in the agent process. (`THREAT-MODEL.md`, `30-mcp-shape.md`.)
- **Resolver authentication is unchanged.** Approvals are honored only on the app's capability channel
(`#45`). A second key class does not add a second approval path.
- **STOP zeroizes everything.** `RevokeAll` zeroizes both the human key and the agent session key and
denies in-flight approvals. Revoke of a session grant zeroizes the local agent key.
- **Wire changes are additive.** New request kinds ship as capabilities under `#31`'s `Hello`
mechanism, preserving byte-stable round-trip (`#28` rules). Never sign `chain_id = 0`; the delegate
contract must be on a hardcoded versioned allowlist of audited deployments.
- **Two-signal model holds.** The agent wallet is cyan (agent class); approvals and "where you are"
stay amber (`DESIGN.md`).

## Consequences

- **Positive:** isolates blast radius; gives the agent a real identity for attribution and receiving;
delivers the manifesto's "autonomy is the point" on an honest, audit-gated path.
- **Cost:** the policy gate becomes per-account (the agent account carries its own float and scope), a
modest but real extension of `Policy`. A second keystore class adds key-lifecycle surface
(grant, rotate, revoke, expiry).
- **Deferred:** chain-enforcement, multi-account agent fleets, and any agent-to-agent delegation are
out of scope here. So is exporting a grant to an external agent runtime
(`smart-account-autonomous` mode), per `#33`.

## Status / next step

**Deferred (2026-06-15, `/plan-ceo-review`).** The first agent demo (auto-shield, LLM in the loop) uses
the authorization that already exists (software policy + approval + STOP), so an agent wallet is not needed
yet. The earlier "spike `#33` first" plan is reversed. Revisit after that demo runs and an audit. When
picked back up: run the `#33` sizing spike, settle the staging decision, then promote this ADR to Accepted.
68 changes: 68 additions & 0 deletions docs/agent-authorization-map.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Deckard — Agent Authorization (the thinking)

> The *why* and the *shape* of how Deckard's agent stays bounded, and what we are building first.
> This doc holds the thinking, not the task list. Work items live in **GitHub issues**, decisions live in
> **`docs/adr/`**, build state lives in **`STATUS.md`**. When those move, this doc does not need to.
>
> Last reconciled: **2026-06-15** (`/plan-ceo-review`).

## What we want (the thesis)

Your money on autopilot, and you can see and stop everything. The agent reads, proposes, and explains.
Only the signer moves money, and every move passes a policy gate you control. So the agent is a **key-less
proposer**: it never holds the key, the daemon does, and you approve by hand. That spine is built
(process-isolated signer, policy gate, capability-gated approval, STOP).

## Three authority models (don't conflate them)

The threads that sound tangled ("agent permissions", "agent wallet", "session keys", "the activity
screen") are three different things wearing similar words:

1. **Key-less proposer (today).** The agent proposes typed intents, you approve, the daemon signs with
*your* key. Limits are software-enforced (the policy gate) plus your approval. **This is all of v1.**
2. **Agent wallet / session keys.** A distinct agent-controlled address with its own float and
blast-radius isolation, granted scoped authority via EIP-7702 session keys. Software-enforced first,
chain-enforced after audit. Positioning and rationale: **ADR-0002**.
3. **Per-origin grant.** Scoped permission for a *dapp origin*, not the agent. Part of dapp connectivity.
Decision: **ADR-0001**.

"Agent wallet" and "session keys" are the same future thing (model 2). Model 3 is about dapps, a different
principal. Keep them apart.

## Current focus — what we are building first

We have never watched an agent do a real task here, so the backlog was inverted: autonomy got ticketed,
the see-and-stop loop did not. The fix is to run one concrete task on the authorization that already
exists, learn, then decide what is next.

**First demo: the agent keeps your incoming funds private.** Claude watches the balance, shields new funds
within your cap, and you see and stop it in an activity view. The LLM is in the loop on purpose, so we
learn how the agent plus approval loop actually feels.

- Activity feed + see-and-stop view: **#60**
- Agent loop (poll balance, propose shield): **#61**

It uses only what exists: the policy gate (`auto_shield_min`, `OverCap`), the mainnet guardrail, STOP. No
new authority.

## What is deferred (and why)

Everything that adds agent *authority* or *reach* waits until the first demo runs and teaches us
something. Building authorization for money we have never moved is premature.

- **Agent wallet / session keys** — ADR-0002 (#33).
- **Machine payments (x402)** — #32, #34.
- **Dapp connectivity / owned bridge** — ADR-0001 (epic #44). Also audit-gated.

This reverses an earlier "spike #33 first" call, on purpose.

## Invariants any future agent work must respect

Grounded in `THREAT-MODEL.md` and `DESIGN.md`. Non-negotiable:

- **The thinking agent stays key-less.** Only the daemon holds keys.
- **One approval path.** Approvals are honored only on the app's capability channel (resolver auth).
- **v1 limits are software-enforced, not chain-enforced.** Do not claim "cannot exceed" until the chain
does.
- **STOP zeroizes the key and denies in-flight work.** Always reachable.
- **Two-signal model:** amber is human, cyan is agent. Identity is a glyph, never the only signal.
19 changes: 17 additions & 2 deletions docs/research/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,35 @@
> Citations `[NN]` point to the KB file that grounds the item. The `/autoplan` consensus + decision
> log are at the end of this file.

> **Reconciliation update (2026-06-14).** This roadmap predates the alpha build, and two things in the
> NOW set below did **not** ship as written. **EIP-7702 session keys** moved to **LATER** (deferred, now
> tracked as issue `#33`; see `STATUS.md`), and with them the **chain-enforced** limits. What shipped is
> the **software-enforced** policy gate plus human hold-to-confirm. So v1's honest claim is *"a compromised
> agent cannot exfiltrate the key, and can only request actions the policy permits and you approve,"* not
> *"cannot exceed on-chain limits."* The full agent-authorization picture, grounded against code, lives in
> [`docs/agent-authorization-map.md`](../agent-authorization-map.md); the agent-wallet / session-key
> positioning is [`docs/adr/0002`](../adr/0002-agent-wallet-and-session-keys.md). Items below are kept as
> the original decision log; inline `⏸` marks what deferred.

## What Deckard is (fixed constraints)

- A **native, cross-platform (macOS + Linux) desktop** Ethereum wallet, written in **Rust on GPUI**.
- **Self-custodial**, **local-first**. Keys live on the user's device.
- Today (v0): a single alloy-generated **secp256k1 EOA**, persisted as **plaintext hex** in the OS config dir.
- North-star: an **"operator wallet"** — an LLM that manages the wallet semi-autonomously, running
locally or wired to the user's chosen AI, **under limits it cannot exceed**.
locally or wired to the user's chosen AI, **under limits you set and can revoke** (software-enforced
in v1, every move approved by hand; chain-enforced is the post-audit upgrade).

## Decisions applied (from the `/autoplan` review)

The review found the v1 draft **engineering-correct but strategically inverted**: it led with a
conventional security floor and enforced the operator's limits in a *same-process software gate* that
all four voices judged unsafe to call "the agent never sees the seed." The decisions below re-spine it.

> **⏸ Update (2026-06-14):** Decision ①B below pulled EIP-7702 / chain-enforced limits into NOW. That half
> **deferred to LATER** (now issue `#33`); v1 shipped the software-enforced gate plus hold-to-confirm. See
> the reconciliation note at the top.

1. **Safety boundary (①B):** the signer runs in an **isolated process** (key + policy inside it; the
AI gets a key-less client), **and** a **minimal EIP-7702 session-key** path is pulled into NOW so
limits are **chain-enforced**, not just software-checked. The honest claim becomes: *"a compromised
Expand Down Expand Up @@ -59,7 +74,7 @@ LATER = gated on a further prerequisite. NEVER = excluded by positioning.
|---|---|---|---|
| **Process-isolated signer daemon** — holds the decrypted key + runs the policy; exposes only a `sign(intent)` RPC over an authenticated local socket; **no "sign arbitrary bytes"**; single-instance lock; audit log | `[05][08]` | The real trust boundary; the AI process never holds the key (Decision ①) | L |
| **Policy gate (inside the daemon)** — caps, allowlists, expiry, sim-on-deviation; **decodes calldata** (approvals/permits/7702 SetCode); **default-deny** unrecognized; returns **typed allow/deny/needs-approval + machine-readable reason + remediation** | `[05]` | Limits the agent can't bypass in-process; typed refusals stop agent flailing | L |
| **Minimal EIP-7702 session keys** — reversible, address-preserving delegation to an audited target (`Simple7702Account`/session-key validator); **chain-enforced** caps/expiry/allowlist; legible "what am I delegating to" UX | `[01][02]` | Makes the operator's limits **unbreakable**, not just local (Decision ①B) | L |
| **Minimal EIP-7702 session keys** *(deferred → LATER 2026-06-14; now `#33`)* — reversible, address-preserving delegation to an audited target (`Simple7702Account`/session-key validator); **chain-enforced** caps/expiry/allowlist; legible "what am I delegating to" UX | `[01][02]` | Makes the operator's limits **unbreakable**, not just local (Decision ①B) | L |
| **Local MCP sidecar** — key-less client of the daemon; `read`/`simulate`/`draft`/`scoped-execute` tools; **refuses flag-based secrets**; stdio or authed-localhost-HTTP with documented auth + revocation | `[04][05]` | The converged integration surface (Splits' one-binary CLI+MCP) | M |
| **Native approval surface** — desktop modal/tray showing intent + asset delta + counterparty + sim source + "why the agent wants this"; **deny / approve-once / approve-rule / pause-agent** | `[05]` | The most-seen operator interaction; Deckard's native edge over browser MCP (Decision ④) | M |
| **Owner-key override / STOP / revoke-all-agent-authority** | `[05]` | The operator panic button (Decision ④) | S |
Expand Down
Loading