EffectFence

Isola chamadas de ferramentas com efeitos colaterais para que agentes concorrentes e novas tentativas produzam exatamente uma execução, reproduzindo um recibo selado para as duplicatas.

Documentação

EffectFence

Registro público: o Índice de Segurança contra Repetições lista quais implementações de pagamento de agentes pagam uma única vez quando a resposta é perdida — verificado como seguro, encontrado e corrigido (com tempo para correção), e como obter verificação. Cada linha aponta para sua prova.

CI crates.io docs.rs License: MIT

Seu enxame não precisa de mais memória. Ele precisa de uma cerca causal em torno dos efeitos colaterais das ferramentas.

Grátis: envie qualquer cliente, facilitador, SDK ou kit de ferramentas que mova dinheiro — seu ou de outra pessoa — e nós o lemos e publicamos um veredito no Índice de Segurança contra Repetições sem custo algum. Os resultados vêm com o mecanismo, o arquivo e a linha, e um teste que falha. Você é contado, nunca nomeado, até enviar uma correção. Enviar para avaliação →

⚡ effectfence — THE STORM
1,000 attempts to charge order #777 ($49.00): concurrent racers + late retries…

ACTUAL EXECUTIONS     :      1   ← the whole point
served sealed receipt :    995
told to stand down    :      4
elapsed               : 22.33ms

💰 double-charges prevented this run: $48,951.00
✅ ONE execution. Every other attempt was fenced, replayed, or refused.

Execute o ataque você mesmo:

cargo run --release --example storm

A guerra territorial entre múltiplos agentes

A tempestade acima é uma ação, muitos corredores. O caso mais difícil é diferentes agentes tomando decisões contraditórias sobre o mesmo recurso de produção — a falha de coordenação agora relatada em sistemas multiagente de produção (cerca de um terço dos incidentes multiagente de 2026). Três agentes autônomos de SRE reagem a um mesmo pico de latência:

cargo run --example turf_war
Agent A (autoscaler): scale node pool UP
  ADMITTED. Fence leased cluster:prod-us-east-1 — executing kubectl scale up.
  DONE. EffectCert minted — verify() -> true

Agent B (cost-optimizer): scale the same pool DOWN
  REFUSED before kubectl ran:
     read-set for `metrics:prod-us-east-1` is stale: B decided on seq 0, world is at seq 1.

Agent C (deploy-bot): roll the deployment BACK
  C's causal view is concurrent with A's: true
  -> escalated to a human instead of corrupting the cluster.

  Actions proposed: 3   executed: 1   cluster: intended, not corrupted.

Cada agente estava individualmente correto para o estado que leu. Executados em concorrência sem coordenação, todas as três chamadas kubectl disparam e o cluster termina em um estado que nenhum deles pretendia — a interrupção de US$ 100 milhões. A cerca permite que exatamente um aja, recusa os outros dois antes que seu efeito colateral seja executado, e diz a cada um o porquê.

Nada acima é simulado — cada chamada é a API real do crate.

EffectFence é uma cerca de concorrência causal para chamadas de ferramentas multiagente. Quando mais de um agente (ou repetição, ou reenvio) pode acabar tentando executar a mesma operação com efeito colateral — cobrar um cartão, enviar um pagamento, provisionar um recurso — o EffectFence garante que exatamente uma tentativa jamais a execute: corridas no mesmo instante são decididas por uma reserva atômica de comparação e troca, e duplicatas tardias recebem o resultado registrado reproduzido em vez de executar novamente. Todo efeito que realmente executa recebe um certificado endereçado por conteúdo encadeado àquilo sobre o qual foi causalmente construído.

Ele é distribuído como uma biblioteca Rust (effectfence::fence) e como um servidor MCP via stdio expondo três ferramentas — fence_prepare, fence_commit, fence_abort — para que agentes possam rotear chamadas de ferramentas com efeito colateral através da cerca em vez de competirem diretamente entre si.

O problema

