mcpField
C
CTRLRun Operator

CTRLRun Operator

本地服务
docs.ctrlrun.dev安全273 周前更新

关于 CTRLRun Operator

CTRLRun Operator 是通过 MCP 客户端处理 CTRLRun 待审批操作的服务,供需要人工审查 Agent 操作的团队使用。它可列出待处理项、查看详情并批准或拒绝。

主要功能

  1. 01列出待审批操作
  2. 02查看操作详情
  3. 03批准或拒绝请求

适用场景

  1. 01查看 Agent 暂停等待的操作
  2. 02审查操作内容后授予批准
  3. 03拒绝不允许执行的请求

MCP 相关

通过 MCP 客户端提供 CTRLRun 审批操作,包括列出待处理项、检查请求以及批准或拒绝。

配置

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

mcp.json · 通用配置
{
"mcpServers": {
"ctrlrun-ctrlrun-mcp-operator": {
"command": "uvx",
"args": [
"ctrlrun"
]
}
}
}
启动命令uvx ctrlrun

本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。

安装包
pypictrlrun0.12.2

工具

还没有获取到工具列表

本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。

详细介绍

ctrlrun

ctrlrun 阻止 AI Agent 在你的工作流中采取错误、受限或恶意的操作。
每个操作在执行前都会根据你的规则进行检查。允许的操作直接通过。
敏感操作等待人工确认。禁止的操作会被拦截。

为 AI Agent 提供执行安全保障。一个 Python 库,位于做出行动决策与执行调用之间。
一个重大操作最多执行一次,完全按照批准的方式进行,并留下回执。
当结果未知时,ctrlrun 会明确说明,而不是猜测。

可在生产环境中运行于单个文件,或跨主机运行于 Postgres。Apache-2.0。

Clones PyPI Downloads Docs CI CodeQL Fuzz Tests ctrlrun verified OpenSSF Scorecard OpenSSF Best Practices License

以下三个步骤的终端录屏。策略文件允许 Agent 进行最高 €500 的退款,将最高 €10,000 的退款转交人工处理,并拒绝其余请求。Agent 自行完成了 €500 的退款;一笔 €5,000 的退款暂停,并指明需要人工应答的请求。ctrlrun approve 针对该确切操作的哈希予以批准。Agent 执行了 €5,000 的退款,随后试图凭同一批准执行 €9,000,这一请求被拒绝:到达服务商的调用数为 1,€9,000 从未发出。

pip install ctrlrun && ctrlrun demo

它做什么

模型靠猜。ctrlrun 不猜。 工单上写的是退款 €500。Agent 却申请 €5,000,多了一个零。工具就在它的清单里,参数格式也无懈可击,而模型对此完全自信。调用之上的任何环节都不会提出异议,因为调用之上的任何环节都不是检查:一个工具可被调用,并不等于允许用这些参数调用它。

没有 ctrlrun有 ctrlrun
没有任何环节检查金额。调用照常执行。多付了 €4,500。你的规则检查金额。调用根本不会发出。错误支付 €0。

ctrlrun 就是那道检查。它读取即将离开你进程的参数,并回答这些参数可能带来什么后果:放行、交给人来确认,还是直接拦下。四条规则承担了这项工作,而每一条都先是本仓库中的一个测试,然后才是这里的一句话。

精确就是精确参数一旦改变,就需要新的审批。
一次就只算一次相同的效果键,共享存储,绝不重复。
未知就是等待在重试之前先确认结果。
每个答案都被保留请求、决策和结果,包括拒绝。

第三条是人们最容易忘记的那一半。一笔正确的 €500 退款在服务商处已提交成功,但返回的应答丢失了,于是 Agent 重试。重试库、Agent 框架和工具循环会把这次失败了和我不知道发生了什么混为一谈。ctrlrun 将两者区分开:丢失的应答是 AMBIGUOUS,绝不是 FAILED,而对一个 AMBIGUOUS 效果的重试会被拒绝,直到有人类或 reconcile 钩子说明发生了什么。

ctrlrun demo 展示的内容:五次失败和五次拒绝,逐字节呈现
$ ctrlrun demo
ctrlrun demo — five ways an agent action goes wrong, and what stops it.
Policy: refunds up to €1,000 are autonomous, up to €10,000 need a human, above that are denied.

