EffectFence
Cercas las llamadas a herramientas con efectos secundarios para que los agentes en competencia y los reintentos produzcan exactamente una ejecución, reproduciendo un recibo sellado a los duplicados.
Documentación
EffectFence
Registro público: el Índice de Seguridad ante Reintentos enumera qué implementaciones de pago a agentes pagan una sola vez cuando se pierde la respuesta — verificado seguro, encontrado y corregido (con tiempo de corrección), y cómo obtener la verificación. Cada fila enlaza a su prueba.
Tu enjambre no necesita más memoria. Necesita una valla causal alrededor de los efectos secundarios de las herramientas.
Gratis: envía cualquier cliente, facilitador, SDK o kit de herramientas que mueva dinero — tuyo o de alguien más — y lo leemos y publicamos un veredicto en el Índice de Seguridad ante Reintentos sin costo. Los hallazgos regresan con el mecanismo, el archivo y la línea, y una prueba fallida. Se te cuenta, nunca se te nombra, hasta que publiques una corrección. Envía para evaluación →
⚡ 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.
Ejecuta el ataque tú mismo:
cargo run --release --example storm
La guerra territorial entre múltiples agentes
La tormenta anterior es una acción, muchos corredores. El caso más difícil es diferentes agentes tomando decisiones contradictorias sobre el mismo recurso de producción — la falla de coordinación ahora reportada en sistemas multi-agente de producción (~un tercio de los incidentes multi-agente de 2026). Tres agentes SRE autónomos reaccionan a un mismo pico de latencia:
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 fue individualmente correcto para el estado que leyó. Ejecutados de forma
concurrente sin coordinación, las tres llamadas kubectl se disparan y el clúster termina en un
estado que ninguno de ellos pretendía — la caída de $100M. La valla permite que exactamente uno actúe,
rechaza a los otros dos antes de que su efecto secundario se ejecute, y le dice a cada uno por qué.
Nada de lo anterior está simulado — cada llamada es la API real del crate.
EffectFence es una valla de concurrencia causal para llamadas de herramientas multi-agente. Cuando más de un agente (o reintento, o re-despacho) puede terminar intentando ejecutar la misma operación con efectos secundarios — cobrar una tarjeta, enviar un pago, aprovisionar un recurso — EffectFence garantiza que exactamente un intento la ejecute: las carreras en el mismo instante se deciden mediante una reserva atómica de comparación e intercambio, y los duplicados tardíos reciben el resultado registrado reproducido en lugar de ejecutarse de nuevo. Cada efecto que sí se ejecuta recibe un certificado con dirección de contenido encadenado a aquello sobre lo que se construyó causalmente.
Se distribuye como una biblioteca de Rust (effectfence::fence) y como un servidor MCP stdio que expone tres herramientas — fence_prepare, fence_commit, fence_abort — para que los agentes puedan enrutar las llamadas de herramientas con efectos secundarios a través de la valla en lugar de competir entre sí directamente.
El problema
En una puerta de enlace multi-agente, más de un llamador puede terminar intentando ejecutar el mismo efecto:
- Dos agentes deciden independientemente que "cobrar al cliente por el pedido #123" debe suceder — en el mismo instante.
- Un supervisor agota el tiempo de espera de una llamada a herramienta y la re-despacha mientras la original sigue en vuelo.
- Un evento reintentado o duplicado dispara la misma decisión de nuevo, minutos después de que el primer intento ya tuvo éxito.
Ingenuamente, cualquiera de estos casos ejecuta el efecto dos veces. Ingenuamente rechazar cada duplicado sin memoria del resultado también es incorrecto: si el primer intento falló, el efecto nunca se ejecuta, y un duplicado que llega después del éxito recibe un error en lugar del resultado que necesita. EffectFence cierra todo esto con control de concurrencia optimista (OCC) más un libro de intenciones: los intentos no se bloquean entre sí, exactamente uno se ejecuta, y cada otro intento aprende lo que realmente sucedió.
Arquitectura
Cuatro piezas componen el protocolo de vallado:
El libro de intenciones es lo que detiene duplicados, no solo carreras. Cada efecto lleva un intent — un id estable para la acción lógica (p. ej. "charge:order-123"). Los intentos que comparten una intención son la misma acción: el primero es admitido y mantiene un arrendamiento; los duplicados concurrentes reciben la indicación de que un intento está en vuelo; los duplicados que llegan después del éxito reciben el certificado registrado reproducido textualmente; los duplicados después de una falla son vallados (el efecto secundario puede o no haberse disparado — eso debe reconciliarse, no reintentarse a ciegas) hasta que se borren explícitamente. Los titulares que fallaron pierden su arrendamiento después de un TTL para que la acción no quede atascada para siempre.
Relojes vectoriales (VectorClock) rastrean las relaciones causales de "sucedió-antes" entre agentes — un contador lógico por agente, unidos mediante máximo por elementos merge, comparados mediante un orden parcial le, con una verificación concurrent para eventos genuinamente desordenados y un SHA-256 estable digest para inclusión en certificados.
Conjuntos de lectura OCC (ReadSetEntry) registran las dependencias causales en las que se basó una decisión: "cuando decidí actuar, el dominio D estaba en la secuencia S." Tanto prepare_effect_fence como commit_effect_cert validan cada entrada contra el estado vivo — si algo se movió, el intento se rechaza como obsoleto en lugar de permitir actuar sobre información desactualizada.
Vallado de dominio CAS es donde se deciden las carreras en el mismo instante. Cada dominio (un ámbito de contención nombrado, p. ej. "order:123") tiene un contador de secuencia AtomicU64. La decisión es un único compare_exchange atómico — exactamente un llamador concurrente puede ganar para cualquier secuencia esperada dada. (Nota de precisión: la búsqueda del contador está detrás de un mutex corto; solo la decisión de carrera en sí es libre de bloqueos. Se agradecen ideas para una ruta completamente libre de bloqueos.)
┌ 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)
Cada efecto confirmado se convierte en un EffectCert: un hash de contenido SHA-256 sobre {intent, parent, domain, seq, tool, args, result, vector_clock, read_set, agent}, encadenado a un hash de certificado parent para el linaje causal. Dos certificados con el mismo hash son, por definición, registros del mismo efecto — EffectCert::verify() recalcula el hash y confirma que no ha sido manipulado ni construido incorrectamente a mano.
Alcance
Esta es una valla de proceso único con un libro duradero. Cada admisión,
resultado y borrado de operador se agrega a un archivo de líneas JSON y se sincroniza con fsync antes
de que la valla responda, de modo que un reinicio del proxy no olvide lo que ya se ejecutó:
un duplicado después del reinicio se reproduce, no se re-ejecuta. Cualquier cosa que estuviera
en vuelo cuando el proceso murió se valla como resultado desconocido en el siguiente
inicio — el titular se ha ido y nadie puede decir si el efecto se disparó — hasta
que un operador reconcilie y lo borre. (Usuarios de la biblioteca: EffectFence::open(path, config); EffectFence::new() permanece en memoria. El binario distribuido elige
$EFFECTFENCE_LEDGER, si no ~/.local/state/effectfence/<name>.jsonl, y
EFFECTFENCE_LEDGER=memory opta por no participar.) Dos cosas que deliberadamente no hace (aún):
- Vallado multi-réplica. El libro es un solo archivo, por lo que protege un proceso de puerta de enlace (a través de sus reinicios), no varias réplicas a la vez. Una puerta de enlace escalada horizontalmente necesita el mismo modelo de intención/dominio/conjunto de lectura respaldado por un almacén compartido (p. ej.
SETNX+CAS en Redis, o una columna de versión optimista en Postgres) — los tipos aquí están pensados para transferirse directamente a ese backend. - Un titular que sobrevive a su arrendamiento. Un arrendamiento en vuelo es reclamable una vez que
expira — eso es lo que hace posible la recuperación de fallos — y nada en una expiración
le dice a la valla si el titular murió o simplemente es más lento que
FenceConfig::lease_ttl(60 s por defecto). Un efecto que se ejecuta más allá de su arrendamiento sin decir nada, por lo tanto, tiene su reclamo tomado y se ejecuta una segunda vez, que es exactamente lo que este crate existe para evitar. El remedio es una llamada:EffectFence::heartbeat(intent)mientras el efecto aún se está ejecutando (aproximadamente cada tercio del arrendamiento).wraphace esto por ti en cada llamada reenviada; los agentes que manejanfence_preparedirectamente deben llamar a la herramientafence_heartbeat. Deja de latir y el arrendamiento caduca según lo programado, por lo que un titular genuinamente muerto aún se recupera. - Aplicación. La valla protege a los agentes que enrutan sus efectos a través de ella; no puede detener a un agente que la elude por completo. Despliégalo en el único punto de estrangulamiento que tus agentes comparten (el proceso de puerta de enlace que posee las herramientas).
La memoria está acotada: los resultados finalizados expiran después de un TTL configurable (FenceConfig::result_ttl, barrido por EffectFence::sweep), las consultas nunca crean estado de seguimiento, y los contadores de dominio son pequeños y desalojables manualmente (evict_domain).
Inicio 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.
}
}
Un duplicado concurrente de la misma intención recibe Err(FenceError::IntentInFlight); una carrera en el mismo instante en el dominio recibe Err(FenceError::DomainRace); de cualquier manera, no debe ejecutar el efecto.
Pruébalo en 10 segundos (nada se instala, nada real se dispara)
cargo install effectfence # or: npx -y effectfence demo
effectfence demo
Doce agentes alcanzan un cargo de $49 en el mismo instante. Verás que llega a un servidor integrado crudo — 12 cargos duplicados — luego las mismas doce llamadas detrás de la valla: exactamente 1. Este binario se comunica consigo mismo, por lo que ninguna llamada real se dispara y no necesitas un servidor propio para ver el punto.
── 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
Primero: ¿tu stack realmente dispara dos veces? (pruébalo)
Antes de instalar una valla, demuestra que la necesitas — en tu propio servidor, no en nuestra demo.
probe es un cliente MCP mínimo. Apúntalo a cualquier servidor MCP, y dispara N
llamadas byte-idénticas a una herramienta concurrentemente — la carrera de llamadas gemelas que
sucede en el instante en que dos agentes alcanzan la misma acción — luego informa cuántos
efectos distintos realmente aterrizaron:
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.
Dispara solo la única herramienta que nombres, con los argumentos exactos que suministres — nunca enumera y golpea un servidor a ciegas. Y es honesto sobre lo que puede ver: resultados distintos son prueba innegable de doble ejecución; resultados idénticos se reportan como no concluyentes desde la respuesta, nunca como un cero que no puede probar.
Luego re-ejecuta la misma sonda a través de la valla y observa DISTINCT effects caer
a 1:
effectfence probe --tool charge_card --args '{"amount":4900}' --calls 12 -- effectfence wrap -- npx -y @your-org/your-mcp-server
Esa es toda la propuesta en dos comandos: las huellas, luego el candado.
Inicio rápido: envuelve un servidor MCP existente (empieza aquí)
La forma más rápida de usar EffectFence es ponerlo delante de un servidor de herramientas que ya ejecutas. Los agentes no tienen que recordar llamar a nada — cada llamada de herramienta se valla automáticamente:
agent/client ──MCP──> effectfence wrap ──MCP──> your real tool server
cargo install effectfence
Luego envuelve cualquier servidor que posea tus herramientas peligrosas:
effectfence wrap -- npx -y @your-org/your-mcp-server
La lista de herramientas se refleja 1:1 desde el hijo (mismos nombres, esquemas, documentos), por lo que nada en tu agente cambia. Lo que cambia: llamadas duplicadas idénticas — misma herramienta, mismos argumentos — ejecutan al hijo una vez; los duplicados posteriores reciben el resultado registrado reproducido, y las llamadas idénticas concurrentes se rechazan en lugar de dispararse dos veces.
Receta de una sola pasta: valla un servidor que muta clústeres
El caso para el que esto existe — varios agentes con kubectl en el mismo clúster.
Claude Code:
claude mcp add k8s-fenced -- effectfence wrap -- npx -y kubernetes-mcp-server
Cursor (~/.cursor/mcp.json) o Claude Desktop (claude_desktop_config.json):
{
"mcpServers": {
"k8s-fenced": {
"command": "effectfence",
"args": ["wrap", "--", "npx", "-y", "kubernetes-mcp-server"]
}
}
}
Cambia kubernetes-mcp-server por el servidor que tenga tus herramientas con escritura —
APIs de nube, herramientas de despliegue, un servidor de pagos. Apunta a cada agente al nombre vallado
y elimina su acceso al crudo; la valla solo es una valla si es la única
puerta.
Obsérvalo funcionar con fence_stats (ver abajo) — replayed y refused son las
ejecuciones duplicadas que no sucedieron.
Alcance honesto del envoltorio
Solo herramientas por ahora (sin paso de recursos/indicaciones). La intención se deriva de
hash(tool + canonical args), por lo que los argumentos byte-idénticos se tratan como la misma
acción — un agente que varía una marca de tiempo en sus argumentos derrota la deduplicación, y esa
dirección falla segura: la llamada se ejecuta, nada se corrompe. El estado es duradero por
comando envuelto (un archivo de libro por comando hijo distinto, por lo que dos servidores con una
herramienta del mismo nombre nunca se valla entre sí); ejecuta una puerta de enlace vallada por conjunto de
herramientas que mutan producción.
Una llamada reenviada envía heartbeats mientras el hijo se ejecuta, por lo que una herramienta más lenta que la concesión mantiene su reclamo en lugar de ser tomada y ejecutada dos veces. Ambas ventanas son configurables si las quieres explícitas: EFFECTFENCE_LEASE_SECS (predeterminado 60) y EFFECTFENCE_RESULT_TTL_SECS (predeterminado 86400). EFFECTFENCE_LEDGER establece dónde vive el libro mayor duradero (predeterminado ~/.local/state/effectfence/wrap-<hash>.jsonl; memory para optar por no participar). Un reinicio de wrap reproduce lo que ya se ejecutó y protege lo que estaba a mitad de llamada cuando murió.
Inicio rápido: servidor MCP (protección explícita)
Úsalo cuando quieras que los agentes protejan deliberadamente — control más rico (read_set, parent, known_clock) que lo que wrap deriva automáticamente.
Listado en el Registro oficial de MCP como mcp-name: io.github.aurumflux20/effectfence
Instalación
Con un kit de herramientas de Rust (rustup.rs):
cargo install effectfence
O compila desde un clon de este repositorio:
cargo build --release # binary at ./target/release/effectfence
Añádelo a Claude
Claude Code (un comando):
claude mcp add effectfence -- effectfence
(Si compilaste desde el código fuente en lugar de cargo install, usa la ruta completa: claude mcp add effectfence -- /path/to/target/release/effectfence.)
Claude Desktop — añade a claude_desktop_config.json:
{
"mcpServers": {
"effectfence": {
"command": "effectfence"
}
}
}
Cualquier proyecto (compartido por equipo) — confirma un .mcp.json en la raíz del proyecto:
{
"mcpServers": {
"effectfence": {
"command": "effectfence"
}
}
}
Eso es todo — sin cuentas, sin configuración requerida. El servidor mantiene su libro mayor en ~/.local/state/effectfence/server.jsonl (anula con EFFECTFENCE_LEDGER), por lo que lo que ya se ejecutó sobrevive a un reinicio del servidor.
Las herramientas
Expone:
-
fence_prepare—{ intent, domain, tool, args, agent, read_set?, parent?, known_clock? }→{status: "fresh", prepared}cuando este intento gana (ejecuta la herramienta, luego informa), o{status: "already_done", cert}cuando esta acción exacta ya se ejecutó (usa el resultado registrado — NO ejecutes la herramienta). Los errores significan no ejecutar. -
fence_commit—{ prepared, result }→{status: "committed", cert}. Los duplicados posteriores de la intención ahora reproducen este certificado. -
fence_abort—{ prepared, reason }→{status: "aborted"}. La intención permanece protegida hasta que se reconcilie y se borre. -
fence_stats— sin argumentos → contadores en vivo desde que el proceso comenzó:admitted(efectos que se ejecutaron),replayed(duplicados que recibieron un resultado registrado),refuseddesglosado por causa (stale_read_set,domain_race,in_flight,prior_failure), mástotal_attemptsyprevented.preventedes el número que importa: cada intento que no ejecutó el efecto.effectfence since boot: admitted=1 replayed=995 refused(stale=0 race=4 in-flight=0 failed=0) total=1000 prevented=999
Los esquemas de entrada de las herramientas se generan automáticamente a partir de los tipos de Rust (a través de schemars), por lo que cualquier cliente MCP puede inspeccionarlos con tools/list.
Pruebas
cargo test # unit tests + chaos tests
cargo clippy --all-targets
tests/chaos_test.rs usa hilos reales del sistema operativo para probar las dos garantías por separado: una carrera de dominio forzada en el mismo instante (sincronización deliberadamente construida para que la colisión esté garantizada, no esperada) admite exactamente un ganador cada vez, y 16 duplicados concurrentes de una intención admiten exactamente una ejecución — con duplicados tardíos reproduciendo el certificado confirmado. Una prueba de estrés de 32 hilos además afirma que los números de secuencia nunca se asignan dos veces.
Escanea tu propio servidor MCP
tools/fencescan.py encuentra herramientas en un servidor MCP que podrían disparar el mismo efecto dos veces. Sin instalación, sin dependencias, sin red:
python3 tools/fencescan.py /path/to/your-mcp-server
Informa candidatos con evidencia y deliberadamente no emite veredicto, porque un externo que lee un repositorio generalmente no puede probar un doble disparo — la protección a menudo vive en un servicio al que el repositorio llama, o en un SDK hermano, y una herramienta cuyo nombre suena a escritura puede solo devolver una carga útil para que otro la firme. La salida incluye una lista explícita de lo que no puede ver.
Fue reescrito después de que la verificación manual eliminara 4 de sus primeras 7 "confirmaciones". Cada fallo ahora es un comportamiento corregido en lugar de una advertencia:
| Se equivocó en esto | Por qué | Ahora |
|---|---|---|
| Marcó herramientas de solo lectura | Una ventana plana después de un nombre de herramienta chocaba con la siguiente herramienta, por lo que las lecturas heredaban el vocabulario de las escrituras | Coincidencia con llaves del propio bloque de la herramienta; un verbo de lectura en el nombre veta |
| Dijo que un repositorio no tenía idempotencia cuando tenía un módulo completo | \b(idempot…) no puede coincidir con deriveIdempotencyKey — sin límite de palabra antes de una mayúscula camelCase | Anclas eliminadas; la misma ceguera ocultó requestId, clientToken |
| No encontró escrituras en ningún lado | Las escrituras viven en ayudantes compartidos, no en la declaración de la herramienta | Recopiladas por repositorio como corroboración, nunca afirmadas como "esta herramienta escribe" |
Pasó por alto method: cond ? "POST" : "GET" | Los literales de cadena se eliminaban antes de la coincidencia, borrando el verbo HTTP en sí | Coincidencia en la línea cruda |
| Imprimió "EN RIESGO" | Eso es una acusación, y se equivocó 4 veces de 7 | No existe campo de veredicto |
Si marca algo en tu servidor y quieres una segunda opinión, abre un issue — una acusación incorrecta cuesta más que una omitida, por lo que un falso positivo aquí también vale la pena informarlo.
Proyecto hermano — once (Python)
Mismo problema, otro runtime. once (pip install once-kernel) es el núcleo de idempotencia en Python construido sobre la misma idea: un efecto secundario se ejecuta exactamente una vez bajo reintentos, reentrega de webhooks y trabajadores concurrentes. Va más allá en durabilidad — un almacén Postgres, concesiones de heartbeat con tokens de protección para que un trabajador obsoleto no pueda resucitar después de que su concesión se reclame, y huellas dactilares canónicas de carga útil RFC 8785.
Usa EffectFence cuando tu protección viva en Rust o frente a un servidor MCP; usa once cuando el efecto secundario sea Python y quieras un almacén duradero. effectfence wrap ha demostrado proteger el propio servidor MCP de once.
Soporte comercial
Las bibliotecas son gratuitas y seguirán siéndolo.
Revisión de seguridad de reintentos — $1,200, reembolsada en su totalidad si no encontramos nada. Leemos una ruta de dinero en tu código y cazamos el defecto que sobrevive a la buena ingeniería: no "¿hay una clave de idempotencia?" (la mayoría de los equipos competentes tienen una), sino qué sucede cuando un pago falla ambiguamente — la solicitud que expiró después de liquidarse, el reintento que acuña un nonce nuevo, la reserva liberada en un fallo que no lo era. Cinco días hábiles, informe escrito vinculado a tus propios archivos y números de línea, sin llamadas.
Es la clase de defecto que cazamos en público: hpp-io/x402-mcp-bridge envió dos correcciones de nuestros hallazgos, mcp-server-kibana fusionó dos PRs. Detalles en SUPPORT.md. Para comenzar: resérvalo y responde al recibo con el repositorio y qué ruta de dinero importa más — o envía un correo a hello@aurumflux.co primero si prefieres hablarlo.
Si no encaja, lo diremos — y si no creemos que podamos encontrar algo, lo decimos en lugar de cobrarte por un certificado de salud limpio.
Licencia
MIT — ver LICENSE.