ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Cherry Studio 工具审批门禁绕过修复设计:defer 展平与注册表执行边界双重防护

Cherry Studio 工具审批门禁绕过修复设计:defer 展平与注册表执行边界双重防护 Cherry Studio 工具审批门禁绕过修复设计defer 展平与注册表执行边界双重防护【免费下载链接】cherry-studio Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端项目地址: https://gitcode.com/CherryHQ/cherry-studio本文围绕 CherryHQ/cherry-studio v2 重构中 Tool Registry 的审批门禁修复方案展开讲解当 MCP 工具被 defer 展平机制隐藏到tool_invoke元工具之后时needsApproval审批卡片为何失效以及审批门禁工具永不 defer 注册表执行边界守卫双重修复如何封堵该旁路。读者读完可掌握 defer 展平与工具审批两套机制的交互原理、isApprovalGated共享判定助手的语义设计以及修复对应的源码位置与测试/验证方法。背景工具审批与 defer 展平的两套机制Cherry Studio 的 v2 重构将内置工具、MCP 工具与元工具统一收敛到单一ToolRegistry工具注册表每类工具以同一个ToolEntry模型注册。其中两个子系统各自承担一项职责而本次修复正是两者交互时产生的漏洞工具审批Tool Approval主进程Main是审批状态的唯一写入方。执行期包装器检查tool.needsApproval与助手的自动放行策略若需要审批则写入approval-requested部件并暂停流渲染层弹出审批卡片用户决策通过ai.tool.respond_approval回传主进程后恢复流转。完整的端到端流程可参考 docs/references/ai/tool-approval.md。defer 展平Defer Exposition当工具数量多、上下文窗口压力大时为避免把所有工具描述和 schema 全部塞进提示词系统将部分工具延迟defer到tool_search/tool_inspect/tool_invoke三个元工具之后。模型先搜索、再查看签名、最后按名调用。这一设计在 v2-refactor-temp/docs/ai/tool-cluster.md 中有完整说明。ToolEntry中有一个关键字段defer: never | always | auto它决定了条目是否参与延迟展平。defer 判定由 shouldDefer.ts 负责核心门槛包括自动池数量必须 ≥MIN_AUTO_DEFER_COUNT5 个、估算 token 成本必须超过上下文窗口的DEFER_THRESHOLD_PCT10%且大于META_TOOLS_OVERHEAD_TOKENS500 token的元工具静态开销。而 applyDeferExposition.ts 负责执行展平把被 defer 的工具名从ToolSet中剥离并注入三个元工具返回deferredEntries供DEFERRED_TOOLS系统提示词段使用。漏洞分析defer 如何绕过 needsApprovalAI SDK 原生门禁只在 ToolSet 成员上生效AI SDK 在调度工具时只对自己能看到的一等工具first-class tool即存在于ToolSet中的工具执行needsApproval检查。在 SDK 的run-tools-transformation.ts约 292-324 行中调度逻辑通过tools[toolName]查找工具——一个被 defer 的工具根本不在ToolSet中因此 SDK 的原生门禁对它永远不会触发。tool_invoke 直接委托内层 execute被 defer 的工具虽然从ToolSet消失但仍可通过tool_invoke元工具按名调用tool_invoke.execute通过注册表registry.getByName(name)拿到条目后直接调用entry.tool.execute(...)见 toolInvoke.ts。这意味着内层工具的needsApproval从头到尾没有被任何人咨询——审批卡片不会出现。后果恰好命中最需要保护的场景漏洞的杀伤力在于它精准命中了 defer 机制的目标场景——工具数量多、上下文压力大。在这种场景下用户标记为总是询问always ask的工具MCP 侧的disabledAutoApproveTools配置 →isMcpToolForcePromptBySource判定一旦被 defer就会在没有任何审批卡片的情况下被执行与用户显式的安全预期完全相悖。当前仓库中needsApproval的唯一生产者是 MCPmcpTools.ts 中needsApproval: async () forcePrompt且该判定是输入无关的——它是一个针对 (server, tool) 对的策略不随调用参数变化。这一特性影响后文修复方案的选择与守卫的权威性边界。修复方案Option A审批门禁工具永不 defer 注册表执行边界守卫修复由两个变更构成两者缺一不可永不 defer 审批门禁工具将其保留在 SDKToolSet中使 SDK 原生门禁对它的触发方式与其他非 defer 工具完全一致。SDK 源码证实ToolSet成员身份就是门禁的全部契约——在第 311 行附近SDK 用真实输入调用isApprovalNeeded并在执行前break。守卫注册表执行边界tool_invoke以及休眠状态的tool_exec必须拒绝执行任何审批门禁工具因为registry.getByName会按名返回任意已注册条目——模型即使没在tool_search结果中看到该工具也可能直接供应一个门禁工具的名称。因此这个守卫是承重墙load-bearing而非仅仅纵深防御由于它在调用期以真实输入运行它也是未来假想的输入相关审批场景下的权威判定点。Option B已否决及其理由修复设计曾考虑 Option B在 AI SDK 侧构建一个审批注册表让tool_invoke自身发出tool-approval-request、注册一个 pending promise、等待决策后再执行或拒绝。该方案被否决原因有三aiSdk/MCP 的审批路径是基于流重启stream-restart-based的没有可等待的 promise只有 Claude Code 拥有ToolApprovalRegistry因此 Option B 等于在同步的execute内部重复实现 SDK 原生门禁引入更多承重代码形态更差Option B 的唯一收益——defer 一个拥有超大 schema 的强制提示工具——属于罕见场景且更适合用正交方式解决。而 Option A 的唯一代价强制提示工具失去 defer 带来的上下文节省并不重要因为强制提示工具本就是刻意保留的少数派自动放行才是默认策略。共享判定助手isApprovalGated单一事实来源修复引入一个新的共享助手 isApprovalGated.ts作为三个调用点共用的唯一判定来源isApprovalGated(tool: Tool, opts: { input?: unknown; toolCallId?: string; messages?: ModelMessage[]; experimental_context?: unknown }): Promiseboolean其语义精确镜像 AI SDK 的is-approval-needed.tsneedsApproval形态判定结果undefined未声明false——无门禁boolean直接返回该布尔值functionawait tool.needsApproval(input, { toolCallId, messages, experimental_context })并返回结果抛异常fail closed返回true——按门禁工具处理Fail closed 设计抛异常的needsApproval返回true因为保持内联 / 拒绝执行两个方向都是安全方向——一个损坏的策略永远不能静默地自动运行工具。实现中捕获异常后还会通过 logger 输出告警logger.warn(needsApproval threw; treating tool as approval-gated, ...)。值得注意的还有调用时机的精度差异源码注释中明确说明defer 构建期调用如mcpTools.ts中决定defer取值时传入空input——这在needsApproval输入无关即当前 MCP 来源策略时是精确的调用期守卫tool_invoke/tool_exec内传入真实输入——它对任何未来的输入相关门禁都是权威的。逐文件改造与源码佐证mcp/mcpTools.tstoEntrydefer 与 needsApproval 锁步在 mcpTools.ts 的toEntry约 81-96 行中forcePrompt只读取一次并同时驱动defer与needsApprovalconst forcePrompt isMcpToolForcePromptBySource(server, mcpTool) return { name: mcpTool.id, namespace: namespaceForServer(server.id), ... defer: forcePrompt ? never : auto, tool: createMcpTool(mcpTool, forcePrompt), ... }关键设计要点同一值驱动两个字段保证两者永远一致不存在门禁开了但工具被 defer的窗口syncMcpToolsToRegistry每次请求都会重建条目因此 defer 决策始终反映当前审批策略用户在设置页修改disabledAutoApproveTools后即刻生效defer 判定留在工具来源处applyDeferExposition保持为纯尊重 defer 标记的逻辑——它不需要了解审批知识也不需要asyncMCP 目前是needsApproval的唯一生产者未来若其他来源引入门禁工具同样必须携带defer: never执行边界守卫作为运行时兜底。forcePrompt的判定实现在 mcpSourcePolicy.ts 的isMcpToolForcePromptBySource35-37 行遍历服务器disabledAutoApproveTools列表匹配工具名、工具 id、mcp__serverName__toolName线格式 id 或mcp__serverName__*通配符中的任意一种即视为强制提示。meta/toolInvoke.ts执行前的审批守卫toolInvoke.ts 在内层execute之前插入守卫98-111 行if ( await isApprovalGated(entry.tool, { input: params ?? {}, toolCallId: options.toolCallId, messages: options.messages, experimental_context: options.experimental_context }) ) { throw new Error(Tool ${name} requires user approval; call it directly instead of via tool_invoke.) }该守卫位于allowedNames校验与registry.getByName解析之后、Guard A未见 schema 拦截与 Guard B参数校验之前错误风格与文件既有的Tool not found一致。错误消息明确指示模型直接调用该工具——因为它现在始终内联。meta/exec/runtime.tstool_exec 的拒绝而非等待姿态exec/runtime.ts 的handleToolCall中放置同一守卫命中门禁时向 worker 发出toolErrorrequires approval; call it directly而非执行。虽然tool_exec当前处于休眠状态未被注入到ToolSet见 applyDeferExposition.ts 中关于刻意不默认注入的注释但该守卫在结构上与tool_invoke完全一致、成本极低可以防止潜在的重引入latent reintroduction在同一变更中闭合审查者关于tool_exec的独立意见其拒绝而非等待的立场非常关键tool_exec在 Node worker 中运行任意 JS 循环无法在循环中途暂停等待交互式审批卡片因此拒绝门禁工具并引导模型内联调用是唯一正确姿态。消费方兼容性deferredEntries 两个消费方均无需改动deferredEntries只被两处消费assembleSystemPrompt.tsDEFERRED_TOOLS按命名空间的计数段与 toolSearch.ts按deferredNames过滤。两者本来就想排除门禁工具——因此不会有破坏单一deferredNames来源保证提示词与搜索始终自动一致。测试覆盖修复配套三类测试覆盖判定助手与两个执行入口mcp/tests/sync.test.ts位于disabledAutoApproveTools中的强制提示工具被注册为defer: never非门禁的同源兄弟工具保持defer: auto。现有测试基线中普通工具默认注册为defer: auto见该文件首个用例修复后需补充门禁用例。meta/tests/toolInvoke.test.ts通过tool_invoke调用门禁工具被拒绝且内层execute不会被调用现有非门禁转发测试原样通过。tests/isApprovalGated.test.ts新增四个用例分别验证undefined→false、boolean 被尊重true/false、函数被 await 且输入与选项正确透传含experimental_context、抛异常→truefail closed。验证步骤按设计文档修复完成后依序执行单测pnpm test运行上述三个测试文件新增 修改静态检查pnpm lintoxlint eslint typecheck——确认async涟漪isApprovalGated使三个调用点变为异步编译通过手动/集成冒烟使用一个其工具在disabledAutoApproveTools中、且工具数足以触发 defershouldDefer门槛≥5 个自动工具、成本 上下文窗口的 10%的 MCP 服务器确认强制提示工具是内联的不在tool_invoke之后调用它出现审批卡片非门禁的同源工具仍然被 defer且通过tool_invoke调用时不出现卡片。文档同步与后续事项修复落地后需要同步三处文档沉淀审批门禁与 defer 互斥的不变式invariantdocs/references/ai/tool-approval.md 与 docs/references/ai/tool-registry.md新增不变式——审批门禁工具永不 defer它们保持内联以使 SDK 原生needsApproval门禁触发。tool_invoke/tool_exec拒绝任何审批门禁工具并指示模型内联调用。v2-refactor-temp/docs/ai/tool-cluster.md在工具簇指南中记录同一不变式。后续独立于本次变更事项包括向评审者回复架构意见 #1 时指出两处执行强制点mcpTools以defer: never注册强制提示工具 tool_invoke/tool_exec执行守卫并附file:line同时说明tool_exec的拒绝而非等待立场关联评审意见 #3 的tool_exec沙箱架构意见 #2dispatch TOCTOU 与崩溃对账与 #4provider.defaultChatEndpoint能力解析作为独立的确认或修正项保留。结语本次修复的核心价值在于明确了一个安全不变式审批决策必须在工具真正执行的边界上被强制。defer 展平优化的是提示词上下文占用它不应也不能削弱用户对敏感工具的审批控制。通过来源处标记defer: never 执行边界isApprovalGated拒绝的双层设计Cherry Studio 在高工具数场景下既保住了上下文节省又守住了总是询问工具的审批底线共享助手isApprovalGated的 fail-closed 语义与调用期真实输入判定则为未来出现输入相关审批策略时提供了权威的判定锚点。【免费下载链接】cherry-studio Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端项目地址: https://gitcode.com/CherryHQ/cherry-studio创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进