Lane B: deckard-mcp sidecar (mcp.v0.1, 6 tools) + signerd hardening - #16
Merged
Conversation
…ion, loud policy fallback - chain-1 guardrail: every auto-Allow on mainnet downgrades to NeedsApproval unless the (THREAT-MODEL-only) operator override is set; matrix test ApprovalMode × chain × override; override var never in client-visible text - app regression guard: hold-to-confirm IS the human approval — NeedsApproval shields now stash the review card and confirm resolves-then-executes (signer::approve_and_execute_blocking, wire-asserted by a recording server) - daemon Shield.to == RelayAdapt(chain) pre-check (policy.rs defers it by charter; table parity-pinned against railgun::chain_config) - every reason/log line URL-redacted at the daemon boundary (sanitize_reason in one_line) + end-to-end canary on a key-bearing RPC URL - load_policy falls back LOUDLY when policy.json exists but doesn't parse - two-client interleaved daemon_e2e: app unlock → MCP propose/approve → STOP cuts both → re-unlock invalidates stale request ids with clear errors
…6 tools) One binary, two faces (the splits-cli pattern): a clap CLI (balance / address / policy / shield --amount-eth / execute / stop / install [--demo] [--write]) and an rmcp v1.7 stdio server exposing the launch profile of 6 deckard_-prefixed tools. Key-less by construction: every write is an Intent proposed over the async SignerClient to deckard-signerd; the one transient secret (the Railgun viewing key riding in RailgunViewGrant) is Zeroizing'd on receipt and never echoed or logged. - amount_eth is a decimal STRING, parsed exactly (no f64, no wei JSON numbers) - wallet_balance omits shielded: honest 'unavailable — read it in the Deckard app (v1 limitation)' string, never a fake 0 - tool descriptions are the agent's docs: units, preconditions (app running + unlocked), shield→request_id→execute sequencing, do-NOT-retry-on-timeout, revoke_all = STOP; T1 asserts they stay keyword-bearing - typed error catalog: every failure is problem+cause+fix (socket-missing, NoVault distinct from locked, expired, revoked, unknown_request, already_executed, broadcast-timeout = status UNKNOWN do-not-retry, …) - connect-time chain probe: a wrong-chain daemon yields an actionable 'install --demo' error instead of a late confusing deny - secret-shaped flags hard-rejected pre-clap, value never echoed (T7) - install [--demo]: prints (writes only with --write + confirmation) the Claude Desktop entry with the absolute binary path; --demo emits the ~/.deckard/demo env block (socket/config dir, chain 11155111, local RPC) - acceptance suite (CI mock-daemon, hard timeouts on every read): T1-T4, T6-T9 amended — T9 is a structural allowlist transcript walk (tx_hash/ request_id permitted both sides) + seeded canary + poisoned-env case - workspace member + CI: linux workspace jobs pick it up; macOS builds the sidecar as a shipping binary; 30-mcp-shape.md gets the mcp.v0.1 amendment note (propose/simulate cut, rationale + roadmap home) deps: rmcp 1.7, clap 4 (both approved)
Thread one resolved runtime config through the app so `just demo` can point every process at a local anvil fork without a rebuild (W2, lane C): - chain id: env DECKARD_CHAIN_ID > settings > default, removing the hardcoded DAEMON/SHIELD_CHAIN_ID consts; threaded to the daemon launch, shield builder, and Railgun sync (the supervisor no longer clobbers the env — the app resolves it first and passes the resolved id through). - config dir: deckard_core::config_dir() now honors DECKARD_CONFIG_DIR (empty = treated as unset); Settings::path() routes through it so vault + settings + policy share one isolated dir (kills the NoVault split). - socket: the app honors DECKARD_SOCKET_PATH so a demo daemon never loses the single-instance flock to an everyday Deckard (which would silently attach the demo app + MCP to the mainnet daemon). - rpc: Settings::effective_rpc() gains the DECKARD_RPC_URL override; the resolved runtime config is logged once with the RPC redacted (redact_url made pub). - fork sync: DECKARD_DEMO_FORK_BLOCK pins the shielded SubsquidSyncer (.with_latest_block, like shield_e2e); runtime DECKARD_VERIFIED_READS=0 makes both the app's EthProvider and the daemon (read_public_balance + server priming) skip mainnet-only Helios, tagged Unsynced, so Balance doesn't stall against a fork — no GPUI rebuild needed (new deckard-core/src/env.rs). - locked surfacing: an external STOP/revoke (e.g. an MCP client) bounces the app to the unlock gate with clear copy. - fork banner: an amber "DEMO FORK — not mainnet" caution in the status strip (DESIGN rule 7: amber alert icon carries the signal, text stays neutral). Pure unit tests back every env override. Lane B already shipped the daemon-side env handling, the chain-1 guardrail, and the resolve path (NeedsApproval -> hold-to-confirm -> Resolve+Execute); this stacks on top. DoD: cargo fmt clean; clippy -D warnings on default + --features tray; cargo test --workspace 128 passed / 1 ignored; no new or changed dependencies.
…IBUTING (W2 lane D)
Work Package D of the OSS launch — the reproducible local-chain demo + contributor
dev loop on top of lanes B (deckard-mcp) and C (app env plumbing + fork pin).
- justfile: shared demo_* vars + three recipes:
- demo: anvil Sepolia fork @ pinned block 10822990 (RPC_URL_SEPOLIA, fixed port
8545, DECKARD_DEMO_FORK_BLOCK override), process trap + double-run guard,
install-if-absent policy, verified-reads off, isolated ~/.deckard/demo, fork banner.
- demo-fund [addr]: anvil_setBalance 10 ETH; no-arg queries the daemon Address.
- demo-check: doctor with defined exit codes (0/10-16) + platform-conditional fixes;
probes the daemon via deckard-mcp address (requires unlock; PolicyGet does not),
diffs PolicyGet vs the intended demo policy (OverCap renders 'over_cap'), prints
chain + policy + auto-allow state.
- policy.demo.json (root): per-tx 0.1 / daily 0.5 ETH, auto_shield_min 0.01, OverCap,
empty allowlist; decimal-string wei + _comment docs. Loads clean (no fallback).
- CONTRIBUTING: 'Demo / local-chain dev loop' (macOS + Linux) + DECKARD_* env table;
refreshed the stale 'deckard-mcp not built' status bullet.
Verified: DoD green (fmt, just check both configs, 128 tests, no dep changes); all
recipe paths exercised live against a Sepolia archive fork; Railgun sync measured ~10s
(shield_e2e). Codex 2-round adversarial review: 2 blockers found + fixed, re-verified.
Adversarial pre-merge review of PR #16 (P2s): - sanitize_reason sliced at rfind's byte index + 1, which lands mid-codepoint when multibyte punctuation (curly quote, ellipsis) precedes a URL scheme — a panic in the daemon's error-formatting path. Advance by char::len_utf8. - THREAT-MODEL.md was referenced by the guardrail code/tests but missing. Documents the same-uid trust boundary (Resolve is honor-system), prompt injection model, the mainnet-guardrail override (its single documentation site), viewing-key discipline, and the STOP-latency v1 tradeoff.
…, zeroize, secret-flag matching, demo chain guard, frame EOF Codex (GPT-5.5 xhigh) adversarial review of PR #16, P2s + P3: - app: non-empty unparsable DECKARD_CHAIN_ID is now a loud startup failure (was: silent fall-through toward mainnet; signerd/mcp already hard-error) - signerd: propose checks chain_mismatch BEFORE locked (needs no key), so the sidecar's connect-time chain probe is conclusive even against a locked daemon; mcp mock reordered for parity and the probe now caches on 'locked' - signerd client: zeroize the encoded request (Unlock passphrase) and raw response (RailgunViewGrant viewing key) frames after use - mcp: secret-flag rejection is delimiter-aware (--rpc-api-key, --private_key, --railgun-viewing-key now refused before clap can echo the value) - justfile: 'just demo' aborts if the local fork reports a chain id other than 11155111 (wrong-chain upstream can no longer launch a false demo) - signerd frame: a truncated length prefix is a malformed-frame error, not a clean EOF Deferred (pre-existing, not a PR-16 regression): settings RPC re-point only respawns the read provider while the signer daemon keeps its launch RPC — seeded as a red-team issue in Lane E.
This was referenced Jun 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Work package B of the approved open-source-demo plan (W1 + the signerd hardening rows).
What ships
New crate
crates/deckard-mcp— the key-less agent surface--mcpstdio server (rmcp v1.7 + clap, both approved deps). Key-less: every write is anIntentover the asyncSignerClient; no key material, no RPC client, no Helios in this process.deckard_wallet_address,deckard_wallet_balance,deckard_policy_get,deckard_shield,deckard_execute,deckard_revoke_all. Rawpropose+simulatestay cut (spec carries the amendment note).amount_eth= decimal string, exact parse;wallet_balanceomits shielded with the honest "unavailable — read it in the Deckard app (v1 limitation)".deckard-mcp install --demo" error.install [--demo]: prints the Claude Desktop entry with the absolute binary path; writes only with--write+ an explicity;--demoemits the~/.deckard/demoenv block (socket/config dir,DECKARD_CHAIN_ID=11155111,DECKARD_RPC_URL=http://127.0.0.1:8545) — key-less, no secret enters Claude's config.RailgunViewGrantisZeroizing'd on receipt, dropped immediately, never in any response/log (transcript-asserted).signerd hardening
chain_id==1without the override env var (named once, in THREAT-MODEL territory — never in reasons/responses) downgrades every auto-Allow → NeedsApproval. Matrix test: ApprovalMode × chain × override. Mock/daemon parity carve-out documented inparity.rs.NeedsApprovalshields show the review card and confirm runsResolve{approved:true}→Execute(signer::approve_and_execute_blocking, wire-asserted). The app's mainnet shield keeps working with the guardrail armed.Shield.to == RelayAdapt(chain)daemon pre-check (closes the policy.rs deferral); address table parity-pinned againstrailgun::chain_config.one_line→sanitize_reason(URL →scheme://host); e2e canary with a key-bearing RPC URL.policy.jsonthat exists but doesn't parse now shouts before running the default any-recipient policy.daemon_e2e: app unlock → MCP propose/approve → MCP STOP cuts both clients → app re-unlock invalidates stale request ids with explanatoryunknown_requesterrors.Acceptance (spec T1–T4, T6–T9 as amended)
Mock-daemon suite in CI, hard 20s timeout on every stdio read, real
deckard-mcp --mcpchild over real JSON-RPC:DECKARD_RPC_URLenv never in transcript · T8 STOP then execute denied · T9 structural allowlist transcript walk (hex permitted only intx_hash/request_id, both sides) + seeded canary (planted key in a forced broadcast error must be flagged — proving the scanner; the real daemon's redaction is pinned in signerd's tests).DoD evidence
cargo fmt --all --check— clean-D warnings, default and--features trayconfigs — cleancargo test --locked --workspace— 117 passed, 1 ignored (the fork-pinnedshield_e2estays#[ignore])rmcp1.7,clap4 (approved); lockfile additions are those two families onlyCoordination note (lane C)
This PR includes the minimal app-side resolve path the guardrail requires (D:
shell.rs/signer.rs). Lane C (env plumbing/fork pin/banner) touches the same files — whoever lands second rebases; the slice here is small and surgical.