Skip to main content
Glama

Synartesis

为 AI 智能体提供撤销层。

check MIT

一个对真实系统拥有写权限的智能体运行了二十步,误读了第七步,然后把其余步骤应用到了错误的记录上。如今你的选择是:根据转录手动撤销,恢复备份但丢失同一窗口内所有合法更改,或者接受损失。

Synartesis 位于你的 MCP 客户端与其通信的服务器之间。它记录每一次工具调用及其所替换的状态,并且能够恢复该状态。对于无法恢复的操作,它会拒绝让智能体在无人监督的情况下执行。

它不是沙箱:你的智能体运行的容器是一次性的,但它在网络上更新的 CRM 行不是。它不是追踪工具:追踪只会告诉你 update_customer 运行了四十次,而不会告诉你之前的值是什么。

它能做什么,不能做什么

每个工具都会获得四种分类之一,你需要在清单中写明:

分类

含义

示例

发生什么

readonly

不改变任何内容

get_customer

记录并转发

reversible

可以精确恢复之前的状态

update_customer

写入前捕获状态;撤销时写回

compensable

无法撤销,但可以抵消

create_charge

另一个调用使其失效

irreversible

两者皆不可

send_email

暂停,直到人类批准

清单中未提及的工具将被视为 irreversible。这是有意为之:静默转发未知的破坏性调用是最应避免的失败。

Related MCP server: mcp-compensator

要求

工具

版本

检查命令

Node

22 或更高

node --version

pnpm

9 或更高

pnpm --version

C 工具链

任意

cc --version

pnpm 随 Node 通过 corepack 提供:

corepack enable pnpm

C 工具链只需使用一次,用于编译 SQLite 的原生绑定。在 macOS 上运行 xcode-select --install;在 Debian 或 Ubuntu 上运行 apt install build-essential。

安装

curl -fsSL https://raw.githubusercontent.com/ArhaanDev24/Synartesis/main/install.sh | bash

或者,如果你更想先阅读,可以从克隆开始:

git clone https://github.com/ArhaanDev24/Synartesis.git && cd Synartesis && ./install.sh

该脚本会检查你的 Node 版本,进行构建,并将 synartesis 和 synartesis-proxy 链接到 PATH 中第一个可写的目录。它不会修改任何 shell 配置文件,也不需要 sudo。传递 --no-link 仅进行构建。

synartesis --help

如果无法链接任何内容,也不会出问题:Synartesis 打印的每条命令都会以实际在你的机器上运行的形式完整写出。

演练

这使用仓库附带的一个玩具 CRM,因此你可以在不指向真实数据的情况下看到整个循环。请从临时目录运行。

mkdir -p /tmp/synartesis-demo && cd /tmp/synartesis-demo

1. 编写策略

init 启动一个服务器,询问它有哪些工具,并写入一个清单。将 SYNARTESIS 替换为你克隆的路径。

node SYNARTESIS/dist/cli.js init crm -- node SYNARTESIS/dist/toy-crm.js --state ./crm.json

打开 synartesis.yaml。每个未声明为只读的工具都会以 irreversible 状态和 TODO 开始。处理这些 TODO 是你的工作。 该夹具的完整策略已随仓库提供,因此直接复制而不是手动输入:

cp SYNARTESIS/manifests/toy-crm.yaml ./synartesis.yaml

然后编辑指定服务器位置的那一行,使其指向你的克隆,并将数据保留在此目录中:

servers:
  crm:
    command: node
    args: ["SYNARTESIS/dist/toy-crm.js", "--state", "./crm.json"]

2. 将你的智能体指向代理

无论你的 MCP 客户端在哪里列出服务器,都将你想要覆盖的服务器条目替换为代理。对于 Claude Desktop 或 Claude Code,这是一个 mcpServers 块:

{
  "mcpServers": {
    "crm": {
      "command": "node",
      "args": ["SYNARTESIS/dist/proxy.js", "--manifest", "/tmp/synartesis-demo/synartesis.yaml"]
    }
  }
}

智能体看到的是相同的工具、相同的名称和相同的结果。这就是重点:你的智能体无需任何更改。

对于本演练,你不需要真正的智能体。以下操作效果相同:

printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"demo-agent","version":"0"}}}' '{"jsonrpc":"2.0","method":"notifications/initialized"}' '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"update_customer","arguments":{"id":"c_001","plan":"free","notes":"wrong edit"}}}' '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"delete_customer","arguments":{"id":"c_002"}}}' | node SYNARTESIS/dist/proxy.js --manifest ./synartesis.yaml --journal ./journal.db > /dev/null