1. Duplicate effect after a lost response

   refund €500  →  remote commits  →  response lost  →  effect: AMBIGUOUS
   agent retries the same refund
   ✗ BLOCKED — effect may already have committed; blind retry refused
   remote refund calls: 1
   only a human moves it on:  ctrlrun resolve refund:txn_1 --committed|--failed

2. Approval mutation

   agent proposes refund €2,000  →  human approves apr_0aa78e0380ba55d77a601dc782f57095 (bound to the action hash)
   agent executes refund €5,000  →
   ✗ BLOCKED — approved action ≠ requested action (mismatch)

3. Concurrent agents, same effect

   Agent A  reserve refund:txn_123  →  ACQUIRED  →  executes
   Agent B  reserve refund:txn_123  →
   ✗ BLOCKED — already reserved (in_progress)

4. Approval replay

   approval apr_dbc8bc6f06690cdf2e2c55a4e591ef3b used once  →  consumed
   same approval presented again                            →
   ✗ BLOCKED — single-use approval already consumed

5. Authority escalation

   human €100,000 delegable  →  finance agent €25,000  →  support agent €2,000
   support agent's grant: dlg_5f8d41938a3f29972d5489d676cd9edb
   support agent requests €50,000  →
   ✗ BLOCKED — outside the delegated grant (authority_constraint)
   remote refund calls: 0
   finance agent tries to delegate €50,000 under its own €25,000  →  refused (containment: constraints)
   support agent requests €1,500  →  authority permits it, and the policy asks a human (apr_f86eca24dd80206ab5189ccb1b62aa55)
   two axes, and an action needs both: the stricter of the pair wins

Receipts (8): .ctrlrun/demo/receipts.jsonl
Events:       .ctrlrun/demo/events.jsonl

Read them:    CTRLRUN_STATE=.ctrlrun/demo/state.db ctrlrun receipts

审批和委托 id 每次运行都会生成;除此之外的一切都与 demo 打印的完全一致,如果两者出现偏差,测试就会失败。无网络,无外部服务,一秒以内。pip install ctrlrun && ctrlrun demo 在本地运行它,耗时大致相同。

它止步于何处。 它不检测提示词注入:它约束的是后果,而不是去读取原因。对于它无法控制的远程服务,它无法承诺精确一次,它拒绝在知情的情况下执行两次,而且它不会回滚任何东西。回执是链式连接的,因此篡改会被检测到;但末尾的截断和伪造的追加则不会,因为能捕捉它们的链头本身就是同一数据库中的一行,而解决这个问题正是 ctrlrun anchor 的用途。回执没有签名:篡改不等于署名。上方的徽章意味着在它所运行的配置中,声明的保证均通过,并不意味着安全、可靠、合规、已认证或已审计: 徽章的含义 · OWASP-AGENTIC-TOP10.md 列出了它未涉及的四个条目。

如果 Agent 只是读取并回答,你不需要 ctrlrun。一旦它能发送、支付、退款、删除、部署、授权、撤销、批准、提交、购买或取消,你就需要它。

三步即可使用

上方的动画就是本节内容,录制自真实的库:一个策略文件,两个 短程序,四条命令,没有任何预先安排。

1. 安装它。

pip install ctrlrun

2. 写下 Agent 可以做什么。 一个文件,ctrlrun.yaml。金额为整数最小 单位,因此 50000 就是 €500。每个区间的两端都必填,因为仅有上限会让 负数金额通过,而对负数金额的退款就是一筆扣款。任何未列出的内容都 会被拒绝;不存在默认允许。

schema: ctrlrun.policy/v2

actions:
  stripe.refund:
    effect: "refund:{payment_id}"
    rules:
      - when: { amount_gte: 0, amount_lte: 50000 }
        decision: allow      # up to €500: the agent acts alone
      - when: { amount_gte: 0, amount_lte: 1000000 }
        decision: approve    # up to €10,000: a human decides
      - decision: deny       # above that: never

3. 包装那个会产生后果的调用。 装饰器指明动作,effect key 指明它在现实世界中产生的后果,context 指明是谁在行动。这里的 stripe 是一个替身,它记录调用而不实际发起调用。

import sys

import ctrlrun


class FakeStripe:
    """Stands in for the provider: it records calls instead of making them."""

    def __init__(self) -> None:
        self.calls: list[tuple[str, int]] = []

    def refund(self, payment_id: str, amount: int) -> dict:
        self.calls.append((payment_id, amount))
        return {"id": f"re_{payment_id}", "amount": amount, "status": "succeeded"}


stripe = FakeStripe()


@ctrlrun.protect("stripe.refund", effect="refund:{payment_id}")
def refund(payment_id: str, amount: int) -> dict:
    return stripe.refund(payment_id, amount)


if __name__ == "__main__":
    with ctrlrun.context(agent="support-agent"):
        print("€500   ->", refund(payment_id="txn_1", amount=50_000)["status"])
        try:
            refund(payment_id="txn_2", amount=500_000)
        except ctrlrun.ApprovalRequired as pending:
            print("€5,000 -> a human decides:", pending.request_id)
            with open("request_id.txt", "w") as handle:
                handle.write(pending.request_id)
        else:
            sys.exit("the €5,000 refund ran without a human; the policy is not in force")
    print("calls that reached the provider:", len(stripe.calls))

€500 的退款自行运行。€5,000 的那笔会停下,并指明需要人类答复的请求:

€500   -> succeeded
€5,000 -> a human decides: apr_63e80076f2cccfee52b17491a4b2e125
calls that reached the provider: 1

人类从 shell 中答复。 授权指明人类所阅读的确切动作的哈希,以及 它何时失效。Id、哈希和日期每次运行都会生成;你得到的会有所不同。

ctrlrun approve "$(cat request_id.txt)"
granted apr_63e80076f2cccfee52b17491a4b2e125 for sha256:22ec1c398e4b93d080b6cba61e5e11b0e21879552ac5dbf63c192d2b2e6af752
expires 2026-09-13T20:10:11.367Z

Agent 出示它,然后试图把它花在别的东西上。 第一次调用正是 人类批准的内容,它会运行。第二次是同一个授权,只改动了一位数字, 它不匹配任何东西:

import sys

import ctrlrun

from agent import refund, stripe

with open("request_id.txt") as handle:
    request_id = handle.read().strip()

with ctrlrun.context(agent="support-agent"), ctrlrun.with_approval(request_id):
    # Exactly what the human read: €5,000 on txn_2.
    print("€5,000 with the approval ->", refund(payment_id="txn_2", amount=500_000)["status"])

    # The same approval, one digit changed.
    try:
        refund(payment_id="txn_2", amount=900_000)
    except ctrlrun.ApprovalMismatch:
        print("€9,000 on that same approval -> refused")
    else:
        sys.exit("a mutated action ran on a human's approval; that is the bug this exists to stop")

print("calls that reached the provider:", len(stripe.calls), "(the €9,000 never left)")
€5,000 with the approval -> succeeded
€9,000 on that same approval -> refused
calls that reached the provider: 1 (the €9,000 never left)

每一次尝试,包括被拒绝的,都留下了回执,ctrlrun receipts 会列出它们。这就是 整个集成:一个策略文件、一个装饰器、一个 context,以及用 with_approval 出示授权。 金额只是示例,并非范围。条件的形式是 <argument>_<op>,因此同一套策略 语言可以像读取 amount_lte 一样轻松地读取 role_in: [reader, viewer] 或 replicas_lte: 10, 下面九个领域各有一个策略。 保护你的第一个动作走的是同一条路径, 并对每个输出都作了说明 · 策略 YAML 参考 · Cookbook:退款、部署、IAM、删除、电子邮件、MCP。

三种使用方式

你大概率不需要适配器。 @protect 覆盖在此进程中运行的任何内容:原始模型调用、LangChain 工具、手写循环、定时任务。网关覆盖以任何语言通过 MCP 访问其工具的任何内容。

你的情况使用方式需要
此进程中的 Python上文展示的 @protect 装饰器除 pip install ctrlrun 外无需其他
任意语言中位于 MCP 服务背后的工具网关:pip install "ctrlrun[gateway]"一条命令,无需更改 Agent 或服务代码
自带审批中断的框架适配器框架需具备人在回路原语

它适用于你能修改和不能修改的 Agent。 WhatsApp、Slack 和 Teams 机器人、ChatGPT、Cursor、Codex、OpenAI Agents:任何你拥有的 AI Agent。ctrlrun 检查的是动作,而不是 Agent,因此如果 Agent 通过你运行的工具服务或 API 执行动作,该动作就会被检查,而 Agent 无需重建、重新部署或被通知。你无法修改的 Agent 说明了每种类型的边界所在。