Em um gateway multiagente, mais de um chamador pode acabar tentando executar o mesmo efeito:

  • Dois agentes decidem independentemente que "cobrar o cliente pelo pedido #123" precisa acontecer — no mesmo instante.
  • Um supervisor expira ao esperar uma chamada de ferramenta e a reenvia enquanto a original ainda está em andamento.
  • Um evento repetido ou duplicado aciona a mesma decisão novamente, minutos após a primeira tentativa já ter sido bem-sucedida.

Ingenuamente, qualquer um desses casos executa o efeito duas vezes. Ingenuamente rejeitar toda duplicata sem memória do resultado também é errado: se a primeira tentativa falhou, o efeito nunca é executado, e uma duplicata que chega após o sucesso recebe um erro em vez do resultado de que precisa. O EffectFence resolve tudo isso com controle de concorrência otimista (OCC) mais um registro de intenções: tentativas não bloqueiam umas às outras, exatamente uma executa, e toda outra tentativa descobre o que realmente aconteceu.

Arquitetura

Quatro peças compõem o protocolo de cercamento:

O registro de intenções é o que impede duplicatas, não apenas corridas. Todo efeito carrega um intent — um id estável para a ação lógica (ex.: "charge:order-123"). Tentativas que compartilham uma intenção são a mesma ação: a primeira é admitida e mantém uma concessão; duplicatas concorrentes são informadas de que uma tentativa está em andamento; duplicatas que chegam após o sucesso recebem o certificado registrado reproduzido literalmente; duplicatas após uma falha são cercadas (o efeito colateral pode ou não ter disparado — isso deve ser reconciliado, não repetido às cegas) até serem explicitamente liberadas. Titulares que falharam perdem sua concessão após um TTL para que a ação não fique travada para sempre.

Relógios vetoriais (VectorClock) rastreiam relações causais de "aconteceu-antes" entre agentes — um contador lógico por agente, unidos via máximo elemento a elemento merge, comparados via uma ordem parcial le, com uma verificação concurrent para eventos genuinamente não ordenados e um SHA-256 estável digest para inclusão em certificados.

Conjuntos de leitura OCC (ReadSetEntry) registram as dependências causais nas quais uma decisão foi baseada: "quando decidi agir, o domínio D estava na sequência S." Tanto prepare_effect_fence quanto commit_effect_cert validam cada entrada contra o estado atual — se algo mudou, a tentativa é rejeitada como obsoleta em vez de permitida a agir com informações desatualizadas.

Cercamento de domínio por CAS é onde corridas no mesmo instante são decididas. Cada domínio (um escopo de contenção nomeado, ex.: "order:123") tem um contador de sequência AtomicU64. A decisão é uma única operação atômica compare_exchange — exatamente um chamador concorrente pode vencer para qualquer sequência esperada. (Nota de precisão: a consulta do contador fica atrás de um mutex curto; apenas a decisão da corrida em si é livre de bloqueio. Ideias para um caminho totalmente livre de bloqueio são bem-vindas.)

              ┌ intent gate ──────── already done? → Replay(recorded cert)   [do NOT run]
              │                      in flight / failed? → rejected          [do NOT run]
 EffectRequest┤
              ├ read-set check ───── dependency moved? → ReadSetStale        [do NOT run]
              │
              └ domain CAS ────────── lost the race? → DomainRace            [do NOT run]
                     │
                     └ Fresh(ticket) → run the effect → commit_effect_cert → EffectCert
                                                      ↘ abort_effect (failed; fenced until reconciled)

Todo efeito confirmado torna-se um EffectCert: um hash de conteúdo SHA-256 sobre {intent, parent, domain, seq, tool, args, result, vector_clock, read_set, agent}, encadeado a um hash de certificado parent para linhagem causal. Dois certificados com o mesmo hash são, por definição, registros do mesmo efeito — EffectCert::verify() recalcula o hash e confirma que ele não foi adulterado ou construído incorretamente.

Escopo