查看损坏情况:

cat crm.json

Ada 被分配了错误的计划,备注也错了,Grace 消失了。

3. 查看它做了什么

node SYNARTESIS/dist/cli.js list --journal ./journal.db
node SYNARTESIS/dist/cli.js show RUN_ID --journal ./journal.db

show 会打印每次调用的分类、状态以及精确的撤销调用,并已解析为字面值。

4. 撤销

三思而后行:

node SYNARTESIS/dist/cli.js undo RUN_ID --dry-run --journal ./journal.db

然后执行:

node SYNARTESIS/dist/cli.js undo RUN_ID --journal ./journal.db
cat crm.json

Grace 回来了,Ada 回到了原来的计划,备注也恢复了。

5. 观察它拒绝

撤销不是粗暴的工具。如果智能体修改记录后,其他东西又修改了该记录,那么写回旧值会破坏那项工作,因此 Synartesis 会停止并显示两个值。

再次运行第 2 步中的损坏命令。这会创建第二次运行,因此从 list 顶部获取运行 ID(按最近优先排序)。然后手动编辑记录:

node -e 'const f="./crm.json",s=JSON.parse(require("fs").readFileSync(f));s.customers.c_001.notes="a human wrote this";require("fs").writeFileSync(f,JSON.stringify(s,null,2))'
node SYNARTESIS/dist/cli.js undo RUN_ID --journal ./journal.db

它会停止,打印预期状态和实际状态,以非零状态退出,并且不更改任何内容。

批准无法撤销的操作

send_email 被归类为 irreversible,因此智能体无法自行发送。该调用会立即被拒绝,并附带一个操作 ID 和批准该操作所需的命令。智能体告诉你,你决定,然后它重试。

它不会在等待时保持调用打开。那是最初的设计,但在真实客户端面前无法成立:一个人注意到、打开终端并做出决定所需的时间窗口,总是比客户端等待工具的时间长,因此无法通过选择更好的超时来调和两者。

批准也不会发生在智能体使用的终端上:代理通过 stdin 和 stdout 进行 MCP 通信,因此那里没有可提示的内容,桌面客户端根本没有终端。请求会进入日志,你可以从任何地方回答:

node SYNARTESIS/dist/cli.js gates --journal ./journal.db
node SYNARTESIS/dist/cli.js approve ACTION_ID --by your-name --journal ./journal.db
node SYNARTESIS/dist/cli.js deny ACTION_ID --by your-name --reason "not this one" --journal ./journal.db

批准是一次性的,并且在一小时后过期,因此它只覆盖所授予的重试,不能静默地授权明天相同的调用。它不绑定到某个会话,因为人们会重启客户端,而困在死会话中的批准根本不算批准。

任何批准都不会因沉默而生效。未回答的请求只会保持未回答状态,在 synartesis gates 中可见,直到有人决定。

智能体在连接时会被告知所有这些,因此它可以解释自己,而不是报告一个不透明的失败。

真实服务器

如果你更愿意跟随步骤而不是阅读说明,这里有一份针对你自己的文件运行此工具的指南,其中门控和漂移检查是值得特意测试的两项。

Synartesis 与电子邮件本身无关。它基于 MCP 协议,因此其主题是你所连接的服务器能做什么:你的文件、你的仓库、你的数据库、你的工单、你的智能体自身的记忆。它能撤销什么完全取决于这些服务器暴露了什么,下面的每个清单都明确说明了其边界。

清单

服务器

它管理的状态

filesystem.yaml

@modelcontextprotocol/server-filesystem

磁盘上的真实文件

memory.yaml

@modelcontextprotocol/server-memory

智能体关于你的知识图谱

git.yaml

mcp-server-git

真实仓库的索引和历史

github.yaml

github/github-mcp-server

问题、拉取请求、文件内容

toy-crm.yaml

本仓库中的夹具

每个分类的完整示例

除 github.yaml 外,其余所有清单都已针对实际运行的服务器进行了检查。两个演示真实运行了完整循环:

./demo/filesystem-demo.sh
./demo/memory-demo.sh

文件系统演示会覆盖一个文件并移动另一个文件,然后恢复两者,接着展示当人类在期间编辑文件时撤销如何拒绝,以及门控如何拒绝创建该服务器无法删除的目录。

