Skip to main content
Glama

AIRLOCK

没有任何东西能够不穿过气闸就进入生产环境。

一个面向不可逆转生产操作的变更控制台。每一次危险的变更——一次 schema 迁移、一次批量数据订正、一次被遗忘权请求、一次退款、一次生产访问授权、四万封邮件——都先用自然语言提出,先对真实系统的影子副本执行,在沙箱中证明,然后才连同附带的证据交给人类审批。

基于 TrueForge 构建,参与 2026 年 8 月 24–30 日的 Agent Harness Hackathon。


思想核心一句话

TrueFoundry 在黑客松页面上的收尾句是 “打造一个你愿意交付 root 权限的 agent。” AIRLOCK 就是对这句话的字面回答:一个表现得仿佛它不具有 root 权限、并且在每次请求之前都先自证其身的 agent。

其他所有审批关卡都是 “agent 说它要去做 X——点同意即可。” 那是在要求人类信任一个计划。而 AIRLOCK 的关卡只有在 agent 产出了一份证书之后才会被提供,证书共有两种:

撤销证书(The Undo Certificate) —— 用于可逆变更。agent 先将变更应用到一个影子分支上,再应用它自身的回滚操作,然后第三次对表计算校验和,证明数据已经回到与此前状态完全相同的状态。它已经执行过了,也已经撤销过了,这里是互相吻合的校验和。现在它才可以发出请求。

范围证书(The Scope Certificate) —— 用于真正不可逆的变更。你无法证明一次删除是可逆的,所以 agent 转为证明相反的东西:在全部系统里,将被精确销毁的范围是什么,并且除此之外没有任何东西会被触及——此外还要附上一份明确的排除清单,说明它刻意不触碰什么及其原因。它无法承诺你可以撤销这件事,它只能承诺它准确知道“这件事”到底指什么。

certificate.status !== "PROVEN"  →  the approval gate is never offered.

不是灰色不可点。不是警告给了你。而是永远不会被渲染。

那条规则是一个类型,不是 if

Approve 控件接受一个 ApprovalGrant。ApprovalGrant 拥有一个只有 openGate() 才能生成的模块私有 symbol,因此开发者无论如何都找不到一个值能为一个未经证明的变更渲染出审批通过——不存在意外,也不存在不修改关卡本身而故意为之的途径。

// packages/contract/src/gate.ts
const GATE_WITNESS: unique symbol = Symbol('airlock.gate.witness');

export interface ApprovalGrant {
  readonly [GATE_WITNESS]: true;   // unforgeable outside this module
  readonly irreversible: boolean;
  readonly seals_required: number;
  readonly final: boolean;
  // …
}

在 gate.typetest.ts 中,六次伪造尝试都被断言为编译错误。如果有人削弱了这个类型,预期的错误就会消失,tsc 会报告一个未使用的 @ts-expect-error,然后构建就失败。

同样展现的规则还在服务端再次运行。完全不经过浏览器的 HTTP API 审批行为,会得到完全相同的拒绝:

$ curl -XPOST localhost:3000/api/dossiers/dos_currency_fix/decision -d '{"decision":"approved"}'
{"error":"CERTIFICATE_FAILED","message":"Verification ran and failed. This change cannot be approved from this dossier."}
403

$ # …and a dossier that lies, claiming match:true with checksums that differ:
{"error":"CHECKSUM_MISMATCH","message":"The data did not return to its starting state after rollback."}
403

AIRLOCK 从不信任验证器自己的 match 标志,它会自己重新计算 pre === post_rollback,因此一个引擎 bug 或一个伪造的内容负载,都无法打开这扇门。

无需安装任何东西直接尝试。 落地页带一个真实可用的关卡:它从一组控件中构建一份真实的 Change Dossier,再传给真正的 openGate()。每次操作组合都是一次真实的判定。去看看你能不能让一扇本不该打开的门打开。


Related MCP server: mcp-nixreview

运行它

git clone https://github.com/Rohit-ATS/Airlock && cd Airlock
npm install
npm run build --workspace @airlock/contract
npm run dev --workspace @airlock/console

路由