Esta é uma cerca de processo único com um registro durável. Toda admissão, resultado e liberação do operador são anexados a um arquivo JSON-lines e sincronizados com fsync antes que a cerca responda, para que uma reinicialização do proxy não esqueça o que já foi executado: uma duplicata após a reinicialização é reproduzida, não reexecutada. Qualquer coisa que estava em andamento quando o processo morreu é cercada como resultado desconhecido na próxima inicialização — o titular se foi e ninguém pode dizer se o efeito disparou — até que um operador reconcilie e libere. (Usuários da biblioteca: EffectFence::open(path, config); EffectFence::new() permanece em memória. O binário distribuído escolhe $EFFECTFENCE_LEDGER, senão ~/.local/state/effectfence/<name>.jsonl, e EFFECTFENCE_LEDGER=memory opta por não participar.) Duas coisas que ele deliberadamente não faz (ainda):

  • Cercamento multi-réplica. O registro é um único arquivo, então protege um processo de gateway (ao longo de suas reinicializações), não várias réplicas ao mesmo tempo. Um gateway escalado horizontalmente precisa do mesmo modelo de intenção/domínio/conjunto de leitura apoiado por um armazenamento compartilhado (ex.: SETNX+CAS no Redis, ou uma coluna de versão otimista no Postgres) — os tipos aqui são feitos para serem transferidos diretamente para esse backend.
  • Um titular que sobrevive à sua concessão. Uma concessão em andamento é reivindicável uma vez que expira — é isso que torna possível a recuperação de falhas — e nada em uma expiração diz à cerca se o titular morreu ou está simplesmente mais lento que FenceConfig::lease_ttl (60 s por padrão). Um efeito que ultrapassa sua concessão sem dizer nada tem, portanto, sua reivindicação assumida e executa uma segunda vez, que é exatamente o que este crate existe para impedir. O remédio é uma chamada: EffectFence::heartbeat(intent) enquanto o efeito ainda está em execução (cerca de um terço da concessão). wrap faz isso por você em cada chamada encaminhada; agentes que dirigem fence_prepare diretamente devem chamar a ferramenta fence_heartbeat. Pare de bater e a concessão expira no prazo, então um titular genuinamente morto é ainda recuperado.
  • Imposição. A cerca protege agentes que roteiam seus efeitos através dela; ela não pode impedir um agente que a contorna completamente. Implante-a no único ponto de estrangulamento que seus agentes compartilham (o processo de gateway que possui as ferramentas).

A memória é limitada: resultados concluídos expiram após um TTL configurável (FenceConfig::result_ttl, varrido por EffectFence::sweep), consultas nunca criam estado de rastreamento, e contadores de domínio são minúsculos e manualmente removíveis (evict_domain).

Início rápido: biblioteca

use effectfence::fence::{
    prepare_effect_fence, commit_effect_cert, Admission, EffectFence, EffectRequest, VectorClock,
};

let fence = EffectFence::new();

let req = EffectRequest {
    intent: "charge:order-123".into(),   // same action -> same intent, always
    parent: None,                        // hash of the cert this follows, if any
    domain: "order:123".into(),          // contention scope
    tool: "charge_card".into(),
    args: serde_json::json!({"amount_cents": 1999}),
    read_set: vec![],                    // other domains this decision cross-checked
    agent: "agent-a".into(),
    known_clock: VectorClock::new(),
};

match prepare_effect_fence(&fence, req)? {
    Admission::Fresh(prepared) => {
        // This attempt won. Actually run the charge...
        let cert = commit_effect_cert(
            &fence,
            prepared,
            serde_json::json!({"charge_id": "ch_123"}),
        )?;
        assert!(cert.verify());
        // (on failure: abort_effect(&fence, prepared, "why") instead)
    }
    Admission::Replay(cert) => {
        // This exact action already ran -- use cert.result, charge nothing.
    }
}

Uma duplicata concorrente da mesma intenção recebe Err(FenceError::IntentInFlight); uma corrida no mesmo instante no domínio recebe Err(FenceError::DomainRace); de qualquer forma, ela não deve executar o efeito.

Experimente em 10 segundos (nada instala, nada real dispara)

cargo install effectfence   # or: npx -y effectfence demo
effectfence demo

Doze agentes alcançam uma cobrança de US$ 49 no mesmo instante. Você verá isso atingir um servidor embutido bruto — 12 cobranças duplicadas — depois as mesmas doze chamadas atrás da cerca: exatamente 1. Este binário está falando consigo mesmo, então nenhuma chamada real dispara e você não precisa de um servidor próprio para ver o ponto.

