Agentic Wallet Guardian

Motor de decisão auto-hospedado que fica entre agentes de IA e a execução em blockchain, retornando ALLOW/WARN/BLOCK antes que qualquer transação seja assinada.

Documentação

Agentic Wallet Guardian

agentic-wallet-guardian-v3 MCP server

📄 Leia o white paper

Um mecanismo de decisão auto-hospedado que fica entre um agente de IA e a execução em blockchain. Agentes enviam uma ação proposta, o Guardian retorna uma decisão explicável ALLOW / WARN / BLOCK antes que qualquer coisa seja assinada ou transmitida.

POST /decision   ->   ALLOW / WARN / BLOCK  (with a reasoned explanation)

Ele roda na sua própria infraestrutura, usando suas próprias regras de política e seus próprios dados de reputação — veja Por que auto-hospedado para entender por que isso importa e como isso difere de chamar uma API de segurança hospedada diretamente.


Por que auto-hospedado

Existem boas alternativas hospedadas para segurança de transações de agentes (AgentGuard da GoPlus, Blockaid, Chainalysis/TRM para conformidade). Se você só quer um score de risco e não se importa com quem vê a consulta, chamar uma dessas diretamente é menos trabalho do que rodar isto. O Guardian existe para os casos em que essa troca não funciona para você:

  • Nada sobre quais carteiras, contratos ou valores seus agentes tocam sai da sua infraestrutura. Verificações de inteligência de ameaças e de permissão/negação de contratos são arquivos JSON locais que você mesmo preenche (veja data/threat_lists/README.md), não uma chamada de consulta a terceiros. Uma API hospedada inerentemente vê cada endereço e valor que você pergunta a ela.
  • Suas regras de política vivem no seu código, não no painel de um fornecedor. Limites de gastos, portões de reputação e quais tipos de ação exigem confirmação são Python puro em guardian/policy/, revisáveis e alteráveis sem esperar o roadmap de produto de ninguém.
  • Sem taxas por chamada ou limites de taxa impostos por terceiros — apenas os que você configura para seus próprios usuários (GUARDIAN_RATE_LIMIT_PER_MINUTE).
  • Sem dependência de fornecedor. Cada fonte de dados externa (endpoint RPC, instância Blockscout, DexScreener) é substituível atrás de uma pequena interface de provedor — veja Arquitetura.

A troca honesta no outro sentido: você também assume a responsabilidade de executá-lo, manter suas listas locais de ameaças atualizadas, e você não recebe a cobertura de cadeias de um fornecedor hospedado ou uma equipe dedicada de pesquisa de ameaças de graça. Esta é a escolha certa para equipes que precisam especificamente de soberania de dados ou personalização profunda de políticas — não uma atualização estrita sobre toda opção hospedada.


Arquitetura

                AI Agent
                    |
                    v
             Action Intent
   { agent_id, wallet, chain, action_type,
     target, amount, metadata }
                    |
                    v
        ┌───────────────────────────────┐
        │   Guardian Decision Engine    │
        ├───────────────────────────────┤
        │  1. Hard Rules                │  <- chain support, sanity checks
        │  2. Wallet Intelligence       │  <- mock | real RPC (web3.py)
        │  3. Token Intelligence        │  <- mock | real DexScreener | real GoPlus
        │  4. Contract Intelligence     │  <- local lists, then mock | real Blockscout | real GoPlus
        │  5. Simulation                │  <- mock | real eth_call dry-run (see below)
        │  6. Threat Intelligence       │  <- local JSON allow/deny lists
        │  7. Anomaly Detection         │  <- vs. this agent's own history (see below)
        │  8. Policy Engine             │  <- spending caps, reputation gates
        │  9. Risk Fusion               │  <- signals -> single 0-100 score
        │ 10. Reputation Adjustment     │
        │ 11. Explanation               │  <- evidence -> human-readable reasons
        └───────────────────────────────┘
                    |
                    v
          ALLOW / WARN / BLOCK
                    |
                    v
          Blockchain Execution