这是什么

/

入口页面——核心论点,内含两个实时演示

/console

操作员控制台:DOING / WAITING / DID

/control

控制室:态势、拒绝记录、账本完整性

控制台在首次运行时从 contracts/examples/ 自动初始化,因此你一进来就有一个实时审批队列,包含 11 个真实变更——两个已经可以批准,六个因六种不同原因被密封在程序中,三个已决记录被密封进一条哈希链中——而不需要数据库不用 API key。 等设定。

这 11 个是 控制台夹具 。它们为了初始化证书卡片、队列和策略引擎和账本而存在。它们不能作为任何数据库健康状态的结果,但我在这些用例会会被重新衔接。而且未披露的演示会被重置(re-base)至当前时间。同时100,好防护。 AIRLOCK_NO_SEED=1 可以空状态启动。

如果你想把 agent 跑起来而不是只展示这些 fixtures,那就让它指向一个 TrueForge 服务器:

npx @truefoundry/trueforge@latest              # http://localhost:8790
NEXT_PUBLIC_TRUEFORGE_BASE_URL=http://localhost:8790 npm run dev --workspace @airlock/console

在 Windows 上,务必使用 Docker。 TrueForge 0.1.4 在 Windows 上无法原生启动(Only URLs with a scheme in: file, data, and node are supported… Received protocol 'c:'),并且它的本地 sandbox 回退方案只能用于 macOS/Linux。详见 docs/TRUEFORGE-NOTES.md。


评审者想看什么

agent 只有一个出入口

AIRLOCK 以一个 MCP server 应用的形式发布(packages/mcp)。挂载它,才是最小权限原则从结构上强制共存:

{ "name": "airlock",
  "command": "npx", "args": ["-y", "@airlock/mcp"],
  "enable_tools": ["@all"],
  "require_approval_for_tools": ["airlock_request_approval"] }

agent 可以读取策略、开启一个变更、附加一份证明、并向人类询问。这就是它拥有的全部动词。没有哪个工具可以对生产实施变更,而且那个能推进会进度变更的工具,包含在 harness 手中,直到有人回应。

生产环境连接器以 @read-only 方式挂在它旁边。由于 TrueForge 子 agent 继承根 agent 的 MCP 作用域,这个保障会被自动扩展到每一个子 agent:任何没有人类参与的生产变更都不可能发生。 这由 CI 的 scripts/check-agents.mjs 断言,因此不可能慢慢漂移。

七类变更

准入测试不是“是不是数据库写入”,而是“如果它出问题,你能收回去吗?”发送四万封邮件与删掉一列的不可逆性是一样的,而且显然更难收场。

类别

证书(Certificate)

审批人

上限

Schema 迁移

UNDO

1

—

数据操作

UNDO

1

5,000,000 条记录

删除(Erasure)

SCOPE 证书

那个 2

1,000 人

访 问授权

SCOPE

2

每项授权都必须有过期日

资金操作

SCOPE

2

£25,000

群发(Comms blast)

SCOPE

2

50,ˌ000 人,强制静默时段

基础设施变更

任选其一(either)

2

周五至周一冻结

策略:第二道问题

证书回答的是:“这个变更真如它声称的那样吗?”策略回答的则是另一个问题:“这个变更到底是否被允许,由谁允许,以及是在此刻吗?”一份证明不是 去问题回答,因为它不是变更的属性,而是组织的属性。

两者都由同一个 openGate 进行判定,因此,一个证据充分充分、却也真的不被允许的变更,会被做这件事的理由精确地揭示出来。完整细节见 docs/POLICY.md,该文件由这些策略生成,保证两处不会发生口径不一致。

有四条规则值得一提:

  • 证明是一份过期食品。 当超过燃料窗口,一张证书就描述了一个已不再存在的系统。访问授权是十分钟,迁移是三十分钟。

  • 生产环境漂移。 打开关卡之前,AIRLOCK 会依据下自己作出证明的篡改更改复查校验和。如果其间有其他迁移产生了变更,整个变更会被密封——即使漂移检查器自己报告一切正常也是这样。 “声明危险”的姿态会被公信;而“声明安全”则会被重新计算。

  • 法定人数按人算,不按点击量。 签字按身份存在,因此同一个审批人签字两次也只有一个审批人——另,主动请求这个变更的人,永远不能在这群人之中。

  • 不保留常规生产访问权。 每一项 grant 都必须带 expiry,所以系统的默认状态是:不是你持有的访问钥匙,而是没有人有钥匙。

