dda4852938
在此之前 36 支閘裡只有 2 支會記錄自己擋了什麼,六種逃生口 (NOT_MY_BRANCH_OK=1/no-ticket-needed/wiki-secret-ok/credential-ok/ solo-ok/kbdb-down)用了完全不留痕 ⇒ 擋對 100 次跟擋錯 100 次在資料上 一模一樣 ⇒ 規則只會累積不會被淘汰。 機制:hooks/lib/gate-log-wrap.sh 包在每一支閘外面(hooks.json 的 82 條 gate command 全部前綴這支 wrapper;2 條 scripts/ refresher 不是閘、不包)。 它從不 exit 2,只把真正那支閘的離開碼原樣傳出去 ⇒ 記錄壞掉最多漏記一筆, 不可能誤攔(票紅線:寧可漏記,不可擋錯)。每次留一筆中繼資料: 哪支閘/何時/對什麼工具/判成什麼/(若繞過)用了哪個逃生口。 - 只記中繼資料,完全不寫入指令原文或 payload ⇒ 金鑰在結構上進不了帳本(D36) - 純 POSIX shell 記錄,不另起 python ⇒ 不讓閘變慢變脆 - 紀錄長在既有閘動作上,零輪詢/零 cron(D20) - 帳本沿用 milestone-account 慣例(不寫進會被 plugin update 換掉的目錄) 報告:scripts/gate-report all 回答票上三題(誤擋嫌疑排序/最近沒擋過的規則/ 同一 session 反覆撞同一支閘)。基線與反面驗證見票留言。 測試:hooks/tests/gate-log.test.sh 22 條(透明/留痕/反面驗證/不變脆/不外洩) + scripts/test-gate-report.sh 8 條(三題數字對不對)。 hooks 檔數 61、hooks.json 註冊 84 均不變(wrapper 住 hooks/lib,非 gate .sh)。 版本:待總管定版(改了會被載入的東西需升版才傳得到,但 tag 由總管打)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TNfNypeDaCVEZQW1Wqwfxq
145 lines
8.6 KiB
Bash
Executable File
145 lines
8.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# gate-log-wrap.sh 的迴歸測試(inkstone/InkStoneCo#48)
|
||
#
|
||
# A 群「透明」——包在外面絕不改變閘的行為:離開碼、stdout、stderr、stdin 全部原樣穿過。
|
||
# 🔴 這是最高紅線:寧可漏記一筆,不可擋錯一次。
|
||
# B 群「留痕」——擋(2)/放行(0)/被逃生口繞過,各留一筆,欄位答得出票上要的五件事。
|
||
# C 群「反面驗證」——故意用一次逃生口,那一筆一定要以 verdict=bypass 出現(票上驗收 #4)。
|
||
# D 群「不變脆」——記錄失敗(帳本目錄不可寫)絕不影響閘的離開碼。
|
||
# E 群「不外洩」——payload 帶金鑰時,帳本裡不得出現金鑰真身(D36)。
|
||
#
|
||
# 全程離線:帳本走 ISEP_GATE_LOG 指到 mktemp 的檔,不打網路、不碰真實帳本。
|
||
set -u
|
||
WRAP="${1:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../lib" && pwd)/gate-log-wrap.sh}"
|
||
TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT
|
||
LED="$TMP/gate-log.jsonl"; : > "$LED"
|
||
PASS=0; FAIL=0; N=0
|
||
|
||
ok() { N=$((N+1)); printf ' ✅ %s\n' "$1"; PASS=$((PASS+1)); }
|
||
bad() { N=$((N+1)); printf ' ❌ %s\n' "$1"; FAIL=$((FAIL+1)); shift; for l in "$@"; do printf ' %s\n' "$l"; done; }
|
||
|
||
# ── 造幾支假閘:行為可控,用來驗「包在外面有沒有原樣穿過」──────────────────
|
||
mkgate() { # mkgate <name> <exit> <stdout> <stderr>
|
||
local f="$TMP/$1.sh"
|
||
{
|
||
echo '#!/bin/sh'
|
||
echo 'IN=$(cat)' # 讀 stdin
|
||
printf 'printf "%%s" "%s"\n' "$3" # 印到 stdout
|
||
printf 'printf "%%s" "%s" >&2\n' "$4" # 印到 stderr
|
||
printf 'printf "STDIN=[%%s]" "$IN" >&2\n' # 把收到的 stdin 回吐到 stderr 供比對
|
||
printf 'exit %s\n' "$2"
|
||
} > "$f"
|
||
chmod +x "$f"; printf '%s' "$f"
|
||
}
|
||
|
||
run() { # run <gate-file> <payload> ; 設定 OUT/ERR/RC/last log line
|
||
local gate="$1" payload="$2"
|
||
ERRF="$TMP/err"; OUTF="$TMP/out"
|
||
printf '%s' "$payload" | ISEP_GATE_LOG="$LED" "$WRAP" "$gate" >"$OUTF" 2>"$ERRF"
|
||
RC=$?; OUT=$(cat "$OUTF"); ERR=$(cat "$ERRF")
|
||
LOGLINE=$(tail -n1 "$LED" 2>/dev/null)
|
||
}
|
||
|
||
jget() { printf '%s' "$1" | python3 -c "import json,sys;print(json.load(sys.stdin).get('$2',''))" 2>/dev/null; }
|
||
|
||
echo "── A 群:透明——閘的行為原樣穿過(寧可漏記,不可擋錯)───────"
|
||
|
||
G=$(mkgate blockgate 2 "" "擋你了")
|
||
run "$G" '{"hook_event_name":"PreToolUse","tool_name":"Bash","session_id":"s1"}'
|
||
[ "$RC" = 2 ] && ok "① 閘 exit 2 → 包完仍 exit 2(不會把擋變成放行,也不會把放行變成擋)" \
|
||
|| bad "① 期望 rc=2 得 $RC"
|
||
|
||
G=$(mkgate passgate 0 "" "")
|
||
run "$G" '{"hook_event_name":"PreToolUse","tool_name":"Bash","session_id":"s1"}'
|
||
[ "$RC" = 0 ] && ok "② 閘 exit 0 → 包完仍 exit 0" || bad "② 期望 rc=0 得 $RC"
|
||
|
||
G=$(mkgate outgate 0 "注入的context" "")
|
||
run "$G" '{"hook_event_name":"UserPromptSubmit","session_id":"s1"}'
|
||
[ "$OUT" = "注入的context" ] && ok "③ 閘的 stdout 原樣穿過(SessionStart/UserPromptSubmit 靠它注入)" \
|
||
|| bad "③ stdout 沒穿過" "得:[$OUT]"
|
||
|
||
G=$(mkgate errgate 2 "" "紅字說明")
|
||
run "$G" '{"hook_event_name":"PreToolUse","tool_name":"Bash","session_id":"s1"}'
|
||
case "$ERR" in *"紅字說明"*) ok "④ 閘的 stderr 原樣穿過(被擋的人看得到為什麼)" ;; *) bad "④ stderr 沒穿過" "得:[$ERR]" ;; esac
|
||
|
||
G=$(mkgate echogate 0 "" "")
|
||
run "$G" '{"tool_name":"Bash","session_id":"s1","tool_input":{"command":"樣本指令XYZ"}}'
|
||
case "$ERR" in *"樣本指令XYZ"*) ok "⑤ stdin 原樣餵給閘(閘拿到的 payload 一字不差)" ;; *) bad "⑤ stdin 沒穿過" "得:[$ERR]" ;; esac
|
||
|
||
# 沒給閘路徑:不擋、不報錯
|
||
printf '%s' '{}' | ISEP_GATE_LOG="$LED" "$WRAP" >/dev/null 2>&1
|
||
[ $? = 0 ] && ok "⑥ 沒給閘路徑 → exit 0(自己絕不擋、絕不報錯)" || bad "⑥ 空參數應 exit 0"
|
||
|
||
echo "── B 群:留痕——三種 verdict 各留一筆,欄位齊全 ──────────────"
|
||
|
||
G=$(mkgate github-contact-guard 2 "" "x")
|
||
run "$G" '{"hook_event_name":"PreToolUse","tool_name":"Bash","session_id":"sess-AAA"}'
|
||
[ "$(jget "$LOGLINE" verdict)" = block ] && ok "⑦ 擋下 → verdict=block" || bad "⑦ verdict 不是 block" "$LOGLINE"
|
||
[ "$(jget "$LOGLINE" gate)" = github-contact-guard ] && ok "⑧ 記得下是哪支閘" || bad "⑧ gate 欄錯" "$LOGLINE"
|
||
[ "$(jget "$LOGLINE" tool)" = Bash ] && ok "⑨ 記得下對什麼工具動作" || bad "⑨ tool 欄錯" "$LOGLINE"
|
||
[ "$(jget "$LOGLINE" sid)" = sess-AAA ] && ok "⑩ 記得下哪個 session(Q3 要用)" || bad "⑩ sid 欄錯" "$LOGLINE"
|
||
[ -n "$(jget "$LOGLINE" ts)" ] && ok "⑪ 記得下什麼時候" || bad "⑪ ts 欄空" "$LOGLINE"
|
||
|
||
G=$(mkgate someguard 0 "" "")
|
||
run "$G" '{"tool_name":"Edit","session_id":"s2"}'
|
||
[ "$(jget "$LOGLINE" verdict)" = pass ] && ok "⑫ 放行 → verdict=pass(活著但沒擋,Q2 靠它)" || bad "⑫ 放行沒記成 pass" "$LOGLINE"
|
||
|
||
echo "── C 群:反面驗證——故意用逃生口,一定要記成 bypass(驗收#4)─"
|
||
|
||
# credential-only-guard 的逃生口 = 內容裡有 credential-ok,且閘放行(0)
|
||
G=$(mkgate credential-only-guard 0 "" "")
|
||
run "$G" '{"tool_name":"Write","session_id":"s3","tool_input":{"new_string":"token=x # credential-ok"}}'
|
||
[ "$(jget "$LOGLINE" verdict)" = bypass ] && ok "⑬ credential-ok 放行 → verdict=bypass" || bad "⑬ 逃生口沒記成 bypass" "$LOGLINE"
|
||
[ "$(jget "$LOGLINE" bypass)" = credential-ok ] && ok "⑭ 記得下用了哪個逃生口(理由)" || bad "⑭ bypass 理由欄錯" "$LOGLINE"
|
||
|
||
# NOT_MY_BRANCH_OK=1 env(owner = not-my-branch-guard),閘放行
|
||
G=$(mkgate not-my-branch-guard 0 "" "")
|
||
printf '%s' '{"tool_name":"Bash","session_id":"s3"}' | NOT_MY_BRANCH_OK=1 ISEP_GATE_LOG="$LED" "$WRAP" "$G" >/dev/null 2>&1
|
||
L=$(tail -n1 "$LED")
|
||
[ "$(jget "$L" bypass)" = NOT_MY_BRANCH_OK ] && ok "⑮ 環境變數逃生口 NOT_MY_BRANCH_OK 也記得下" || bad "⑮ env 逃生口沒記" "$L"
|
||
|
||
# solo-ok = 檔案 /tmp/.solo-ok-<sid>(owner = subagent-first-guard)
|
||
SID="solo-test-$$"; touch "/tmp/.solo-ok-$SID"
|
||
G=$(mkgate subagent-first-guard 0 "" "")
|
||
run "$G" "{\"tool_name\":\"Agent\",\"session_id\":\"$SID\"}"
|
||
[ "$(jget "$LOGLINE" bypass)" = solo-ok ] && ok "⑯ 檔案式逃生口 solo-ok 也記得下" || bad "⑯ solo-ok 沒記" "$LOGLINE"
|
||
rm -f "/tmp/.solo-ok-$SID"
|
||
|
||
# 逃生口只有在「放行」時才算 bypass:同一個 token 但閘照樣擋(2) → 維持 block
|
||
G=$(mkgate credential-only-guard 2 "" "x")
|
||
run "$G" '{"tool_name":"Write","session_id":"s3","tool_input":{"new_string":"# credential-ok"}}'
|
||
[ "$(jget "$LOGLINE" verdict)" = block ] && ok "⑰ token 在場但閘照樣擋 → 維持 block(逃生口沒生效不算繞過)" || bad "⑰ 錯記成 bypass" "$LOGLINE"
|
||
|
||
# owner-scoped:別支閘的指令裡剛好有 credential-ok,不該被記成它繞過
|
||
G=$(mkgate github-contact-guard 0 "" "")
|
||
run "$G" '{"tool_name":"Bash","session_id":"s3","tool_input":{"command":"echo credential-ok"}}'
|
||
[ "$(jget "$LOGLINE" verdict)" = pass ] && ok "⑱ 非擁有者的閘看到別人的 token 字樣 → 仍是 pass(不誤記)" || bad "⑱ 誤記成 bypass" "$LOGLINE"
|
||
|
||
echo "── D 群:不變脆——記錄失敗不影響閘的離開碼 ──────────────────"
|
||
|
||
G=$(mkgate blockgate2 2 "" "x")
|
||
printf '%s' '{"tool_name":"Bash","session_id":"s4"}' \
|
||
| ISEP_GATE_LOG="/this/dir/does/not/exist/nope.jsonl" "$WRAP" "$G" >/dev/null 2>&1
|
||
[ $? = 2 ] && ok "⑲ 帳本目錄不可寫 → 閘照樣 exit 2(記錄壞掉不擋錯)" || bad "⑲ 記錄失敗改變了 rc"
|
||
|
||
G=$(mkgate passgate2 0 "" "")
|
||
printf '%s' '{"tool_name":"Bash","session_id":"s4"}' \
|
||
| ISEP_GATE_LOG="/dev/null" "$WRAP" "$G" >/dev/null 2>&1
|
||
[ $? = 0 ] && ok "⑳ 明示停用(/dev/null)→ 閘照樣放行、不留痕" || bad "⑳ /dev/null 改變了 rc"
|
||
|
||
echo "── E 群:不外洩——payload 帶金鑰,帳本裡不得出現金鑰真身(D36)─"
|
||
|
||
SECRET="ghp_ABCDEF1234567890abcdef1234567890XYZ"
|
||
G=$(mkgate github-contact-guard 2 "" "x")
|
||
run "$G" "{\"tool_name\":\"Bash\",\"session_id\":\"s5\",\"tool_input\":{\"command\":\"curl -H 'Authorization: Bearer $SECRET' x\"}}"
|
||
if grep -qF "$SECRET" "$LED"; then bad "㉑ 金鑰真身出現在帳本裡(D36 破口)" "$LOGLINE"; else ok "㉑ 帳本只有中繼資料,金鑰真身進不去(D36)"; fi
|
||
|
||
# 每一行都是合法 JSON
|
||
BADJSON=0
|
||
while IFS= read -r l; do [ -z "$l" ] && continue; printf '%s' "$l" | python3 -c 'import json,sys;json.load(sys.stdin)' 2>/dev/null || BADJSON=$((BADJSON+1)); done < "$LED"
|
||
[ "$BADJSON" = 0 ] && ok "㉒ 帳本每一行都是合法 JSON" || bad "㉒ 有 $BADJSON 行不是合法 JSON"
|
||
|
||
echo ""
|
||
echo "通過 $PASS 條,失敗 $FAIL 條(共 $N)"
|
||
[ "$FAIL" = 0 ]
|