b9533527cc
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
192 lines
9.8 KiB
Bash
Executable File
192 lines
9.8 KiB
Bash
Executable File
#!/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__*`,但那套工具
|
||
# **在雲端這台根本叫不出來**(ToolSearch:No 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: Edit(ISEP#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
|