跳转至

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"。其中两条判据不合理:

  1. sched-guard 语义错位:动词表把只读查询与变更动作混在一张表,于是本守卫要求的确认动作 (squeue -w <node> / bjobs -m <host>)自己也会被拦一次。按拦截文案去确认,反而被拦。
  2. webfetch-guard 按次数拦:本会话第 2 次及以后的 WebFetch 一律拦。取数 logs/guard_events.jsonl 实测 deny 11 次、其中 9 次被原样重发推翻(82%),每次推翻白多一次往返。 被推翻的正是「跨步骤的独立抓取需求」,不是判据想拦的「连续抓多页」。

2. 目标

改后应达到的可验证状态:

  1. sched-guard:命令含 squeue / sinfo / bjobs / bqueues / bhosts 时不再输出任何 hook 协议; 含 scontrol / srun / sbatch / scancel / bsub / bkill 时仍拦一次,原样重发即放行。
  2. webfetch-guard:距上一次抓取不足 120 秒的 WebFetch 拦一次(原样重发即放行); 距上次抓取超过 120 秒的独立抓取直接放行;本会话首抓放行。
  3. 两个守卫的「缺 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 同步,请勿在站点侧修改)