跳转至

SOP:守卫补强三件(G/H 真机验证 · webfetch-guard 落盘 · 新增判据 K)

主题目录:guards-strengthen-20261007/|变更日期:2026-10-07|起因:用户问「有没挡住需要补强制的项」

1. 背景

盘点 logs/guard_events.jsonl(全量 56 条)时发现三处缺口:

  1. glob-grep-guard 的判据 G/H 声明为强制,却零触发记录——脚本本身会落盘,所以不是"不写日志", 而是从未命中。无法区分"没违规场景"与"matcher 静默失效"(LS matcher 10-07 才加进 hooks.json, 加完从未验证);
  2. webfetch-guard 不落 guard_events——这是个"提醒式"守卫(只 additionalContext、不阻断), 但它触发过几次完全不可知,既无法判断是否在跑,也无法按数据决定是否该升级;
  3. ctx-scout 漏用无任何实时手段——MB §二十六 D 组连续多轮记「漏用 1」,全靠自觉。 与判据 A 的分工:A 管「合并成一条消息」(省往返),漏用管的是「该不该外派」(省主上下文 token)。

2. 目标

  1. 把 G/H 从"声明"变成"事实":真机构造一次违规调用,确认被拦;
  2. 给 webfetch-guard 落盘(与其它三个守卫同格式),使其触发次数可统计;
  3. 新增判据 K(仅提醒):滑动窗口内只读检索达阈值 → 提醒「可整包外派 ctx-scout」, 每会话至多一次、不占 INJECT_MAX。

3. 前置条件

  • 可写 glob-grep-guard/、webfetch-guard/、merge-guard/ 三个脚本;
  • Hook 脚本改动即时生效(10-07 实测),故每步都先离线验证再交;
  • 验证 G/H 会故意触发拦截(自伤一次),属必要代价。

4. 影响范围

项 说明
涉及对象 三个守卫脚本(glob-grep-guard 仅验证、不改;webfetch-guard 加落盘;merge-guard 加判据 K)
是否停服 否;无窗口要求,改完下一次调用即生效
行为变化 ① G/H 行为不变(仅确认生效);② webfetch-guard 每次注入多写一行日志;③ 新增 K 提醒(每会话最多一次、约 70 字)
不受影响 A/B/D/E/F/I/J 七条强制判据、指纹放行、DENY_MAX / INJECT_MAX 上限

5. 分步操作

步骤 1:备份(必做)

TS=$(date +%Y%m%d-%H%M%S)
D=~/Documents/services/token_proxy/guards-strengthen-20261007/backup/home/rain/Documents/services/token_proxy
mkdir -p "$D/glob-grep-guard" "$D/webfetch-guard" "$D/merge-guard"
cp -p ~/Documents/services/token_proxy/glob-grep-guard/glob-grep-guard.py "$D/glob-grep-guard/glob-grep-guard.py.bak.$TS"
cp -p ~/Documents/services/token_proxy/webfetch-guard/webfetch-guard.py   "$D/webfetch-guard/webfetch-guard.py.bak.$TS"
cp -p ~/Documents/services/token_proxy/merge-guard/merge-guard.py         "$D/merge-guard/merge-guard.py.bak.$TS"

步骤 2:验证 G/H(真机,故意触发一次)

先离线确认判据逻辑,再真机确认装配(matcher + hooks.json)。

cd ~/Documents/services/token_proxy && python3 - <<'PY'
import importlib.util, os
s = importlib.util.spec_from_file_location("gg", "glob-grep-guard/glob-grep-guard.py")
g = importlib.util.module_from_spec(s); s.loader.exec_module(g)
P = os.path.expanduser("~/Documents/services/token_proxy")
print("G 空 path ->", g._is_shallow_ls("LS", {}))                      # True
print("G 项目根   ->", g._is_shallow_ls("LS", {"path": P}))            # True
print("G docs 子目录 ->", g._is_shallow_ls("LS", {"path": P + "/docs"}))  # False
print("H **/*.md 无 path ->", g._is_unbounded("Glob", {"pattern": "**/*.md"}))   # True
print("H docs/**/*.md  ->", g._is_unbounded("Glob", {"pattern": "docs/**/*.md"}))  # False
PY

预期:四个 True / False 如上。随后真机各发一次违规调用(LS 指向项目根、Glob 用 **/*.md 不带 path), 应收到 deny 并给出「强制检索约束·已拦截」。

坑:验证用的这两次调用会被拒绝——不要原样重发(那会真的执行并可能输出数万字符)。 被拒不等于失效,恰恰是生效的证据。

步骤 3:webfetch-guard 补落盘

坑:emit() 是唯一的注入出口,落盘要放在 emit 里(而非 main 的两处调用点), 否则"缺 session_id 直接 emit"那条分支会漏记。

cd ~/Documents/services/token_proxy
python3 -m py_compile webfetch-guard/webfetch-guard.py && echo "语法通过"
echo '{"hook_event_name":"PreToolUse","tool_name":"WebFetch","session_id":"T1","agent_id":"a","tool_input":{"url":"https://example.com/x"}}' \
  | python3 webfetch-guard/webfetch-guard.py > /dev/null
tail -1 logs/guard_events.jsonl     # 预期:hook=webfetch-guard, mark=W, action=remind

验证后清理测试行(grep -v '"session": "T1"' > tmp && mv)并删 webfetch-guard/state/T1__a。

步骤 4:新增判据 K

在 merge-guard.py 加三处:常量(K_WINDOW_SEC=600 / K_READ_LIKE_MIN=5 / K_KEEP_TIMES=20)、 PROMPTS["K"]、主流程的滑动窗口逻辑;状态新增 read_ts 与 k_fired 两个键。

坑:用滑动窗口(read_ts 时间戳)而非会话累计——后者会把几小时前的检索算进来, 让"这批检索该不该外派"的判断失准。且 K 不占 INJECT_MAX(它管的是另一件事)但与 A~J 同用一套 emit/异常兜底。

cd ~/Documents/services/token_proxy && python3 -m py_compile merge-guard/merge-guard.py && echo "语法通过"
python3 - <<'PY'
import json, subprocess, sys
H, SID = "merge-guard/merge-guard.py", "T2"
def run(i):
    e = {"hook_event_name": "PreToolUse", "session_id": SID, "agent_id": "a",
         "tool_name": "Read", "tool_input": {"file_path": "/tmp/k%d.txt" % i}}
    return subprocess.run([sys.executable, H], input=json.dumps(e),
                          capture_output=True, text=True).stdout
for i in range(1, 7):
    out = run(i)
    print("第 %d 次只读检索 -> %s" % (i, "含外派提醒" if "外派提醒" in out else "静默"))
PY

预期:前 4 次静默、第 5 次出现外派提醒、第 6 次静默(k_fired 已置位)。 验证后清理 guard_events 里的 T2 行与 merge-guard/state/T2__a。

6. 验证

层级 命令 预期
语法 py_compile 三个脚本 无输出
G/H 判据逻辑 步骤 2 的离线断言 四个 True/False 如上
G/H 真机 各发一次违规调用 均收到 deny + 「强制检索约束·已拦截」
webfetch 落盘 步骤 3 多一行 mark=W
K 判据 步骤 4 第 5 次触发、第 6 次静默
清理 grep -c '"session": "T' logs/guard_events.jsonl 0

7. 回滚

备份位置:guards-strengthen-20261007/backup/home/rain/Documents/services/token_proxy/{glob-grep-guard,webfetch-guard,merge-guard}/, 台账见 backup/README.md。

B=~/Documents/services/token_proxy/guards-strengthen-20261007/backup/home/rain/Documents/services/token_proxy
cp -p "$B/webfetch-guard/webfetch-guard.py.bak.20261007-081507" ~/Documents/services/token_proxy/webfetch-guard/webfetch-guard.py
cp -p "$B/merge-guard/merge-guard.py.bak.20261007-081507"       ~/Documents/services/token_proxy/merge-guard/merge-guard.py
python3 -m py_compile ~/Documents/services/token_proxy/merge-guard/merge-guard.py && echo "回滚后语法通过"

回滚后验证:webfetch-guard 不再写 guard_events;merge-guard 的 grep -c "K_READ_LIKE_MIN" 为 0(判据 K 消失)。 glob-grep-guard 无需回滚(只验证未改动)。

8. 风险说明

风险 缓解
验证 G/H 时若未命中,调用会真执行并输出大量内容 先离线确认判据逻辑;真机只各发一次、不重发
K 引入噪声(提醒文本每轮重放) 每会话至多一次、约 70 字;阈值待样本校准(总账可挂观察项)
K 与 A 抢同一批调用 A 优先(本批已触发 A 时 K 不注入);且两者都属"合并/外派"同一目标
滑动窗口用 time.time()(非单调钟) 只用于 600 s 量级窗口比较,系统时钟跳变影响可忽略
状态文件新增键破坏旧 state 读取一律 .get() 带默认值,旧 state 可正常解析

附录:交付物清单

文件 说明
webfetch-guard/webfetch-guard.py(改) GUARD_LOG + _log_event(),emit(event) 落盘
merge-guard/merge-guard.py(改) 判据 K(常量 / 提示 / 滑动窗口逻辑 / read_ts+k_fired 状态键)
guards-strengthen-20261007/SOP.md 本文档
guards-strengthen-20261007/backup/ 三个脚本备份与台账

来源:/home/rain/Documents/services/token_proxy/guards-strengthen-20261007/SOP.md | 最后更新:2026-10-07 08:17(由 build/sync.py 同步,请勿在站点侧修改)