← 返回信息流

dev.to #ai短讯

我为 AI 伴侣构建四层审批网关:一个永不拒绝启发式规则的实现细节

dev.to作者:Krish Verma教程AI评分:50/100

作者分享了为桌面端 AI 伴侣设计权限控制系统的经验。原系统存在逻辑漏洞,将标记为只读或来自可信服务(如 Composio)的连接视为完全免审,导致 Gmail 等应用可无提示发送邮件。修复方案是重构审批逻辑,引入更细粒度的四层审批网关,确保即使连接经过验证,其具体操作仍需接受审查,从而消除安全盲区。

当我把 Gmail 连接到我的桌面 AI 伴侣时,我意识到我的审批系统存在漏洞。旧规则是二元的:标记为 readOnlyHint: true 的工具会跳过审批门,其他所有工具都会请求审批——而受信任的服务器则完全跳过审批门。Composio 被视为可信的,这意味着已连接的 Gmail 可以在零提示的情况下发送邮件。“可信”意味着“连接经过了审查”,但审批门将其视为“其操作可能在未经审查的情况下运行”。这就是漏洞所在,修复它意味着从头重新设计审批流程。

Ankita 是我的开源(MIT)、本地优先的桌面伴侣——一个小巧的伴侣,却拥有更多的可能性。它在你的实际机器上运行工具:浏览器、MCP 服务器、Shell 命令。因此,审批门不仅仅是一个锦上添花的功能,它是整个信任模型的核心。以下是我所构建的内容。

四个层级,而非两个

src/integrations/mcp-tiers.mjs 用四个明确的层级取代了二元规则:

  • Tier 0 — 自动允许:只读工具。查看不花费任何代价。
  • Tier 1 — 询问一次:未分类工具的默认设置。
  • Tier 2 — 总是询问:破坏性动词——发送、删除、发布、支付。
  • Tier 3 — 拒绝:只能通过显式黑名单条目访问。

解析顺序严格且可检查:黑名单 → 每工具规则 → 每应用规则 → 应用默认值 → 启发式规则。关键属性是:Tier 3 只能通过黑名单访问。这意味着启发式规则永远不会静默拒绝。它可以升级提示,但绝不会发出拒绝指令。

这是一种故意的不对称。破坏性动词匹配器故意做得很粗糙:

export const DESTRUCTIVE_VERBS = /(send|delete|publish|pay|transfer|remove|destroy|revoke)/i;

子字符串匹配,没有单词边界——因此 payment 和 resend 也会匹配。代码中的注释说得很清楚:误报只会增加一个额外的提示,而询问已经是默认设置,因此没有理由为了使用单词边界而承担漏报的风险。错误提问只是一个小小的烦恼;错误发送的邮件则是不可逆转的。错误预算完全倾向于过度询问。

审批门自我监管

我最自豪的部分是 lowersProtection():

/** 降低保护级别的层级变更本身也值得审批。 */
lowersProtection(serverId, previousTier, nextTier) { ... }

伴侣可以建议层级变更——“Gmail 一直问我,我可以把它设置为自动允许吗?”——但任何降低保护级别的变更本身就是一个需要审批的事件。代理不能悄悄地放松自己的束缚。放松需要你的眼睛;收紧则不需要。

旧代码中还有一个相关的陷阱值得指出:MCP 服务器上的 trusted 标志。mcp-manager.mjs 中的 tierFor() 带有此注释:“trusted 故意不被咨询:它意味着服务器连接已由 Ankita 审查,而不是说其操作可以在未经审查的情况下运行。”对连接的信任并不意味着对其可能采取的每个行动的信任。每次调用仍会解析其自身的层级。

元工具无法隐藏真实操作

Composio 暴露了像 COMPOSIO_MULTI_EXECUTE_TOOL 这样的元工具,其参数命名了真实操作。按工具名称进行分类会将该工具称为“一个工具”,从而错过它包含 Gmail 发送的事实。因此 enclosedActions() 解析参数,将每个 slug 拆分(GMAIL_SEND_EMAIL → { app: "gmail", action: "send email" }),而重要的层级是最糟糕的那个封闭层级。用户看到的永远不是元工具的名称——describeCall() 渲染为 "gmail: send email",因为计划明确指出 COMPOSIO_MULTI_EXECUTE_TOOL 对用户毫无意义,而 "gmail: send email" 则告诉他们他们正在批准什么。

邮件在默认工具集中作为最高后悔的操作获得特殊待遇:新的 Composio 连接开始时,Gmail 被固定在“总是询问”级别,而不是继承通用默认值。读取操作保持无害。

“为什么它发了那封邮件?”这个问题必须能够回答

所有决策都会经过 recordDecision(),该函数会维护每个服务器的允许/请求/拒绝计数器。当出现意外情况时,首要问题是“门控做出了什么决定,以及为什么”——而审计日志提供了答案,而不是耸耸肩的无奈。

在桌面端,待处理的审批保存在 ApprovalRegistry(desktop/electron/approvals.mjs)中,它在每次请求进入时存储请求本身——而不仅仅是发出的事件——因此像伴侣岛这样的迟订阅者可以列出仍待处理的项目并进行渲染。门控和 UI 读取的是同一个事实来源。

所有这些都由测试支撑:层级解析套件全部通过(14/14),覆盖了最封闭层级的规则、黑名单优先于一切的路径,以及只读发现的快捷方式。

我反复回归的设计决策是:我们故意让启发式算法单向运行——它可以升级至提示级别,但从不发出拒绝。可审查性优于覆盖率。拥有拒绝权的启发式算法会悄无声息地拦截更多高风险调用,代价则是无人审查且无法追溯审计的拒绝。

我真的不确定正确的界限在哪里。如果你在为自家代理构建审批门控,你会让启发式算法执行拒绝吗?在信任它之前,你需要满足什么条件?

代码都在仓库里,如果你想深入研究:https://github.com/akyourowngames/A.N.K.I.T.A —— src/integrations/mcp-tiers.mjs 共 286 行,除了一个共享文件辅助工具外无其他依赖。我很想听听你会如何划定不同的界限。

译文已达到本站中文翻译的字数上限,剩余内容请查看原文。

阅读原文