#!/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 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 ; 設定 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-(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 ]