账本防篡改

一个可以被随意修改的变更控制,审计日志只是一个变更控制表演后方。每一笔已决的变更,都与它前一条的哈希一起密封在一起,所以修改任何历史记录都会破坏它后面的所有链接:

$ npm run verify:ledger
  ok  #000  dos_orders_index         a41f9c02be7d8e5f31c4…
  FAIL #001  dos_gdpr_batch          9e02cc71a4bb0d3f2871…
         fault      : content-modified
FAIL — the chain breaks at record 1. Every record after that point is no longer trustworthy.

这并不能让账本在所有概率上都 不可防——任何能够重写此文件的人依然可以重新计算结果并攻击整条链。它真正带来的是,篡改会被任何人直观看到,只要他们持有旧版本的某一个哈希,这个才是真正重要的性质,因为审查你的人不是篡改它的人。

单个 receipts 可以独立取出来并独立验证,不需要控制台接入: GET /api/dossiers/{id}/receipt → node scripts/verify-ledger.mjs receipt.json.

落地页在自己的浏览器中运行这套验证:改写一条记录,然后观察整条链断裂。

三区控制台

Savile Row 的评审标准要求一个界面能展示 agent 正在做什么,它在等待什么,以及它已经做了什么——而且要在不可逆步骤之前询问。于是就有了那三个区域,名字就这样起。

  • DOING —— 当前正在执行的实时状态:子 agent 泳道并行运行,每条带有它自己所属的模型和当前 运行费用,沙箱日志在下面不断往里打印,工具调用实时解析。

  • WAITING —— 等待队列:每一个等待人类回应的变更,它此刻被什么阻塞、还需要多少签名、已经等了多久。

  • DID —— 不可变变更账本:什么让谁请求的,谁批准的,哪份证书,哪些校验和,以及最终封住的 receipt。

控制室

/control 面向另一类观众。不只是“我应该批准这个吗”,而是“这个系统在掌握着什么,它拒绝过什么,关联他告诉我它做过什么?”

头条数字是关卡拒绝过多少次,而不是批准过多少次——排空队列并不是安全的,能及变更从未能否干净,以及拒绝的原因。它还在浏览器内再验证账本是否连贯,而此时没有听一个自说自话的服务器说“一切正常”。

Harness 面板

一条常驻的侧边栏,展示 TrueForge 全部 22 项能力。每一项都会保持暗色,直到一个真实的 harness 事件证明,然后点亮,并附带时间日戳和指向证明步骤的链接。

应用代码无法点亮一 盏灯。 唯一的写入者是 detectors.ts,它由一个包裹在 observedServer.ts 项目中真实 TrueForge 事件流之侧的 pass- 观察器提供。这些能力都来源于观察事件,再原样转交出去——绝不自行合成、不当增改顺序或丢弃。一个没有触发某项能力的运行会在低于 22 的位置结束,这才是这份结果。

暗状态的行会刻意保持可见。隐藏“没有真正前往”的路径,会让计数值失去意义;把未点亮的行为摆放出来,才能让已点亮的东西可信。落地页发出每盏灯都是暗的,因为在落地页上,还没有发生过任何运行。

见 docs/CAPABILITIES.md——文档来自 registry 生成,所以我在文档里宣称的和面板上能够证明的,不会被信息。

一条证书卡片

一个判定横幅:规模大小、生效中的策略及其不同意的理由; 签名; 前进和回滚操作并置其中;受影响的表及其真实行数;锁 profile 以及表重写警告;校验三元组;漂移确认;整个代码库的爆炸半径;排除清单;按不同模型统计的运行费用;以及最终凭据与综合比对纪录。

这个校验三元组就是整个光门可见的核心证明:第 1 行和第 3 行被合并在一起,第 2 行刻意弱化显示,因为它本身被预期与它们不同;而当哈希不一致导致匹配失败时,不是出现一个红色的大叉,而是标出那个哈希真正不同的那一个字符。

紧急权限许可

紧急通道(Break-Glass)

受策略门控、默认关闭,而且它不会打开闸门——BreakGlassOverride 所携带的私有符号与 ApprovalGrant 不同,也没有任何函数能同时接受两者。它真正做的是:记录一位具名绕过了一道封死的门,并附上一份至少 40 个字符的书面理由,永久性地写入与其余一切内容相同的哈希链中。

保留它的理由:人们终究会这么做。在每一个组织里,都会有这么一刻——安全路径不可用,于是有人直接打开一个 psql 会话。一个装自己对那种情形视而不见的控制平面,并不能阻止这种绕过行为,它只是保证这类行为不会有任何记录。要启用它需要同时翻开两个开关,而 带MONEY_MOVEMENT 的 ERASURE 与 COMMS_BLAST 都是直接禁止它的。


架构

contracts/dossier.schema.json     the Change Dossier — the one contract everything shares
packages/contract/                types, the gate, policy, receipts, capabilities, detectors
  src/gate.ts                     the invariant, as an unforgeable type
  src/policy.ts                   quorum, ceilings, freshness, freezes, no standing access
  src/receipt.ts                  the tamper-evident hash chain, isomorphic
  src/detectors.ts                the ONLY thing that can light a lamp
  src/capabilities.ts             the 22, each with its load-bearing use and its evidence
packages/mcp/                     AIRLOCK as an MCP server — the agent's one doorway
apps/console/                     Next.js 15, React 19, Tailwind v4
  app/page.tsx                    the landing page
  app/console/                    the three-zone operator console
  app/control/                    the control room
  src/server/observedServer.ts    the passthrough tap on the real TrueForge stream
agents/                           four agent specs: least privilege, model routing
skills/                           seven skill packs, one per domain the agent must not improvise

控制台本身就是 SDK。 TrueForgeUI 接受一个自定义布局组件,并渲染在它自己的 provider 栈里;因此 AIRLOCK 是以 layout={AirlockConsole} 这一形式被传入。该进程里的会话记录、消息编辑器、线程列表、工具审批卡片、ask-user 卡片和 MCP OAuth 屏幕,全部是 @truefoundry/trueforge-ui 自己的组件,只是被重新定义了主题。它不是在旁边另砌出来的一套仿制品,而是真真正建于其中。


郑重说明

原计划中有三件事,最后全部被证实是建立在并不存在的 API 之上的;我们没有冒充,而是把它们的实现改成了另一种写法。完整细节见 docs/TRUEFORGE-NOTES.md §4。

  1. 子代理是动态的,不是声明式的。 TrueForge 在运行时通过 create_sub_agent 生成它们;规范里根本没有“逐子代理声明块”这种东西。因此,原计划里“四个具名子代理,各自拥有自己的工具作用域”这种描述不可能实现。

  2. 按子代理划分的工具作用域是不存在的。 文档已经写得很明白:“子代理与 root agent 能够访问的是同一套 MCP 工具,以及同一个沙箱环境。” 所以 AIRLOCK 转而在 agent 的边界上应用最小特权原则——全部生产连接器以 @read-only 的方式挂载,唯一的正向路线,是放在我们自己的 MCP server 上、而且由 harness 持有的那一个 tool。因为子代理继承的是同一套 scope,所以 整场运行过程中,没有任何一个主体可以在没有一个人在场的条件下触碰生产环境。 这种说法,远比“工具箱更小”要来得强得多,并且这是属实真的。

  3. 按子代理分配模型路由同样不存在。 路由在 agent 边界上是真存在的——参见 airlock-scout、airlock-privacy 与 airlock-treasury ——而每条通道上展示出的模型和成本,都是真正读取 thread.created.agentInfo.model 与 turn.done.state.metrics.total_cost_in_usd 得到的。

