mcpField
E
EffectFence

EffectFence

本地服务
github.com/aurumflux20AI 与智能体07 周前更新

关于 EffectFence

EffectFence 是 Rust 库和 stdio MCP 服务,用于阻止有副作用的工具调用重复执行或基于过期状态执行。它面向处理重试或并发 Agent 的网关,提供单进程持久化账本、结果重放和因果证书。

主要功能

  1. 01通过意图跟踪阻止重复副作用
  2. 02以原子比较处理同域竞争
  3. 03执行前拒绝过期读取集
  4. 04重放已完成操作的记录结果
  5. 05将准入和结果持久化到 JSONL 账本
  6. 06包装现有 MCP 服务

适用场景

  1. 01防止重试或重新分发导致重复扣款
  2. 02协调多个 Agent 修改同一生产资源
  3. 03在现有工具服务前部署防护层
  4. 04探测工具服务是否会并发重复执行

MCP 相关

stdio MCP 服务提供防护操作的准备、提交和中止工具,以及用于活动租约的心跳工具。`wrap` 命令可代理现有 MCP 服务,让其工具调用经过防护。

配置

本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。

暂未收录安装方式

我们还没有获取到这个服务的安装包或远程地址,请按项目 README 中的说明安装。

代码仓库
安装包
npmeffectfence0.3.0
cargoeffectfence0.3.0

工具

5

收录的工具列表 · 更新于 3 周前

参数类型必填说明
reasonstring是效果失败(或结果未知)的原因。
preparedany是先前 `fence_prepare` 调用返回的已准备效果票据,原样传回。

详细介绍

EffectFence

公开登记册:重试安全性索引 列出了哪些 Agent 付款实现会在答案丢失时只付款一次——已验证安全、已发现并修复(附修复耗时),以及如何获得验证。每一项都链接到相应的证明。

CI crates.io docs.rs 许可证:MIT

你的 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。

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。