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

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.

⚡ 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

EffectFence é uma cerca de concorrência causal para chamadas de ferramentas em multi-agentes. Quando mais de um agente (ou uma nova tentativa, ou um 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 execute a operação: corridas no mesmo instante são decididas por uma reserva atômica de compare-and-swap, e duplicatas tardias recebem o resultado registrado de volta em vez de executar novamente. Cada 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 que expõe três ferramentas — fence_prepare, fence_commit, fence_abort — para que os agentes possam rotear chamadas de ferramentas com efeitos colaterais através da cerca em vez de competirem diretamente entre si.

O problema

Em um gateway multi-agente, 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 por 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 uma dessas situações executa o efeito duas vezes. Rejeitar ingenuamente 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: as 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 cerca:

O registro de intenções é o que impede duplicatas, não apenas corridas. Cada 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 limpas. 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 permitir agir com informações desatualizadas.

Cerca de domínio 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 é um único compare_exchange atômico — 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 se torna 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 em memória, de processo único — o estado vive atrás de um Arc e é perdido na reinicialização. Isso é suficiente para fechar corridas e duplicatas entre threads/tarefas concorrentes em um processo de gateway. Duas coisas que ela deliberadamente não faz (ainda):

  • Cerca entre processos/reinicializações. 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.
  • 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 os 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.

Início rápido: servidor MCP

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

Instalação

Com um toolchain Rust (rustup.rs):

cargo install effectfence

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

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

Adicione-o ao Claude

Claude Code (um comando):

claude mcp add effectfence -- effectfence

(Se você construiu 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 a 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 configuração, sem variáveis de ambiente, sem contas. O servidor mantém seu estado de cerca em memória durante a vida do processo.

As ferramentas

Ele expõe:

  • fence_prepare{ intent, domain, tool, args, agent, read_set?, parent?, known_clock? }{status: "fresh", prepared} quando esta tentativa vence (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.

Os esquemas de entrada das ferramentas são gerados automaticamente a partir dos tipos Rust (via schemars), para que qualquer cliente MCP possa 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 adicionalmente 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 relata candidatos com evidências e deliberadamente não emite nenhum 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 de suas primeiras 7 "confirmações". Cada falha agora é um comportamento corrigido, não uma ressalva:

Ele errou nistoPor quêAgora
Sinalizou ferramentas somente leituraUma janela plana após um nome de ferramenta alcançava a próxima ferramenta, então leituras herdavam o vocabulário das 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 auxiliares 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 relatar.

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 executa exatamente uma vez sob novas tentativas, reentrega de webhooks e workers concorrentes. Ele vai além em durabilidade — um armazenamento Postgres, concessões com heartbeat e tokens de cerca para que um worker obsoleto não possa ressuscitar após sua concessão ser reivindicada, e impressões digitais canônicas de payload RFC 8785.

Use o EffectFence quando sua cerca 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 tem sido comprovado cercando o próprio servidor MCP de once.

Suporte comercial

As bibliotecas são gratuitas e permanecem gratuitas. Se você quiser ajuda para aplicá-las a um código que já movimenta dinheiro — uma auditoria de escopo fixo de cada caminho com efeito colateral, testada sob tempestade, com um teste de CI que o mantenha cercado — veja SUPPORT.md ou envie um e-mail para hello@aurumflux.co.

Se não for adequado, diremos isso.

Licença

MIT — veja LICENSE.