
EffectFence
io.github.aurumflux20由 GitHub 账号 aurumflux20 发布v0.3.0更新于 Oct 24, 2026
Stops agents double-firing side effects: 1,000 racing duplicates, exactly one execution.
概览
为有副作用的工具调用加围栏,使重复或并发的代理尝试只执行一次,后续重复请求重放已记录的结果。
- 可以做什么
- EffectFence 是面向多代理工具调用的因果并发围栏。它通过 stdio 暴露 fence_prepare、fence_commit、fence_abort 和 fence_stats,并提供 wrap 模式,可置于现有 MCP 工具服务器之前并 1:1 镜像其工具。同一瞬间的竞争由原子 compare-exchange 预留决定;迟到的重复请求会重放已记录的结果而不再执行,每个真正执行的效果都会生成内容寻址的证书。probe 命令可对服务器发起相同调用,以显示其是否重复触发。
- 适用场景
- 当多个代理、重试或重新派发可能触及同一个有副作用的操作(扣款、发放付款、扩缩容或回滚集群),而重复执行会造成损害时使用。它应部署在代理共用的唯一入口处。
- 运行要求
- 以 stdio 在本地运行;可通过 cargo install effectfence 或 npx effectfence 安装。无需账号或 API 密钥。可选环境变量:EFFECTFENCE_LEDGER、EFFECTFENCE_LEASE_SECS、EFFECTFENCE_RESULT_TTL_SECS。账本默认位于 ~/.local/state/effectfence 下的文件。仅支持桌面端。
安装
在 SourceWeft 中
- 打开 控制台中的 EffectFence,将其添加到工作区。
- 为需要使用其工具的对话启用该服务。
Desktop only,通过 STDIO。 STDIO 服务会启动本地进程,因此需要 SourceWeft 桌面宿主。
其他 MCP 客户端
参照 仓库 中的启动说明。
README
EffectFence
Public register: the Retry-Safety Index lists which agent-payment implementations pay once when the answer is lost — verified safe, found & fixed (with time-to-fix), and how to get verified. Every row links to its proof.
[CI] [crates.io] [docs.rs] [License: MIT]
Your swarm doesn't need more memory. It needs a causal fence around tool side effects.
Free: submit any client, facilitator, SDK or toolkit that moves money — yours or someone else's — and we read it and publish a verdict on the Retry-Safety Index at no cost. Findings come back with the mechanism, the file and line, and a failing test. You are counted, never named, until you ship a fix. Submit for grading →
Run the attack yourself:
The multi-agent turf war
The storm above is one action, many racers. The harder case is different agents making contradictory decisions on the same production resource — the coordination failure now reported across production multi-agent systems (~a third of 2026 multi-agent incidents). Three autonomous SRE agents react to one latency spike:
Each agent was individually correct for the state it read. Run concurrently
without coordination, all three kubectl calls fire and the cluster ends in a
state none of them intended — the $100M outage. The fence lets exactly one act,
refuses the other two before their side effect runs, and tells each one why.
Nothing above is mocked — every call is the real crate API.
EffectFence is a causal concurrency fence for multi-agent tool calls. When more than one agent (or retry, or re-dispatch) can end up trying to run the same side-effecting operation — charge a card, send a payout, provision a resource — EffectFence guarantees exactly one attempt ever executes it: same-instant races are decided by an atomic compare-exchange reservation, and late duplicates get the recorded outcome replayed instead of running again. Every effect that does run gets a content-addressed certificate chained to whatever it was causally built on.
It ships as a Rust library (effectfence::fence) and as a stdio MCP server exposing three tools — fence_prepare, fence_commit, fence_abort — so agents can route side-effecting tool calls through the fence instead of racing each other directly.
The problem
In a multi-agent gateway, more than one caller can end up trying to run the same effect:
- Two agents independently decide "charge the customer for order #123" needs to happen — at the same instant.
- A supervisor times out waiting for a tool call and re-dispatches it while the original is still in flight.
- A retried or duplicated event triggers the same decision again, minutes after the first attempt already succeeded.
Naively, any of these double-runs the effect. Naively rejecting every duplicate with no memory of outcome is also wrong: if the first attempt crashed, the effect never runs at all, and a duplicate that arrives after success gets an error instead of the result it needs. EffectFence closes all of it with optimistic concurrency control (OCC) plus an intent ledger: attempts don't block each other, exactly one executes, and every other attempt learns what actually happened.
Architecture
Four pieces compose into the fencing protocol:
The intent ledger is what stops duplicates, not just races. Every effect carries an intent — a stable id for the logical action (e.g. "charge:order-123"). Attempts sharing an intent are the same action: the first is admitted and holds a lease; concurrent duplicates are told an attempt is in flight; duplicates arriving after success get the recorded certificate replayed verbatim; duplicates after a failure are fenced (the side effect may or may not have fired — that must be reconciled, not blindly retried) until explicitly cleared. Crashed holders lose their lease after a TTL so the action isn't stuck forever.
Vector clocks (VectorClock) track causal "happened-before" relationships across agents — one logical counter per agent, joined via elementwise-max merge, compared via a le partial order, with a concurrent check for genuinely unordered events and a stable SHA-256 digest for inclusion in certificates.
OCC read-sets (ReadSetEntry) record the causal dependencies a decision was based on: "when I decided to act, domain D was at sequence S." Both prepare_effect_fence and commit_effect_cert validate every entry against live state — if anything moved, the attempt is rejected as stale rather than allowed to act on outdated information.
CAS domain fencing is where same-instant races are decided. Each domain (a named contention scope, e.g. "order:123") has an AtomicU64 sequence counter. The decision is a single atomic compare_exchange — exactly one concurrent caller can win for any given expected sequence. (Precision note: the counter lookup sits behind a short mutex; only the race decision itself is lock-free. Ideas for a fully lock-free path are welcome.)
Every committed effect becomes an EffectCert: a SHA-256 content hash over {intent, parent, domain, seq, tool, args, result, vector_clock, read_set, agent}, chained to a parent cert hash for causal lineage. Two certs with the same hash are, by definition, records of the same effect — EffectCert::verify() recomputes the hash and confirms it hasn't been tampered with or hand-built incorrectly.
Scope
This is a single-process fence with a durable ledger. Every admission,
outcome and operator clear is appended to a JSON-lines file and fsync'd before
the fence answers, so a restart of the proxy does not forget what already ran:
a duplicate after the restart is replayed, not re-executed. Anything that was
in flight when the process died is fenced as an unknown outcome on the next
start — the holder is gone and nobody can say whether the effect fired — until
an operator reconciles and clears it. (Library users: EffectFence::open(path, config); EffectFence::new() stays in-memory. The shipped binary picks
$EFFECTFENCE_LEDGER, else ~/.local/state/effectfence/<name>.jsonl, and
EFFECTFENCE_LEDGER=memory opts out.) Two things it deliberately does not do (yet):
- Multi-replica fencing. The ledger is one file, so it protects one gateway process (across its restarts), not several replicas at once. A horizontally scaled gateway needs the same intent/domain/read-set model backed by a shared store (e.g.
SETNX+CAS in Redis, or an optimistic version column in Postgres) — the types here are meant to carry over directly to that backend. - A holder that outlives its lease. An in-flight lease is claimable once it
expires — that is what makes crash recovery possible — and nothing in an expiry
tells the fence whether the holder died or is simply slower than
FenceConfig::lease_ttl(60 s by default). An effect that runs past its lease without saying anything therefore has its claim taken over and runs a second time, which is the very thing this crate exists to stop. The remedy is one call:EffectFence::heartbeat(intent)while the effect is still running (about every third of the lease).wrapdoes this for you on every forwarded call; agents drivingfence_preparedirectly should call thefence_heartbeattool. Stop beating and the lease lapses on schedule, so a genuinely dead holder is still recovered. - Enforcement. The fence protects agents that route their effects through it; it cannot stop an agent that bypasses it entirely. Deploy it at the one choke point your agents share (the gateway process that owns the tools).
Memory is bounded: finished outcomes expire after a configurable TTL (FenceConfig::result_ttl, swept by EffectFence::sweep), queries never create tracking state, and domain counters are tiny and manually evictable (evict_domain).
Quickstart: library
A concurrent duplicate of the same intent gets Err(FenceError::IntentInFlight); a same-instant race on the domain gets Err(FenceError::DomainRace); either way it must not run the effect.
Try it in 10 seconds (nothing installs, nothing real fires)
Twelve agents reach for one $49 charge at the same instant. You'll see it hit a built-in server raw — 12 duplicate charges — then the same twelve calls behind the fence: exactly 1. This binary is talking to itself, so no real call fires and you need no server of your own to see the point.
First: does YOUR stack actually double-fire? (probe it)
Before you install a fence, prove you need one — on your own server, not our demo.
probe is a bare MCP client. Point it at any MCP server, and it fires N
byte-identical calls at one tool concurrently — the twin-caller race that
happens the instant two agents reach for the same action — then reports how many
distinct effects actually landed:
It fires only the one tool you name, with the exact arguments you supply — it never enumerates and hammers a server blindly. And it is honest about what it can see: distinct results are undeniable proof of double-execution; identical results are reported as inconclusive from the response, never as a zero it can't prove.
Then re-run the same probe through the fence and watch DISTINCT effects drop
to 1:
That is the whole pitch in two commands: the footprints, then the lock.
Quickstart: wrap an existing MCP server (start here)
The fastest way to use EffectFence is to put it in front of a tool server you already run. Agents don't have to remember to call anything — every tool call is fenced automatically:
Then wrap whatever server owns your dangerous tools:
The tool list is mirrored 1:1 from the child (same names, schemas, docs), so nothing in your agent changes. What changes: identical duplicate calls — same tool, same arguments — execute the child once; later duplicates get the recorded result replayed, and concurrent identical calls are refused rather than double-firing.
One-paste recipe: fence a cluster-mutating server
The case this exists for — several agents with kubectl on the same cluster.
Claude Code:
Cursor (~/.cursor/mcp.json) or Claude Desktop (claude_desktop_config.json):
Swap kubernetes-mcp-server for whichever server holds your write-bearing tools —
cloud APIs, deploy tooling, a payments server. Point every agent at the fenced name
and remove their access to the raw one; the fence is only a fence if it is the only
door.
Watch it work with fence_stats (see below) — replayed and refused are the
duplicate executions that did not happen.
Honest scope of wrap
Tools only for now (no resource/prompt passthrough). Intent is derived from
hash(tool + canonical args), so byte-identical arguments are treated as the same
action — an agent that varies a timestamp in its arguments defeats dedup, and that
direction fails safe: the call runs, nothing is corrupted. State is durable per
wrapped command (one ledger file per distinct child command, so two servers with a
tool of the same name never fence each other); run one fenced gateway per set of
production-mutating tools.
A forwarded call heartbeats while the child runs, so a tool slower than the lease
keeps its claim instead of being taken over and run twice. Both windows are
settable if you want them explicit: EFFECTFENCE_LEASE_SECS (default 60) and
EFFECTFENCE_RESULT_TTL_SECS (default 86400). EFFECTFENCE_LEDGER sets where the
durable ledger lives (default ~/.local/state/effectfence/wrap-<hash>.jsonl;
memory to opt out). A restart of wrap replays what already ran and fences
whatever was mid-call when it died.
Quickstart: MCP server (explicit fencing)
Use this when you want agents to fence deliberately — richer control (read_set,
parent, known_clock) than wrap derives automatically.
Listed in the official MCP Registry as
mcp-name: io.github.aurumflux20/effectfence
Install
With a Rust toolchain (rustup.rs):
Or build from a clone of this repo:
Add it to Claude
Claude Code (one command):
(If you built from source instead of cargo install, use the full path: claude mcp add effectfence -- /path/to/target/release/effectfence.)
Claude Desktop — add to claude_desktop_config.json:
Any project (team-shared) — commit a .mcp.json at the project root:
That's it — no accounts, no required configuration. The server keeps its ledger at
~/.local/state/effectfence/server.jsonl (override with EFFECTFENCE_LEDGER), so
what already ran survives a restart of the server.
The tools
It exposes:
-
fence_prepare—{ intent, domain, tool, args, agent, read_set?, parent?, known_clock? }→{status: "fresh", prepared}when this attempt wins (run the tool, then report back), or{status: "already_done", cert}when this exact action already ran (use the recorded result — do NOT run the tool). Errors mean do not run. -
fence_commit—{ prepared, result }→{status: "committed", cert}. Later duplicates of the intent now replay this cert. -
fence_abort—{ prepared, reason }→{status: "aborted"}. The intent stays fenced until reconciled and cleared. -
fence_stats— no arguments → live counters since the process started:admitted(effects that ran),replayed(duplicates handed a recorded result),refusedbroken out by cause (stale_read_set,domain_race,in_flight,prior_failure), plustotal_attemptsandprevented.preventedis the number that matters: every attempt that did not run the effect.
Tool input schemas are generated automatically from the Rust types (via schemars), so any MCP client can introspect them with tools/list.
Testing
tests/chaos_test.rs uses real OS threads to prove the two guarantees separately: a forced same-instant domain race (synchronization deliberately constructed so the collision is guaranteed, not hoped for) admits exactly one winner every time, and 16 concurrent duplicates of one intent admit exactly one execution — with late duplicates replaying the committed cert. A 32-thread stress test additionally asserts sequence numbers are never double-allocated.
Scan your own MCP server
tools/fencescan.py finds tools in an MCP server that could fire the same effect
twice. No install, no dependencies, no network:
It reports candidates with evidence and deliberately renders no verdict, because an outsider reading a repository usually cannot prove a double-fire — the guard often lives in a service the repo calls, or in a sibling SDK, and a tool whose name sounds like a write may only return a payload for someone else to sign. Output includes an explicit list of what it cannot see.
It was rewritten after hand-verification killed 4 of its first 7 "confirmations". Each failure is now a fixed behaviour rather than a caveat:
If it flags something in your server and you want a second pair of eyes, open an issue — a wrong accusation costs more than a missed one, so a false positive here is worth reporting too.
Sibling project — once (Python)
Same problem, other runtime. once (pip install once-kernel) is the Python idempotency kernel built on the same idea: a side effect runs exactly once under retries, webhook redelivery, and concurrent workers. It goes further on durability — a Postgres store, heartbeat leases with fence tokens so a stale worker can't resurrect after its lease is reclaimed, and RFC 8785 canonical payload fingerprints.
Use EffectFence when your fence lives in Rust or in front of an MCP server; use once when the side effect is Python and you want a durable store. effectfence wrap has been proven fencing once's own MCP server.
Commercial support
The libraries are free and stay free.
Retry Safety Review — $1,200, refunded in full if we find nothing. We read one money path in your codebase and hunt the defect that survives good engineering: not "is there an idempotency key" (most competent teams have one), but what happens when a payment fails ambiguously — the request that timed out after it settled, the retry that mints a fresh nonce, the reservation released on a failure that wasn't one. Five working days, written report tied to your own file and line numbers, no calls.
It's the class of defect we hunt in public: hpp-io/x402-mcp-bridge shipped two
fixes from our findings, mcp-server-kibana merged two PRs. Details in
SUPPORT.md. To start: book it and reply to the
receipt with the repository and which money path matters most — or email
[email protected] first if you'd rather talk it through.
If it isn't a fit we'll say so — and if we don't think we can find anything, we say that instead of billing you for a clean bill of health.
License
MIT — see LICENSE.
来源:README.md,提交 d7f73e4
工具
0版本历史
1- v0.3.0最新Sep 16, 2026
评分
已满足适用检查项的 50%
该评分衡量此收录条目的完整程度,依据注册中心条目、端点与公开来源计算得出。它不是对服务器本身的评价,也不表示服务器安全或当前可用:可连通性是带日期的检查结果,评分也不包含安装量或用户评价。
- 近 30 天内可连通25 分
请保持端点能够响应 MCP 握手;SourceWeft 每 30 天探测一次。
- 已声明认证方式10 分
请在注册中心条目中声明凭据请求头,或保持端点能够响应探测,以便判定其认证方式。
- 已列出工具15 分
请让服务器在无凭据时响应 tools/list,或发布可在沙箱中无凭据启动的软件包。
- 描述必需15 分
- 图标10 分
- 文档15 分
- 许可证10 分