── Act 1: the raw server, no fence ──
  DISTINCT effects    : 12      PROVEN DOUBLE-FIRE
── Act 2: the SAME twelve calls, behind the fence ──
  DISTINCT effects    : 1       12 callers, 12 clean answers, one execution

Primeiro: sua pilha realmente dispara duas vezes? (teste-a)

Antes de instalar uma cerca, prove que você precisa de uma — no seu próprio servidor, não em nossa demonstração. probe é um cliente MCP simples. Aponte-o para qualquer servidor MCP, e ele dispara N chamadas byte-idênticas em uma ferramenta concorrentemente — a corrida de chamadas gêmeas que acontece no instante em que dois agentes alcançam a mesma ação — então relata quantos efeitos distintos realmente chegaram:

effectfence probe --tool charge_card --args '{"amount":4900}' --calls 12 -- npx -y @your-org/your-mcp-server
EffectFence probe — twin-caller race report
--------------------------------------------
  identical calls   : 12
  DISTINCT effects  : 12

  PROVEN DOUBLE-FIRE. 12 byte-identical calls produced 12 DIFFERENT
  results. Each distinct result is a separate real execution of one
  intended action — the duplicate side effect you cannot take back.

Ele dispara apenas a única ferramenta que você nomeia, com os argumentos exatos que você fornece — ele nunca enumera e martela um servidor às cegas. E é honesto sobre o que pode ver: resultados distintos são prova inegável de dupla execução; resultados idênticos são relatados como inconclusivos a partir da resposta, nunca como um zero que não pode provar.

Então reexecute a mesma sondagem através da cerca e observe DISTINCT effects cair para 1:

effectfence probe --tool charge_card --args '{"amount":4900}' --calls 12 -- effectfence wrap -- npx -y @your-org/your-mcp-server

Essa é toda a proposta em dois comandos: as pegadas, depois o bloqueio.

Início rápido: envolva um servidor MCP existente (comece aqui)

A maneira mais rápida de usar o EffectFence é colocá-lo na frente de um servidor de ferramentas que você já executa. Os agentes não precisam lembrar de chamar nada — toda chamada de ferramenta é cercada automaticamente:

agent/client ──MCP──> effectfence wrap ──MCP──> your real tool server
cargo install effectfence

Então envolva qualquer servidor que possua suas ferramentas perigosas:

effectfence wrap -- npx -y @your-org/your-mcp-server

A lista de ferramentas é espelhada 1:1 do filho (mesmos nomes, esquemas, documentação), então nada no seu agente muda. O que muda: chamadas duplicatas idênticas — mesma ferramenta, mesmos argumentos — executam o filho uma vez; duplicatas posteriores recebem o resultado registrado reproduzido, e chamadas idênticas concorrentes são recusadas em vez de dispararem duas vezes.

Receita de uma colagem: cerque um servidor que muta o cluster

O caso para o qual isso existe — vários agentes com kubectl no mesmo cluster.

Claude Code:

claude mcp add k8s-fenced -- effectfence wrap -- npx -y kubernetes-mcp-server

Cursor (~/.cursor/mcp.json) ou Claude Desktop (claude_desktop_config.json):

{
  "mcpServers": {
    "k8s-fenced": {
      "command": "effectfence",
      "args": ["wrap", "--", "npx", "-y", "kubernetes-mcp-server"]
    }
  }
}

Troque kubernetes-mcp-server por qualquer servidor que contenha suas ferramentas com efeito de escrita — APIs de nuvem, ferramentas de implantação, um servidor de pagamentos. Aponte cada agente para o nome cercado e remova o acesso deles ao bruto; a cerca só é uma cerca se for a única porta.

Veja funcionar com fence_stats (veja abaixo) — replayed e refused são as execuções duplicadas que não aconteceram.

Escopo honesto do envolvimento

Apenas ferramentas por enquanto (sem passagem de recursos/prompts). A intenção é derivada de hash(tool + canonical args), então argumentos byte-idênticos são tratados como a mesma ação — um agente que varia um carimbo de tempo em seus argumentos derrota a deduplicação, e essa direção falha com segurança: a chamada é executada, nada é corrompido. O estado é durável por comando envolvido (um arquivo de registro por comando filho distinto, então dois servidores com uma ferramenta de mesmo nome nunca se cercam mutuamente); execute um gateway cercado por conjunto de ferramentas que mutam a produção.

