# warden — the verified-custody wallet warden is the acting end of pacta. Where the rest of the project *decides* which cryptographic code is trustworthy, warden *runs on that decision*: it builds an Ed25519 custody boundary out of the four independently proven curve25519-dalek forks, and puts them to work guarding money- adjacent signatures — inbound and outbound. It is agent-native first. The primary interface is an MCP server; a human CLI is provided for operation and inspection. The design research behind that choice is in [docs/agent-native.md](docs/agent-native.md). --- ## The one idea **Inbound acceptance requires a unanimous quorum of provably-equivalent verifiers. Outbound signatures must pass the same quorum before release.** Each quorum member is an Ed25519 verifier compiled from a source workspace whose correctness certificates are machine-checked in Lean 4 and replay- attested in the public [Lean Transparency Log](https://ltl.zkdefi.org). The four members — `dalek`, `anza`, `risc0`, `betrusted` — are genuinely different codebases, but each is *proven* to decide the same predicate: ``` accept(A, m, R, s) ⇔ decompress(R) = [k](−A) + [s]B ``` Classic N-version programming hopes independent implementations won't share a bug. warden doesn't hope: on the proven domain the members *cannot* disagree about semantics, so a runtime disagreement is not a difference of opinion — it is evidence of build corruption, a memory fault, or tampering. That turns "the verifiers disagreed" from a shrug into an alarm with a machine-checked guarantee behind it. --- ## Trust posture (read this before trusting it) | surface | assurance | |---|---| | inbound verification | **custody-grade** — quorum of certificate-covered verify paths | | outbound firewall (verify-after-sign) | **custody-grade** — same quorum | | outbound signing itself | **trusted base** — the attested artifact, not a third implementation; not covered by any theorem | | SHA-512 | opaque oracle inside the theorems | | wire parsers | outcomes are hypotheses | | reproducible builds, side channels | not claimed (that is R5) | | ML-DSA / PQC | fail-closed: no proven implementation exists | The asymmetry is the point: warden is strongest exactly where it matters most for custody — deciding whether an inbound authorization is real — and honest about the weaker outbound edge, which it fences with the firewall. --- ## Anatomy of a wallet A wallet is a directory of evidence, not a database of secrets: ``` capsule.json the sealed custody capsule: quorum members, their attested source commits, the transparency-log receipts that authorized them (the R4 gate), and the policy in force ledger.jsonl append-only, SHA-256 hash-chained event log keys/ wallet identities (local signer keys are 0600; the airgap identity has no private key on this host) incidents/ quorum divergences and firewall quarantines, full trails receipts/ refusal receipts — signed, machine-actionable quarantine/ signatures the firewall refused to release airgap/ outbox/inbox for the Precursor-style gap signer latch.json present and latched=true when custody is frozen ``` Every state change — inbound verify, outbound sign, refusal, incident, latch, unlatch — is a hash-chained ledger entry. `pacta wallet verify-ledger` recomputes the chain; a single altered byte anywhere in the history is caught. --- ## The R4 gate, in executable form `pacta wallet init` refuses to create a wallet unless, **for every member**: 1. the built binary's provenance names an attested source commit; 2. the evidence dir holds that component's attestation + inclusion receipt; 3. the attestation re-validates locally — verdicts are **re-derived from observed axiom cones**, never taken from the provider's label; 4. the local score reaches the required tier (default **R4**); 5. the receipt's inclusion proof verifies against its signed tree head, under the log public key you pass; 6. the source commit the binary was compiled from matches the attested one. Miss any of these and wallet creation fails with the reasons listed. A custody wallet that cannot show its evidence has no business existing. `--trusted-provider` is **required**: you must name whose *observations* you are consuming. You are never asked to trust their verdicts. ### You hold the ruler The allowed-axioms lists the gate checks against are a **requirements card** that ships in *your* tooling ([the ed25519 profile](src/pacta/profiles/ed25519.py)) — small enough to read over coffee, re-derivable from the attested repos' own docs and check scripts, and replaceable with a card you write yourself from first principles. Verdicts are re-derived against *your* copy, name by name; the provider's labels are ignored. If your card is stricter than today's supply (e.g. "SHA-512 itself proven"), the gap is itemized and your options are honest: accept a named line item, walk away, or prove the missing piece and enter it into the log — *if your ruler is stricter than our supply, your ruler is our roadmap.* The full walk-through is [lecture 11](notebooks/11_the_customers_eye_view.ipynb). --- ## Quickstart ```bash # 0. one-time: build the dogfood signer + the four quorum members pacta dogfood-build --source <…>/curve25519-dalek-source pacta wallet build-quorum --sources-root <…>/sources # dalek anza risc0 betrusted # 1. fetch fresh evidence for each fork from the live log for c in dalek anza risc0 betrusted; do pacta log-fetch --url https://ltl.zkdefi.org \ --component ${c}-ed25519-verified --out-dir ./evidence done # 2. create the wallet (R4 gate) pacta wallet init --wallet ./my-warden --evidence ./evidence \ --log-public-key ./log.pub --trusted-provider local-pacta-provider # 3. inspect pacta wallet status --wallet ./my-warden pacta wallet card --wallet ./my-warden # the self-proving custody card # 4. serve the agent-native surface pacta wallet mcp --wallet ./my-warden # stdio JSON-RPC MCP server ``` --- ## Agent-native surface (MCP) `pacta wallet mcp` speaks MCP over stdio JSON-RPC. Eight outcome-first tools with read-only/destructive annotations; strict input schemas; results carry evidence; errors are structured objects that include the signed refusal receipt, never prose. Tool classes are rate-limited (custody 30/min, verify 120/min, liveness 240/min). | tool | does | |---|---| | `wallet_status` | custody posture: members/tiers, latch, ledger head + chain integrity, counts | | `verify_inbound` | run the quorum on (payload, signature, public_key); unanimity or a classified incident | | `request_signature` | intent-bound outbound signing through the firewall; refusal object on any gate | | `custody_card` | the self-proving card (embedded inclusion proofs; recompute, don't believe) | | `posture_challenge` | nonce → firewalled, signed posture attestation with the quorum trail | | `list_incidents` | divergences and quarantines, newest-first | | `explain_refusal` | fetch a refusal receipt by index (or latest) | | `airgap_pending` | parked gap-signing requests and whether the device answered | Refusal codes (every refusal names one): `EVIDENCE_REQUIRED`, `POLICY_DENIED`, `CUSTODY_LATCHED`, `EVIDENCE_STALE`, `MALFORMED_INTENT`, `SIGNER_UNAVAILABLE`, `FIREWALL_QUARANTINE`, `PENDING_AIRGAP`, `RATE_LIMITED`. ### The custody card is self-proving Unlike an A2A agent card that you take on the operator's signature, the warden card embeds, per member, the transparency-log **inclusion proof** and **signed tree head**. A counterparty recomputes the Merkle roots and checks the STH signature against the log key it already pins — trust by recomputation, not by assertion. See `verify_posture_attestation` and the log's own `verify.py` for the ~40-line client side. --- ## The spending policy (POLICY_DENIED) Optional `policy.json` in the wallet directory - the rules you would give a teenager with a debit card, checked before the signer ever runs: ```json { "outbound": { "max_amount_per_request": 100.0, "max_amount_per_day": 500.0, "counterparty_allowlist": ["alice"], "counterparty_denylist": ["mallory"] }, "identities": { "warden": { "max_amount_per_request": 10.0 } }, "ledger": { "rotate_at": 100000 } } ``` Amount rules bind on `intent.amount`; list rules bind on `intent.counterparty` - policy makes those fields **mandatory**, so a request that omits them is refused, never waved past. Daily ceilings sum the released amounts in the ledger's last 24 hours per identity. Per-identity overrides win over `outbound` defaults. No `policy.json` means unrestricted (and `wallet_status` says so). `pacta wallet policy --wallet