You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Today the agent's "Rules" are a single flat Policy struct: a bespoke top-level key per capability
(auto_shield_min_wei, allow_swap_tokens, …), one global approval mode, and default-allow on
recipients (allow_to: [] = any). It cannot scale to the wallet-action roadmap without per-feature
policy-plumbing churn, and it default-allows the most dangerous axis.
This issue replaces it with a small, default-deny, per-action typed rule list — the user-facing "Rules" — kept as a single pure evaluate() with native U256 caps, and teaches the agent to
actually send money (within the Rules). Decision + rationale, standards survey, and the corrected
spec: docs/adr/0005-policy-and-authorization-model.md.
Grounded by a primary-source deep-research pass (the uint256-vs-64-bit finding) and two codex
adversarial reviews. User-facing term is "Rules"; the internal deckard-contract type stays Policy and the file stays policy.json (cosmetic rename optional, not v1).
In scope — delivered as ordered PRs inside this one issue
PR1 — schema (deckard-contract)
Replace flat Policy with a versioned per-action Rule list: Rule enum (Send/Shield/Unshield/Swap/ContractCall variants carrying their own constraints), Allowlist { DenyAll, Any, Only(Vec<Address>) } with #[serde(default)] = DenyAll, Effect { Deny } (default: "allow" forbidden), one globaldaily_cap_wei, advisory auto_shield_min_wei.
Rewrite evaluate (default-deny; per-rule per-tx cap + global daily cap in U256; Allowlist lattice) preserving Decision / deny_reasons / RequestId::ZERO and the mock⇄daemon parity contract. evaluate_order reads the Swap rule's tokens (replaces allow_swap_tokens); still always NeedsApproval, no cap fields.
Rewrite the JSON+CBOR round-trip tests for the new shape (keep the byte-stable charter — this is an intentional versioned break of Policy only; Intent/Decision/RPC enums stay frozen).
Verify (do not rebuild) that the existing single-mutex serialization + execute-time re-evaluate (daemon.rs:1131) + signerd: reserve daily-cap before signing + durable rollback-resistant accounting (C3) #108 reserve-before-sign (daemon.rs:1164) correctly cover a value-bearing auto-allowed Send (today's only auto-allow is value == 0 shield); add a regression test.
PR3 — the send approval screen
The send card (recipient + amount + token + any resolution), per DESIGN.md two-signal model.
Ship three named Rules presets on the v2 schema, all default-deny: shield-only (one shield rule, everything else denied — the safest useful agent); ask-me-everything (send/shield/swap all approval: always); locked (panic — rules: [], everything denied; a switchable frozen rulebook, distinct from the runtime revoked STOP).
Selection (CLI flag / ⌘K). Default shipped Rules: shield auto-allow, send approval: always (recipients any; every send human-approved), swap always-cards.
Rewrite policy.demo.json → v1; just demoupgrade-not-skip a v0 file (today installs-if-absent); fix just demo-check jq paths + intended-values; CHANGELOG the schema break + demo-machine upgrade step.
Per-action daily budgets (a SpendStore-schema change).
The #33 / #48 grant/attenuation model — they adopt this vocabulary but stay distinct enforcers.
#31 wire-kind discovery (reframed, deferred).
New actions beyond what exists: Unshield / ContractCall rules are defined forward-compat but not yet reachable (the daemon denies non-Send/Shield with unsupported_v1 before evaluate; the shaped relayer-approve skips evaluate).
Why
Today the agent's "Rules" are a single flat
Policystruct: a bespoke top-level key per capability(
auto_shield_min_wei,allow_swap_tokens, …), one global approval mode, and default-allow onrecipients (
allow_to: [] = any). It cannot scale to the wallet-action roadmap without per-featurepolicy-plumbing churn, and it default-allows the most dangerous axis.
This issue replaces it with a small, default-deny, per-action typed rule list — the user-facing
"Rules" — kept as a single pure
evaluate()with nativeU256caps, and teaches the agent toactually send money (within the Rules). Decision + rationale, standards survey, and the corrected
spec:
docs/adr/0005-policy-and-authorization-model.md.Grounded by a primary-source deep-research pass (the uint256-vs-64-bit finding) and two codex
adversarial reviews. User-facing term is "Rules"; the internal
deckard-contracttype staysPolicyand the file stayspolicy.json(cosmetic rename optional, not v1).In scope — delivered as ordered PRs inside this one issue
PR1 — schema (
deckard-contract)Policywith a versioned per-actionRulelist:Ruleenum (Send/Shield/Unshield/Swap/ContractCall variants carrying their own constraints),Allowlist { DenyAll, Any, Only(Vec<Address>) }with#[serde(default)] = DenyAll,Effect { Deny }(default: "allow"forbidden), one globaldaily_cap_wei, advisoryauto_shield_min_wei.evaluate(default-deny; per-rule per-tx cap + global daily cap inU256;Allowlistlattice) preservingDecision/deny_reasons/RequestId::ZEROand the mock⇄daemon parity contract.evaluate_orderreads theSwaprule'stokens(replacesallow_swap_tokens); still alwaysNeedsApproval, no cap fields.policy_store.rs): strict typed deser; reject duplicate actions (loud); v0 file (absentversion) → loud reject ("rewrite to v1"); malformed → deny-all + loud log.Policyonly;Intent/Decision/RPC enums stay frozen).mock.rs/MockSigner::new,tests/harness_slice.rs,deckard-mcp/src/sidecar.rs::policy_json()(+ fixtures),deckard-app/src/welcome.rs::agent_policy_rows.PR2 — agent send over MCP
deckard_sendMCP tool → existingIntent{ kind: Send }→evaluate.palette_commands.rs+Shell::run_palette_command) — palette-reachability rule.daemon.rs:1131) + signerd: reserve daily-cap before signing + durable rollback-resistant accounting (C3) #108 reserve-before-sign (daemon.rs:1164) correctly cover a value-bearing auto-allowed Send (today's only auto-allow isvalue == 0shield); add a regression test.PR3 — the send approval screen
DESIGN.mdtwo-signal model.PR4 — starter presets + demo
approval: always); locked (panic —rules: [], everything denied; a switchable frozen rulebook, distinct from the runtimerevokedSTOP).approval: always(recipientsany; every send human-approved), swap always-cards.policy.demo.json→ v1;just demoupgrade-not-skip a v0 file (today installs-if-absent); fixjust demo-checkjq paths + intended-values; CHANGELOG the schema break + demo-machine upgrade step.Not in scope
SetPolicyRPC — deferred (→ Scenario recipes: policy variants with transcripts, each backed by an acceptance test #29, narrowed to authoring/PAP).SpendStore-schema change).#33/#48grant/attenuation model — they adopt this vocabulary but stay distinct enforcers.#31wire-kind discovery (reframed, deferred).Unshield/ContractCallrules are defined forward-compat but not yet reachable (the daemon denies non-Send/Shieldwithunsupported_v1beforeevaluate; the shaped relayer-approve skipsevaluate).Definition of done
cargo fmt --all --checkclean.just checkgreen (default and--features tray).cargo test --workspacegreen;evaluate/evaluate_ordermock⇄daemon parity green; round-trip tests rewritten + green.