Uma chamada encaminhada envia heartbeats enquanto o filho executa, então uma ferramenta mais lenta que o lease mantém sua reivindicação em vez de ser assumida e executada duas vezes. Ambas as janelas são configuráveis se você quiser explicitá-las: EFFECTFENCE_LEASE_SECS (padrão 60) e EFFECTFENCE_RESULT_TTL_SECS (padrão 86400). EFFECTFENCE_LEDGER define onde o ledger durável fica (padrão ~/.local/state/effectfence/wrap-<hash>.jsonl; memory para optar por não usar). Uma reinicialização do wrap reproduz o que já foi executado e cerca o que estava no meio de uma chamada quando morreu.


Início rápido: servidor MCP (fencing explícito)

Use isto quando quiser que os agentes façam fencing deliberadamente — controle mais rico (read_set, parent, known_clock) do que o wrap deriva automaticamente.

Listado no Registro Oficial MCP como mcp-name: io.github.aurumflux20/effectfence

Instalação

Com um toolchain Rust (rustup.rs):

cargo install effectfence

Ou compile a partir de um clone deste repositório:

cargo build --release   # binary at ./target/release/effectfence

Adicione ao Claude

Claude Code (um comando):

claude mcp add effectfence -- effectfence

(Se você compilou a partir do código-fonte em vez de cargo install, use o caminho completo: claude mcp add effectfence -- /path/to/target/release/effectfence.)

Claude Desktop — adicione ao claude_desktop_config.json:

{
  "mcpServers": {
    "effectfence": {
      "command": "effectfence"
    }
  }
}

Qualquer projeto (compartilhado pela equipe) — faça commit de um .mcp.json na raiz do projeto:

{
  "mcpServers": {
    "effectfence": {
      "command": "effectfence"
    }
  }
}

É isso — sem contas, sem configuração obrigatória. O servidor mantém seu ledger em ~/.local/state/effectfence/server.jsonl (substitua com EFFECTFENCE_LEDGER), então o que já foi executado sobrevive a uma reinicialização do servidor.

As ferramentas

Ele expõe:

  • fence_prepare — { intent, domain, tool, args, agent, read_set?, parent?, known_clock? } → {status: "fresh", prepared} quando esta tentativa vencer (execute a ferramenta e depois reporte), ou {status: "already_done", cert} quando esta ação exata já foi executada (use o resultado registrado — NÃO execute a ferramenta). Erros significam não executar.

  • fence_commit — { prepared, result } → {status: "committed", cert}. Duplicatas posteriores da intenção agora reproduzem este certificado.

  • fence_abort — { prepared, reason } → {status: "aborted"}. A intenção permanece cercada até ser reconciliada e limpa.

  • fence_stats — sem argumentos → contadores ao vivo desde o início do processo: admitted (efeitos que foram executados), replayed (duplicatas que receberam um resultado registrado), refused divididos por causa (stale_read_set, domain_race, in_flight, prior_failure), além de total_attempts e prevented. prevented é o número que importa: toda tentativa que não executou o efeito.

    effectfence since boot: admitted=1 replayed=995 refused(stale=0 race=4 in-flight=0 failed=0) total=1000 prevented=999
    

Os esquemas de entrada das ferramentas são gerados automaticamente a partir dos tipos Rust (via schemars), então qualquer cliente MCP pode inspecioná-los com tools/list.

Testes

cargo test    # unit tests + chaos tests
cargo clippy --all-targets

tests/chaos_test.rs usa threads reais do SO para provar as duas garantias separadamente: uma corrida de domínio forçada no mesmo instante (sincronização deliberadamente construída para que a colisão seja garantida, não esperada) admite exatamente um vencedor toda vez, e 16 duplicatas concorrentes de uma intenção admitem exatamente uma execução — com duplicatas tardias reproduzindo o certificado confirmado. Um teste de estresse com 32 threads também afirma que números de sequência nunca são alocados duas vezes.

