Skip to content

feat: deckard_send MCP tool + value-bearing auto-allow safety (#135 PR2) - #161

Merged
hellno merged 3 commits into
mainfrom
hellno/deckard-send-pr2
Jun 30, 2026
Merged

hellno merged 3 commits into
mainfrom
hellno/deckard-send-pr2

Conversation

@hellno

@hellno hellno commented Jun 30, 2026

Copy link
Copy Markdown
Owner

Summary

Adds deckard_send, the 8th mcp.v0.1 tool: a key-less, agent-channel propose path for a native-ETH transfer that flows through the daemon's policy evaluate exactly like deckard_shield. Within the policy's Send rule it auto-allows; over the cap it returns needs_approval; the sidecar only proposes (no self-approve). This makes a within-cap Send carrying real ETH the first value-bearing action the agent can take on its own authority within the Rules, so it also adds an anvil e2e regression that proves the #108 reserve-before-sign + execute-time re-evaluate path has no single-session double-spend — two within-cap auto-allows that together exceed the daily cap cannot both move funds. The palette is verify-only (the human Send action already has its ⌘K command; deckard_send is agent-channel), so deckard-app is untouched.

Linked issue

Part of #135 — PR2 of the policy-v2 keystone (PR1 was #160). Does not close the epic.

Definition of Done

  • cargo fmt --all --check is clean
  • just check is green — clippy -D warnings on both the default config and --features tray (and deckard-signerd --features dev-signerd-bin)
  • cargo test --workspace is green
  • No new or changed dependencies in Cargo.toml / Cargo.lock
  • Any visual/UI change follows DESIGN.md — N/A: no UI change (deckard-app untouched; the human Send ⌘K command already exists)
  • Secrets (seed / key / passphrase) stay in Zeroizing and are never logged or Debug-printed — the deckard-mcp sidecar is key-less and holds no secrets; the new tool's response echoes only decision / request_id / to / amount_eth / next
Evidence (paste command output here)
$ cargo fmt --all --check
# clean, exit 0

$ just check
cargo clippy --locked --workspace --all-targets -- -D warnings
    ... Checking deckard-app v0.0.1-alpha ...
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 18.89s
cargo clippy --locked -p deckard-app --all-targets --features tray -- -D warnings
    Finished `dev` profile target(s)
cargo clippy --locked -p deckard-signerd --all-targets --features dev-signerd-bin -- -D warnings
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.77s
# all three passes green, no warnings

$ TMPDIR=/tmp cargo test --workspace
... deckard_mcp (lib)      test result: ok. 11 passed; 0 failed
... acceptance.rs          test result: ok. 16 passed; 0 failed   (T1 eight-tool + 2 new send tests)
... durable_cap_e2e.rs     test result: ok. 2 passed; 0 failed
... send_autoallow_toctou_e2e.rs  test result: ok. 1 passed; 0 failed
... deckard-app (bin)      test result: ok. 59 passed; 0 failed
... (every workspace suite green; 0 failures)

Notes for reviewers

  • The real work is the verification, not the tool. The crux is that PR1 lets a within-cap native-ETH Send auto-allow, so the daemon's execute now gates real fund movement. The path was reviewed sound: execute is fully serialized under the single daemon.lock().await.handle() dispatch (server.rs:169), and it re-evaluates an auto-allow against the live post-commit spent_today before reserving (daemon.rs:1312). The new test is guard-pinning — it fails (panics DOUBLE-SPEND) if that re-evaluate is removed.
  • A cross-model adversarial review (codex) walked the propose/execute/STOP interleavings and the broadcast-timeout branch and returned ACHIEVED, 0 P0/P1.
  • Out of scope (deliberate): an ERC-20 Send reserves 0 against the daily cap (the cap is ETH-wei; token atoms aren't comparable) — deckard_send only ever builds native ETH, so no token path is introduced here. A per-token cap is a separate follow-up.
  • Test caveat (macOS): the signerd e2e suites need TMPDIR=/tmp to keep Unix-socket paths under SUN_LEN; the new test runs (not skipped) wherever anvil is on PATH.

hellno added 3 commits June 30, 2026 19:30
Adds an 8th mcp.v0.1 tool, deckard_send, mirroring deckard_shield: it
parses a decimal-ETH amount and a strict 0x recipient (ENS is not
resolved here), builds a native-ETH Send intent (token: None, empty
calldata — the daemon's calldata_ok requires it), and proposes it with
ProposalOrigin::Agent. Within the policy's Send rule it auto-allows;
over the cap it returns needs_approval. The sidecar is key-less and only
proposes — there is no self-approve path.

This is the agent-channel side of the first value-bearing auto-allow:
a within-cap Send carrying real ETH can now execute on the agent's own
authority within the Rules.

Updates every tool-count drift guard: acceptance T1 (now the eight-tool
profile + two new send tests), the quickstart tool table, the mcp-shape
doc, README, and the clap/about/module copy.
…ouble-spend (#135 PR2)

Adds an anvil e2e regression for the #108 reserve-before-sign +
execute-time re-evaluate path, now that a within-cap native-ETH Send
auto-allows and moves real funds.

Two distinct within-cap Sends (0.03 each; per-tx 0.04, daily 0.05) both
return Decision::Allow at propose time while spent_today is still 0 —
the TOCTOU window. After the first commits, the second is Denied
(cap_exceeded) at the execute-time re-evaluate (daemon.rs:1312), and the
non-funded recipient's on-chain balance stays 0 — no double-spend past
the daily cap. The test is guard-pinning: it fails if the re-evaluate is
removed. durable_cap_e2e.rs only covered restart recovery, not this
single-session close.
The linux job died with "No space left on device" during the
Swatinem/rust-cache@v2 restore: extracting the multi-GB cached target
dir exhausted the runner's disk before the "Free up disk space" step
ran. The step's own comment says to reclaim space "first", but it was
ordered AFTER the cache restore. Move it before the restore so neither
the restore nor the build runs out of space. No code change; macOS is
unaffected (it has the headroom and runs no free-space step).
@hellno
hellno merged commit 3b9a992 into main Jun 30, 2026
5 checks passed
hellno added a commit that referenced this pull request Jul 1, 2026
The #135 keystone shipped end to end — the v2 Rules schema (PR1, #160), agent send
over MCP (PR2, #161), and the named starter presets (PR4, #162) — with
evaluate/evaluate_order mock⇄daemon parity green and the send approval card
(PR3) already in the app. Flip the status Proposed → Accepted and reword the
closing section to record what shipped and that in-app authoring (#29) is next.
hellno added a commit that referenced this pull request Jul 1, 2026
#162)

* feat(policy): starter Rules presets + DECKARD_POLICY_PRESET launch selection (#135 PR4)

Ship three named starter postures for the agent's Rules, all default-deny on the
v1 schema, selectable at daemon launch:

- shield-only: shield auto-allowed, send and swap denied (the safest useful agent)
- ask-me-everything: shield, send, and swap all human-approved
- locked: no rules, every action denied (a frozen rulebook, distinct from the
  runtime revoke STOP)

Selection is launch-time via DECKARD_POLICY_PRESET, consulted only when there is no
policy.json (an authored file always wins; a malformed file still falls back to the
loud deny-all, and a preset can never revive it). An unknown value warns and uses
the friendly default.

The app spawns signerd with env_clear() + FORWARDED_ENV_ALLOWLIST (finding C1), so
DECKARD_POLICY_PRESET is added to that allowlist or it would be stripped at spawn.
Safe: every preset is same-or-more-restrictive than the no-env default, so forwarding
it can only narrow the agent's authority, never escalate it.

Live in-app preset switching is deferred to #29 (needs the SetPolicy RPC; the daemon
reads policy only at startup).

DoD: fmt clean; just check green (both configs); cargo test --workspace 396 passed.

* docs(adr): promote ADR-0005 (policy & authorization model) to Accepted

The #135 keystone shipped end to end — the v2 Rules schema (PR1, #160), agent send
over MCP (PR2, #161), and the named starter presets (PR4, #162) — with
evaluate/evaluate_order mock⇄daemon parity green and the send approval card
(PR3) already in the app. Flip the status Proposed → Accepted and reword the
closing section to record what shipped and that in-app authoring (#29) is next.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant