Skip to content

fix(bridge): 拦截泄漏成正文的畸形工具调用,不再当回复外发 - #795

Open
liuhaoyang wants to merge 3 commits into
deepcoldy:masterfrom
liuhaoyang:fix/bridge-leaked-tool-call-guard
Open

fix(bridge): 拦截泄漏成正文的畸形工具调用,不再当回复外发#795
liuhaoyang wants to merge 3 commits into
deepcoldy:masterfrom
liuhaoyang:fix/bridge-leaked-tool-call-guard

Conversation

@liuhaoyang

@liuhaoyang liuhaoyang commented Aug 9, 2026

Copy link
Copy Markdown

背景

修复 #794:长会话中模型偶尔把本应是结构化 tool_use 的内容写进普通 text block(畸形 <invoke …> 片段),该回合随后以 stop_reason=end_turn 结束且不含 tool_use。Claude transcript fallback 会把这段序列化工具调用当作正常 final 原样转发到飞书,用户看到的是一串没被执行的工具调用 XML。

本 PR 实现 #794 里的 P0 防泄漏,不动 P1(注入格式改造留待单独灰度)。

改动

  1. src/services/bridge-fallback-gate.ts — 新增纯函数 looksLikeLeakedToolCall(text),采用两阶段 O(n) 扫描

    • 保守匹配:仅在成对高置信特征时命中 —— <invoke name="…"><parameter name="…"> 子标签,或配 </invoke> 闭合标签。光秃 <invoke>、prose 里提到 "invoke"、代码里的 invoke(...)、单独的 <parameter>不命中
    • 两阶段 O(n) 实现:先用一次线性 exec 定位首个合法 opener,再只在其后缀里查 <parameter name> 子标签或 </invoke> 闭合。命中语义 = 「最早的 opener 之后(任意靠后位置)存在终止符」。不用惰性 [\s\S]*? 中缀正则——那种写法在「大量未闭合 <invoke name> 起点」的病态输入下会灾难性回溯(O(n²))。
    • 已知局限:一段完整、合法<invoke name="…">…</invoke>(用户在代码/日志/bug 报告里粘贴或讨论)也会命中——仅凭文本形态无法与真实泄漏区分。这是纯文本启发式的既定取舍;把调用点限制在 transcript-drain 兜底路径(绝不作用于显式 botmux send)是实际安全的保证。
  2. src/worker.ts(emitReadyTurns) — 在 transcript fallback 外发点接入检测。

    • 仅非 adopt 模式:命中时不原样发送,改发明确提示(worker.leaked_tool_call)并 log不静默吞,避免用户误判任务完成。
    • 显式 botmux send 不经过此路径,不受影响;adopt 模式保持逐字透传(botmux-unaware CLI,transcript drain 是其唯一通道,且可能合法输出此类内容)。
  3. src/i18n/zh.ts / en.ts — 新增 worker.leaked_tool_call 提示(中英双语)。

    • 文案不声称整轮未执行:trailingAssistantText 取的是「本轮最后一次真实 tool_use 之后」的尾部文本,命中时前面的真实工具调用可能已经执行并产生副作用。因此文案说明「本轮可能只完成了部分操作,这段之前的工具调用可能已真实执行,请先检查当前状态,再决定补做剩余部分还是重试,避免重复执行」,不无条件建议重发
  4. test/bridge-fallback-gate.test.ts — 新增单测覆盖:

    • 命中真实泄漏形态(count 前缀 + <invoke>+<parameter>、prose+block、仅开合标签、截断无闭合)。
    • 误杀防御:空 / undefined、普通提到 invoke、<invoke> 元素讨论、光秃 <invoke>、代码里的 invoke()、单独 <parameter> —— 均不命中。
    • 两阶段扫描语义:终止符出现在 opener 之前不命中、后置 opener 闭合仍命中(multi-opener)、多个 opener 全不闭合不命中。
    • 线性时间守卫:5 万个未闭合裸 opener 的病态输入在 200ms 内返回(回归护栏,旧惰性正则在此规模需数秒~数十秒)。

验证

  • pnpm exec tsc --noEmit:通过。
  • bridge-fallback-gate 单文件:65 tests 全绿。
  • 等价性交叉验证:两阶段 O(n) 实现与原惰性正则在 25 万组确定性随机输入(含 multi-opener、前置终止符、大小写/引号/命名空间前缀变体)上结果完全一致。
  • 性能:850KB / 5 万裸 opener 的病态输入连续 500 次,p50 0.66ms、最大 1.07ms(旧惰性正则同规模约数十秒)。
  • 邻近回归:bridge gate + Claude transcript + turn queue 217 tests 全绿。
  • 全量 unit:14,589 通过;另有若干失败集中在 master 已有的 slash 文档 /introduce、Pi/Codex App 状态时序、群路由 setup 等与本 PR 无关的模块(同类时序失败在未合本 PR 的 master 上亦可复现)。本 PR 完整 diff 仅 5 个 bridge/i18n 文件。

说明

  • 未附真实 transcript,避免泄漏内部信息;测试用等价人造样例。
  • 这是缓解而非根治:畸形工具调用仍由模型产生,本 PR 保证它不再被当作正常回复原样外发,且提示措辞不误导用户重复已产生的副作用。P1(注入格式)另议。

Closes #794

长会话中模型偶尔把本应是结构化 tool_use 的内容写进普通 text block
(畸形 <invoke …> 片段),该回合随后以 stop_reason=end_turn 结束且不含
tool_use。Claude transcript fallback 会把这段序列化工具调用当作正常
final 原样转发到飞书,用户看到的就是一串没执行的工具调用 XML。

新增保守检测 looksLikeLeakedToolCall():仅在成对高置信特征
(<invoke name=…> 配 <parameter name=…> 或 </invoke>)时命中,避免
误杀用户正常讨论/粘贴 XML、代码里的 invoke()、单独标签等。

仅作用于 transcript fallback 外发点(worker emitReadyTurns,非 adopt):
命中时改发明确失败提示(新增 i18n worker.leaked_tool_call)并记日志,
不静默吞。显式 botmux send 不经过此路径,不受影响;adopt 模式保持透传。

补 10 条单测(真实泄漏形态命中 + 误杀防御)。
@liuhaoyang
liuhaoyang requested a review from deepcoldy as a code owner August 9, 2026 03:43
liuhaoyang and others added 2 commits August 10, 2026 21:02
- 文案(zh/en): trailingAssistantText 取「最后一次真实 tool_use 之后」的尾部,
  命中泄漏时前面的真实工具调用可能已执行并产生副作用。原文案「本轮未真正执行/
  建议重发」会误导用户重发→重复副作用。改为提示「本轮可能只完成部分操作,先检查
  当前状态再决定补做或重试」,不再无条件劝重发。
- looksLikeLeakedToolCall 改两阶段 O(n) 扫描: 先线性定位首个合法 opener,再只在其
  后缀查 <parameter name> 子标签或 </invoke> 闭合。原惰性 [\s\S]*? 中缀在「大量
  未闭合 <invoke name> 起点」时灾难回溯 O(n²)(5万起点 28s),两阶段扫描根除(1ms)。
  命中语义与旧正则完全等价(交叉验证覆盖 multi-opener/前后位置/大小写/命名空间前缀)。
- 测试: +4 用例(terminator 在 opener 前不命中/后置 opener 闭合命中/多 opener 全不
  闭合不命中/病态输入线性时间守卫<200ms);原 59 全绿=语义等价;两处变异各精确杀死
  对应新测试证有牙。
@deepcoldy
deepcoldy force-pushed the fix/bridge-leaked-tool-call-guard branch from f6daaa5 to 49f4373 Compare August 10, 2026 13:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

长会话中畸形工具调用泄漏成正文,被 Claude transcript fallback 当回复发到飞书

2 participants