SOP:收窄两处 Hook 的强制拦截面(sched-guard / webfetch-guard)¶
- 主题目录:
hook-deny-tune-20261012/ - 执行时间:2026-10-12
- 改动对象:
sched-guard/sched-guard.py、webfetch-guard/webfetch-guard.py - 触发来源:用户提问「现在 hook 还有哪些内容会强制 deny,看下是否合理」,核实后裁定「执行吧」
1. 背景¶
盘点 11 个自建 Hook 后确认:merge-guard 已无强制通道(ENFORCE_MARKS 于 2026-10-11 清空),
但仍有 7 个守卫真会返回 permissionDecision: "deny"。其中两条判据不合理:
- sched-guard 语义错位:动词表把只读查询与变更动作混在一张表,于是本守卫要求的确认动作
(
squeue -w <node>/bjobs -m <host>)自己也会被拦一次。按拦截文案去确认,反而被拦。 - webfetch-guard 按次数拦:本会话第 2 次及以后的 WebFetch 一律拦。取数
logs/guard_events.jsonl实测 deny 11 次、其中 9 次被原样重发推翻(82%),每次推翻白多一次往返。 被推翻的正是「跨步骤的独立抓取需求」,不是判据想拦的「连续抓多页」。
2. 目标¶
改后应达到的可验证状态:
- sched-guard:命令含
squeue/sinfo/bjobs/bqueues/bhosts时不再输出任何 hook 协议; 含scontrol/srun/sbatch/scancel/bsub/bkill时仍拦一次,原样重发即放行。 - webfetch-guard:距上一次抓取不足 120 秒的 WebFetch 拦一次(原样重发即放行); 距上次抓取超过 120 秒的独立抓取直接放行;本会话首抓放行。
- 两个守卫的「缺 session_id 只提醒不拦」与「任何异常静默 exit 0」行为不变。
3. 前置条件¶
- 对
sched-guard/与webfetch-guard/有写权限; - 系统
python3(仅用标准库)可用; - 动手前先取一次基线:
python3 cost_audit/wrapup.py或读logs/guard_events.jsonl的 W / S 计数, 用于事后判断拦截频率是否下降。
4. 影响范围¶
- 影响主机:单机(本机
~/Documents/services/token_proxy),无远端。 - 是否停服:否。Hook 由
hooks.json以python3 <脚本>方式逐次调用,改脚本文件即时生效, 无需重启 Trae,也不打断正在跑的会话。 - 在跑作业:本机无调度在跑作业;不涉及 Slurm / LSF 节点。
- 窗口期:无。改动只在下次对应工具调用时生效。
5. 分步操作¶
步骤 1:备份两个守卫脚本(高危:覆盖前必做)¶
两个脚本分别是各自的实现体,覆盖前先做可回滚备份(命名 <原名>.bak.<YYYYmmdd-HHmmss>),
统一放在本主题的 backup/ 下、保持来源目录层次。
cd ~/Documents/services/token_proxy
TS=$(date +%Y%m%d-%H%M%S)
mkdir -p hook-deny-tune-20261012/backup/sched-guard hook-deny-tune-20261012/backup/webfetch-guard
cp -p sched-guard/sched-guard.py "hook-deny-tune-20261012/backup/sched-guard/sched-guard.py.bak.$TS"
cp -p webfetch-guard/webfetch-guard.py "hook-deny-tune-20261012/backup/webfetch-guard/webfetch-guard.py.bak.$TS"
sha256sum sched-guard/sched-guard.py webfetch-guard/webfetch-guard.py
预期:两条 cp 无输出、退出码 0,backup/ 下出现两个 .bak.<时间戳> 文件;sha256sum 打印改动前的两个校验值。
失败判断:权限不足或目标目录不存在时 cp 非 0,先修权限再重试,不要跳过备份。
步骤 2:收窄 sched-guard 动词表¶
坑:这张表原先同时承担「要拦的变更动作」与「让你先执行的只读确认」两个角色,
不能整表删除,只能按「是否改变节点或作业状态」拆。scontrol 既含只读子命令(show)
又含变更子命令(update / resume),故整体保留;纯查状态请改用不再被拦的 sinfo / squeue。
改动点(三处,均在 sched-guard/sched-guard.py):
# ① 头部文档:新增「2026-10-12 收窄动词表」一节,并把判据清单改成变更类清单
# ② 常量:
# SCHED_VERBS = ("scontrol", "srun", "sbatch", "scancel", "bsub", "bkill")
# (移出 "squeue", "sinfo", "bjobs", "bqueues", "bhosts")
# ③ DENY_TEXT:文案改为「含**变更类**调度器动词」,并注明两个查询已不再被拦
预期:python3 -c "import importlib.util,sys;..." 或直接跑第 6 章验证脚本,见「只读查询不触发」全部 PASS。
失败判断:漏改 SCHED_VERBS 会出现「只读查询仍被拦」;漏改 DENY_TEXT 仅是文案不符,不影响行为。
步骤 3:webfetch-guard 由「按次数」改为「按连续窗口」¶
坑:旧实现用 state 里的计数器 n(首次计数、之后一律拦)。新实现改用时间戳 last_ts,
旧 state 文件里只有 n、没有 last_ts,必须把缺失的 last_ts 视为「很久以前」,
否则升级瞬间会把所有在跑的会话一次性拦死。
改动点(四处,均在 webfetch-guard/webfetch-guard.py):
# ① 头部文档:新增「2026-10-12 触发条件由按次数改为按连续窗口」一节(含 82% 推翻率数据)
# ② 新增常量:CONTINUOUS_WINDOW_SECONDS = 120
# ③ DENY_TEXT:文案改为「刚刚已抓过网页(不足 2 分钟)」
# ④ main():删去 n 计数分支,改为
# last = float(st.get("last_ts") or 0) # 无该键 → 视为久远,放行
# if fp in denied: 放行(allow_fp)
# if last and (now - last) < CONTINUOUS_WINDOW_SECONDS: deny 并记 last_ts
# 否则: 更新 last_ts 后放行
预期:见第 6 章「wf」系列用例全 PASS。 失败判断:若「旧状态(无 last_ts)」用例失败,说明升级瞬间会误拦既有会话,必须回滚重做。
步骤 4:同步离线用例(同批完成,避免仓库内用例与实际行为漂移)¶
# ① 新增本次验收脚本:hook-deny-tune-20261012/verify_guards.py(两脚本全覆盖,24 项)
# ② 修正 rule-acceptance-20261008/offline_verify_sched_guard.py:V2 期望改为 ["scontrol"];
# E1 断言由 allow 改为 deny;E2 由「bkill 去重」改为「原样重发放行」;E5 改为「只读查询不再触发」
# ③ 重写 webfetch-guard/tests_guard_cases.py:原文件描述的是 2026-10-08 升 deny 前的提醒式行为,
# 早与该脚本不符;本次按「连续窗口」重写为 7 项
6. 验证¶
三条命令,全部可复跑;verify_guards.py 须连跑两遍且结果一致(幂等复验)。
cd ~/Documents/services/token_proxy
python3 -m py_compile sched-guard/sched-guard.py webfetch-guard/webfetch-guard.py
python3 hook-deny-tune-20261012/verify_guards.py # 预期:共 24 项,失败 0 项
python3 hook-deny-tune-20261012/verify_guards.py # 第二遍须同结果
python3 rule-acceptance-20261008/offline_verify_sched_guard.py # 预期:全部通过(14 项)
python3 webfetch-guard/tests_guard_cases.py # 预期:全部通过(7 项)
隔离做法:verify_guards.py 把两个脚本各复制到临时目录再运行(STATE_DIR 随脚本位置推导),
因此不写真实 state/,也不污染 logs/guard_events.jsonl。
实测结果(2026-10-12):两遍均「共 24 项,失败 0 项」;两个既有用例均「全部通过」。
7. 回滚¶
备份位置:hook-deny-tune-20261012/backup/,逐条登记见 backup/README.md。
cd ~/Documents/services/token_proxy
cp -p hook-deny-tune-20261012/backup/sched-guard/sched-guard.py.bak.<时间戳> sched-guard/sched-guard.py
cp -p hook-deny-tune-20261012/backup/webfetch-guard/webfetch-guard.py.bak.<时间戳> webfetch-guard/webfetch-guard.py
回滚后验证:logs/guard_events.jsonl 中 sched-guard 的 S 事件应重新出现 squeue 类命中;
且需同步回滚用例(offline_verify_sched_guard.py 的 V2 / E5 断言与 tests_guard_cases.py 已按新行为改写,
回滚脚本后这两个文件会失败,属预期,须一并还原或改回旧断言)。
回滚触发条件(任一即回滚):
- 变更类调度命令(
srun/sbatch/scancel/bsub/bkill/scontrol update)未被拦, 且随后发生了对在跑作业节点的误操作; - webfetch-guard 在 120 秒窗口内未拦住连续抓取(
guard_events.jsonl中 W 的deny计数长期为 0 而 WebFetch 调用频繁)。
8. 风险说明¶
| 风险 | 说明 | 缓解 |
|---|---|---|
| 漏拦(sched) | 只读查询不再拦;若有人用 scontrol show node 查询,仍会被拦一次(保留 scontrol 的副作用) |
拦截文案已注明「查状态请用 sinfo / squeue」;确需执行原样重发即放行 |
| 漏拦(webfetch) | 跨轮的独立抓取不再拦,抓取页数可能上升 | 保留 120 秒窗口拦「连抓」;若成本上升可下调窗口或改回按次 |
| 升级瞬间误拦 | 旧 state 只有 n、无 last_ts |
代码把缺失 last_ts 视为久远;用例「wf 旧状态(无 last_ts)放行」专门守住这条 |
| 缺 session_id | webfetch 只提醒不拦(不变);sched 仍直接拒绝且无放行闭环(既有行为,本次未改动) | 另立一条待观察项,见第 9 章 |
| 用例漂移 | 仓库内旧用例与新行为不符时会一直失败 | 本次同批修正两个既有用例 |
9. 附带修正:docs/BASELINE.md 的 Hook 清单(2026-10-12,用户追加批准)¶
核实过程中发现 BASELINE.md §1 的清单已过期,且与同文件 §2 自相矛盾,用户裁定「顺手修掉」。改动三处:
| 位置 | 改前 | 改后 |
|---|---|---|
| §1「Hook 脚本」行 | 7 个脚本 / 10 条挂载,目录清单缺 4 个 | 11 个脚本 / 14 条挂载,目录清单列全 |
| §1「工程外写死路径」行 | hooks.json 的 7 条 $HOME 路径 |
11 条(每个脚本一条) |
| §2「全局 Hook(3 事件)」条 | 称 merge-guard 的 A/B/D/E/F/I 为强制拦截、WebFetch 仅提醒 |
改为当前 7 个强制守卫逐条列明;merge-guard 全部仅提醒,与 §1 一致 |
备份:backup/docs/BASELINE.md.bak.20261012-133120(改前 39,461 B,sha256 a059aeb1...)。
判据(可复跑,两遍须同结果):
cd ~/Documents/services/token_proxy
python3 hook-deny-tune-20261012/verify_baseline_list.py
10. 后续观察(未做项)¶
- 收窄后的实际误拦率与拦截频次:看
logs/guard_events.jsonl的 W / S 事件,settle 数日后回填优化总账。
来源:
/home/rain/Documents/services/token_proxy/hook-deny-tune-20261012/SOP.md| 最后更新:2026-10-12 13:32(由build/sync.py同步,请勿在站点侧修改)