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
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 nisto | Por quê | Agora |
|---|---|---|
| Sinalizou ferramentas somente leitura | Uma janela plana após um nome de ferramenta alcançava a próxima ferramenta, então leituras herdavam o vocabulário das escritas | Correspondê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 nenhum | Escritas vivem em auxiliares compartilhados, não na declaração da ferramenta | Coletadas 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 HTTP | Correspondência na linha bruta |
| Imprimiu "EM RISCO" | Isso é uma acusação, e estava errado 4 vezes em 7 | Nenhum 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.