Skip to content

interrogate:跟进上游 #986,审查者改为用实际运行证明问题的对抗者 - #32

Merged
mtmtian merged 7 commits into
mainfrom
claude-mt/interrogate-adversaries-cc0beb
Oct 30, 2026
Merged

mtmtian merged 7 commits into
mainfrom
claude-mt/interrogate-adversaries-cc0beb

Conversation

@mtmtian

@mtmtian mtmtian commented Oct 30, 2026 •

Copy link
Copy Markdown
Owner

意图

跟进上游 cursor/plugins pstack #986(ab1f702c,包含在 0.15.36 的 pin c742dd61 里)。interrogate 的审查者改为对抗者:每个对抗者在自己的 worktree 里设法弄坏改动,只报告能用实际运行证明的问题。父会话按 arena Phase F,在该对抗者的 worktree 里亲自重跑每条发现的命令,并跳过 arena 的 C、D、E 三个阶段。

#31 里写的「interrogate 保持原样(暂缓)」由本 PR 取代。

与上游的差异(fork 决定,见 ADAPTATION.md#interrogate)

  • 对抗者名单来自 config.mjs show --host 的 interrogate reviewers,是跨厂商面板:inherit-parent、另一主厂商的 xagent、xagent:gemini(辅助席位)。上游从模型表读取,并且一律用 generalPurpose。
  • 每个对抗者都是能改文件、能跑命令的 worker。xagent: 对抗者跑 rw,各自独占一个 worktree,原因是 claude 和 gemini 的 ro 是 plan 模式,不能跑命令。
  • 如果 xagent 表里某个 agent 的 rw 条目限制了可跑的命令(目前是 claude、gemini、pi),这个对抗者就只能跑 bin/xagent-allowed-commands 里的命令,所以 brief 要求它用白名单内的测试命令来证明发现。
  • 输出要写明:每个对抗者用的是哪个模型;哪些对抗者共用继承模型(这些是独立运行,不算不同模型家族);哪些对抗者一条命令都没跑。

导览(建议查看顺序)

  1. skills/interrogate/SKILL.md

    • 改了什么:按上游重写,只保留上面列出的 fork 差异;四个 references 已删除。
    • 怎么验证:看 Fan out 和 Verify 两节;check-fork 有三条守卫锁住这些差异。
  2. skills/xagent/SKILL.md:50

    • 改了什么:说明对抗者跑 rw、使用独占 worktree,命令受该 agent 的 rw 白名单限制。
  3. scripts/pstack-host-e2e.py 模块 docstring

    • 这里是验收规则的唯一来源,ADAPTATION 只用一句话指向它。
    • 写者槽位:要求有修复证据,且 test_stats.py 不能改动。
      • xagent 写者:stats.py 的最后一次写入,必须落在同一 worker 在该 worktree 里某次尝试的运行窗口内(prompt.md 到 meta.json 的 mtime,这两个文件都由 xagent 自己写)。所以掉线后只做验证的重试仍算写者;父会话在两轮攻击之间同步修复不算;被 xagent 判为 model_mismatch 的运行实际是另一个模型跑的,也不算。
      • 原生写者:这次派发必须用编辑工具改过 stats.py。每次派发单独判定,所以只做验证的后续派发不算写者。
    • 对抗者槽位:只接受 rw 尝试。worktree 里要有最终版 stats.py 和未改动的 test_stats.py,且这两个文件都不能晚于尝试开始时间;允许附带复现文件。
    • 所有者:按(类型、agent 与模型)区分,同一个 worker 不能攻击自己的修复。
  4. validate_receipts / _rerun_by_parent:父会话把每次重跑记入 findings.json。每条记录的 command 要满足两点:

    • 按空白切分后,与父会话自己跑过的某条命令完全相等;
    • 那条命令用绝对路径或工作目录指向该对抗者用过的 worktree。

    只引用命令文本的 heredoc、echo 不算重跑。

  5. scripts/e2e-native-evidence.py

    • Claude:只取顶层 Bash。
    • Codex:从父会话自己的 rollout 读 CommandExecution,带 cwd;父会话和子 agent 共用一个 _codex_command。
  6. 测试

    • 先写 Given/When/Then 场景(A1–A8、F1–F4),再改 pstack-host-e2e.test.py(29 个)和 pstack-host-e2e-native.test.py(29 个)。
    • 其中有一个负例取自真实 Claude 运行:父会话把多条重跑批量写进一个用 shell 变量的脚本,这种写法应判为失败。
  7. 文档:README、docs/guide/01-setup.md、04-design.md、10-recipes-and-pitfalls.md、poteto-help 都改成了对抗者的说法。

  8. ADAPTATION 与守卫

    • 新增 ### interrogate 一节,删掉「暂缓」那条。
    • 新增三条守卫:interrogate-policy-panel、interrogate-adversaries-rw(这条也覆盖白名单那句)、interrogate-parent-rerun,每条都有对应的植入丢失测试。
    • 插件版本升到 0.15.36-shared.2。

验证证据

检查 结果 说明
node scripts/check-fork.mjs PASS 10 条 denylist 规则、32 条 required 规则
node --test scripts/check-fork.test.mjs PASS 11/11 植入丢失:三条新规则各自只让一条规则失败
python3 scripts/pstack-host-e2e.test.py PASS 29/29
python3 scripts/pstack-host-e2e-native.test.py PASS 29/29
bash scripts/check.sh runtime PASS macOS,coreutils gnubin 放在 PATH 最前,TMPDIR=$(getconf DARWIN_USER_TEMP_DIR)
其中 statechart 的 TLC 测试(59 个) NOT RUN 本机没有 tla2tools.jar,下载需要另行同意;与本次改动无关
变异反向验证 PASS 四条 harness 规则和三条原生证据规则逐一植入缺陷,测试都能抓到;写者窗口规则的三种错误写法(逐次判定、组内最早开始时间、去掉窗口终点)和去掉 model_mismatch 过滤,也都被测试抓到;删掉白名单那句后守卫失败
真实宿主 E2E,Codex(57b33f1 时的 harness) PASS 11/11 .pstack/e2e/host-20261030-181512/codex:写者 xagent:pi 用 rw;对抗者:原生 1 个,xagent:claude、xagent:gemini 各 1 个;没有发现,findings.json 是 []
真实宿主 E2E,Claude(57b33f1 时的 harness) FAIL 10/11 只有 findings_rerun_by_parent 失败,原因见下一节
用最新 harness(9b14073)离线复验上面那次运行 Claude 仍 FAIL 同一项;Codex PASS 不调用任何厂商,结论与原运行一致;两台宿主写者的 stats.py 写入都落在各自尝试的运行窗口内
第 2 次真实宿主 E2E(1b74371 时的 harness,含新提示词) PASS 22/22 .pstack/e2e/host-20261030-194548,Claude、Codex 各 11/11。两台宿主的写者都是 xagent:pi rw,一次修好;对抗者是原生 1 个加另一主厂商的 xagent、xagent:gemini,共 3 个,全部 PASS、没有发现,两边的 findings.json 都是 []
用最新 harness(9b14073)离线复验第 2 次运行 PASS 两台宿主全部检查通过
有发现时,新重跑提示词与账本匹配的真实路径 NOT RUN 第 2 次运行两台宿主各 3 个对抗者全部 PASS、没有发现,findings.json 都是 [],父会话没有可重跑的命令。所以「每条重跑单独一次 shell 调用」、Claude 按 tool_result 判定是否执行、Codex 按 rollout 的整数 exit_code 匹配,这几条在有发现时的真实效果都还没验证;目前的证据只有 fixture,以及第 1 次运行(批量脚本的失败形态)。另外,Codex 被拒命令的形态(exit_code: null)是合成的,真实 rollout 里只见过 completed/0 和 failed/1。要验证这条路径,只能再跑一次真实 E2E 并碰上有发现的情况,对抗者能不能找到问题无法事先控制
bash scripts/check.sh build NOT RUN 本 PR 没改 TS 源码或构建输入;运行需要 bun install(下载),且本机 Bun 是 1.4.4,不是固定的 1.4.2;交给 CI
thermos 第 1 轮(57b33f1) 3 个 blocking,已在 30b5e41 修复 第 2 轮两位审查者都确认已修复
thermos 第 2 轮(947354d,与 30b5e41 同树) 1 个 blocking,已在 b0748c0 修复 xagent 写者掉线后只验证的重试会判失败;审查者建议的修法会弄坏复用 worktree 的场景,改用运行窗口规则
thermos 第 3 轮(b0748c0) 1 个 blocking,已在 d328f92 修复 model_mismatch 掉线运行被算成配置模型的修复。两位审查者都报了,一位判 blocking、一位判 Low,按 blocking 处理
Codex PR 审查(d328f92) 1 条 P2,已在 776877d 修复 Claude 父会话中被权限或 hook 拦下、实际没有执行的 Bash 调用,被算成了重跑证据。现在 Claude 只认可 tool_result 不是错误、或内容以 Exit code N 开头的调用,Codex 只认可带整数 exit_code 的命令;去掉任一侧过滤的变异都会被测试抓到
Codex PR 复审(9b14073) 没有发现 bot 的审查状态记录为 has_any_findings: false
thermos 第 4 轮(b0748c0..9b14073,即 d328f92、1b74371、776877d、9b14073) 0 个 blocking,4 类 non-blocking 两位审查者并行审查,都拿到了前几轮发现的状态。第 3 轮的 R3-F1~F4、R3-B1/B3/B4 和 Codex 的 P2 都确认已修复。两位审查者各自在副本里重跑了两套测试(各 29 个)和 check-fork,都通过;又各自做了四个变异(去掉 model_mismatch 过滤、去掉 Claude 过滤、只去掉 Exit code 分支、去掉 Codex 过滤),每个都让测试失败;用 HEAD 离线复验两次真实运行,结果与上面一致。4 类 non-blocking 都没改,见「已知限制」中第 4 轮那几条。结论对应 HEAD 9b14073

Claude 那项 FAIL 的原因与处理

Claude 父会话确实发现了问题。第一轮 codex 和 gemini 报出 ISSUES,共保留 3 条发现,父会话修复后第二轮全部 PASS;账本格式也正确。

失败的原因是重跑方式:父会话把三条重跑写进同一段多行 Bash 脚本,先 W=/…/wt,再执行 cd $W/r1-codex && python3 -m unittest …。宿主日志里没有任何一条命令能和账本逐字对上。

harness 的规则是「账本里的命令必须是父会话真实跑过的那一条」,所以这里判 FAIL 是对的,不应该为了这次放宽匹配。做了两处改动:

  • 提示词改为:每条重跑单独一次 shell 调用,写成 cd <绝对路径> && <命令>,不用 shell 变量,并把这条命令原样记入账本;
  • 把这次真实出现的批量脚本写法加为负例测试。

第 2 次真实运行(含新提示词)22/22 通过,但这次对抗者都没有发现,父会话没有需要重跑的东西,所以新提示词在有发现时的效果仍未在真实宿主上验证。

已知限制

  • 「尝试开始时间」:xagent 取 prompt.md 的 mtime,原生对抗者取宿主日志里的 timestamp。要伪造它,就得改文件时间或改写宿主日志,这不在「各 agent 互相配合」的验收威胁模型之内。
  • findings.json 是否完整,是宿主自己的说法。兜底靠最终的独立测试和合约探针。
  • xagent 写者的修复证据是文件时间,比原生的「编辑工具调用」弱:如果对抗者违反 brief 去改写 stats.py,它可能填上写者槽位。这同样在合作代理的威胁模型之外。
  • 复用对抗者 worktree 时,二次攻击可能被判成写者,导致对抗者槽位落空,属于误拒、不是误放行。只在两种情况下发生:上一次掉线的攻击改写过 stats.py;或者父会话在上一次攻击运行期间,用保留 mtime 的方式(cp -p、rsync -a)复制了修复。提示词要求二次攻击用新 worktree;真实运行用的是 git apply 和普通 cp,都不会触发。
  • 没做的 non-blocking 项:
    • validate_receipts 的账本检查没有拆成独立函数,检查名也没有收成常量;
    • 每台宿主的证据仍分两次解析,Claude 的父会话命令扫描和子 agent 命令扫描还没合并;
    • bin/xagent-allowed-commands 首行注释和 check-fork 的一条 hint 仍只说 writer(pre-existing)。改 bin/xagent* 就要跑 scripts/xagent-e2e.sh,它会调用所有厂商,所以这次不动;
    • 原生写者的后续派发如果只做验证,也会遇到同类问题(pre-existing)。现在已作为规则写进 docstring。
  • thermos 第 4 轮的 non-blocking 项(都由本 PR 引入,都没改)。改任何一条都会移动 HEAD,需要再审一轮;第 1 条还会改掉第 2 次真实 E2E 用过的提示词。是否处理由你决定:
    1. 「交回写者」超出了超时预算(d328f92)。
      • 提示词允许父会话把发现交回写者,这等于在两轮攻击之间多出一轮写者,但 --host-timeout 的默认值仍按三轮各一次重试计算(WORST_CASE_ATTEMPTS = 3 * 2)。
      • 按四轮算,最坏情况约需 8108 s,默认值是 6246 s,差约 1862 s。只有每次尝试都跑满时限时才会触发,结果是宿主超时、判 FAIL,属于误拒,不会误判通过。真实运行里每次尝试只要几分钟。
      • 「交回写者」和「每个 worker 最多重试一次」之间的关系,提示词也没写清楚。
      • 最省事的修法是删掉这几个词,只留「在你自己的 checkout 里修」,写者槽位不受影响;另一种修法是把预算改成 4 * 2,同步改 docstring 和 --help。
    2. 被拒的重跑不会出现在 checks.json(776877d,两位审查者都提了)。
      • 解析层直接丢掉没执行的命令,所以报告里只能看到 findings_rerun_by_parent: false,分不清「没重跑」还是「重跑被拒」,要回 host.log 里查。
      • 仓库别处的做法是解析层打标记(如 NativeDispatch.usable)、由 harness 判定。可以给 ShellCommand 加一个 ran 标记,由 _rerun_by_parent 来判断。
    3. model_mismatch 用例(d328f92,两位审查者都提了)。
      • 这个负向断言写在一个正向用例里,失败时看到的用例名会误导排查。
      • 它用的回执 model 是 None,而 xagent 只有在固定了模型时才会给出 model_mismatch。
      • 规则本身没问题:审查者用真实形状(固定模型加 resolved_models)做探针,结果正确。
    4. 小项:
      • _xagent_attempts 里同一个分组 key 写了两遍;
      • 原生测试里 ran 一词有三种意思;
      • docstring 第 22 行插句后没有重新换行,「counts for no one」容易被理解成整张回执都被忽略(实际只排除它的运行窗口);
      • Claude 父会话命令现在按结果返回的顺序列出,只影响报告里的顺序。
  • 未核实:Claude 的 Bash 超时结果如果是 is_error、又不以 Exit code 开头,那么执行过的重跑会被排除,造成误拒。真实日志和本机转录里都没有超时样本;父会话重跑的都是短单测,可能性低。

披露

  • npm 事件:开工前跑的基线 check.sh 触发了 statechart 的 render.test.ts,它对 ~/.cache/statechart 执行了 npm install,改写了那里的 lockfile。没有装新包。base 分支现在默认跳过这一步。
  • 提交重接:同步上游 pstack 0.15.36:Orchestrate 去掉 orch、skill 更新、statechart 编译器 #31 合并后,两个提交用 git commit-tree 原样重接到 main(b605a32),树和提交信息都没变:57b33f1→65d8940,947354d→30b5e41。b605a32 的树与原 base 13a118a 完全相同。被强推的只有本 PR 自己的分支,当时还没有开 PR。
  • 没有刷新已安装的插件副本,也没有改 ~/.config/pstack/config.json。安装刷新等合并后由你决定。

🤖 Generated with Claude Code

mtmtian and others added 4 commits October 30, 2026 19:00
- interrogate 按上游 ab1f702c 重写:对抗者在各自 worktree 里试图弄坏改动,
  只报告跑出来的问题;父会话按 arena Phase F 在该对抗者的 worktree 里自己重跑,
  跳过 arena C/D/E;删除 reviewer-prompt、rubric、lead-judgment、
  code-quality-review 四个 references
- 与上游的差异:名单从 config.mjs show 的 interrogate reviewers 读取,
  对抗者是能改文件、能跑命令的 worker(不写 generalPurpose);
  xagent 对抗者跑 rw,掉线按 xagent 的重试与未决规则处理
- xagent skill:对抗者跑 rw、各用独占 worktree,因为 claude 和 gemini 的
  ro 是 plan 模式,跑不了命令;命令受该 agent 的 rw 白名单约束
- 宿主 E2E harness:对抗者槽位只接受 rw 尝试,要求 worktree 持有最终版
  stats.py 和未改的 test_stats.py,且两者都不晚于尝试开始时间
  (xagent 取 prompt.md 的 mtime,原生取宿主日志里的派发时间戳),允许复现文件;
  父会话把每次重跑记入 findings.json,每条都要对上宿主日志里父会话自己跑的命令
  (Claude 顶层 Bash、Codex --json 的 command_execution);删除 e2e-snapshot.py
- 测试先写 Given/When/Then 场景(A1–A8、F1–F4),再改两个测试文件;
  对四条 harness 规则和三条原生证据规则逐一做了变异反向验证
- README、design 与 recipes 指南、poteto-help 的 interrogate 说法同步上游
- ADAPTATION 新增 interrogate 一节,替换「暂缓」状态;守卫新增
  interrogate-adversaries-rw 与 interrogate-parent-rerun,各有植入丢失测试
- 插件版本升到 0.15.36-shared.2

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 写者与对抗者各用一个角色判定:写者要有「修复」证据(xagent 的 stats.py
  晚于 prompt.md 写入,原生是对 stats.py 的编辑工具调用),test_stats.py 不变;
  所有者按 (类型, agent 与模型) 区分,同一个 worker 不能攻击自己的修复
- 父会话重跑账本:command 按空白切分后须与父会话自己跑的某条命令完全相等,
  且该命令以绝对路径或工作目录指向这个对抗者用过的 worktree;只引用命令的
  heredoc、echo 不算重跑。Codex 改从父会话 rollout 读命令,带 cwd
- 真实 Claude E2E 里父会话把三条重跑写进同一段带 W= 变量的脚本,账本无法逐字
  对上,findings_rerun_by_parent 失败;提示词改为每条重跑单独一次 shell 调用、
  绝对路径、不用变量,并加了这一真实形态的回归测试
- interrogate:告诉 xagent:claude/gemini/pi 对抗者只能跑 rw 白名单里的命令;
  输出写明每个对抗者的模型、哪些共用继承模型(独立运行,不算不同模型家族),
  以及哪些对抗者一条命令都没跑
- 守卫拆成 interrogate-policy-panel 与 interrogate-adversaries-rw(含白名单句),
  各有植入丢失;ADAPTATION 第二段压缩为指向 docstring 的一句,Verification
  段去掉「文件时间」;setup 指南改用对抗者说法

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- thermos 第 2 轮两位审查者都指出:写者证据只和本次尝试的 prompt.md 比,
  xagent 写者修好 stats.py 后掉线、重试只跑测试时,写者槽位会判失败,
  是上一提交修 R1-Q1 时引入的回退
- 改为运行窗口规则:stats.py 最后一次写入落在同一 worker 在该 worktree
  某次尝试的运行期间(prompt.md 到 meta.json 的 mtime),才算修复;
  父会话在两轮攻击之间同步修复不算。审查者建议的「组内最早开始时间」
  会把复用 worktree 的二次攻击误判成写者,变异测试证实了这一点
- 新增回归测试(掉线后只验证的重试填写者槽位;两次运行之间的写入不算),
  fixture 回执的 meta.json 时间改为尝试结束时间;三种变异写法都被测试抓到
- docstring:xagent 写者规则移到 xagent 段;无时间戳的原生派发只是不能
  填对抗者槽位
- interrogate 改为引用 xagent 表里该 agent 的 rw 条目,不再手抄 agent 名单;
  ADAPTATION 写明原生对抗者没有 mode
- 测试:账本测试用字面量期望值,父命令统一传 (script, cwd),
  时间解析不再借用被测的私有函数

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- thermos 第 3 轮两位审查者都指出:运行窗口按回执里请求的模型分组,
  一次 model_mismatch 掉线(实际是另一个模型回答的)留下的修复,
  会被算到配置的模型名下;建组时排除这类运行,回归测试加一个子用例,
  去掉过滤的变异会被测试抓到
- 跨尝试的写者判定抽成 _xagent_attempts,_xagent_attempt 只接收
  repaired 布尔值,和原生路径对称
- 宿主提示词:已核实的问题在父会话自己的 checkout 修,或交回写者;
  docstring 写明写者窗口之外的写入会让写者槽位落空、原生写者按每次派发
  各自的编辑判定、复用 worktree 的二次攻击在什么情况下会被判成写者
- xagent skill 不再在表格下方另列受限 agent;docstring 换行;
  测试用 fixture 的 ran() 设运行窗口,iso() 参数改名避免遮蔽

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-30T12:00:26.890358Z 9b14073 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d328f92e15

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/e2e-native-evidence.py Resolved
mtmtian and others added 2 commits October 30, 2026 19:57
- Codex 在 PR 审查里指出:Claude 父会话的 Bash 调用被权限规则或 hook
  拦下、根本没执行时,解析器仍把它当成重跑证据,账本对上它就能通过
- Claude:只收 tool_result 不是错误、或错误内容以 "Exit code N" 开头的调用
  (真实日志里非零退出就是这种形态),被拒绝或没有结果的调用不算
- Codex:只收父会话 rollout 里带整数 exit_code 的 CommandExecution,
  被拒绝的命令不算
- 测试加入「重跑被拒绝」和「重跑被 Codex 拒绝执行」两个反例,通过的用例
  改用真实的非零退出形态;去掉任一侧过滤的变异都被测试抓到;
  用真实运行离线复验,两台宿主的父会话命令数不变(31、28)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mtmtian
mtmtian merged commit 283fd07 into main Oct 30, 2026
3 checks passed
@mtmtian
mtmtian deleted the claude-mt/interrogate-adversaries-cc0beb branch October 30, 2026 16:17
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.

1 participant