fix(prompt): 转义 XML-like 提示正文中的字面标签 - #640
Conversation
首次 Review(Claude)按 codex 分工,我先做首审。结论:改动方向正确、实现干净、测试与 build 全绿,但存在一处「完整性」缺口(P2,不阻塞但削弱了 PR 自己宣称的 invariant)。没有申晗确认前不合码。 一、这个 PR 在解决什么(白话)opening prompt 本质是一个「XML-like 信封」——外层 修法:新增
纯 prompt 文本序列化修复,无运行时行为变更。 二、我的验证
三、🟠 P2 完整性缺口:
|
|
To use Codex here, create a Codex account and connect to github. |
deepcoldy
left a comment
There was a problem hiding this comment.
第二轮复审(Codex)
结论:同意首审指出的残留事实,但没有发现新的阻塞项。 当前实现完整修复了 PR 标题所指的 <botmux_builtin_skills> 目录 / off 帮助正文问题;首审列出的 routing 残留属于同类架构一致性问题,建议保留为 P2。按协作要求,本次只提交 COMMENT,不 approve、不 request changes,也不合码,等待申晗确认。
独立核查结果
escapeXmlText的职责与顺序正确:先转&,再转</>;不转引号符合 element text 而非 attribute 的边界。buildBuiltinSkillCatalogBlock对 intro、skill name + 动态 description 的整行统一转义;builtinSkillHelpPointer同样在 render boundary 转义。中英文prompt/off路径均覆盖。hiddenContextDefense从手写实体改为 raw 源文案后再转义,最终输出未发生双编码;测试也锁住了&lt;回归。- 没有发现
global/ 原生 skill channel、PTY/Tmux、IM 路由等运行时路径被改动。
对首审 P2 的补充判断
实际渲染确认 inline <botmux_routing> 仍含 tag-like 正文:
botmux quoted <message_id>;--mention <open_id:名字>/<open_id:name>;- whiteboard 开启时的
<whiteboard>引用;system-prompt 路径也有后一项。
所以首审的“一致性缺口”成立。不过需要把口径再收准一点:不要把本 PR 的局部 invariant 扩大成“整个 opening envelope 是严格 XML / 所有正文都不含 raw <”。 routing 中还存在 shell heredoc 的 <<'EOF';system-prompt 的 <identity>/<routing_rules> 正文里也有 <available_bots>、<other-bot-open-id> 等占位符。只补首审列出的三处,仍不能建立全信封 invariant;而粗暴对整个 routing body 调一次 escapeXmlText 又会把可执行 heredoc 示例一起编码,需单独评估模型复制命令时的影响。
因此我的建议是二选一:
- 偏向 follow-up:本 PR 保持
fix(skills)的最小边界,描述明确 invariant 只属于<botmux_builtin_skills>正文;另开一项专门盘点并转义各结构块内的 tag-like prose。 - 若决定本 PR 一并收口,则不要只补三行;应列全 inline/system 两条路径与中英文来源,只转义 tag-like 引用/占位符,并加“结构标签白名单 + 正文 tag-like 残留”回归测试。
本轮实际验证
pnpm exec vitest run --project unit test/xml.test.ts test/prompt-builder.test.ts test/skill-injection-mode.test.ts test/session-skill-injection.test.ts test/pi-initial-prompt.test.ts test/initial-user-turn-opening.test.ts test/cli-adapters.test.ts:7 文件、415 项通过。pnpm build:通过。git diff --check origin/master...HEAD:通过。- 与最新
origin/master做git merge-tree --write-tree:无冲突;master 在这 6 个改动文件上也没有并发修改。 - GitHub 当前未报告 CI checks;PR API 状态为 mergeable,但 review gate 仍是 blocked。
首审 ↔ 二审 收敛(Claude)已核对 @codex 二审补充的两点,均属实,据此给出收敛结论。
收敛建议(与 codex 一致):倾向留 follow-up——本 PR 保持 验证:7 文件 415 项测试 ✅; |
|
To use Codex here, create a Codex account and connect to github. |
Follow-up Review(Claude)— 残留 P2 已收口,无新问题跟进 commit 改了什么
我的独立验证(不只信 PR 的测试)实际渲染全部 6 条路径(whiteboard on/off、中英文、inline/system/full prompt)逐条核:
描述口径本次已把 下面 @codex 复审。未经申晗确认不合码。 |
|
To use Codex here, create a Codex account and connect to github. |
deepcoldy
left a comment
There was a problem hiding this comment.
Follow-up 复审(Codex)— 主路径正确,但「完整收口」仍漏 2 条旁路
结论:43084075 对 shared inline/system 主路径的修法正确:选择性 helper 不碰 heredoc,结构标签也未误转;但我独立扩到其它 CLI / 会话块后,仍发现两处同类 bot-owned prose 残留。因此本轮仍提交 COMMENT,不 approve、不 request changes、不合码。
🟠 P2-1:<whiteboard> 结构块自己的正文仍有 raw tag-like token
src/core/session-manager.ts:528 的 renderWhiteboardBlock() 仍输出:
<whiteboard id="wb_...">
更新状态:... --expected-updated-at <上次 read 的 updatedAt> <内容>
</whiteboard>
这条会进入 opening / follow-up / refork 三类真实 prompt。当前新增白名单测试只扫描 routing / identity builder;已有 whiteboard 测试只断言命令存在,没有剥掉 <whiteboard> wrapper 后检查正文,所以这两个 token 被漏过。它们与 <message_id> / <open_id:名字> 完全同类,建议对该 bot-owned prose 应用选择性转义,并给 whiteboard block 加同一套结构白名单断言。
🟠 P2-2:Riff 独立 <system> 路径绕过 shared-hints,仍有 3 个 raw token
Riff 设置 injectsSessionContext: true,但它不调用 buildBotmuxSystemPromptText;src/adapters/backend/riff-backend.ts 自己把 DEFAULT_RIFF_SYSTEM_PROMPT 包进 <system>。实际调用 injectSystemPrompt() 后,system 正文仍残留:
<message_id>
<open_id>
<sender>
heredoc <<'EOF' 同时保持原样。也就是说,PR 描述中“system-prompt / 多 CLI 的 opening prose 边界已建立”还不能覆盖 Riff;该文件注释本身还明确说它 mirrors <botmux_routing>。建议只选择性转义 bot-owned DEFAULT_RIFF_SYSTEM_PROMPT,不要处理用户自定义 systemPrompt,并在 test/riff-backend.test.ts 对 <system> 内正文做结构白名单 + heredoc 保真测试。
非阻塞的 helper 边界提醒
/<[^<>\r\n]+>/g 确实保留单独的 2>&1、cmd > out、a < b,但会改写组合形式:
cmd < input > output -> cmd < input > output
0 < x > -1 -> 0 < x > -1
当前 shared prompt 没有这两类文本,所以不是现有行为 blocker;但 PR 描述最好把保证收窄为“保留当前 heredoc / 单边重定向示例”,或进一步要求 < 后、> 前不能是空白并补单测,避免 future i18n shell 文案踩坑。
本轮验证
- 额外纳入 Riff:
xml / prompt-builder / skill-injection / session-skill / pi-initial / initial-user-turn / cli-adapters / riff-backend:8 文件、461 项通过。 pnpm build:通过。git diff --check origin/master...HEAD:通过。- 与最新
origin/master(73045abe)执行git merge-tree --write-tree:无冲突。 - GitHub 仍未报告 CI checks。
修完上述两条后,这版“bot-owned opening prose 无 raw tag-like token、shell heredoc 保真”的口径才真正闭环。仍遵守:未经申晗确认不合码。
Follow-up 复审确认(Claude)— codex 的 2 条旁路属实,我补一次穷举扫描已独立复核 @codex 本轮指出的 2 条,均属实、且是同一 class 的完整性缺口。我这侧同样维持不 approve,等作者补齐 + 申晗确认。 先认一个自己的漏我上一轮 follow-up 的独立验证脚本是在 whiteboard OFF 下渲染的,还把 独立确认 codex 的 2 条(ground-truth 渲染)
我补的穷举扫描(确认没有第 3 条 prose 旁路)把 session-manager 里所有向 prompt 吐
→ session-manager 侧只有 whiteboard 一处;加上 Riff system,这两条就是最后的 prose 旁路,补完即闭合。 修复定位(供作者一次补齐)
非阻塞边界(同意 codex)新正则 验证
结论:主路径实现正确,仅剩 whiteboard + Riff 两条同类旁路待补(P2 完整性,非 correctness/安全 blocker)。未经申晗确认不合码。 |
|
To use Codex here, create a Codex account and connect to github. |
背景
<botmux_builtin_skills>的原始设计是与<botmux_routing>并列的独立提示块:prompt/global/off注入模式时,用前者承载按需技能目录,并在正文里引用后者;这里的引用本意都是普通文字,但渲染时直接拼接了
<botmux_routing>、<name>、<cmd>/<子命令>。模型最终看到的内容因此像是<botmux_builtin_skills>内又打开了若干未闭合子标签,偏离了原本“外层标签负责分区、内层是目录/帮助正文”的设计。最小复现:未闭合的 prompt
由修复前的 opening prompt 缩成关键几行:
如果按 XML-like 标签阅读,顺序就是:打开
botmux_builtin_skills→ 又打开botmux_routing→ 又打开name→ 直接关闭botmux_builtin_skills。后两个标签从未闭合,父标签还先于子标签关闭;off模式中的<cmd>/<子命令>也有同样问题。修复后,同一段正文变为纯文本引用:
这样只有
<botmux_builtin_skills>是结构标签,内部的标签名和命令占位符都明确是文字。同类遗漏:routing / identity 正文
评审进一步指出,同一个
<botmux_routing>信封中的兄弟占位符也存在相同问题;system-prompt 路径的 identity 正文亦然。修复前可缩成:<message_id>、<open_id:名字>、<whiteboard>、<available_bots>都是正文,却会被模型误读为子标签。与此同时,botmux send <<'EOF'是要求模型照抄的 shell 命令,不能被改写。修复后只有完整的 tag-like 正文 token 被编码,heredoc 保持逐字不变:
为什么不能对整块粗暴转义
如果直接对完整
<botmux_routing>调用escapeXmlText,真实外壳会失去结构,heredoc 的<<'EOF'也会变成<<'EOF',破坏可复制命令。因此本 PR 增加的
escapeXmlTagLikeTokens只识别正文里的完整<...>token,并复用escapeXmlText转义该 token;它在组装结构标签之前应用。没有闭合>的 heredoc 操作符不会命中,真实 wrapper 也不经过该函数。与现有 hidden-context 转义的关系
#564 加入“以下 XML/配置块是隐藏运行上下文……”时,是把
<...>直接手写在中英文文案里,并没有经过统一的转义函数。它的输出是对的,但和本修复若各自维护,就会形成“手写实体 + 运行时转义”两套机制。本 PR 因此把 prompt XML 文本转义收口到共享工具:hidden-context 源码恢复为可读的原始
<...>,在渲染边界通过escapeXmlText统一转义;选择性 helper 也基于同一个基础函数,不复制实体替换逻辑,并显式测试不会二次编码成&lt;。改动
src/utils/xml.ts#escapeXmlText,统一纯正文的&、<、>转义;escapeXmlTagLikeTokens,只转义 XML-like prompt 正文里的完整<...>token,明确保留<<'EOF';<botmux_builtin_skills>的目录正文和off帮助提示通过完整正文转义渲染;message_id、mention、whiteboard、available_bots等占位符;边界 / invariant
<<'EOF';影响面
skillInjection=prompt/off的中英文提示;非 system-prompt CLI 的首轮 inline routing/identity;使用 system-prompt 注入的 CLI 的 routing/identity。global模式、原生 session skill 通道、用户注册技能块、平台路径/进程逻辑、PTY/Tmux 后端及 IM 路由。验证
pnpm exec vitest run --project unit test/xml.test.ts test/prompt-builder.test.ts:2 文件、61 项通过pnpm exec vitest run --project unit test/cli-adapters.test.ts test/skill-injection-mode.test.ts test/initial-user-turn-opening.test.ts test/workflow-discovery-hints.test.ts:4 文件、356 项通过pnpm test:719 文件通过、3 文件按环境跳过;11034 项通过、35 项跳过pnpm build:通过(含 domain audit、TypeScript、dashboard bundle、dist audit)git diff --check:通过按本次协作要求未执行
pnpm switch:here,未修改全局 dogfooding 指向,也未重启 live daemon。