适配器存在的唯一原因:将 approve 决策路由到框架自己的中断中,让人在他们已经应答的地方应答。绝不会有第二个表示同意的地方。ctrlrun-langgraph 提供预防,因为恢复过程会携带参数,核心会对照哈希重新检查这些参数。ctrlrun-openai-agents 提供归因,因为该 SDK 记录的是某次调用被批准,而不是其参数是什么。这三者都不只适用于 Agent:工作进程、Webhook 处理器和定时任务同样无法区分首次尝试和重试。

它是官方 LangChain 中间件集成。 ctrlrun-langchain 已列在 LangChain 的中间件集成中。它的 CTRLRunMiddleware 放入 create_agent(middleware=[...]),并通过 LangChain 自己的 wrap_tool_call 对每次工具调用进行管控,包括不是你编写的工具:被拒绝的调用永远不会到达工具,而模型会被告知是哪条规则拒绝了它。使用 LangChain 中间件提供了相关设置。

工作原理

每次受保护的调用,无论以何种方式到达,都会经过同样的七个步骤。只有之后,它才会到达你的系统。

  normalize  →  decide  →  approve  →  reserve  →  execute  →  resolve  →  record
  1. 规范化:一个动作,一个 id。 该调用变成一个 Action:一个名称、规范化参数(键已排序,无浮点数)、一个资源、主体。其 SHA-256 就是动作哈希。
  2. 决策:允许、询问或阻止。 先看权限(这个主体究竟能否提出该动作,以及在什么范围内?),再看策略(这个动作获得多少自主权?)。未知动作、缺失策略或缺失主体都是 deny。沉默绝不是许可。
  3. 批准:绑定到该动作。 由人针对动作哈希作出答复。批准是一次性的,会过期,并且除了那个确切的动作之外不匹配任何东西,因此答复之后被改动的参数会使它失效,需要由人重新答复。指定一个 preconditions= 提供方,批准还会绑定到它被授予时所依据的资源状态,并在预留之前严格重新检查:这收窄了答复与执行之间的窗口,从数分钟的斟酌缩短到数毫秒。它并未关闭这个窗口,因为重新检查是一次网络调用,无法在原子写入内部运行。
  4. 预留:只被认领一次。 效果键,refund:txn_1 或 namespace:prod-eu:checkout,在一次原子写入中被占用。第二个调用者,无论在另一个进程中还是在另一台主机上,都会被拒绝。
  5. 执行:你的代码运行。 只有由你抛出的 NotExecuted 才意味着 FAILED;其他所有异常和所有超时都意味着 AMBIGUOUS。判断你正面对的是哪一种才是困难的部分,所以 ctrlrun.transport 替你完成:来自标准库 urllib 和 http.client 的 urlopen、HTTPConnection 和 HTTPSConnection,仅当它们打开的连接未被交付任何请求字节时才抛出 NotExecuted。在一个字节之后,每种失败都保持它原本的异常,结果为 AMBIGUOUS。没有任何设置能放宽这一点。
  6. 解决:未知不等于失败。 一个 AMBIGUOUS 效果会保留其键并拒绝重试,直到 ctrlrun resolve,或一个询问了远端的 reconcile 钩子,说明发生了什么。任何东西都不会凭猜测运行两次。
  7. 记录:无论哪种结果都留一张凭据。 一份可移植的 JSON 凭据:谁、什么、决策、批准、效果键、结果,以及作出决策的策略的哈希,链接到它之前的那张凭据。拒绝也会得到一张。

谁可以提出请求,以及可以提出多少。 策略决定动作,且看不到是谁在提出请求。谁究竟可以提出请求是第二个维度:权限。每个主体都需要一项授权,委托不能扩大授权,而一个动作需要两个维度同时满足,取两者中更严格的那个。自 0.9 起,授权还可以携带一个预算,一个在滚动窗口内设有上限的指标,在同一写入内部于预留时消耗,因此一千笔各自通过 amount_lte 的退款,其总额不能超过授权所允许的。一个 AMBIGUOUS 效果会占用其预算直到它被解决,否则一个能够制造歧义的 Agent 就能制造权限。预算限定下一次预留可以做什么;它无法召回一个已经在执行中的动作。权限包含完整的模型。

状态默认存放在 SQLite 中,一个无需服务器、无需运维的文件,并且预留会跨进程保持,而不仅仅是跨线程。当有不止一台主机写入时,将它指向 Postgres:pip install "ctrlrun[postgres]",一个 URL,同样的保证由同一套测试套件评级。用 ctrlrun verify 在你自己的环境中证明它,它会针对你的策略在一个暂存存储中运行内核自身的失败场景。它不访问网络:它打开的唯一套接字是连向你指定的存储,以及它自己绑定的回环监听器,它正是以此对传输分类器进行评级。

