2026-07-06 21:35:47 +00:00
""" warden ' s Model Context Protocol surface - the agent-native front door.
A dependency - free MCP server over stdio JSON - RPC 2.0 . It follows the AX
canon distilled in docs / agent - native . md : outcome - first tools , strict
input schemas , and results that carry their own evidence so a calling
agent never has to trust an adjective . Errors are structured objects
( ` ` code ` ` / ` ` missing ` ` / ` ` remediation ` ` ) , not prose - a refused agent
gets a machine - actionable receipt it can hand its principal .
This is intentionally tiny and stdlib - only : an agent should be able to
read the whole trust surface in one sitting . It speaks enough of MCP
( ` ` initialize ` ` , ` ` tools / list ` ` , ` ` tools / call ` ` ) to be driven by any MCP
client , and degrades to a plain JSON - RPC endpoint for scripts .
"""
from __future__ import annotations
import base64
import json
import sys
from pathlib import Path
from typing import Any , Callable
from . custodycard import build_custody_card , posture_challenge
from . wallet import Refusal , Wallet
PROTOCOL_VERSION = " 2025-06-18 "
SERVER_INFO = { " name " : " warden " , " version " : " 1.0.0 " }
def _b64_to_bytes ( field : str , value : Any ) - > bytes :
if not isinstance ( value , str ) :
raise _ToolError ( " MALFORMED_INTENT " , f " { field } must be base64 string " , [ field ] , " send base64 " )
try :
return base64 . b64decode ( value , validate = True )
except Exception as exc : # noqa: BLE001
raise _ToolError ( " MALFORMED_INTENT " , f " { field } is not valid base64: { exc } " , [ field ] , " re-encode as base64 " )
class _ToolError ( Exception ) :
warden hardening round: policy engine, ledger rotation, MCP UX, treasury LIVE, ops docs
Tier 2:
- request_signature decomposed into named gates (latch, freshness,
intent, policy, signer, firewall)
- ledger: O(1) tail-read appends under a dedicated lock file (survives
rotation rename); hash-chained segment rotation at policy
ledger.rotate_at; verify-ledger walks all segments to genesis;
archive tampering detected (tested)
- docs/threat-model.md (attacker matrix 1-9, proven-vs-trusted, design
invariants) + docs/runbook-latch.md (diagnose-first recovery)
- lecture 10: executable corrupt-a-member exercise (capsule pin catches
one appended byte), honest note on what the pin does NOT stop
Lightweight policy engine (POLICY_DENIED wired):
- policy.json: per-request/per-day amount ceilings, counterparty
allow/deny lists, per-identity overrides; rules make their intent
fields mandatory; daily sums from the ledger
-
Agent UX:
- signed refusal receipts travel inside MCP errors (receipt +
receipt_path in structuredContent)
- airgap over MCP: request_signature signer=airgap + request_id,
new airgap_pending tool; park -> list -> device answers -> complete
(tested end-to-end)
- all 8 tools carry readOnly/destructive annotations
- sliding-window rate limiter per tool class (custody/verify/liveness);
RATE_LIMITED refusal code; surface control, not ledgered
warden-treasury LIVE:
- treasury.py: stdlib base58, compact-u16, legacy+v0 wire parsing;
every required signature quorum-verified over exact message bytes;
completeness gap named in every verdict; RPC fetch uses response as
bytes only
-
- live-quorum test: synthetic Solana tx signed with wallet key ->
authentic via 4 proven forks; flipped byte -> not authentic
100 tests green (was 85).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 11:38:42 +00:00
def __init__ (
self ,
code : str ,
reason : str ,
missing : list [ str ] ,
remediation : str ,
receipt : dict [ str , Any ] | None = None ,
receipt_path : str | None = None ,
) - > None :
2026-07-06 21:35:47 +00:00
super ( ) . __init__ ( reason )
self . payload = { " code " : code , " reason " : reason , " missing " : missing , " remediation " : remediation }
warden hardening round: policy engine, ledger rotation, MCP UX, treasury LIVE, ops docs
Tier 2:
- request_signature decomposed into named gates (latch, freshness,
intent, policy, signer, firewall)
- ledger: O(1) tail-read appends under a dedicated lock file (survives
rotation rename); hash-chained segment rotation at policy
ledger.rotate_at; verify-ledger walks all segments to genesis;
archive tampering detected (tested)
- docs/threat-model.md (attacker matrix 1-9, proven-vs-trusted, design
invariants) + docs/runbook-latch.md (diagnose-first recovery)
- lecture 10: executable corrupt-a-member exercise (capsule pin catches
one appended byte), honest note on what the pin does NOT stop
Lightweight policy engine (POLICY_DENIED wired):
- policy.json: per-request/per-day amount ceilings, counterparty
allow/deny lists, per-identity overrides; rules make their intent
fields mandatory; daily sums from the ledger
-
Agent UX:
- signed refusal receipts travel inside MCP errors (receipt +
receipt_path in structuredContent)
- airgap over MCP: request_signature signer=airgap + request_id,
new airgap_pending tool; park -> list -> device answers -> complete
(tested end-to-end)
- all 8 tools carry readOnly/destructive annotations
- sliding-window rate limiter per tool class (custody/verify/liveness);
RATE_LIMITED refusal code; surface control, not ledgered
warden-treasury LIVE:
- treasury.py: stdlib base58, compact-u16, legacy+v0 wire parsing;
every required signature quorum-verified over exact message bytes;
completeness gap named in every verdict; RPC fetch uses response as
bytes only
-
- live-quorum test: synthetic Solana tx signed with wallet key ->
authentic via 4 proven forks; flipped byte -> not authentic
100 tests green (was 85).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 11:38:42 +00:00
if receipt is not None :
# The signed refusal receipt travels WITH the error: the refused
# agent can hand its principal a provable, stamped "the wallet
# said no, and this is why" instead of an unsigned anecdote.
self . payload [ " receipt " ] = receipt
self . payload [ " receipt_path " ] = receipt_path
def _refusal_error ( refusal : Any ) - > _ToolError :
return _ToolError (
refusal . code ,
refusal . reason ,
refusal . missing ,
refusal . remediation ,
receipt = refusal . receipt ,
receipt_path = str ( refusal . receipt_path ) if refusal . receipt_path else None ,
)
class _RateLimiter :
""" Sliding-window throttle per tool class. Liveness reads are cheap and
generous ; custody mutations are scarce on purpose . Limits exist so a
hostile counterparty cannot grind the ledger or the signer ; they are a
surface control , not a custody event - refusals here are NOT ledgered . """
WINDOW = 60.0
LIMITS = { " custody " : 30 , " verify " : 120 , " liveness " : 240 }
def __init__ ( self ) - > None :
self . _hits : dict [ str , list [ float ] ] = { }
def check ( self , category : str ) - > None :
import time
now = time . monotonic ( )
hits = self . _hits . setdefault ( category , [ ] )
cutoff = now - self . WINDOW
while hits and hits [ 0 ] < cutoff :
hits . pop ( 0 )
if len ( hits ) > = self . LIMITS [ category ] :
raise _ToolError (
" RATE_LIMITED " ,
f " { category } budget exhausted ( { self . LIMITS [ category ] } calls / { int ( self . WINDOW ) } s) " ,
[ ] ,
" back off and retry after the window rolls; liveness reads have a higher budget than custody calls " ,
)
hits . append ( now )
_TOOL_CATEGORY = {
" request_signature " : " custody " ,
" verify_inbound " : " verify " ,
" posture_challenge " : " custody " , # it produces a firewalled signature
" wallet_status " : " liveness " ,
" custody_card " : " liveness " ,
" list_incidents " : " liveness " ,
" explain_refusal " : " liveness " ,
" airgap_pending " : " liveness " ,
}
2026-07-06 21:35:47 +00:00
TOOLS : list [ dict [ str , Any ] ] = [
{
" name " : " wallet_status " ,
" description " : " Custody posture: quorum members and tiers, latch state, ledger head and chain integrity, incident and refusal counts. Read this first. " ,
" inputSchema " : { " type " : " object " , " properties " : { } , " additionalProperties " : False } ,
warden hardening round: policy engine, ledger rotation, MCP UX, treasury LIVE, ops docs
Tier 2:
- request_signature decomposed into named gates (latch, freshness,
intent, policy, signer, firewall)
- ledger: O(1) tail-read appends under a dedicated lock file (survives
rotation rename); hash-chained segment rotation at policy
ledger.rotate_at; verify-ledger walks all segments to genesis;
archive tampering detected (tested)
- docs/threat-model.md (attacker matrix 1-9, proven-vs-trusted, design
invariants) + docs/runbook-latch.md (diagnose-first recovery)
- lecture 10: executable corrupt-a-member exercise (capsule pin catches
one appended byte), honest note on what the pin does NOT stop
Lightweight policy engine (POLICY_DENIED wired):
- policy.json: per-request/per-day amount ceilings, counterparty
allow/deny lists, per-identity overrides; rules make their intent
fields mandatory; daily sums from the ledger
-
Agent UX:
- signed refusal receipts travel inside MCP errors (receipt +
receipt_path in structuredContent)
- airgap over MCP: request_signature signer=airgap + request_id,
new airgap_pending tool; park -> list -> device answers -> complete
(tested end-to-end)
- all 8 tools carry readOnly/destructive annotations
- sliding-window rate limiter per tool class (custody/verify/liveness);
RATE_LIMITED refusal code; surface control, not ledgered
warden-treasury LIVE:
- treasury.py: stdlib base58, compact-u16, legacy+v0 wire parsing;
every required signature quorum-verified over exact message bytes;
completeness gap named in every verdict; RPC fetch uses response as
bytes only
-
- live-quorum test: synthetic Solana tx signed with wallet key ->
authentic via 4 proven forks; flipped byte -> not authentic
100 tests green (was 85).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 11:38:42 +00:00
" annotations " : { " readOnlyHint " : True } ,
2026-07-06 21:35:47 +00:00
} ,
{
" name " : " verify_inbound " ,
" description " : " Run the quorum on an inbound (payload, signature, public_key). Acceptance requires unanimity of the proven verifiers; divergence is classified and, if unexplained, latches custody. All inputs base64. " ,
" inputSchema " : {
" type " : " object " ,
" properties " : {
" payload_b64 " : { " type " : " string " , " description " : " message bytes, base64 " } ,
" signature_b64 " : { " type " : " string " , " description " : " 64-byte Ed25519 signature, base64 " } ,
" public_key_b64 " : { " type " : " string " , " description " : " 32-byte raw Ed25519 public key, base64 " } ,
" context " : { " type " : " string " , " description " : " free-text label recorded in the ledger " } ,
} ,
" required " : [ " payload_b64 " , " signature_b64 " , " public_key_b64 " ] ,
" additionalProperties " : False ,
} ,
warden hardening round: policy engine, ledger rotation, MCP UX, treasury LIVE, ops docs
Tier 2:
- request_signature decomposed into named gates (latch, freshness,
intent, policy, signer, firewall)
- ledger: O(1) tail-read appends under a dedicated lock file (survives
rotation rename); hash-chained segment rotation at policy
ledger.rotate_at; verify-ledger walks all segments to genesis;
archive tampering detected (tested)
- docs/threat-model.md (attacker matrix 1-9, proven-vs-trusted, design
invariants) + docs/runbook-latch.md (diagnose-first recovery)
- lecture 10: executable corrupt-a-member exercise (capsule pin catches
one appended byte), honest note on what the pin does NOT stop
Lightweight policy engine (POLICY_DENIED wired):
- policy.json: per-request/per-day amount ceilings, counterparty
allow/deny lists, per-identity overrides; rules make their intent
fields mandatory; daily sums from the ledger
-
Agent UX:
- signed refusal receipts travel inside MCP errors (receipt +
receipt_path in structuredContent)
- airgap over MCP: request_signature signer=airgap + request_id,
new airgap_pending tool; park -> list -> device answers -> complete
(tested end-to-end)
- all 8 tools carry readOnly/destructive annotations
- sliding-window rate limiter per tool class (custody/verify/liveness);
RATE_LIMITED refusal code; surface control, not ledgered
warden-treasury LIVE:
- treasury.py: stdlib base58, compact-u16, legacy+v0 wire parsing;
every required signature quorum-verified over exact message bytes;
completeness gap named in every verdict; RPC fetch uses response as
bytes only
-
- live-quorum test: synthetic Solana tx signed with wallet key ->
authentic via 4 proven forks; flipped byte -> not authentic
100 tests green (was 85).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 11:38:42 +00:00
" annotations " : { " readOnlyHint " : False , " destructiveHint " : False , " idempotentHint " : True } ,
2026-07-06 21:35:47 +00:00
} ,
{
" name " : " request_signature " ,
warden hardening round: policy engine, ledger rotation, MCP UX, treasury LIVE, ops docs
Tier 2:
- request_signature decomposed into named gates (latch, freshness,
intent, policy, signer, firewall)
- ledger: O(1) tail-read appends under a dedicated lock file (survives
rotation rename); hash-chained segment rotation at policy
ledger.rotate_at; verify-ledger walks all segments to genesis;
archive tampering detected (tested)
- docs/threat-model.md (attacker matrix 1-9, proven-vs-trusted, design
invariants) + docs/runbook-latch.md (diagnose-first recovery)
- lecture 10: executable corrupt-a-member exercise (capsule pin catches
one appended byte), honest note on what the pin does NOT stop
Lightweight policy engine (POLICY_DENIED wired):
- policy.json: per-request/per-day amount ceilings, counterparty
allow/deny lists, per-identity overrides; rules make their intent
fields mandatory; daily sums from the ledger
-
Agent UX:
- signed refusal receipts travel inside MCP errors (receipt +
receipt_path in structuredContent)
- airgap over MCP: request_signature signer=airgap + request_id,
new airgap_pending tool; park -> list -> device answers -> complete
(tested end-to-end)
- all 8 tools carry readOnly/destructive annotations
- sliding-window rate limiter per tool class (custody/verify/liveness);
RATE_LIMITED refusal code; surface control, not ledgered
warden-treasury LIVE:
- treasury.py: stdlib base58, compact-u16, legacy+v0 wire parsing;
every required signature quorum-verified over exact message bytes;
completeness gap named in every verdict; RPC fetch uses response as
bytes only
-
- live-quorum test: synthetic Solana tx signed with wallet key ->
authentic via 4 proven forks; flipped byte -> not authentic
100 tests green (was 85).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 11:38:42 +00:00
" description " : " Outbound signing with intent binding, spending policy, and the quorum firewall. Provide intent.purpose (recorded as WHY) and the payload; amount/counterparty feed the policy engine when rules exist; signer ' airgap ' parks the request for the gap device (resend with the returned request_id after the device answers). The produced signature is quorum-verified before release and quarantined if it fails. " ,
2026-07-06 21:35:47 +00:00
" inputSchema " : {
" type " : " object " ,
" properties " : {
" payload_b64 " : { " type " : " string " , " description " : " message bytes to sign, base64 " } ,
" purpose " : { " type " : " string " , " description " : " why this signature is requested (recorded in the ledger) " } ,
" identity " : { " type " : " string " , " description " : " wallet identity name (default: warden) " } ,
warden hardening round: policy engine, ledger rotation, MCP UX, treasury LIVE, ops docs
Tier 2:
- request_signature decomposed into named gates (latch, freshness,
intent, policy, signer, firewall)
- ledger: O(1) tail-read appends under a dedicated lock file (survives
rotation rename); hash-chained segment rotation at policy
ledger.rotate_at; verify-ledger walks all segments to genesis;
archive tampering detected (tested)
- docs/threat-model.md (attacker matrix 1-9, proven-vs-trusted, design
invariants) + docs/runbook-latch.md (diagnose-first recovery)
- lecture 10: executable corrupt-a-member exercise (capsule pin catches
one appended byte), honest note on what the pin does NOT stop
Lightweight policy engine (POLICY_DENIED wired):
- policy.json: per-request/per-day amount ceilings, counterparty
allow/deny lists, per-identity overrides; rules make their intent
fields mandatory; daily sums from the ledger
-
Agent UX:
- signed refusal receipts travel inside MCP errors (receipt +
receipt_path in structuredContent)
- airgap over MCP: request_signature signer=airgap + request_id,
new airgap_pending tool; park -> list -> device answers -> complete
(tested end-to-end)
- all 8 tools carry readOnly/destructive annotations
- sliding-window rate limiter per tool class (custody/verify/liveness);
RATE_LIMITED refusal code; surface control, not ledgered
warden-treasury LIVE:
- treasury.py: stdlib base58, compact-u16, legacy+v0 wire parsing;
every required signature quorum-verified over exact message bytes;
completeness gap named in every verdict; RPC fetch uses response as
bytes only
-
- live-quorum test: synthetic Solana tx signed with wallet key ->
authentic via 4 proven forks; flipped byte -> not authentic
100 tests green (was 85).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 11:38:42 +00:00
" amount " : { " type " : " number " , " description " : " policy units for spending rules (required when the policy has amount ceilings) " } ,
" counterparty " : { " type " : " string " , " description " : " who this benefits (required when the policy has counterparty lists) " } ,
" signer " : { " type " : " string " , " enum " : [ " local " , " airgap " ] , " description " : " signing backend (default local) " } ,
" request_id " : { " type " : " string " , " description " : " airgap request id when completing a parked request " } ,
2026-07-06 21:35:47 +00:00
} ,
" required " : [ " payload_b64 " , " purpose " ] ,
" additionalProperties " : False ,
} ,
warden hardening round: policy engine, ledger rotation, MCP UX, treasury LIVE, ops docs
Tier 2:
- request_signature decomposed into named gates (latch, freshness,
intent, policy, signer, firewall)
- ledger: O(1) tail-read appends under a dedicated lock file (survives
rotation rename); hash-chained segment rotation at policy
ledger.rotate_at; verify-ledger walks all segments to genesis;
archive tampering detected (tested)
- docs/threat-model.md (attacker matrix 1-9, proven-vs-trusted, design
invariants) + docs/runbook-latch.md (diagnose-first recovery)
- lecture 10: executable corrupt-a-member exercise (capsule pin catches
one appended byte), honest note on what the pin does NOT stop
Lightweight policy engine (POLICY_DENIED wired):
- policy.json: per-request/per-day amount ceilings, counterparty
allow/deny lists, per-identity overrides; rules make their intent
fields mandatory; daily sums from the ledger
-
Agent UX:
- signed refusal receipts travel inside MCP errors (receipt +
receipt_path in structuredContent)
- airgap over MCP: request_signature signer=airgap + request_id,
new airgap_pending tool; park -> list -> device answers -> complete
(tested end-to-end)
- all 8 tools carry readOnly/destructive annotations
- sliding-window rate limiter per tool class (custody/verify/liveness);
RATE_LIMITED refusal code; surface control, not ledgered
warden-treasury LIVE:
- treasury.py: stdlib base58, compact-u16, legacy+v0 wire parsing;
every required signature quorum-verified over exact message bytes;
completeness gap named in every verdict; RPC fetch uses response as
bytes only
-
- live-quorum test: synthetic Solana tx signed with wallet key ->
authentic via 4 proven forks; flipped byte -> not authentic
100 tests green (was 85).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 11:38:42 +00:00
" annotations " : { " readOnlyHint " : False , " destructiveHint " : False , " idempotentHint " : False } ,
} ,
{
" name " : " airgap_pending " ,
" description " : " List parked airgap signing requests (outbox) and whether the device has answered (inbox). Use the request_id with request_signature to complete one. " ,
" inputSchema " : { " type " : " object " , " properties " : { } , " additionalProperties " : False } ,
" annotations " : { " readOnlyHint " : True } ,
2026-07-06 21:35:47 +00:00
} ,
{
" name " : " custody_card " ,
" description " : " The self-proving business card: quorum membership with embedded transparency-log inclusion proofs a counterparty can recompute, signing provenance, honesty ledger. No trust required. " ,
" inputSchema " : { " type " : " object " , " properties " : { } , " additionalProperties " : False } ,
warden hardening round: policy engine, ledger rotation, MCP UX, treasury LIVE, ops docs
Tier 2:
- request_signature decomposed into named gates (latch, freshness,
intent, policy, signer, firewall)
- ledger: O(1) tail-read appends under a dedicated lock file (survives
rotation rename); hash-chained segment rotation at policy
ledger.rotate_at; verify-ledger walks all segments to genesis;
archive tampering detected (tested)
- docs/threat-model.md (attacker matrix 1-9, proven-vs-trusted, design
invariants) + docs/runbook-latch.md (diagnose-first recovery)
- lecture 10: executable corrupt-a-member exercise (capsule pin catches
one appended byte), honest note on what the pin does NOT stop
Lightweight policy engine (POLICY_DENIED wired):
- policy.json: per-request/per-day amount ceilings, counterparty
allow/deny lists, per-identity overrides; rules make their intent
fields mandatory; daily sums from the ledger
-
Agent UX:
- signed refusal receipts travel inside MCP errors (receipt +
receipt_path in structuredContent)
- airgap over MCP: request_signature signer=airgap + request_id,
new airgap_pending tool; park -> list -> device answers -> complete
(tested end-to-end)
- all 8 tools carry readOnly/destructive annotations
- sliding-window rate limiter per tool class (custody/verify/liveness);
RATE_LIMITED refusal code; surface control, not ledgered
warden-treasury LIVE:
- treasury.py: stdlib base58, compact-u16, legacy+v0 wire parsing;
every required signature quorum-verified over exact message bytes;
completeness gap named in every verdict; RPC fetch uses response as
bytes only
-
- live-quorum test: synthetic Solana tx signed with wallet key ->
authentic via 4 proven forks; flipped byte -> not authentic
100 tests green (was 85).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 11:38:42 +00:00
" annotations " : { " readOnlyHint " : True } ,
2026-07-06 21:35:47 +00:00
} ,
{
" name " : " posture_challenge " ,
" description " : " Proof-of-posture: send a nonce (8..128 chars), receive a signed posture attestation whose signature passed the outbound firewall, with the full quorum trail attached. An auditable heartbeat. " ,
" inputSchema " : {
" type " : " object " ,
" properties " : { " nonce " : { " type " : " string " } } ,
" required " : [ " nonce " ] ,
" additionalProperties " : False ,
} ,
warden hardening round: policy engine, ledger rotation, MCP UX, treasury LIVE, ops docs
Tier 2:
- request_signature decomposed into named gates (latch, freshness,
intent, policy, signer, firewall)
- ledger: O(1) tail-read appends under a dedicated lock file (survives
rotation rename); hash-chained segment rotation at policy
ledger.rotate_at; verify-ledger walks all segments to genesis;
archive tampering detected (tested)
- docs/threat-model.md (attacker matrix 1-9, proven-vs-trusted, design
invariants) + docs/runbook-latch.md (diagnose-first recovery)
- lecture 10: executable corrupt-a-member exercise (capsule pin catches
one appended byte), honest note on what the pin does NOT stop
Lightweight policy engine (POLICY_DENIED wired):
- policy.json: per-request/per-day amount ceilings, counterparty
allow/deny lists, per-identity overrides; rules make their intent
fields mandatory; daily sums from the ledger
-
Agent UX:
- signed refusal receipts travel inside MCP errors (receipt +
receipt_path in structuredContent)
- airgap over MCP: request_signature signer=airgap + request_id,
new airgap_pending tool; park -> list -> device answers -> complete
(tested end-to-end)
- all 8 tools carry readOnly/destructive annotations
- sliding-window rate limiter per tool class (custody/verify/liveness);
RATE_LIMITED refusal code; surface control, not ledgered
warden-treasury LIVE:
- treasury.py: stdlib base58, compact-u16, legacy+v0 wire parsing;
every required signature quorum-verified over exact message bytes;
completeness gap named in every verdict; RPC fetch uses response as
bytes only
-
- live-quorum test: synthetic Solana tx signed with wallet key ->
authentic via 4 proven forks; flipped byte -> not authentic
100 tests green (was 85).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 11:38:42 +00:00
" annotations " : { " readOnlyHint " : False , " destructiveHint " : False } ,
2026-07-06 21:35:47 +00:00
} ,
{
" name " : " list_incidents " ,
" description " : " Quorum divergences and firewall quarantines recorded by this wallet, newest-first, with severities and trails. " ,
" inputSchema " : { " type " : " object " , " properties " : { } , " additionalProperties " : False } ,
warden hardening round: policy engine, ledger rotation, MCP UX, treasury LIVE, ops docs
Tier 2:
- request_signature decomposed into named gates (latch, freshness,
intent, policy, signer, firewall)
- ledger: O(1) tail-read appends under a dedicated lock file (survives
rotation rename); hash-chained segment rotation at policy
ledger.rotate_at; verify-ledger walks all segments to genesis;
archive tampering detected (tested)
- docs/threat-model.md (attacker matrix 1-9, proven-vs-trusted, design
invariants) + docs/runbook-latch.md (diagnose-first recovery)
- lecture 10: executable corrupt-a-member exercise (capsule pin catches
one appended byte), honest note on what the pin does NOT stop
Lightweight policy engine (POLICY_DENIED wired):
- policy.json: per-request/per-day amount ceilings, counterparty
allow/deny lists, per-identity overrides; rules make their intent
fields mandatory; daily sums from the ledger
-
Agent UX:
- signed refusal receipts travel inside MCP errors (receipt +
receipt_path in structuredContent)
- airgap over MCP: request_signature signer=airgap + request_id,
new airgap_pending tool; park -> list -> device answers -> complete
(tested end-to-end)
- all 8 tools carry readOnly/destructive annotations
- sliding-window rate limiter per tool class (custody/verify/liveness);
RATE_LIMITED refusal code; surface control, not ledgered
warden-treasury LIVE:
- treasury.py: stdlib base58, compact-u16, legacy+v0 wire parsing;
every required signature quorum-verified over exact message bytes;
completeness gap named in every verdict; RPC fetch uses response as
bytes only
-
- live-quorum test: synthetic Solana tx signed with wallet key ->
authentic via 4 proven forks; flipped byte -> not authentic
100 tests green (was 85).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 11:38:42 +00:00
" annotations " : { " readOnlyHint " : True } ,
2026-07-06 21:35:47 +00:00
} ,
{
" name " : " explain_refusal " ,
" description " : " Fetch a previously issued refusal receipt by index, or the latest. Returns the machine-actionable receipt (code, missing, remediation, signature). " ,
" inputSchema " : {
" type " : " object " ,
" properties " : { " index " : { " type " : " integer " , " description " : " receipt number; omit for latest " } } ,
" additionalProperties " : False ,
} ,
warden hardening round: policy engine, ledger rotation, MCP UX, treasury LIVE, ops docs
Tier 2:
- request_signature decomposed into named gates (latch, freshness,
intent, policy, signer, firewall)
- ledger: O(1) tail-read appends under a dedicated lock file (survives
rotation rename); hash-chained segment rotation at policy
ledger.rotate_at; verify-ledger walks all segments to genesis;
archive tampering detected (tested)
- docs/threat-model.md (attacker matrix 1-9, proven-vs-trusted, design
invariants) + docs/runbook-latch.md (diagnose-first recovery)
- lecture 10: executable corrupt-a-member exercise (capsule pin catches
one appended byte), honest note on what the pin does NOT stop
Lightweight policy engine (POLICY_DENIED wired):
- policy.json: per-request/per-day amount ceilings, counterparty
allow/deny lists, per-identity overrides; rules make their intent
fields mandatory; daily sums from the ledger
-
Agent UX:
- signed refusal receipts travel inside MCP errors (receipt +
receipt_path in structuredContent)
- airgap over MCP: request_signature signer=airgap + request_id,
new airgap_pending tool; park -> list -> device answers -> complete
(tested end-to-end)
- all 8 tools carry readOnly/destructive annotations
- sliding-window rate limiter per tool class (custody/verify/liveness);
RATE_LIMITED refusal code; surface control, not ledgered
warden-treasury LIVE:
- treasury.py: stdlib base58, compact-u16, legacy+v0 wire parsing;
every required signature quorum-verified over exact message bytes;
completeness gap named in every verdict; RPC fetch uses response as
bytes only
-
- live-quorum test: synthetic Solana tx signed with wallet key ->
authentic via 4 proven forks; flipped byte -> not authentic
100 tests green (was 85).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 11:38:42 +00:00
" annotations " : { " readOnlyHint " : True } ,
2026-07-06 21:35:47 +00:00
} ,
]
class WalletMCP :
warden hardening round: policy engine, ledger rotation, MCP UX, treasury LIVE, ops docs
Tier 2:
- request_signature decomposed into named gates (latch, freshness,
intent, policy, signer, firewall)
- ledger: O(1) tail-read appends under a dedicated lock file (survives
rotation rename); hash-chained segment rotation at policy
ledger.rotate_at; verify-ledger walks all segments to genesis;
archive tampering detected (tested)
- docs/threat-model.md (attacker matrix 1-9, proven-vs-trusted, design
invariants) + docs/runbook-latch.md (diagnose-first recovery)
- lecture 10: executable corrupt-a-member exercise (capsule pin catches
one appended byte), honest note on what the pin does NOT stop
Lightweight policy engine (POLICY_DENIED wired):
- policy.json: per-request/per-day amount ceilings, counterparty
allow/deny lists, per-identity overrides; rules make their intent
fields mandatory; daily sums from the ledger
-
Agent UX:
- signed refusal receipts travel inside MCP errors (receipt +
receipt_path in structuredContent)
- airgap over MCP: request_signature signer=airgap + request_id,
new airgap_pending tool; park -> list -> device answers -> complete
(tested end-to-end)
- all 8 tools carry readOnly/destructive annotations
- sliding-window rate limiter per tool class (custody/verify/liveness);
RATE_LIMITED refusal code; surface control, not ledgered
warden-treasury LIVE:
- treasury.py: stdlib base58, compact-u16, legacy+v0 wire parsing;
every required signature quorum-verified over exact message bytes;
completeness gap named in every verdict; RPC fetch uses response as
bytes only
-
- live-quorum test: synthetic Solana tx signed with wallet key ->
authentic via 4 proven forks; flipped byte -> not authentic
100 tests green (was 85).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 11:38:42 +00:00
def __init__ (
self ,
wallet_dir : str | Path ,
log_url : str = " https://ltl.zkdefi.org " ,
state_dir : str | Path | None = None ,
) - > None :
self . wallet = Wallet ( wallet_dir , state_dir = state_dir )
2026-07-06 21:35:47 +00:00
self . log_url = log_url
warden hardening round: policy engine, ledger rotation, MCP UX, treasury LIVE, ops docs
Tier 2:
- request_signature decomposed into named gates (latch, freshness,
intent, policy, signer, firewall)
- ledger: O(1) tail-read appends under a dedicated lock file (survives
rotation rename); hash-chained segment rotation at policy
ledger.rotate_at; verify-ledger walks all segments to genesis;
archive tampering detected (tested)
- docs/threat-model.md (attacker matrix 1-9, proven-vs-trusted, design
invariants) + docs/runbook-latch.md (diagnose-first recovery)
- lecture 10: executable corrupt-a-member exercise (capsule pin catches
one appended byte), honest note on what the pin does NOT stop
Lightweight policy engine (POLICY_DENIED wired):
- policy.json: per-request/per-day amount ceilings, counterparty
allow/deny lists, per-identity overrides; rules make their intent
fields mandatory; daily sums from the ledger
-
Agent UX:
- signed refusal receipts travel inside MCP errors (receipt +
receipt_path in structuredContent)
- airgap over MCP: request_signature signer=airgap + request_id,
new airgap_pending tool; park -> list -> device answers -> complete
(tested end-to-end)
- all 8 tools carry readOnly/destructive annotations
- sliding-window rate limiter per tool class (custody/verify/liveness);
RATE_LIMITED refusal code; surface control, not ledgered
warden-treasury LIVE:
- treasury.py: stdlib base58, compact-u16, legacy+v0 wire parsing;
every required signature quorum-verified over exact message bytes;
completeness gap named in every verdict; RPC fetch uses response as
bytes only
-
- live-quorum test: synthetic Solana tx signed with wallet key ->
authentic via 4 proven forks; flipped byte -> not authentic
100 tests green (was 85).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 11:38:42 +00:00
self . limiter = _RateLimiter ( )
2026-07-06 21:35:47 +00:00
self . handlers : dict [ str , Callable [ [ dict [ str , Any ] ] , dict [ str , Any ] ] ] = {
" wallet_status " : self . _wallet_status ,
" verify_inbound " : self . _verify_inbound ,
" request_signature " : self . _request_signature ,
warden hardening round: policy engine, ledger rotation, MCP UX, treasury LIVE, ops docs
Tier 2:
- request_signature decomposed into named gates (latch, freshness,
intent, policy, signer, firewall)
- ledger: O(1) tail-read appends under a dedicated lock file (survives
rotation rename); hash-chained segment rotation at policy
ledger.rotate_at; verify-ledger walks all segments to genesis;
archive tampering detected (tested)
- docs/threat-model.md (attacker matrix 1-9, proven-vs-trusted, design
invariants) + docs/runbook-latch.md (diagnose-first recovery)
- lecture 10: executable corrupt-a-member exercise (capsule pin catches
one appended byte), honest note on what the pin does NOT stop
Lightweight policy engine (POLICY_DENIED wired):
- policy.json: per-request/per-day amount ceilings, counterparty
allow/deny lists, per-identity overrides; rules make their intent
fields mandatory; daily sums from the ledger
-
Agent UX:
- signed refusal receipts travel inside MCP errors (receipt +
receipt_path in structuredContent)
- airgap over MCP: request_signature signer=airgap + request_id,
new airgap_pending tool; park -> list -> device answers -> complete
(tested end-to-end)
- all 8 tools carry readOnly/destructive annotations
- sliding-window rate limiter per tool class (custody/verify/liveness);
RATE_LIMITED refusal code; surface control, not ledgered
warden-treasury LIVE:
- treasury.py: stdlib base58, compact-u16, legacy+v0 wire parsing;
every required signature quorum-verified over exact message bytes;
completeness gap named in every verdict; RPC fetch uses response as
bytes only
-
- live-quorum test: synthetic Solana tx signed with wallet key ->
authentic via 4 proven forks; flipped byte -> not authentic
100 tests green (was 85).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 11:38:42 +00:00
" airgap_pending " : self . _airgap_pending ,
2026-07-06 21:35:47 +00:00
" custody_card " : self . _custody_card ,
" posture_challenge " : self . _posture_challenge ,
" list_incidents " : self . _list_incidents ,
" explain_refusal " : self . _explain_refusal ,
}
# -- tool implementations ------------------------------------------------
def _wallet_status ( self , _ : dict [ str , Any ] ) - > dict [ str , Any ] :
return self . wallet . posture ( )
def _verify_inbound ( self , args : dict [ str , Any ] ) - > dict [ str , Any ] :
payload = _b64_to_bytes ( " payload_b64 " , args . get ( " payload_b64 " ) )
signature = _b64_to_bytes ( " signature_b64 " , args . get ( " signature_b64 " ) )
public_key = _b64_to_bytes ( " public_key_b64 " , args . get ( " public_key_b64 " ) )
if len ( signature ) != 64 :
raise _ToolError ( " MALFORMED_INTENT " , " signature must be 64 bytes " , [ " signature_b64 " ] , " send a 64-byte Ed25519 signature " )
if len ( public_key ) != 32 :
raise _ToolError ( " MALFORMED_INTENT " , " public key must be 32 bytes " , [ " public_key_b64 " ] , " send a 32-byte raw Ed25519 key " )
result = self . wallet . verify_inbound ( payload , signature , public_key , context = str ( args . get ( " context " , " " ) ) )
return result . to_dict ( )
def _request_signature ( self , args : dict [ str , Any ] ) - > dict [ str , Any ] :
import hashlib
payload = _b64_to_bytes ( " payload_b64 " , args . get ( " payload_b64 " ) )
purpose = args . get ( " purpose " )
if not isinstance ( purpose , str ) or not purpose . strip ( ) :
raise _ToolError ( " MALFORMED_INTENT " , " purpose is required " , [ " purpose " ] , " state why the signature is requested " )
warden hardening round: policy engine, ledger rotation, MCP UX, treasury LIVE, ops docs
Tier 2:
- request_signature decomposed into named gates (latch, freshness,
intent, policy, signer, firewall)
- ledger: O(1) tail-read appends under a dedicated lock file (survives
rotation rename); hash-chained segment rotation at policy
ledger.rotate_at; verify-ledger walks all segments to genesis;
archive tampering detected (tested)
- docs/threat-model.md (attacker matrix 1-9, proven-vs-trusted, design
invariants) + docs/runbook-latch.md (diagnose-first recovery)
- lecture 10: executable corrupt-a-member exercise (capsule pin catches
one appended byte), honest note on what the pin does NOT stop
Lightweight policy engine (POLICY_DENIED wired):
- policy.json: per-request/per-day amount ceilings, counterparty
allow/deny lists, per-identity overrides; rules make their intent
fields mandatory; daily sums from the ledger
-
Agent UX:
- signed refusal receipts travel inside MCP errors (receipt +
receipt_path in structuredContent)
- airgap over MCP: request_signature signer=airgap + request_id,
new airgap_pending tool; park -> list -> device answers -> complete
(tested end-to-end)
- all 8 tools carry readOnly/destructive annotations
- sliding-window rate limiter per tool class (custody/verify/liveness);
RATE_LIMITED refusal code; surface control, not ledgered
warden-treasury LIVE:
- treasury.py: stdlib base58, compact-u16, legacy+v0 wire parsing;
every required signature quorum-verified over exact message bytes;
completeness gap named in every verdict; RPC fetch uses response as
bytes only
-
- live-quorum test: synthetic Solana tx signed with wallet key ->
authentic via 4 proven forks; flipped byte -> not authentic
100 tests green (was 85).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 11:38:42 +00:00
intent : dict [ str , Any ] = { " purpose " : purpose , " payload_sha256 " : hashlib . sha256 ( payload ) . hexdigest ( ) }
if args . get ( " amount " ) is not None :
intent [ " amount " ] = args [ " amount " ]
if args . get ( " counterparty " ) is not None :
intent [ " counterparty " ] = args [ " counterparty " ]
signer = None
if args . get ( " signer " ) == " airgap " :
from . wallet import AirgapSigner
signer = AirgapSigner ( self . wallet . airgap_dir )
2026-07-06 21:35:47 +00:00
result = self . wallet . request_signature (
warden hardening round: policy engine, ledger rotation, MCP UX, treasury LIVE, ops docs
Tier 2:
- request_signature decomposed into named gates (latch, freshness,
intent, policy, signer, firewall)
- ledger: O(1) tail-read appends under a dedicated lock file (survives
rotation rename); hash-chained segment rotation at policy
ledger.rotate_at; verify-ledger walks all segments to genesis;
archive tampering detected (tested)
- docs/threat-model.md (attacker matrix 1-9, proven-vs-trusted, design
invariants) + docs/runbook-latch.md (diagnose-first recovery)
- lecture 10: executable corrupt-a-member exercise (capsule pin catches
one appended byte), honest note on what the pin does NOT stop
Lightweight policy engine (POLICY_DENIED wired):
- policy.json: per-request/per-day amount ceilings, counterparty
allow/deny lists, per-identity overrides; rules make their intent
fields mandatory; daily sums from the ledger
-
Agent UX:
- signed refusal receipts travel inside MCP errors (receipt +
receipt_path in structuredContent)
- airgap over MCP: request_signature signer=airgap + request_id,
new airgap_pending tool; park -> list -> device answers -> complete
(tested end-to-end)
- all 8 tools carry readOnly/destructive annotations
- sliding-window rate limiter per tool class (custody/verify/liveness);
RATE_LIMITED refusal code; surface control, not ledgered
warden-treasury LIVE:
- treasury.py: stdlib base58, compact-u16, legacy+v0 wire parsing;
every required signature quorum-verified over exact message bytes;
completeness gap named in every verdict; RPC fetch uses response as
bytes only
-
- live-quorum test: synthetic Solana tx signed with wallet key ->
authentic via 4 proven forks; flipped byte -> not authentic
100 tests green (was 85).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 11:38:42 +00:00
intent ,
payload ,
signer = signer ,
key_name = str ( args . get ( " identity " , " warden " ) ) ,
request_id = args . get ( " request_id " ) ,
2026-07-06 21:35:47 +00:00
)
if isinstance ( result , Refusal ) :
warden hardening round: policy engine, ledger rotation, MCP UX, treasury LIVE, ops docs
Tier 2:
- request_signature decomposed into named gates (latch, freshness,
intent, policy, signer, firewall)
- ledger: O(1) tail-read appends under a dedicated lock file (survives
rotation rename); hash-chained segment rotation at policy
ledger.rotate_at; verify-ledger walks all segments to genesis;
archive tampering detected (tested)
- docs/threat-model.md (attacker matrix 1-9, proven-vs-trusted, design
invariants) + docs/runbook-latch.md (diagnose-first recovery)
- lecture 10: executable corrupt-a-member exercise (capsule pin catches
one appended byte), honest note on what the pin does NOT stop
Lightweight policy engine (POLICY_DENIED wired):
- policy.json: per-request/per-day amount ceilings, counterparty
allow/deny lists, per-identity overrides; rules make their intent
fields mandatory; daily sums from the ledger
-
Agent UX:
- signed refusal receipts travel inside MCP errors (receipt +
receipt_path in structuredContent)
- airgap over MCP: request_signature signer=airgap + request_id,
new airgap_pending tool; park -> list -> device answers -> complete
(tested end-to-end)
- all 8 tools carry readOnly/destructive annotations
- sliding-window rate limiter per tool class (custody/verify/liveness);
RATE_LIMITED refusal code; surface control, not ledgered
warden-treasury LIVE:
- treasury.py: stdlib base58, compact-u16, legacy+v0 wire parsing;
every required signature quorum-verified over exact message bytes;
completeness gap named in every verdict; RPC fetch uses response as
bytes only
-
- live-quorum test: synthetic Solana tx signed with wallet key ->
authentic via 4 proven forks; flipped byte -> not authentic
100 tests green (was 85).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 11:38:42 +00:00
raise _refusal_error ( result )
2026-07-06 21:35:47 +00:00
return result
warden hardening round: policy engine, ledger rotation, MCP UX, treasury LIVE, ops docs
Tier 2:
- request_signature decomposed into named gates (latch, freshness,
intent, policy, signer, firewall)
- ledger: O(1) tail-read appends under a dedicated lock file (survives
rotation rename); hash-chained segment rotation at policy
ledger.rotate_at; verify-ledger walks all segments to genesis;
archive tampering detected (tested)
- docs/threat-model.md (attacker matrix 1-9, proven-vs-trusted, design
invariants) + docs/runbook-latch.md (diagnose-first recovery)
- lecture 10: executable corrupt-a-member exercise (capsule pin catches
one appended byte), honest note on what the pin does NOT stop
Lightweight policy engine (POLICY_DENIED wired):
- policy.json: per-request/per-day amount ceilings, counterparty
allow/deny lists, per-identity overrides; rules make their intent
fields mandatory; daily sums from the ledger
-
Agent UX:
- signed refusal receipts travel inside MCP errors (receipt +
receipt_path in structuredContent)
- airgap over MCP: request_signature signer=airgap + request_id,
new airgap_pending tool; park -> list -> device answers -> complete
(tested end-to-end)
- all 8 tools carry readOnly/destructive annotations
- sliding-window rate limiter per tool class (custody/verify/liveness);
RATE_LIMITED refusal code; surface control, not ledgered
warden-treasury LIVE:
- treasury.py: stdlib base58, compact-u16, legacy+v0 wire parsing;
every required signature quorum-verified over exact message bytes;
completeness gap named in every verdict; RPC fetch uses response as
bytes only
-
- live-quorum test: synthetic Solana tx signed with wallet key ->
authentic via 4 proven forks; flipped byte -> not authentic
100 tests green (was 85).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 11:38:42 +00:00
def _airgap_pending ( self , _ : dict [ str , Any ] ) - > dict [ str , Any ] :
pending = [ ]
outbox = self . wallet . airgap_dir / " outbox "
inbox = self . wallet . airgap_dir / " inbox "
for req in sorted ( outbox . glob ( " *.request.json " ) ) :
request_id = req . name . removesuffix ( " .request.json " )
body = json . loads ( req . read_text ( encoding = " utf-8 " ) )
pending . append ( {
" request_id " : request_id ,
" created_at " : body . get ( " created_at " ) ,
" payload_sha256 " : body . get ( " payload_sha256 " ) ,
" intent " : body . get ( " intent " ) ,
" device_answered " : ( inbox / f " { request_id } .response.json " ) . exists ( ) ,
} )
return { " pending " : pending , " count " : len ( pending ) }
2026-07-06 21:35:47 +00:00
def _custody_card ( self , _ : dict [ str , Any ] ) - > dict [ str , Any ] :
return build_custody_card ( self . wallet , self . log_url )
def _posture_challenge ( self , args : dict [ str , Any ] ) - > dict [ str , Any ] :
try :
result = posture_challenge ( self . wallet , str ( args . get ( " nonce " , " " ) ) )
except ValueError as exc :
raise _ToolError ( " MALFORMED_INTENT " , str ( exc ) , [ " nonce " ] , " send an 8..128 character nonce " )
if isinstance ( result , Refusal ) :
warden hardening round: policy engine, ledger rotation, MCP UX, treasury LIVE, ops docs
Tier 2:
- request_signature decomposed into named gates (latch, freshness,
intent, policy, signer, firewall)
- ledger: O(1) tail-read appends under a dedicated lock file (survives
rotation rename); hash-chained segment rotation at policy
ledger.rotate_at; verify-ledger walks all segments to genesis;
archive tampering detected (tested)
- docs/threat-model.md (attacker matrix 1-9, proven-vs-trusted, design
invariants) + docs/runbook-latch.md (diagnose-first recovery)
- lecture 10: executable corrupt-a-member exercise (capsule pin catches
one appended byte), honest note on what the pin does NOT stop
Lightweight policy engine (POLICY_DENIED wired):
- policy.json: per-request/per-day amount ceilings, counterparty
allow/deny lists, per-identity overrides; rules make their intent
fields mandatory; daily sums from the ledger
-
Agent UX:
- signed refusal receipts travel inside MCP errors (receipt +
receipt_path in structuredContent)
- airgap over MCP: request_signature signer=airgap + request_id,
new airgap_pending tool; park -> list -> device answers -> complete
(tested end-to-end)
- all 8 tools carry readOnly/destructive annotations
- sliding-window rate limiter per tool class (custody/verify/liveness);
RATE_LIMITED refusal code; surface control, not ledgered
warden-treasury LIVE:
- treasury.py: stdlib base58, compact-u16, legacy+v0 wire parsing;
every required signature quorum-verified over exact message bytes;
completeness gap named in every verdict; RPC fetch uses response as
bytes only
-
- live-quorum test: synthetic Solana tx signed with wallet key ->
authentic via 4 proven forks; flipped byte -> not authentic
100 tests green (was 85).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 11:38:42 +00:00
raise _refusal_error ( result )
2026-07-06 21:35:47 +00:00
return result
def _list_incidents ( self , _ : dict [ str , Any ] ) - > dict [ str , Any ] :
incidents = [ ]
for path in sorted ( self . wallet . incidents_dir . glob ( " *.json " ) , reverse = True ) :
incidents . append ( json . loads ( path . read_text ( encoding = " utf-8 " ) ) )
return { " incidents " : incidents , " count " : len ( incidents ) }
def _explain_refusal ( self , args : dict [ str , Any ] ) - > dict [ str , Any ] :
receipts = sorted ( self . wallet . receipts_dir . glob ( " *.json " ) )
if not receipts :
raise _ToolError ( " EVIDENCE_REQUIRED " , " no refusal receipts issued yet " , [ ] , " there is nothing to explain " )
idx = args . get ( " index " )
if idx is None :
path = receipts [ - 1 ]
else :
match = [ p for p in receipts if p . stem == f " { int ( idx ) : 04d } " ]
if not match :
raise _ToolError ( " EVIDENCE_REQUIRED " , f " no receipt { idx } " , [ ] , f " valid indices 0.. { len ( receipts ) - 1 } " )
path = match [ 0 ]
return json . loads ( path . read_text ( encoding = " utf-8 " ) )
# -- JSON-RPC plumbing ---------------------------------------------------
def handle ( self , message : dict [ str , Any ] ) - > dict [ str , Any ] | None :
method = message . get ( " method " )
msg_id = message . get ( " id " )
if method == " initialize " :
return self . _ok ( msg_id , {
" protocolVersion " : PROTOCOL_VERSION ,
" capabilities " : { " tools " : { " listChanged " : False } } ,
" serverInfo " : SERVER_INFO ,
} )
if method in ( " notifications/initialized " , " initialized " ) :
return None
if method == " tools/list " :
return self . _ok ( msg_id , { " tools " : TOOLS } )
if method == " tools/call " :
params = message . get ( " params " ) or { }
name = params . get ( " name " )
args = params . get ( " arguments " ) or { }
handler = self . handlers . get ( name )
if handler is None :
return self . _err ( msg_id , - 32602 , f " unknown tool { name } " )
try :
warden hardening round: policy engine, ledger rotation, MCP UX, treasury LIVE, ops docs
Tier 2:
- request_signature decomposed into named gates (latch, freshness,
intent, policy, signer, firewall)
- ledger: O(1) tail-read appends under a dedicated lock file (survives
rotation rename); hash-chained segment rotation at policy
ledger.rotate_at; verify-ledger walks all segments to genesis;
archive tampering detected (tested)
- docs/threat-model.md (attacker matrix 1-9, proven-vs-trusted, design
invariants) + docs/runbook-latch.md (diagnose-first recovery)
- lecture 10: executable corrupt-a-member exercise (capsule pin catches
one appended byte), honest note on what the pin does NOT stop
Lightweight policy engine (POLICY_DENIED wired):
- policy.json: per-request/per-day amount ceilings, counterparty
allow/deny lists, per-identity overrides; rules make their intent
fields mandatory; daily sums from the ledger
-
Agent UX:
- signed refusal receipts travel inside MCP errors (receipt +
receipt_path in structuredContent)
- airgap over MCP: request_signature signer=airgap + request_id,
new airgap_pending tool; park -> list -> device answers -> complete
(tested end-to-end)
- all 8 tools carry readOnly/destructive annotations
- sliding-window rate limiter per tool class (custody/verify/liveness);
RATE_LIMITED refusal code; surface control, not ledgered
warden-treasury LIVE:
- treasury.py: stdlib base58, compact-u16, legacy+v0 wire parsing;
every required signature quorum-verified over exact message bytes;
completeness gap named in every verdict; RPC fetch uses response as
bytes only
-
- live-quorum test: synthetic Solana tx signed with wallet key ->
authentic via 4 proven forks; flipped byte -> not authentic
100 tests green (was 85).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 11:38:42 +00:00
self . limiter . check ( _TOOL_CATEGORY . get ( name , " custody " ) )
2026-07-06 21:35:47 +00:00
payload = handler ( args )
return self . _ok ( msg_id , {
" content " : [ { " type " : " text " , " text " : json . dumps ( payload , indent = 2 , sort_keys = True ) } ] ,
" structuredContent " : payload ,
" isError " : False ,
} )
except _ToolError as exc :
return self . _ok ( msg_id , {
" content " : [ { " type " : " text " , " text " : json . dumps ( exc . payload , indent = 2 , sort_keys = True ) } ] ,
" structuredContent " : exc . payload ,
" isError " : True ,
} )
except Exception as exc : # noqa: BLE001 - never crash the server
err = { " code " : " INTERNAL " , " reason " : f " { type ( exc ) . __name__ } : { exc } " , " missing " : [ ] , " remediation " : " inspect the wallet directory " }
return self . _ok ( msg_id , {
" content " : [ { " type " : " text " , " text " : json . dumps ( err , indent = 2 , sort_keys = True ) } ] ,
" structuredContent " : err ,
" isError " : True ,
} )
if msg_id is None :
return None
return self . _err ( msg_id , - 32601 , f " method not found: { method } " )
@staticmethod
def _ok ( msg_id : Any , result : dict [ str , Any ] ) - > dict [ str , Any ] :
return { " jsonrpc " : " 2.0 " , " id " : msg_id , " result " : result }
@staticmethod
def _err ( msg_id : Any , code : int , message : str ) - > dict [ str , Any ] :
return { " jsonrpc " : " 2.0 " , " id " : msg_id , " error " : { " code " : code , " message " : message } }
def serve_stdio ( self , stdin : Any = None , stdout : Any = None ) - > None :
stdin = stdin or sys . stdin
stdout = stdout or sys . stdout
for line in stdin :
line = line . strip ( )
if not line :
continue
try :
message = json . loads ( line )
except json . JSONDecodeError :
continue
response = self . handle ( message )
if response is not None :
stdout . write ( json . dumps ( response ) + " \n " )
stdout . flush ( )