Cada fonte de dados nas etapas 2–4 é uma pequena interface de provedor com uma implementação mock (zero configuração, zero chamadas de rede) e uma real, selecionada por fonte via variável de ambiente — veja .env.example. Mudar do modo demo para uma implantação real é uma mudança de configuração, não de código.

Estrutura do repositório

guardian/
    config.py          GuardianConfig - the one place that reads os.environ
    core/               ActionIntent, Signal, Decision, EvaluationContext
                            (zero external dependencies - no pydantic/FastAPI)
    decision/           DecisionEngine (orchestrator), RiskFusionEngine, hard rules
    reasoning/          explanation + confidence builders
    intelligence/
        wallet/           analyzer.py + providers.py (mock | RpcWalletDataProvider)
        token/            analyzer.py + providers.py (mock | DexScreenerTokenDataProvider | GoPlusTokenDataProvider)
        contract/         analyzer.py + providers.py (mock | BlockscoutContractDataProvider | GoPlusContractDataProvider)
        simulation/       pre-execution dry-run (mock | real eth_call) + tx_builder.py (real calldata for transfer/approve)
        goplus_client.py  shared GoPlus Token Security API client (used by both contract + token)
        threat/           blocklist.py (local AddressList) + intelligence.py
    policy/             PolicyEngine + policy templates (spending caps, reputation gates)
    reputation/         AgentReputation (score derived from decision history)
    memory/             storage.py (protocol) + InMemoryStorage + sqlite_storage.py
api/
    main.py             FastAPI app: /decision, /health, /capabilities, /agents/{id}/history, /demo/{scenario}
    security.py         API-key auth dependency + rate-limit middleware
    schemas.py          pydantic request/response models (API boundary only)
mcp_server.py           MCP stdio server - same DecisionEngine, no HTTP required
data/threat_lists/      local, operator-maintained allow/deny lists (empty by default - see its README)
scripts/
    refresh_ofac_list.py   fetch OFAC's public SDN list into the local threat list
tests/                  101 tests covering the engine, policy, reputation, and every provider