有三个对于用户的判断,所依据的信号我们并没能从文档中确认——也就是 Code Mode 工具名、大结果卸载时是否产生标记、以及一个“压缩事件”是不是真的会发出。这三项都被标为“未证实”。如果某一次实际运行确认不了它们,对应的灯就一直熄灭,这从而有效分之下的分母随之下降,分母被迫得一个。诚实的 19/19,好过一个被点派生灯立刻能被拆穿的、注水的 22/22。

发现的两个上游 bug

  • @truefoundry/trueforge-ui@0.2.4 有一个依赖冲突:@assistant-ui/core 声明 peer 依赖为 zustand@^5,而 OpenUI 渲染器则把 zustand@^4 拿进来,被 npm 提升到了更顶层的位置,于是构建会因为 useShallow is not exported from 'zustand/shallow'而失败。我们用根域package.json里的overrides` 块把它绕开了。

  • 它的 styles.css 在 @layer tfy-agent-ui-utilities 中自带了一整套 Tailwind 工具类进口;当它出现在 tailwindcss 之后被引入时,这个 @layer 注册顺序会更晚,结果 SDK 内置的 .hidden 压过了你定义的 .xl\:flex,不管 media query 怎么写——因此让宿主应用里的每一套响应式变体被静默全部打破。好在这个已是明 gitlint 验证于用 globals.css 里明确给出了一个 @layer 顺序声明来修正。


测试

npm test        # 92 tests, 11 fixtures, 4 agent specs

四个测试套件,每个都在锁住一层属性,而不是锁住实现:

套件

它锁住的是什么

gate.test.mjs

在任意 class、状态、查看者的组合下,任何非 PROVEN 证书都无法打开闸门

policy.test.mjs

法定人数以人为准计算;冻结时间按伦敦墙钟时间判;对安全的声明会被重新计算;break-glass 永远变不成一次 approval

receipt.test.mjs

对一条已安全封存的记录进行编辑、重排或删除,会是在那条发生震惊的渠道上被检测出来

harness.test.mjs

除了“真实出现的 harness 事件”之外,别的任何东西都不能点亮仪表灯——噪声、重复的连接器、只字提及某张图的文字,一个都点不亮

mcp/server.test.mjs

只有破坏性的工具一个是,而它正好被攥在手里等待审批;根本不存在能应用变更的工具

另外还有两条在 CI 里触及的・结构性检查:

  • check-fixtures.mjs — 每一个 fixture 都必须对着契约完成解析,还必须得出与它的文件名一拍即合闸门结论;这样如果一个叫 .standing.json 的 fixture 它真是由于“没有 expired”而被拒,而不是由于某个谁也没注意的旁因。

  • check-agents.mjs — 任何一个生产用连接器都不可写;而一个能在某个位置写东西的 agent,凡是能写,就必须装载 AIRLOCK,并且只能有一个 tool“钉牢置留审批之中”。

各个生成物(contracts/dossier.schema.json、docs/CAPABILITIES.md、docs/POLICY.md 以及那组 fixtures)全部由 npm run gen 产出,而且是幂等的,所以“文档说它、实现做它”这两者永远不可能漂移走。


团队

Rohit Maruri —— 控制台、落地页、控制室、Harness 面板、证书卡片、闸门、策略引擎、防篡改账本、MCP server、agent 定义与技能、契约、webhook 与角色。

Damir —— 验证引擎、shadow branch 生命周期、scope calculation、种子数据。

MIT 许可。

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Pauses AI agent execution and routes approval requests to humans via Slack or email, with cryptographically signed proof of the human's decision.
    44 npm
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    A safety gate for agent-proposed NixOS configuration changes, grading security-relevant option deltas, attesting closures for vulnerabilities, and requiring human approval with a tamper-evident audit ledger.
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    A human-in-the-loop governance interlock for AI agents. Agents propose changes, a human countersigns the exact plan, and then it executes stage by stage with precondition checks, verification, and auditing.
    Apache 2.0
  • A
    license
    Not graded
    quality
    D
    maintenance
    An MCP approval gate that ensures agents execute only human-approved, hash-verified actions, providing fail-closed consent for consequential operations.
    MIT