SOP:守卫补强三件(G/H 真机验证 · webfetch-guard 落盘 · 新增判据 K)¶
主题目录:
guards-strengthen-20261007/|变更日期:2026-10-07|起因:用户问「有没挡住需要补强制的项」
1. 背景¶
盘点 logs/guard_events.jsonl(全量 56 条)时发现三处缺口:
glob-grep-guard的判据 G/H 声明为强制,却零触发记录——脚本本身会落盘,所以不是"不写日志", 而是从未命中。无法区分"没违规场景"与"matcher 静默失效"(LSmatcher 10-07 才加进hooks.json, 加完从未验证);webfetch-guard不落guard_events——这是个"提醒式"守卫(只additionalContext、不阻断), 但它触发过几次完全不可知,既无法判断是否在跑,也无法按数据决定是否该升级;ctx-scout漏用无任何实时手段——MB §二十六 D 组连续多轮记「漏用 1」,全靠自觉。 与判据 A 的分工:A 管「合并成一条消息」(省往返),漏用管的是「该不该外派」(省主上下文 token)。
2. 目标¶
- 把 G/H 从"声明"变成"事实":真机构造一次违规调用,确认被拦;
- 给
webfetch-guard落盘(与其它三个守卫同格式),使其触发次数可统计; - 新增判据 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同步,请勿在站点侧修改)