Skip to content

Rules engine v2: per-action policy + agent send over MCP + starter presets #135

Description

@hellno

Why

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 global daily_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.
  • Loader (policy_store.rs): strict typed deser; reject duplicate actions (loud); v0 file (absent version) → loud reject ("rewrite to v1"); malformed → deny-all + loud log.
  • 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).
  • Update by-name readers: 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_send MCP tool → existing Intent{ kind: Send }evaluate.
  • ⌘K palette command (palette_commands.rs + Shell::run_palette_command) — palette-reachability rule.
  • 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.
  • Before/after screenshots attached (GUI visual-verification rule).

PR4 — starter presets + demo

  • 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 demo upgrade-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.

Not in scope

  • In-app authoring / SetPolicy RPC — deferred (→ Scenario recipes: policy variants with transcripts, each backed by an acceptance test #29, narrowed to authoring/PAP).
  • 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).

Definition of done

  • cargo fmt --all --check clean.
  • just check green (default and --features tray).
  • cargo test --workspace green; evaluate / evaluate_order mock⇄daemon parity green; round-trip tests rewritten + green.
  • Send approval card before/after screenshots attached.
  • No new or changed dependencies.
  • ADR-0005 promoted to Accepted.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestsecuritySecurity-relevant

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions