SOP:修复 merge-guard 的 allow_fp 漏记¶
变更日期:2026-10-07 执行人:Agent(用户授权) 对象:merge-guard/merge-guard.py
1. 背景¶
allow_fp 是全局规则指定的误报唯一机器判据——「被拦后原样重发」即说明该次拦截不成立;它被用来判断「某类强制规则是否该放宽」(如「判据 I 修复后 allow_fp I 应消失,若仍 ≥50% 则降回提醒级」)。
2026-10-07 实测发现该信号系统性漏记:
- state 文件
merge-guard/state/6ac5932fc1804934d3e7e22a__solo_agent记录denied: ['b8e372a101441b3b', '54eaf3ae5cfb723d']、denies: 2—— 两次 deny 的指纹都入了册; - 但
logs/guard_events.jsonl中这两次(08:34:34、08:37:51)之后都没有 allow_fp 行,两次原样重发均被静默放行; - 同一轮收尾自查的 guard 计数为
1/0/1(deny 1、allow_fp 0),现场复现。
根因:allow_fp 的判定被嵌在 if hit: 内,只有「重发时仍命中某条判据」才记录。而原样重发恰恰是「相同目标、快速连续」:
- 判据 A 要求
len(set(keys)) == len(keys)(目标互不相同)→ 重发必然不成立; - 判据 B 要求
separated()(相邻间隔 ≥STEP_GAP_SEC= 1.5 s)→ 快速重发(<1.5 s)被当作「同一步内并行调用」排除。
于是 hit=None → 走「静默放行且不写日志」。对照样本:08:16:08 deny(A) → 08:16:17 allow_fp(mark=B),两次相隔 9 s,因满足 B 的 separated 才记上。
2. 目标¶
allow_fp 的判定与本次判据命中解耦:只要本次调用的指纹在 denied 册中(即此前被拦过),就静默放行并记一条 allow_fp。
可验证状态:离线用例 C1 由「失败(allow_fp=0 条)」变为「通过(allow_fp=1 条)」,其余用例(A/B/C 判据、防假记录)全部回归通过;真机表现为 logs/guard_events.jsonl 中 A 类拦截后的原样重发不再丢记录。
3. 前置条件¶
- 对
merge-guard/merge-guard.py有写权限; python3可用;- 无其它会话正在密集触发该 Hook(本脚本按
(session_id, agent_id)隔离状态,改动不影响在途会话的既有 state)。
4. 影响范围¶
- 主机:单机(本机 token_proxy 工程),一个文件。
- 组件:
merge-guard.py的 PreToolUse 主流程与文档头。 - 是否停服:否。Hook 脚本同会话下一次请求即生效,无需重启 Trae、不影响代理
ds/glm/mi。 - 窗口期:无要求。
- 在跑作业:无影响。
5. 分步操作¶
步骤 1:备份(高危前置)¶
cd /home/rain/Documents/services/token_proxy
TS=$(date +%Y%m%d-%H%M%S)
mkdir -p merge-guard-allowfp-20261007/backup/merge-guard
cp -p merge-guard/merge-guard.py \
merge-guard-allowfp-20261007/backup/merge-guard/merge-guard.py.bak.$TS
预期:backup/merge-guard/ 下出现 merge-guard.py.bak.<TS>(24554 B)。
失败判断:目录不可写或 cp 非 0 → 停止,勿继续改文件。
步骤 2:先复现缺陷(用备份的旧版跑用例)¶
python3 merge-guard-allowfp-20261007/verify_allowfp.py \
--script merge-guard-allowfp-20261007/backup/merge-guard/merge-guard.py.bak.<TS>
预期:C1 失败(deny=True,重发输出='',allow_fp=0 条),C2~C5 通过,退出码 1。
失败判断:若 C1 直接通过,说明先前的诊断有误 —— 停下来重新定位,不要继续改。
坑:用例必须在隔离目录跑(verify_allowfp.py 会把被测脚本复制到临时伪工程结构,state 与 guard 日志都落在临时目录)。若直接对生产脚本跑而不隔离,会污染 logs/guard_events.jsonl 与 merge-guard/state/。
步骤 3:改判定位置(三处必须成套)¶
把「原样重发」判定移出 if hit:,以 item["fp"] in denied 为条件:
- 新增外层
if item["fp"] in denied:分支,记allow_fp(mark取本次判定、无则记-); - 原
if hit:改为elif hit:; - 删除内层已冗余的
item["fp"] not in denied条件与旧的elif mark in ENFORCE_MARKS and item["fp"] in denied:分支; - 文档头「强制与放行」段补一句「放行与记账都只看指纹、与本次是否命中判据无关」。
坑:四处必须成套改。只加外层不删旧 elif,会造成同一调用记两条 allow_fp;只删旧 elif 不改外层,则 hit=None 时仍漏记(等于没修)。
python3 -c "import ast; ast.parse(open('merge-guard/merge-guard.py', encoding='utf-8').read()); print('语法通过')"
grep -n 'in denied\|allow_fp' merge-guard/merge-guard.py # 预期:外层判定 + 文档头说明各 1 处
步骤 4:(无)本次不涉及其它文件。¶
6. 验证¶
| 层级 | 命令 | 预期 |
|---|---|---|
| 语法 | python3 -c "import ast; ast.parse(open('merge-guard/merge-guard.py').read())" |
无异常 |
| 用例(5 项) | python3 merge-guard-allowfp-20261007/verify_allowfp.py |
全部通过,退出码 0 |
| 关键项 C1 | 同上 | A 类 deny 后快速重发 → allow_fp=1 条(修复前为 0) |
| 回归 C2~C5 | 同上 | 连续只读仍 deny(A);同文件连续 Read 仍 deny(B);同文件连续 Edit 仍提醒(C);未拦截调用不产生 allow_fp |
| 真机 | 观察 logs/guard_events.jsonl |
A 类 deny 后的原样重发出现 action=allow_fp 行;wrapup.py 的 guard 计数中 allow_fp 不再恒 0 |
实测(2026-10-07):修复前 C1 失败(allow_fp=0)、修复后 C1 通过(allow_fp=1),C2~C5 全程通过。
7. 回滚¶
备份位置:merge-guard-allowfp-20261007/backup/merge-guard/merge-guard.py.bak.20261007-084025(登记见 backup/README.md)。
cd /home/rain/Documents/services/token_proxy
cp -p merge-guard-allowfp-20261007/backup/merge-guard/merge-guard.py.bak.20261007-084025 \
merge-guard/merge-guard.py
python3 merge-guard-allowfp-20261007/verify_allowfp.py \
--script merge-guard/merge-guard.py # 回滚后预期:C1 重新变为失败
回滚后验证:C1 恢复失败(allow_fp=0)即确认已回到旧版行为。回滚触发量化:真机出现「同一指纹被反复 deny(死锁)」或「同一次调用记两条 allow_fp」→ 立即回滚。
8. 风险说明¶
| 风险点 | 缓解措施 |
|---|---|
| 误删/漏删旧分支导致「fp 在册却仍被 deny」(死锁) | 三处成套改 + 离线用例 C1 覆盖「拦后重发必放行」 |
| 同一次调用记两条 allow_fp | 旧 elif ... in denied 分支已删除;C1 断言 allow_fp=1 条(非 ≥1) |
| 未拦截调用被误记 allow_fp | 外层条件严格为 item["fp"] in denied;用例 C5 专测该项 |
| 行为差异 | 唯一差异:非强制判据(C)且该指纹已在册时,旧版会提醒、新版静默放行(同一指纹此前已放行过,不再重复提醒)。属边缘且更合理 |
| 污染真实 state / 日志 | 用例强制在临时目录隔离运行 |
来源:
/home/rain/Documents/services/token_proxy/merge-guard-allowfp-20261007/SOP.md| 最后更新:2026-10-07 08:41(由build/sync.py同步,请勿在站点侧修改)