guardian/* é intencionalmente livre de dependências (apenas biblioteca padrão, exceto onde um provedor real precisa de httpx ou web3), para que o núcleo de decisão possa ser testado unitariamente, embutido em outro serviço, ou portado para um framework web diferente sem arrastar FastAPI junto. Apenas api/ toca em pydantic/FastAPI.


Transparência sobre o estado atual

Isto é infraestrutura de decisão real, executável, testada, com fontes de dados reais (não mock) disponíveis para cada sinal — mas "disponível" não é o mesmo que "ligar um interruptor e confiar cegamente." Detalhes:

  • Carteira (provedor RPC): is_contract e tx_count (baseado em nonce) são confiáveis com qualquer endpoint JSON-RPC. A idade da carteira exige um nó com capacidade de arquivo e está desativada por padrão (GUARDIAN_RPC_ESTIMATE_AGE=false) — a maioria dos endpoints RPC públicos gratuitos não serve estado histórico, então isso falha de forma segura para "desconhecido" em vez de adivinhar.
  • Contrato (provedor Blockscout): consultas reais de status de verificação contra uma instância pública do Blockscout. O esquema exato de resposta e os limites de taxa podem mudar — isto é escrito para degradar para "desconhecido" em qualquer resposta inesperada, nunca para fabricar uma resposta, mas não foi testado sob carga contra tráfego de produção.
  • Token (provedor DexScreener): dados reais de liquidez, mas combinar um símbolo de ticker simples a um par on-chain é inerentemente ambíguo (muitos tokens não relacionados compartilham um símbolo, e golpistas deliberadamente criam imitações). O provedor escolhe o par de maior liquidez na cadeia solicitada e relata sua própria confiança de correspondência em vez de apresentar um palpite como certo — para qualquer coisa onde essa ambiguidade importa, combine por endereço de contrato em vez de símbolo.
  • Contrato + Token (provedor GoPlus): segurança real de contrato (dono pode drenar, mintável, autodestrutível, dono oculto) e segurança de negociação (honeypot, imposto de compra/venda, lista negra, transferências pausáveis, concentração de detentores) da API Token Security da GoPlus — significativamente mais tipos de sinal do que Blockscout/DexScreener dão individualmente, já que a análise estática da própria GoPlus cobre ambos em uma chamada. Dois limites reais: só tem dados para contratos que realmente analisou (principalmente contratos de token, não contratos genéricos de dApp/router), e GoPlusTokenDataProvider precisa de um endereço de contrato — um símbolo simples como "PEPE" não pode ser resolvido e é honestamente relatado como não verificável em vez de adivinhado.
  • Lista de endereços sancionados é real, com dados populados: 103 endereços (100 EVM + 3 Solana) da lista SDN do OFAC, via 0xB10C/ofac-sanctioned-digital-currency-addresses — verificado de ponta a ponta (um endereço conhecidamente sancionado corretamente aciona BLOCK através do pipeline completo) e verificado para refletir corretamente remoções, não apenas adições (os endereços do Tornado Cash, removidos da lista SDN em março de 2025, estão corretamente ausentes). Reexecute scripts/refresh_ofac_list.py periodicamente — sanções mudam em ambas as direções.
  • malicious_contracts.json / verified_contracts.json ainda são entregues vazios de propósito (veja data/threat_lists/README.md) — não há uma única fonte autoritativa para "contrato malicioso" da mesma forma que a lista do OFAC é autoritativa para sanções, então popular estes é uma decisão de julgamento para quem opera esta instância, não algo para semear por padrão com entradas não verificadas.
  • Simulação é real, mas condicional. RpcSimulationProvider (GUARDIAN_SIMULATION_PROVIDER=rpc) genuinamente executa um dry-run de uma transação via eth_call/eth_estimateGas contra o estado atual da cadeia — um revert retorna com seu motivo real, não um palpite, e valores de ERC-20 approve() são decodificados de calldata real em vez de inferidos. Isso ativa quando o chamador fornece calldata bruto via intent.metadata["data"], OU — novo — quando GUARDIAN_TX_BUILDER=rpc também está definido e a intenção é um transfer ou approve simples (veja o próximo item). Uma intenção swap sem transação construída ainda não tem nada para dry-run — o Guardian relata isso honestamente (simulation_not_attempted) em vez de adivinhar.
  • Construção de transação fecha parte dessa lacuna, deliberadamente não toda. RpcTransactionBuilder (GUARDIAN_TX_BUILDER=rpc) transforma uma intenção semântica transfer/approve em calldata real — busca o decimals() real do token via RPC em vez de assumir 18 (uma suposição errada ali escalaria o valor por ordens de magnitude), e deliberadamente não tem registro de endereço de token codificado: um símbolo simples como "USDC" é recusado em vez de adivinhado, já que um endereço errado aqui não seria apenas um sinal de risco ruim, seria um artefato que poderia acabar em uma transação real. swap é construído apenas contra Uniswap V2 Router02 (um contrato imutável e bem conhecido — seletores de função calculados localmente via Web3.keccak, não copiados da memória) — cotação on-chain real de getAmountsOut(), max_slippage_bps fornecido pelo chamador obrigatório (nunca um padrão, mesmo raciocínio dos decimais acima). bridge é um problema genuinamente aberto de roteamento L2/bridge em geral — dezenas de protocolos, modelos de confiança muito diferentes — mas este módulo lida com uma fatia bem delimitada: depósitos L1 -> L2 através da ponte oficial OP Stack da própria cadeia de destino (atualmente: Base e Optimism — depositETHTo/depositERC20To em L1StandardBridge, ambos os endereços verificados de forma independente — Base contra o rótulo do Etherscan mais basehub.org, Optimism contra o registro oficial ethereum-optimism/superchain-registry mais uma segunda configuração independente de ferramenta de dev — antes de serem codificados). Retiradas L2 -> L1 NÃO são construídas — esse é um fluxo genuinamente diferente, muito mais lento, de prova/janela de desafio, não uma variante da chamada de depósito. Ponte para qualquer outro lugar, ou via qualquer ponte não canônica, retorna None em vez de adivinhar.
  • Armazenamento: InMemoryStorage (padrão, zero configuração), SQLiteStorage (GUARDIAN_STORAGE_BACKEND=sqlite — persiste entre reinicializações, sem infraestrutura externa), ou PostgresStorage (GUARDIAN_STORAGE_BACKEND=postgres + GUARDIAN_POSTGRES_DSN — a opção certa para múltiplas réplicas atrás de um balanceador de carga, onde o modelo de escrita única do SQLite se torna o gargalo; pip install -r requirements-postgres.txt). Testado contra uma instância Postgres local real, não mockado — veja tests/test_postgres_storage.py. Redis está ainda aberto se você quiser especificamente; a interface de dois métodos MemoryBackend é pequena o suficiente para implementar contra qualquer coisa.
  • Autenticação/limite de taxa da API são intencionalmente mínimos — construídos para uma instância auto-hospedada atrás do seu próprio limite de rede, não um gateway multi-tenant. Coloque um gateway de API real na frente se precisar disso.
  • Não auditado por segurança. O mecanismo de política e a lógica de fusão de risco não foram revisados por ninguém fora deste repositório. Trate BLOCK como um sinal forte, não uma garantia, até que isso aconteça.

Tudo a jusante de um Signal — fusão, política, reputação, explicação, a API — não precisa mudar conforme qualquer um dos itens acima é endurecido ainda mais. Esse limite é o contrato de design real aqui.


Início rápido

Modo demo de zero configuração (provedores mock, armazenamento em memória, sem autenticação):

pip install -r requirements.txt
uvicorn api.main:app --reload

Ou com Docker:

docker compose up --build

Experimente os cenários prontos:

curl http://localhost:8000/demo/safe
curl http://localhost:8000/demo/unknown
curl http://localhost:8000/demo/malicious

Ou envie sua própria intenção:

curl -X POST http://localhost:8000/decision \
  -H "Content-Type: application/json" \
  -d '{
        "agent_id": "trading-agent-001",
        "wallet": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
        "chain": "ethereum",
        "action_type": "swap",
        "from_token": "ETH",
        "to_token": "USDC",
        "amount": 5
      }'

Indo do demo para uma implantação real auto-hospedada

Copie .env.example para .env e ajuste:

cp .env.example .env

No mínimo para uma implantação real: defina GUARDIAN_API_KEY (autenticação está desativada por padrão), GUARDIAN_STORAGE_BACKEND=sqlite (persistência), e quaisquer variáveis GUARDIAN_*_PROVIDER que você quiser apontar para dados reais em vez de mock — veja os comentários em .env.example para cada opção, e os docstrings de RpcWalletDataProvider/BlockscoutContractDataProvider/ DexScreenerTokenDataProvider/GoPlusContractDataProvider/ GoPlusTokenDataProvider para o que cada um realmente te dá.

MCP (sem HTTP necessário)

Para frameworks de agentes que falam MCP (LangChain, CrewAI, Claude Desktop, etc.), mcp_server.py expõe o mesmo mecanismo de decisão como duas ferramentas (evaluate_action, get_agent_history) via stdio — instale requirements-mcp.txt em seu próprio ambiente virtual (veja o comentário no topo desse arquivo para saber por que ele não pode compartilhar um ambiente com requirements.txt) e aponte seu cliente MCP para python mcp_server.py.


Executando os testes

pip install -r requirements.txt -r requirements-chain.txt
pytest -q

requirements-chain.txt (web3) só é necessário para os testes do provedor RPC; o resto da suíte roda apenas com requirements.txt. O núcleo guardian/* não tem dependências externas além disso, então também é executável com:

PYTHONPATH=. python3 -m unittest discover -s tests -v

CI (.github/workflows/ci.yml) roda a suíte completa em cada push/PR contra Python 3.11 e 3.12.


Decisões assinadas e verificáveis (OAA)

Cada decisão que este serviço retorna — de uma única verificação de política até o pipeline completo — é assinada como um token OAA (Open Agent Attestation): um JWT assinado com Ed25519 que envolve a decisão, a ação e o motivo.

Qualquer pessoa com a chave pública pode verificar uma decisão offline, sem chamar de volta a instância do Guardian que a emitiu — útil para um auditor, um serviço downstream, ou apenas um registro que você quer confiar depois sem confiar no servidor que o produziu.

python examples/example_oaa_attestation.py
python examples/example_full_pipeline.py   # capability -> intent -> engine -> OAA

A implementação de referência do OAA tem cerca de 150 linhas (oaa.py/attestation.py upstream) e é compartilhada, sem modificações, entre este projeto e agent-guardrail — mesmo formato de assinatura, mesmo caminho de verificação, sem fork por projeto.


Usando o Guardian na frente da MetaMask Agent Wallet

O Guard Mode / Beast Mode da MetaMask Agent Wallet aplicam os mesmos limites de gasto estáticos e allowlists para cada agente. O Guardian é uma segunda verificação independente na frente deles: esta ação específica parece correta para este agente, agora mesmo — antes de o CLI mm ser invocado.

skills/guardian-check/ é uma Agent Skill padrão — o mesmo formato aberto que a própria MetaMask usa para mm (npx skills add MetaMask/agent-skills). Instale-a junto com a skill da própria MetaMask em qualquer runtime compatível com Agent-Skills (Claude Code, Cursor, Codex, OpenClaw), e o agente chamará uma instância do Guardian em execução para uma decisão ALLOW/WARN/BLOCK antes de executar qualquer comando mm que mova fundos — mm send, mm swap, mm bridge, mm perps, mm predict trade, mm earn, mm aave, mm pay.

O Guardian nunca detém chaves e nunca executa nada — mm continua sendo a única coisa que assina ou transmite. Este é um portão de decisão que o agente é instruído a consultar primeiro, não uma modificação no pipeline da própria MetaMask (não há hook público para isso hoje).

uvicorn api.main:app --reload   # run Guardian locally
export GUARDIAN_API_URL="http://localhost:8000"
python skills/guardian-check/scripts/check.py \
  --agent-id my-agent --wallet 0x... --chain ethereum \
  --action-type transfer --target 0x... --amount 50

Roadmap

  1. Substituir os analisadores mock de wallet/token/contrato por fontes de dados reais. Concluído — veja Honestidade sobre o estado atual para saber o que "real" cobre e ainda não cobre por fonte.
  2. Integrar simulação real de pré-execução. Concluído para transfer/ approve de ponta a ponta (GUARDIAN_SIMULATION_PROVIDER=rpc + GUARDIAN_TX_BUILDER=rpc — veja Honestidade sobre o estado atual). swap precisa de roteamento DEX real. Concluído contra o Uniswap V2 Router02 — cotação getAmountsOut() real on-chain, max_slippage_bps explícito fornecido pelo chamador (nunca padronizado), nenhum calldata construído sem uma cotação real. Corrigido um bug real encontrado durante a construção: a simulação estava fazendo dry-run contra intent.target (o destinatário/gastador codificado dentro do calldata ERC-20) em vez do contrato real sendo chamado (from_token) — o que significa que a simulação de transfer/approve "tinha sucesso" silenciosamente contra qualquer destinatário EOA, independentemente de a chamada real ter revertido. BuiltTransaction agora carrega um to explícito; veja tests/test_tx_builder.py para os testes de regressão que teriam detectado isso. bridge ainda em aberto. Concluído para depósitos L1->L2 para Base e Optimism via o L1StandardBridge oficial do OP Stack (depositETHTo/depositERC20To) — outros destinos, outros protocolos de ponte e retiradas L2->L1 todos permanecem em aberto; veja Honestidade sobre o estado atual.
  3. Popular feeds de threat-intel / sanções; parar de enviar conjuntos vazios. Concluído para sanções (sanctioned_addresses.json — 103 endereços reais da lista OFAC SDN, atualizáveis via scripts/refresh_ofac_list.py). malicious_contracts.json / verified_contracts.json permanecem vazios por design — não existe uma única fonte autoritativa para populá-los da mesma forma que a lista da OFAC faz para sanções.
  4. Trocar InMemoryStorage por um backend persistente. SQLiteStorage está disponível; um backend Postgres/Redis ainda está em aberto para implantações multi-réplica. PostgresStorage concluído — testado contra uma instância Postgres local real (tests/test_postgres_storage.py), mesma interface MemoryBackend de dois métodos que os outros backends. Redis permanece em aberto se especificamente desejado.
  5. Adicionar um wrapper de servidor MCP. Concluído (mcp_server.py). Um SDK empacotado em Python/TypeScript sobre a API REST ainda está em aberto.
  6. Publicar uma especificação OpenAPI e um endpoint de demonstração hospedado.
  7. Fazer o motor de políticas e a fusão de risco serem revisados/auditados antes que alguém confie em um BLOCK deste serviço em produção — é uma ferramenta de segurança, então precisa do mesmo escrutínio que aplica aos outros.
  8. Adicionar limites de capacidade por agente (escopo de delegação). Concluído — guardian/policy/capabilities.py. Opt-in: um operador pode conceder a um agente específico uma capacidade com escopo (tipos de ação permitidos, cadeias permitidas, limites de gasto por ação e diários, uma expiração) com zero material de chave privada envolvido. Agentes sem concessão não são afetados — veja examples/example_capability_limits.py. Gerenciamento real de chaves (session keys, abstração de conta) permanece deliberadamente fora do escopo — um problema categoricamente de maior risco.
  9. Verificar a intenção declarada contra resultados de simulação decodificados. Concluído para approveguardian/decision/intent_verification.py detecta o caso em que um agente declara um valor, mas o calldata real que recebeu codifica um valor significativamente diferente (mas ainda finito). Isso é distinto do sinal existente de "approval ilimitado", que só detecta valores próximos de uint256-max — veja examples/example_intent_verification.py.
  10. Sinalizar ações que se desviam do padrão histórico do próprio agente. Concluído — guardian/intelligence/anomaly/analyzer.py. Distinto de reputação (uma pontuação de confiança única) e política (estática, limites definidos pelo operador): isso compara a intenção atual contra o histórico registrado deste agente específico — novo tipo de ação, nova cadeia ou um valor que é um outlier estatístico em comparação com o que este agente fez antes, mesmo que esteja dentro dos limites da política e a reputação do agente esteja boa. Relata honestamente "histórico insuficiente" em vez de adivinhar uma linha de base com menos de 5 pontos de dados anteriores — veja tests/test_anomaly_detection.py.
  11. Ficar na frente de uma carteira de agente real, não apenas aceitar intents de um chamador de API genérico. Concluído para MetaMask Agent Wallet — skills/guardian-check/ é uma Agent Skill padrão que um agente instala junto com a skill mm da própria MetaMask; o agente a chama antes de executar qualquer comando mm que mova fundos e só prossegue em ALLOW. Testado de ponta a ponta contra uma instância uvicorn ao vivo (ALLOW/WARN/BLOCK/config-error todos exercitados de verdade, não apenas afirmados) — veja a seção "Usando o Guardian na frente da MetaMask Agent Wallet" acima. Não existe hook público (ainda) para executar dentro do pipeline da própria MetaMask; isso funciona na camada de orquestração do agente.

Projetos relacionados

Mesmo autor, mesmo princípio aplicado em outros lugares:

  • agent-guardrail — um firewall de políticas genérico para chamadas de ferramentas de agentes de IA (não específico de blockchain). Publicado no PyPI, MIT, 46 testes.
  • x402-attest — atestações criptograficamente assinadas (Ed25519), verificáveis de forma independente, para decisões de política de pagamento entre agentes. Prova de conceito inicial.
  • open-agent-attestation — especificação aberta neutra de fornecedor (JWT+EdDSA) para assinar decisões de política de agentes, verificável por qualquer pessoa. O x402-attest acima usa um formato personalizado; esta é a versão generalizada. Rascunho v0.1.

Licença

MIT — veja LICENSE.