内存演示更为尖锐。智能体向图谱中添加了两个人,其中一人已经存在,服务器静默忽略了重复项。因此,撤销必须恰好移除其中一个:逆操作是根据服务器声称创建的内容构建的,而不是根据智能体请求的内容,因此先存在的人能在撤销中幸存。同一会话随后尝试删除一个实体,但被阻止,因为删除实体也会删除所有关联它的关系,而一个逆调用无法同时恢复两者。

每个清单的边界

限制才是有趣的部分,它们是服务器的属性,而不是 Synartesis 的属性。

  • filesystem:move_file 仅凭其参数即可撤销,因此无需声明预读,也无法对其检查漂移。create_directory 是 irreversible,不是因为目录珍贵,而是因为这个服务器没有提供删除目录的方法。

  • memory:add_observations 和 delete_observations 是精确的对立操作,但对同一字段的命名不一致。路径可以读取字段,但不能重命名字段,因此该逆操作根本无法编写,该调用改为被门控。

  • git:这个服务器提供的几乎所有读取都以面向人类的散文形式返回,因此几乎无法从捕获的状态中反转任何内容,无论底层 git 操作多么可逆。提交被门控,因为这个服务器没有提供 reset、revert 或移动分支的方法。

如果你自己编写清单,有两件事值得了解,它们是通过在真实服务器上运行这些清单而不是阅读文档发现的:

$result 是结构化块,它不必与文本块匹配。内存服务器在文本块中返回 create_entities 的裸列表,并在 structuredContent 中返回 {"entities": [...]}。Synartesis 遍历结构化块,因为那是机器可读的契约。

而 synartesis check 证明工具存在,而不是路径可解析。它无法做到后者:因为尚未进行任何调用,所以没有结果可遍历。在依赖逆操作之前,先运行一次并阅读 synartesis show。

编写清单

清单是整个产品。对于你熟悉的 API,应该只需十五分钟。

version: 1

servers:
  crm:
    command: node
    args: ["./crm-server.js"]

tools:
  - match: "crm.get_customer"
    class: readonly

  # Read the record before overwriting it, then write that record back.
  - match: "crm.update_customer"
    class: reversible
    snapshot:
      tool: "crm.get_customer"
      args:
        id: "$.id"
    inverse:
      tool: "crm.update_customer"
      args:
        id: "$.id"
        name: "$snapshot.name"
        plan: "$snapshot.plan"

  # Nothing to read beforehand; the id only exists once the call returns.
  - match: "crm.create_customer"
    class: compensable
    inverse:
      tool: "crm.delete_customer"
      args:
        id: "$result.id"

  - match: "crm.send_*"
    class: irreversible
    gate: always

值可以引用三种内容:

前缀

引用内容

可用位置

$.

智能体发送的参数

snapshot 和 inverse

$snapshot.

预读捕获的内容

inverse

$result.

正向调用返回的内容

inverse

其他任何内容都是字面量。引用可以单独出现,此时值保持其类型,也可以放在句子中,此时以文本形式替换:

sha: "$result.content.sha"                 # the value itself
message: "Revert agent change to $.path"   # text with the path substituted

写 $$ 表示字面美元符号。没有表达式、条件或函数,将来也不会有:一旦这变成一种语言,它就不再是十五分钟能写出来的东西。

路径可以用 [0] 索引列表,并用 [] 从每个元素读取一个字段:

labels: "$snapshot.labels[].name"   # [{name: "bug"}, ...] becomes ["bug", ...]

这涵盖了常见情况:API 返回的字段比它接受的更丰富,GitHub 的问题标签就是如此。[] 从每个元素读取相同的键,仅此而已:它仍然是路径,不是转换。引用复制值,不能计算值,因此需要真正不同形状的 API 的逆操作应省略该字段,并明确说明。

其他需要了解的事项:

  • match 支持 *,它在一个段内匹配:crm.send_* 匹配 crm.send_email 但不匹配 crm.a.b。无论规则书写的顺序如何,最具体的模式胜出。

  • 补丁的逆操作应恢复每一个字段,而不是重新应用补丁。如果同一条记录在一次运行中被编辑两次,部分逆操作会留下第二次编辑所触及的字段。

  • gate: on_write 是用于诸如原始 SQL 运行器之类工具的一种启发式方法,这类工具的破坏性无法从工具名称中读出。任何它无法确信地读作单条读取语句的内容都会被门控。在确定性重要的地方使用 gate: always。

  • 格式错误的清单会阻止代理启动,并指出需要修复的文件和行。它绝不会在无法理解的策略下运行。

