关于 EffectFence
EffectFence 是 Rust 库和 stdio MCP 服务,用于阻止有副作用的工具调用重复执行或基于过期状态执行。它面向处理重试或并发 Agent 的网关,提供单进程持久化账本、结果重放和因果证书。
主要功能
- 01通过意图跟踪阻止重复副作用
- 02以原子比较处理同域竞争
- 03执行前拒绝过期读取集
- 04重放已完成操作的记录结果
- 05将准入和结果持久化到 JSONL 账本
- 06包装现有 MCP 服务
适用场景
- 01防止重试或重新分发导致重复扣款
- 02协调多个 Agent 修改同一生产资源
- 03在现有工具服务前部署防护层
- 04探测工具服务是否会并发重复执行
MCP 相关
stdio MCP 服务提供防护操作的准备、提交和中止工具,以及用于活动租约的心跳工具。`wrap` 命令可代理现有 MCP 服务,让其工具调用经过防护。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
我们还没有获取到这个服务的安装包或远程地址,请按项目 README 中的说明安装。
工具
5收录的工具列表 · 更新于 3 周前
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| reason | string | 是 | 效果失败(或结果未知)的原因。 |
| prepared | any | 是 | 先前 `fence_prepare` 调用返回的已准备效果票据,原样传回。 |
详细介绍
EffectFence
公开登记册:重试安全性索引 列出了哪些 Agent 付款实现会在答案丢失时只付款一次——已验证安全、已发现并修复(附修复耗时),以及如何获得验证。每一项都链接到相应的证明。
你的 Agent 群不需要更多记忆,而需要一道围绕工具副作用的因果栅栏。
免费:提交任何会转移资金的客户端、中介服务、SDK 或工具包——无论是你自己的还是别人的——我们都会阅读并免费在重试安全性索引上发布评估结果。反馈会列出相关机制、文件和行号,以及一个失败测试。在你发布修复之前,我们会统计你的提交,但绝不公开你的名字。提交评估 →
⚡ 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.
亲自运行攻击:
cargo run --release --example storm
多 Agent 争夺战
上面的风暴是多个竞争者争抢同一个操作。更棘手的情况是不同的 Agent 对同一个生产资源做出相互矛盾的决定——这种协调失效如今已在生产环境的多 Agent 系统中频繁出现(约占 2026 年多 Agent 事故的三分之一)。三个自主 SRE Agent 对同一次延迟飙升做出响应:
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.
每个 Agent 根据自己读取到的状态来看都做对了。若并发运行,三个 kubectl 调用都会执行,集群最终进入无人预期的状态——造成 1 亿美元的故障。栅栏确保只有一个 Agent 能执行操作,在另外两个产生副作用之前拒绝它们,并告诉它们原因。
上面的一切都不是模拟的——每次调用都使用真实的 crate API。
EffectFence 是一道用于多 Agent 工具调用的因果并发栅栏。当多个 Agent(或重试、重新分发)可能尝试执行同一个带副作用的操作时——例如扣款、发放款项、配置资源——EffectFence 保证该操作只会被尝试执行一次:同一时刻发生的竞争通过原子比较并交换预留来裁决,迟到的重复请求则会重放已记录的结果,而不会再次执行。每个实际执行的副作用都会获得一份内容寻址证书,并与其因果依赖链相连。
它以 Rust 库(effectfence::fence)的形式提供,也可作为 stdio MCP 服务,提供三个工具——fence_prepare、fence_commit、fence_abort——让 Agent 能通过栅栏路由带副作用的工具调用,而不是直接彼此竞争。
问题
在多 Agent 网关中,多个调用方可能尝试执行同一个副作用:
- 两个 Agent 各自独立地决定“需要为订单 #123 向客户收费”——而且就在同一时刻。
- 主管等待工具调用时超时,并在原调用仍在执行时重新分发该调用。
- 重试或重复事件在第一次尝试已成功数分钟后,再次触发相同的决策。
采用朴素做法,这些情况都会导致副作用被执行两次。若不记录结果就直接拒绝所有重复请求,同样是错误的:如果第一次尝试崩溃,副作用就永远不会执行;而成功后到达的重复请求也只会收到错误,而不是它需要的结果。EffectFence 通过乐观并发控制(OCC)和意图账本解决所有这些问题:各次尝试不会相互阻塞,只有一次会执行,其他每次尝试都能得知实际发生了什么。
架构
四个部分共同构成围栏协议:
意图账本负责阻止重复操作,而不仅仅是竞态。每个效果都带有一个 intent——逻辑操作的稳定标识(例如 "charge:order-123")。共享同一意图的尝试属于同一操作:首次尝试会获准并持有租约;并发重复尝试会收到“已有尝试正在进行”的通知;成功后到达的重复尝试会逐字重放已记录的证书;失败后到达的重复尝试会被围栏阻止(副作用可能已经触发,也可能没有——必须进行核对,不能盲目重试),直到明确清除。崩溃的持有者会在 TTL 到期后失去租约,避免操作永远卡住。
向量时钟(VectorClock)用于跟踪各 Agent 之间因果性的“先于发生”关系——每个 Agent 有一个逻辑计数器,通过逐元素取最大值的 merge 合并,通过 le 偏序比较,并通过 concurrent 检查是否存在真正无序的事件;同时提供稳定的 SHA-256 digest,以便纳入证书。
OCC 读集(ReadSetEntry)记录决策所依据的因果依赖:“当我决定执行操作时,域 D 的序列号为 S。”prepare_effect_fence 和 commit_effect_cert 都会根据实时状态验证每个条目——如果有任何状态发生变化,该尝试就会因过时而被拒绝,而不是被允许依据过期信息执行操作。
CAS 域围栏用于裁定同一时刻发生的竞态。每个域(一个具名的争用范围,例如 "order:123")都有一个 AtomicU64 序列计数器。决策通过一次原子 compare_exchange 完成——对于任何给定的预期序列号,并发调用者中恰好只有一个能获胜。(精度说明:计数器的查找由一个短暂的互斥锁保护;只有竞态裁决本身是无锁的。欢迎提出实现完全无锁路径的方案。)
┌ 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)
每个已提交的效果都会成为一个 EffectCert:它是对 {intent, parent, domain, seq, tool, args, result, vector_clock, read_set, agent} 计算出的 SHA-256 内容哈希,并链接到一个 parent 证书哈希,以记录因果谱系。两个哈希相同的证书,按定义就是同一效果的记录——EffectCert::verify() 会重新计算哈希,并确认其未被篡改,也不是以错误方式手工构造的。
范围
这是一个单进程围栏,带有持久账本。每次准入、结果和操作员清除都会追加到 JSON-lines 文件,并在围栏作出响应前执行 fsync,因此代理重启后不会忘记已经运行过的内容:重启后遇到重复请求时会重放,而不是重新执行。进程死亡时仍在执行中的任何操作,在下次启动时都会被围栏标记为结果未知——持有者已消失,没人能确定效果是否已触发——直到操作员核对并清除。(库的用户:EffectFence::open(path, config);EffectFence::new() 仍使用内存。随附的二进制文件会选择
$EFFECTFENCE_LEDGER,否则使用 ~/.local/state/effectfence/<name>.jsonl,而
EFFECTFENCE_LEDGER=memory 可选择不使用账本。)有两件事它暂时刻意不做:
- 多副本围栏。 账本是一个文件,因此它保护的是一个网关进程(包括其重启),而不是同时保护多个副本。水平扩展的网关需要相同的意图/域/读取集模型,并由共享存储提供支持(例如 Redis 中的
SETNX+CAS,或 Postgres 中的乐观版本列)——这里的类型旨在直接沿用到该后端。 - 租约过期后仍存活的持有者。 进行中的租约过期后即可被重新认领——这正是崩溃恢复能够实现的原因——而过期本身无法让围栏判断持有者是已经死亡,还是只是比
FenceConfig::lease_ttl(默认 60 s)运行得更慢。因此,若某项操作运行超过租约且未发送任何信号,其认领权就会被接管,并再次运行;而这正是这个 crate 要阻止的情况。解决办法只需一次调用:在操作仍运行时调用EffectFence::heartbeat(intent)(大约每隔租约时长的三分之一调用一次)。wrap会为每次转发的调用自动执行此操作;直接驱动fence_prepare的 Agent 应调用fence_heartbeat工具。停止发送心跳后,租约会按计划失效,因此真正死亡的持有者仍能得到恢复。 - 强制执行。 围栏保护通过它路由操作的 Agent;它无法阻止完全绕过围栏的 Agent。应将它部署在 Agent 共享的唯一关口处(拥有这些工具的网关进程)。
内存使用有上限:已完成的结果会在可配置的 TTL 后过期(FenceConfig::result_ttl,由 EffectFence::sweep 清理);查询不会创建跟踪状态;域计数器很小,且可手动逐出(evict_domain)。
快速入门:库
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.
}
}
同一意图的并发重复请求会得到 Err(FenceError::IntentInFlight);域上同一时刻发生竞争时会得到 Err(FenceError::DomainRace);无论哪种情况,都不能运行该操作。
10 秒试用(无需安装,不会触发真实操作)
cargo install effectfence # or: npx -y effectfence demo
effectfence demo
十二个 Agent 同时尝试发起一笔 49 美元的扣款。你会看到它先以原始方式到达内置服务——产生 12 笔重复扣款——然后让相同的十二次调用通过围栏:结果恰好为 1。这个二进制文件只与自身通信,因此不会触发真实调用;你也无需自备服务,就能看到其中的要点。
── 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
首先:你的技术栈真的会重复触发吗?(探测一下)
安装围栏前,先验证自己是否需要它——在你自己的服务上验证,而不是用我们的演示。
probe 是一个精简的 MCP 客户端。将它指向任意 MCP 服务,它就会对一个工具并发发起 N 次参数完全相同的调用——这正是两个 Agent 同时尝试执行同一操作时发生的双调用竞争——然后报告实际产生了多少个不同的效果:
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.
它只会调用你指定的那个工具,并使用你提供的确切参数——绝不会盲目枚举并轰炸服务。它也会如实说明自己能观察到什么:不同的结果是重复执行的确凿证据;相同的结果则会报告为无法从响应得出结论,而不会虚报一个无法证明的零。
然后通过围栏重新运行相同的探测,观察 DISTINCT effects 降至
1:
effectfence probe --tool charge_card --args '{"amount":4900}' --calls 12 -- effectfence wrap -- npx -y @your-org/your-mcp-server
两条命令就能说明全部要点:先看效果,再上锁。
快速入门:包装现有 MCP 服务(从这里开始)
使用 EffectFence 最快的方式,是将它放在你已经在运行的工具服务前面。Agent 无需记住调用任何内容——每次工具调用都会自动经过保护:
agent/client ──MCP──> effectfence wrap ──MCP──> your real tool server
cargo install effectfence
然后包装负责提供危险工具的任意服务:
effectfence wrap -- npx -y @your-org/your-mcp-server
工具列表会与子服务 1:1 镜像(名称、架构和文档都相同),因此 Agent 无需做任何改动。变化在于:完全相同的重复调用——工具和参数相同——只会在子服务上执行一次;之后的重复调用会重放记录的结果,而并发的相同调用会被拒绝,不会重复触发。
一键粘贴配方:保护会修改集群的服务
本工具正是为这种情况而设计的——多个 Agent 在同一个集群上使用 kubectl。
Claude Code:
claude mcp add k8s-fenced -- effectfence wrap -- npx -y kubernetes-mcp-server
Cursor(~/.cursor/mcp.json)或 Claude Desktop(claude_desktop_config.json):
{
"mcpServers": {
"k8s-fenced": {
"command": "effectfence",
"args": ["wrap", "--", "npx", "-y", "kubernetes-mcp-server"]
}
}
}
将 kubernetes-mcp-server 换成提供写入类工具的服务,例如云 API、部署工具或支付服务。让每个 Agent 都连接到受保护的名称,并移除它们对原始服务的访问权限;只有它是唯一入口时,保护机制才有效。
使用 fence_stats(见下文)查看运行情况——replayed 和 refused 分别表示成功阻止的重复执行和被拒绝的重复执行。
包装功能的适用范围
目前仅支持工具(不透传资源或提示词)。意图根据 hash(tool + canonical args) 推导,因此字节完全相同的参数会被视为同一操作——如果 Agent 在参数中改变时间戳,就能绕过去重;而这种情况会以安全失败的方式处理:调用照常执行,不会造成破坏。状态会按被包装的命令持久化(每个不同的子命令对应一个账本文件,因此两个提供同名工具的服务互不影响);每组会修改生产环境的工具运行一个受保护的网关。
转发调用期间,只要子服务仍在运行,就会定期发送心跳,因此执行时间超过租约期限的工具仍会保留其占用状态,不会被接管并重复运行。如需显式设置,可调整这两个时间窗口:EFFECTFENCE_LEASE_SECS(默认值为 60)和 EFFECTFENCE_RESULT_TTL_SECS(默认值为 86400)。EFFECTFENCE_LEDGER 用于设置持久化账本的存放位置(默认路径为 ~/.local/state/effectfence/wrap-<hash>.jsonl;设为 memory 可选择不持久化)。包装进程重启后,会重放已运行的操作,并保护进程退出时尚在调用中的操作。
快速入门:MCP 服务(显式保护)
如果你希望 Agent 主动进行保护,并获得比 wrap 自动推导更丰富的控制能力(read_set、parent、known_clock),请使用此方式。
此服务已列入 官方 MCP Registry,名称为 mcp-name: io.github.aurumflux20/effectfence
安装
使用 Rust 工具链(rustup.rs):
cargo install effectfence
或者从此代码库克隆并构建:
cargo build --release # binary at ./target/release/effectfence
添加到 Claude
Claude Code(一条命令):
claude mcp add effectfence -- effectfence
(如果你是从源代码构建,而不是通过 cargo install 安装,请使用完整路径:claude mcp add effectfence -- /path/to/target/release/effectfence。)
Claude Desktop——添加到 claude_desktop_config.json:
{
"mcpServers": {
"effectfence": {
"command": "effectfence"
}
}
}
任意项目(团队共享)——在项目根目录提交一个 .mcp.json:
{
"mcpServers": {
"effectfence": {
"command": "effectfence"
}
}
}
就是这样——无需账户,也无需必需配置。服务会将账本保存在 ~/.local/state/effectfence/server.jsonl(可通过 EFFECTFENCE_LEDGER 覆盖),因此服务重启后,已运行的操作仍会保留。
工具
它提供:
-
fence_prepare—{ intent, domain, tool, args, agent, read_set?, parent?, known_clock? }→ 如果本次尝试胜出,则返回{status: "fresh", prepared}(运行工具,然后报告结果);如果这一精确操作已运行,则返回{status: "already_done", cert}(使用记录的结果——不要运行工具)。出错意味着不要运行。 -
fence_commit—{ prepared, result }→{status: "committed", cert}。此后,意图的重复请求会重放此证书。 -
fence_abort—{ prepared, reason }→{status: "aborted"}。该意图会持续受到围栏保护,直到完成协调并清除。 -
fence_stats— 无参数 → 进程启动以来的实时计数器:admitted(已运行的效果)、replayed(已交付记录结果的重复请求)、refused按原因细分(stale_read_set、domain_race、in_flight、prior_failure),以及total_attempts和prevented。prevented是关键指标:每一次未运行该效果的尝试都会计入其中。effectfence since boot: admitted=1 replayed=995 refused(stale=0 race=4 in-flight=0 failed=0) total=1000 prevented=999
工具输入架构由 Rust 类型自动生成(通过 schemars),因此任何 MCP 客户端都可以使用 tools/list 对其进行检查。
测试
cargo test # unit tests + chaos tests
cargo clippy --all-targets
tests/chaos_test.rs 使用真实的 OS 线程分别验证这两项保证:强制制造同一时刻的领域竞争(通过精心设计同步方式确保发生冲突,而非寄希望于此),每次都恰好允许一个胜出者;对同一意图发起 16 个并发重复请求,也恰好只执行一次——较晚到达的重复请求会重放已提交的证书。此外,32 线程压力测试还会断言序列号绝不会重复分配。
扫描你自己的 MCP 服务
tools/fencescan.py 用于查找 MCP 服务中可能重复触发同一效果的工具。无需安装、无需依赖项、无需联网:
python3 tools/fencescan.py /path/to/your-mcp-server
它会报告有证据支持的候选项,并且有意不作判断,因为外部人员通常无法仅凭仓库内容证明某项操作会触发两次——防护逻辑往往位于仓库调用的服务或同级 SDK 中;名称听起来像写操作的工具,也可能只是返回一个待他人签名的负载。输出中会明确列出它无法查看的内容。
在人工核验推翻了最初 7 条“确认”中的 4 条后,我们重写了它。现在,每个问题都已作为固定行为修正,而不再只是附带说明:
| 它曾误判的内容 | 原因 | 现在的处理方式 |
|---|---|---|
| 标记了只读工具 | 工具名称后的一段固定范围延伸到了下一个工具,导致只读工具也继承了写操作相关词汇 | 按大括号匹配工具自身的代码块;名称中包含读操作动词时会否决标记 |
| 声称仓库没有幂等性机制,尽管仓库里有完整模块 | \b(idempot…) 无法匹配 deriveIdempotencyKey——驼峰命名中大写字母前没有单词边界 | 移除锚点;同样的问题也让它漏掉了 requestId、clientToken |
| 没有找到任何写操作 | 写操作位于共享辅助函数中,而非工具声明里 | 按仓库收集,作为佐证;绝不声称“此工具会写入” |
漏掉了 method: cond ? "POST" : "GET" | 匹配前先剥除了字符串字面量,导致 HTTP 动词本身被删掉 | 直接匹配原始行 |
| 输出了“有风险” | 这是指控,而且 7 次中错了 4 次 | 不再提供判断字段 |
如果它标记了你服务中的某项内容,而你希望有人再帮你检查一次,请提交 issue——错误指控的代价高于漏报,因此这里的误报也值得报告。
兄弟项目 — once(Python)
同样的问题,不同的运行环境。once(pip install once-kernel)是基于相同理念构建的 Python 幂等内核:在重试、Webhook 重新投递和并发工作进程的情况下,副作用恰好执行一次。它在持久性方面更进一步——提供 Postgres 存储、带有 fence token 的心跳租约,确保租约被回收后,过期工作进程无法复活,以及符合 RFC 8785 的规范化负载指纹。
如果你的 fence 在 Rust 中,或位于 MCP 服务前面,请使用 EffectFence;如果副作用由 Python 执行,且你需要持久化存储,请使用 once。effectfence wrap 已被验证可为 once 自己的 MCP 服务提供 fencing。
商业支持
这些库免费提供,并将一直免费。
重试安全审查 — $1,200,如果我们一无所获,将全额退款。 我们会审查你代码库中的一条资金处理路径,寻找那些即使经过良好工程实践仍会出现的缺陷:不是“有没有幂等键”(大多数有能力的团队都有),而是当支付以模糊状态失败时会发生什么——请求超时后实际已完成、重试时生成了新的 nonce,或在并未真正失败的情况下释放了预留。五个工作日内交付书面报告,注明你代码中的具体文件和行号,不需要通话。
这正是我们公开寻找的那类缺陷:hpp-io/x402-mcp-bridge 根据我们的发现发布了两项修复,mcp-server-kibana 合并了两个 PR。详情请见
SUPPORT.md。开始前往:预约,并回复收据邮件,告知我们代码库地址以及最重要的资金处理路径;如果你更愿意先聊聊,也可以先发送邮件至
[email protected]。
如果不适合,我们会直说;如果我们认为找不到任何问题,也会如实告知,而不是收取费用后给你一份“健康证明”。
许可证
MIT — 详见 LICENSE。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。