Files
ISEP/hooks/browser-verify-guard.sh
T
Leo b9533527cc fix: 前端驗證閘先看這台有沒有瀏覽器能力,再要求用它(inkstone/ISEP#121)
browser-verify-guard.sh 舊版每回合都要求 mcp__Claude_Browser__*,但那套工具在
雲端這台叫不出來(ToolSearch:No matching deferred tools),容器內 chromium 也
連不出去(ERR_TUNNEL_CONNECTION_FAILED)⇒ 2026-09-01 一整天每回合誤攔一次,
在懲罰一個「這台辦不到」的要求。

閘現在先判斷「這個 session 有哪種瀏覽器能力」(正向存在性檢查,不是關鍵字):
- CF Browser Rendering:環境變數 CLOUDFLARE_API_TOKEN_YOULIN_CC_USE +
  CLOUDFLARE_ACCOUNT_ID_YOULIN 都在場(雲端首選)
- Browser MCP:這個 session 的 transcript 出現過 Claude_Browser__
- 容器內 chromium:ISEP#121 實測雲端連不出去,不當能力來源

判準是「有沒有用某種瀏覽器能力」這個動作:這回合叫過 Claude_Browser__(computer…)
或打過 .../browser-rendering/ 端點 ⇒ 放行。有能力卻沒用它 ⇒ 擋,且教實際可用的
那一種(CF 教 browser-rendering 端點的 curl,MCP 教三步)。一種能力都沒有 ⇒
訊息改成「缺什麼、怎麼補」(找 leo 幫 token 加 Browser Rendering: Edit/明說
「本次無前端」),且每個 session 只提醒一次(stamp),不再每回合誤攔。

測試 hooks/tests/browser-verify-guard.test.sh(14/14,全離線):
- C 群演練「一種能力都沒有」:第一次提醒一次、同 session 第二次放行(不再誤攔)
- B 群:有 CF 能力教 CF 不教 MCP;只有 MCP 能力教 MCP 不教 CF
- A 群:用過某種瀏覽器能力/明說本次無前端 ⇒ 放行;fail-open

docs/TESTING.md 加 A43;docs/hooks-inventory.md 更新該支說明(+0 支、+0 條、
腳本不變:62 支、88 條當場數出)。

版本待總管定(plugin.json 未動)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TGxitYq49FzYC7EFkbhzF5
2026-09-18 18:00:11 +00:00

192 lines
9.8 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/sh
# browser-verify-guard.sh — Stop hook:**宣稱驗過前端,卻沒有真的用瀏覽器載一次 → 擋**。
#
# 🔴 leo 2026-08-08:「**你的環境有 web,你應該用 web 驗,你已經開啓了卻沒有完成,
# 你要把這個列入規定。**」
#
# 病根(同日實撞,leo 抓到而不是我發現):
# 我「驗前端」的方式是 `curl | grep` 找一個字串——文案在就宣稱通過。
# 但 **curl 拿到的是 HTML 原始碼:它不執行 JS、不載入 config.js、不發 API 請求**。
# 於是使用者真正看到的整條紅色錯誤
# 「設定檔沒載入(config.js),這個頁面連不到你的服務」,**我一次都沒看到**。
# ⇒ 判準升級:`HTTP 200 不算驗過` → `grep 到字串也不算驗過`
# → **只有「瀏覽器載入後看起來能用」才算**。
#
# ── inkstone/ISEP#121:閘要先看「這台有沒有瀏覽器能力」,再要求用它 ──────
# 2026-09-01 實撞:這支閘每回合都要求 `mcp__Claude_Browser__*`,但那套工具
# **在雲端這台根本叫不出來**ToolSearchNo matching deferred tools found),
# 容器內 chromium 也連不出去(ERR_TUNNEL_CONNECTION_FAILED)。
# ⇒ 它一整天每回合都在懲罰一個「這台辦不到」的要求。
#
# 正解不是取消驗證,是**先判斷這個 session 有哪種瀏覽器能力,再教對應的那一種**:
# · CF Browser Rendering(雲端首選)= 環境變數 CLOUDFLARE_API_TOKEN_YOULIN_CC_USE
# CLOUDFLARE_ACCOUNT_ID_YOULIN 都在場(token 需 Browser Rendering: Edit
# · Browser MCP 這個 session 的 transcript 出現過 Claude_Browser__* 工具
# · 容器內 chromium ISEP#121 實測雲端網路被擋,不可靠 → 不當能力來源
#
# 有能力才要求用它(且教實際可用的那一種);**一種都沒有時**,訊息改成
# 「缺什麼、怎麼補」,且**每個 session 只提醒一次**(stamp)——不再每回合誤攔。
#
# ── 判準是「有沒有用瀏覽器能力」這個動作,不是措辭 ──────────────────────
# 驗過的證據(正向存在性檢查,任一種即放行):
# · 這回合叫過 Claude_Browser__(preview_start|navigate|computer|…)
# · 這回合打過 CF 的 .../browser-rendering/... 端點
#
# 逃生口:這次真的沒有前端可驗(純後端/純腳本)⇒ 在回覆裡明說「本次無前端」,
# 本 hook 認得這句話就放行——留痕,不是默默略過。
set -eu
INPUT="$(cat 2>/dev/null || echo "")"
[ -z "$INPUT" ] && exit 0
# 這回合的文字+工具名(最後 40 則)
TURN="$(printf '%s' "$INPUT" | python3 -c '
import sys, json
buf = []
try:
d = json.load(sys.stdin)
t = d.get("transcript_path") or ""
if t:
import os
if os.path.exists(t):
for line in open(t, encoding="utf-8", errors="ignore").readlines()[-40:]:
try:
m = json.loads(line)
except Exception:
continue
c = (m.get("message") or {}).get("content")
if isinstance(c, list):
for b in c:
if not isinstance(b, dict):
continue
if b.get("type") == "text":
buf.append(b.get("text", ""))
elif b.get("type") == "tool_use":
buf.append(b.get("name", ""))
buf.append(json.dumps(b.get("input", {}), ensure_ascii=False))
elif isinstance(c, str):
buf.append(c)
except Exception:
pass
print("\n".join(buf))
' 2>/dev/null || echo "")"
[ -z "$TURN" ] && exit 0
# 整個 session 的 transcript 路徑與 session_id(能力偵測與 stamp 用)
TP="$(printf '%s' "$INPUT" | python3 -c 'import sys,json
try: print(json.load(sys.stdin).get("transcript_path") or "")
except Exception: print("")' 2>/dev/null || echo "")"
SID="$(printf '%s' "$INPUT" | python3 -c 'import sys,json,re
try:
s=str(json.load(sys.stdin).get("session_id") or "nosid")
print(re.sub(r"[^A-Za-z0-9_.-]","_",s)[:64])
except Exception: print("nosid")' 2>/dev/null || echo "nosid")"
# ── 這回合有沒有「前端/畫面驗過」的宣稱?沒有就與本閘無關 ──
printf '%s' "$TURN" | grep -qiE '前端(驗|測|複驗|確認)|畫面(驗|確認|複驗)|用戶.{0,6}看到的|驗前端|抓實際畫面' || exit 0
# ── 逃生口:明說沒有前端 ⇒ 放行(留痕) ──
printf '%s' "$TURN" | grep -qE '本次無前端|沒有前端可驗|純後端|不涉及前端' && exit 0
# ── 驗過的證據:這回合真的用過某種瀏覽器能力(正向存在性檢查)──
printf '%s' "$TURN" | grep -qE 'Claude_Browser__(preview_start|navigate|computer|get_page_text|read_console_messages|snapshot)' && exit 0
printf '%s' "$TURN" | grep -qE 'browser-rendering/' && exit 0 # CF Browser Rendering 端點
# ── 這個 session 有哪種瀏覽器能力?(正向存在性檢查)──
CF_CAP=0
[ -n "${CLOUDFLARE_API_TOKEN_YOULIN_CC_USE:-}" ] && [ -n "${CLOUDFLARE_ACCOUNT_ID_YOULIN:-}" ] && CF_CAP=1
MCP_CAP=0
if [ -n "$TP" ] && [ -f "$TP" ]; then
grep -q 'Claude_Browser__' "$TP" 2>/dev/null && MCP_CAP=1
fi
# ── 一種能力都沒有:不能要求做不到的事 → 每個 session 只提醒一次「缺什麼、怎麼補」──
if [ "$CF_CAP" -eq 0 ] && [ "$MCP_CAP" -eq 0 ]; then
STAMP="/tmp/.browser-cap-warned-$SID"
[ -f "$STAMP" ] && exit 0 # 這個 session 已經提醒過 ⇒ 不再誤攔
: > "$STAMP" 2>/dev/null || true
cat >&2 <<'EOF'
🖥️ 前端驗證警察:你宣稱驗了畫面,但**這個 session 一種瀏覽器能力都沒有**——
沒有能力就無法看畫面,所以問題不是「你沒驗」,是「這台缺瀏覽器能力,去補」。
【inkstone/ISEP#121】閘要先看這台有沒有瀏覽器能力,才不會要求你做一件辦不到的事。
補齊任一種即可看畫面(雲端首選第①種):
① CF Browser Rendering ── 需要兩個環境變數在場,且 token 要有 Browser Rendering: Edit
CLOUDFLARE_API_TOKEN_YOULIN_CC_USE
CLOUDFLARE_ACCOUNT_ID_YOULIN
缺權限就通知 leo 幫該 token 加 Browser Rendering: EditISEP#115 就是這樣解開的)。
② Browser MCP ── mcp__Claude_Browser__*(本機通常有;雲端這台叫不出來)。
③ 容器內 chromium ── ISEP#121 實測雲端網路被擋(ERR_TUNNEL_CONNECTION_FAILED),
不可靠,別依賴它當唯一驗法。
在補上能力之前:這次若真的沒有前端可驗(純後端/純腳本),
在回覆裡明說「本次無前端」即放行。
本閘每個 session 只在這裡提醒一次,不會每回合攔你(inkstone/ISEP#121)。
EOF
exit 2
fi
# ── 有能力卻沒用它 → 擋,並教「這台實際可用的那一種」──
if [ "$CF_CAP" -eq 1 ]; then
cat >&2 <<'EOF'
🖥️ 前端驗證警察:你宣稱驗了畫面,但**這回合沒有用過瀏覽器**。
【leo 2026-08-08】「**你的環境有 web,你應該用 web 驗,你已經開啓了卻沒有完成。**」
【為什麼 curl | grep 不算】curl 拿到的是 **HTML 原始碼**——
它**不執行 JS、不載入 config.js、不發 API 請求**。
08-08 實撞:我 grep 到文案就說「前端驗過」,
而使用者實際看到的是整條紅色「設定檔沒載入(config.js)」——**那個畫面我一次都沒看到**。
這台有 CF Browser Rendering(雲端首選)。用它看畫面,不是看原始碼:
# 跑完 JS 的 DOM(能看到 config.js 載入後的真實內容)
curl -sS -X POST \
"https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID_YOULIN}/browser-rendering/content" \
-H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN_YOULIN_CC_USE}" \
-H "Content-Type: application/json" \
--data '{"url":"<用戶會走的網址>"}'
# 截圖(用戶會看到的那個畫面)
curl -sS -X POST \
"https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID_YOULIN}/browser-rendering/screenshot" \
-H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN_YOULIN_CC_USE}" \
-H "Content-Type: application/json" \
--data '{"url":"<用戶會走的網址>"}' --output /tmp/screen.png
要看的是:有沒有錯誤橫幅/資料是不是還卡在「載入中…」/DOM 裡有沒有紅字錯誤。
⚠️ 順帶:用 curl 查線上狀態時,`?cb=$RANDOM` **繞不掉 Cloudflare 邊緣快取**——
要加 `-H "Cache-Control: no-cache"`,否則你可能是在驗快取而不是驗線上。
本次真的沒有前端可驗 ⇒ 在回覆裡明說「本次無前端」即放行。
EOF
exit 2
fi
# MCP_CAP == 1(且沒有 CF):教 Browser MCP
cat >&2 <<'EOF'
🖥️ 前端驗證警察:你宣稱驗了畫面,但**這回合沒有用過瀏覽器**。
【leo 2026-08-08】「**你的環境有 web,你應該用 web 驗,你已經開啓了卻沒有完成。**」
【為什麼 curl | grep 不算】curl 拿到的是 **HTML 原始碼**——
它**不執行 JS、不載入 config.js、不發 API 請求**。
這台有 Browser MCP。改用這三步(看畫面,不是看原始碼):
mcp__Claude_Browser__preview_start {url: "<用戶會走的網址>"}
mcp__Claude_Browser__computer {action: "screenshot"}
mcp__Claude_Browser__read_console_messages {onlyErrors: true}
要看的是:有沒有錯誤橫幅/資料是不是還卡在「載入中…」/console 有沒有紅字。
⚠️ 順帶:用 curl 查線上狀態時,`?cb=$RANDOM` **繞不掉 Cloudflare 邊緣快取**——
要加 `-H "Cache-Control: no-cache"`,否則你可能是在驗快取而不是驗線上。
本次真的沒有前端可驗 ⇒ 在回覆裡明說「本次無前端」即放行。
EOF
exit 2