命令

命令

作用

init <server> -- <cmd>

检查服务器并起草清单

list

每次记录的运行

show <runId>

单次运行的时间线,包含每一步的撤销

gates

等待决策的内容

approve <actionId>

允许被挂起的调用

deny <actionId>

拒绝一个

undo <runId>

反转一次运行,最新的操作优先

undo <runId> --replan

相同,但根据当前清单重建每次撤销

check

加载清单并对照其命名的服务器进行验证

--manifest 和 --journal 是自动查找的,而非手动输入。两者都从当前目录向上查找,就像版本控制工具查找其根目录一样,因此在包含 synartesis.yaml 的项目内,每个命令都无需任何标志即可工作。尚不存在的日志会被放置在策略旁边,这样创建它的代理和读取它的 CLI 无需任何指示就能保持一致。

其他标志:undo 上的 --dry-run、--to <seq> 和 --replan,approve 和 deny 上的 --all,list、show 和 gates 上的 --json。

退出码:0 成功,1 中止或拒绝,2 用法或配置错误。

代理接受 --manifest、--journal、--gate-timeout <seconds> 和 --log-level。它将结构化 JSON 记录到 stderr;stdout 保留给协议流量。

它不做什么

  • 它无法撤回已被看到的内容。 已读的电子邮件、已发布的消息、没有备份就被删除的文件。这就是门控存在的原因。

  • 可补偿操作无法检查漂移。 它们不声明预读,因此撤销会补偿它们并在其报告中将其标记为 [unverified]。

  • 撤销在不确定性前停止,并跳过仅属永久性的操作。 漂移、未知结果或失败的逆向调用都会使其停止,因为继续越过这些可能会破坏某些东西。一个根本无法撤销的操作(如已发送的电子邮件)会被报告并保留在原处,同时其他所有内容都被还原:无论停止多少次都无法撤回它,而停止只会让其余部分也出错。无论哪种方式,运行都会被标记为 partial。

  • 在飞行途中被中断的调用被记录为未知,而非失败。撤销拒绝越过它,因为无法确定它是否已生效。

  • 撤销的好坏取决于记录它的策略。 逆操作在调用发生时解析,而不是在撤销时解析,因此清单中的错误会被烙进在其下进行的每一次运行。undo --replan 使用已捕获的状态从修正后的清单重建它们,这就是出路。

观察它的工作

Synartesis 不是守护进程,也不可能是。MCP 客户端自行生成 stdio 服务器并拥有其生命周期,因此没有任何长期运行的东西可以夹在中间看到这些调用。人们从守护进程那里想要的通常是确认它在那里并且正在做某事,而这需要一个可以查看的地方,而不是一个后台进程:

synartesis watch

它会在代理工作时重绘:已调用了什么、每个调用属于什么类别,以及任何等待决策的内容,并附上批准它的命令。Ctrl-C 停止它。如果通过管道而非在终端中运行,它会打印一次状态然后退出。

信任

清单指定命令,Synartesis 运行它们。对于不是你编写的清单,要像对待来自同一来源的 shell 脚本一样:先阅读它。这里没有沙箱,也不打算有。

开发

pnpm test
pnpm typecheck && pnpm lint

每次推送都会在 Linux 和 macOS 上跨 Node 22 和 24 运行这些测试,外加演示和安装程序。

许可证

MIT。参见 LICENSE。

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    A policy-enforcing MCP gateway that intercepts all tool calls to downstream MCP servers, applying allow/deny/ask rules with human approval and audit logging for safe access to dangerous tools.
    5 npm
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    MCP proxy that journals mutating tool calls and enables undo via compensation. It adds checkpoint, list_changes, undo_to, and explain_blast_radius meta-tools while forwarding all original downstream tools unchanged.
    MIT
  • A
    license
    A
    quality
    B
    maintenance
    An MCP proxy that enforces policy on every tool call, blocking or flagging actions before they reach downstream MCP servers.
    1
    47 npm
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Provides AI agents with a transparent proxy and journal for every tool action, including pre-state snapshots and approval gates for risky operations. Enables per-action undo, rewind to a point in time, and a kill switch that agents cannot override.
    6 npm
    Business Source 1.1