保证@protectGatewayAdapter
审批绑定 —— 审批与确切的动作绑定;被篡改或重放的审批会被拒绝。是是依 adapter 而定,为预防或归因
一次效果,仅一次 —— 一个逻辑效果在跨线程、跨进程和跨主机的情况下至多发生一次。是是是
未知不等于失败 —— 未知的结果是 AMBIGUOUS,而非 FAILED,并会阻止盲目重试。是是是
失败即拒绝 —— 未知的动作、缺失的策略或缺失的主体都会被拒绝。是是是
权限与委派 —— 每个主体都需要授权,委派不能扩大授权,且授权限定总量。是是是
回执 —— 每个已执行的动作都会留下一个可移植的 JSON 回执,记录由谁、做了什么以及结果。是是是

九个领域中的同一形态

ctrlrun 中没有任何东西知道什么是退款。一个动作是一个 name、canonical arguments、一个 effect key 和一个 resource,而无论它来自哪个领域,对它提出的三个问题都相同:这个动作 获得多少自主权、是否有一个人批准了这个确切的动作,以及这个效果是否已经发生过。承载你领域信息 的有两样东西,而这两样都由你来编写。

  • effect key 是系统中唯一的领域知识。 它是这样一个字符串,表明两次调用是同一个现实世界后 果:refund:{payment_id}、namespace:{cluster}:{name}、grant:{user_id}:{role}、 prescription:{patient_id}:{drug}。把它命名好,重试就无法执行两次;省略它,至多一次就没 有任何可依托的对象。
  • 条件是参数,而非金额。 该语言是 <argument>_<op>,因此同样的运算符可以像读取 amount_lte 一样轻松地读取 replicas_lte: 10、role_in: [reader, viewer] 和 to_domain_eq: acme.com。一个从未开具过发票的领域也可以使用区间。
领域自主执行由人决定绝不
DevOpsk8s.scale_deployment 至 10 个副本terraform.applyk8s.delete_namespace
安全运营firewall.add_deny_rulefirewall.add_allow_ruleedr.disable_protection
医疗appointment.reschedulepatient.export_recordprescription.change_dose
法律document.draft_internaldocument.file_with_courtcontract.execute
人力资源在区间内 pto.approvepayroll.runemployee.delete_record
保险claim.request_documents超出区间时 claim.approve_payoutpolicyholder.delete
电子商务在区间内 inventory.adjustprice.updatecustomer.delete
公共服务eligibility.precheckbenefit.terminaterecord.delete
支付€500 以下的 stripe.refund超过该金额的 stripe.refundstripe.delete_customer

从左到右读取任何一行,它就是同一条规则换上了不同的名词。安全这一行值得读两遍:向防火墙添加 一条 deny 规则是自主执行的,而添加一条 allow 规则则不是,这是任何金额阈值都无法告诉你 的。策略是你写下关于自己领域判断的地方;ctrlrun 则是让它得以成立的东西。

文档

docs.ctrlrun.dev 是文档站点:包含概念、指南、实用手册以及 完整参考。

从这里开始为什么 · 保护你的第一个操作
核心理念,以及如何运用它们概念 · 指南 · 实用手册
Agent 与 MCP你无法修改的 Agent · MCP 概览 · 五分钟上手网关 · 从你的助手中审批
真正投入运行生产环境 · Postgres · 恢复 · 运维
每一个键、标志和错误参考 · 常见问题
对比幂等键 · 框架的 human-in-the-loop · 护栏库 · 持久化工作流 · 治理工具包
什么成立,什么不成立威胁模型 · verify 证明了什么 · CLAIMS.md,每一句话都对应到它的测试 · 这是如何构建的

贡献

欢迎提交 issue 和 pull request: CONTRIBUTING.md 和 CODE_OF_CONDUCT.md 包含 协作约定, SECURITY.md 说明了如何报告 漏洞。本文件中的每一项声明背后都有测试支撑,因此对文字的修改通常 意味着对测试套件的修改。 CHANGELOG.md 和 https://docs.ctrlrun.dev/ROADMAP 说明了 它的发展方向。每个版本都带有来自 GitHub Actions 的 PyPI 来源证明。

许可证

Apache-2.0。执行内核现在是并且将始终保持完全开源。

评论

支持 Markdown

第一次接触 MCP?

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

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