Analise seu próprio servidor MCP

tools/fencescan.py encontra ferramentas em um servidor MCP que poderiam disparar o mesmo efeito duas vezes. Sem instalação, sem dependências, sem rede:

python3 tools/fencescan.py /path/to/your-mcp-server

Ele reporta candidatos com evidências e deliberadamente não emite veredito, porque um observador externo lendo um repositório geralmente não consegue provar um disparo duplo — a proteção geralmente vive em um serviço que o repositório chama, ou em um SDK irmão, e uma ferramenta cujo nome parece uma escrita pode apenas retornar um payload para outra pessoa assinar. A saída inclui uma lista explícita do que ele não consegue ver.

Ele foi reescrito após verificação manual eliminar 4 das primeiras 7 "confirmações". Cada falha agora é um comportamento corrigido, não uma ressalva:

Ele errou nistoPorquêAgora
Sinalizou ferramentas somente leituraUma janela plana após um nome de ferramenta colidia com a próxima ferramenta, então leituras herdavam o vocabulário de escritasCorrespondência por chaves do próprio bloco da ferramenta; um verbo de leitura no nome veta
Disse que um repositório não tinha idempotência quando tinha um módulo inteiro\b(idempot…) não consegue corresponder a deriveIdempotencyKey — sem limite de palavra antes de uma maiúscula camelCaseÂncoras removidas; a mesma cegueira escondia requestId, clientToken
Não encontrou escritas em lugar nenhumEscritas vivem em helpers compartilhados, não na declaração da ferramentaColetadas por repositório como corroboração, nunca afirmadas como "esta ferramenta escreve"
Perdeu method: cond ? "POST" : "GET"Literais de string eram removidos antes da correspondência, apagando o próprio verbo HTTPCorrespondência na linha bruta
Imprimiu "EM RISCO"Isso é uma acusação, e estava errado 4 vezes em 7Nenhum campo de veredito existe

Se ele sinalizar algo no seu servidor e você quiser uma segunda opinião, abra uma issue — uma acusação errada custa mais do que uma perdida, então um falso positivo aqui também vale a pena reportar.

Projeto irmão — once (Python)

Mesmo problema, outro runtime. once (pip install once-kernel) é o kernel de idempotência em Python construído sobre a mesma ideia: um efeito colateral é executado exatamente uma vez sob tentativas, reentrega de webhook e workers concorrentes. Ele vai além em durabilidade — um armazenamento Postgres, leases de heartbeat com fence tokens para que um worker obsoleto não possa ressuscitar após seu lease ser reivindicado, e impressões digitais canônicas de payload RFC 8785.

Use EffectFence quando seu fence viver em Rust ou na frente de um servidor MCP; use once quando o efeito colateral for Python e você quiser um armazenamento durável. effectfence wrap provou fazer fencing do próprio servidor MCP do once.

Suporte comercial

As bibliotecas são gratuitas e continuarão gratuitas.

Revisão de Segurança de Retry — US$ 1.200, reembolsada integralmente se não encontrarmos nada. Lemos um caminho de dinheiro no seu código e caçamos o defeito que sobrevive à boa engenharia: não "existe uma chave de idempotência" (a maioria das equipes competentes tem uma), mas o que acontece quando um pagamento falha ambiguamente — a requisição que expirou depois de ser liquidada, a tentativa que gera um nonce novo, a reserva liberada em uma falha que não era uma. Cinco dias úteis, relatório escrito vinculado aos seus próprios arquivos e números de linha, sem reuniões.

É a classe de defeito que caçamos publicamente: hpp-io/x402-mcp-bridge enviou duas correções de nossas descobertas, mcp-server-kibana mesclou dois PRs. Detalhes em SUPPORT.md. Para começar: agende e responda ao recibo com o repositório e qual caminho de dinheiro importa mais — ou envie um e-mail para hello@aurumflux.co primeiro se preferir conversar antes.

Se não for adequado, diremos — e se não acharmos que podemos encontrar algo, dizemos isso em vez de cobrar por um atestado de saúde limpo.

Licença

MIT — veja LICENSE.