#!/bin/sh # gate-log-wrap.sh — 透明記錄層:包在每一支閘外面,把「這支閘這一次判成什麼」留一筆。 # (inkstone/InkStoneCo#48:36 支閘對著虛空開火,沒有任何資料 # 說得出任一支擋對幾次、擋錯幾次、被逃生口繞過幾次。) # # ── 它解什麼 ──────────────────────────────────────────────────────────────── # 在此之前只有 2 支閘會記錄自己擋了什麼,六種逃生口(NOT_MY_BRANCH_OK=1/ # no-ticket-needed/wiki-secret-ok/credential-ok/solo-ok/kbdb-down)用了**完全不留痕**。 # ⇒ 一條閘擋對 100 次跟擋錯 100 次,在資料上長得一模一樣 ⇒ 規則只會累積不會被淘汰。 # 這一層讓「規則有沒有在運作」變成查得出來的事實:每一次閘動作(擋/放行/繞過)留一筆。 # # ── 為什麼是「包在外面」而不是改 61 支閘的內部 ────────────────────────────── # ① 一份機制,一處測——不是散進 61 個檔各留一手(那正是「疊床架屋」)。 # ② 閘的離開碼是唯一權威:包在外面就能對每一支統一抓到「擋(2)/放行(0)」, # 完全不必動任何一支閘的邏輯。 # hooks.json 把每一支閘的 command 從 # ${CLAUDE_PLUGIN_ROOT}/hooks/.sh # 改成 # ${CLAUDE_PLUGIN_ROOT}/hooks/lib/gate-log-wrap.sh ${CLAUDE_PLUGIN_ROOT}/hooks/.sh # 閘路徑當第一個參數傳進來;`bash "$GATE"` 是子行程,所以閘內部的 $0/BASH_SOURCE[0] # 仍是閘自己的路徑,`source .../lib/...` 那套定位照常成立。 # # ── 紅線(InkStoneCo#48,逐條落在這支上)──────────────────────────────────── # 🔴 寧可漏記一筆,不可擋錯一次:這支**永遠只把真正那支閘的離開碼原樣傳出去**, # 它自己的邏輯裡沒有任何一條 `exit 2`。記錄整段包在一個永遠成功的子殼裡 # (`( … ) 2>/dev/null || true`),記錄壞掉最多漏記一筆,不可能造成誤攔。 # 🔴 不得讓閘變慢或變脆:記錄全程純 POSIX shell,不另外起 python;閘本來就要跑, # 這裡只多一次輕量 shell fork。 # 🔴 D36 金鑰不落地:**只記中繼資料(閘名/事件/工具名/判成什麼/session/逃生口代號)**, # **完全不寫入 payload 或指令原文** ⇒ 金鑰在結構上就進不了紀錄,不必事後遮罩。 # 🔴 零輪詢(D20):紀錄長在既有的閘動作上,不新增任何排程/cron/輪詢/事件 fan-out。 # 🔴 不新增第二套落地方式:沿用 milestone-account 的 jsonl 帳本慣例 # (不寫進會被 `plugin update` 整個換掉的 plugin 目錄;env 可覆寫路徑供測試)。 # # 記錄檔位置(沿用 scripts/milestone-account 的 _resolve_dir 順序): # ISEP_GATE_LOG(env,測試/停用用) → $CLAUDE_PROJECT_DIR/system-dev/gate-log/ → # plugin 根的 system-dev/gate-log/ → ~/.claude/isep-gate-log/ # 檔名 gate-log.jsonl。用 `scripts/gate-report` 讀它回答三題。 GATE="$1" [ -n "$GATE" ] || exit 0 # 沒給閘路徑:什麼都不做,放行(絕不擋、絕不報錯) shift 2>/dev/null || true # 讀一次 stdin(閘要吃的 payload) INPUT=$(cat 2>/dev/null || true) # ── 先跑真正那支閘 ────────────────────────────────────────────────────────── # stdin 原樣餵進去;stdout/stderr 原樣流出去(閘的紅字說明、注入的 context 都不動)。 # pipeline 最後一段是閘 ⇒ $? 就是閘的離開碼。 printf '%s' "$INPUT" | "$GATE" "$@" rc=$? # ── 記錄:全程 best-effort,包在永遠成功的子殼裡,絕不影響 rc、絕不外洩任何字 ── ( GATE_NAME=${GATE##*/}; GATE_NAME=${GATE_NAME%.sh} # 只從 payload 抽固定幾個「天生安全」的欄位;全部再過一層白名單字元清洗, # 保證組出來的 JSON 一定合法、且不可能夾帶金鑰或引號破壞結構。 _field() { # _field —— 印出第一個同名字串欄位的值(清洗後) printf '%s' "$INPUT" \ | sed -n "s/.*\"$1\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p" \ | head -n1 \ | tr -cd 'A-Za-z0-9_.:@/#+-' \ | cut -c1-100 } EVENT=$(_field hook_event_name) TOOL=$(_field tool_name) SID=$(_field session_id) # 判成什麼 if [ "$rc" = "2" ]; then VERDICT=block elif [ "$rc" = "0" ]; then VERDICT=pass else VERDICT=error; fi # 逃生口偵測(owner-scoped:只有**擁有**那個逃生口的閘才檢查它自己那一個, # 避免「指令裡剛好出現 credential-ok」被誤記到別支閘頭上)。 BYPASS= case "$GATE_NAME" in not-my-branch-guard) [ "${NOT_MY_BRANCH_OK:-}" = "1" ] && BYPASS=NOT_MY_BRANCH_OK ;; subagent-first-guard) [ -n "$SID" ] && [ -f "/tmp/.solo-ok-$SID" ] && BYPASS=solo-ok ;; history-first-guard) [ -f "${KBDB_STAMP_DIR:-/tmp}/.kbdb-down" ] && BYPASS=kbdb-down ;; credential-only-guard) case "$INPUT" in *credential-ok*) BYPASS=credential-ok ;; esac ;; wiki-secret-scan) case "$INPUT" in *wiki-secret-ok*) BYPASS=wiki-secret-ok ;; esac ;; mistake-needs-ticket-guard) case "$INPUT" in *no-ticket-needed*) BYPASS=no-ticket-needed ;; esac ;; esac # 逃生口只有在「閘放行了」時才算真的被繞過;閘照樣擋下 ⇒ 逃生口沒生效,維持 block。 if [ -n "$BYPASS" ] && [ "$VERDICT" = "pass" ]; then VERDICT=bypass; fi # 是不是子 session(給「同一支 agent 反覆撞同一支閘」那一題用) if [ "${CLAUDE_CODE_CHILD_SESSION:-}" = "1" ]; then CHILD=1; else CHILD=0; fi # 記錄檔位置(沿用 milestone-account 慣例;不寫進 plugin 目錄) LEDGER=${ISEP_GATE_LOG:-} if [ -z "$LEDGER" ]; then _root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." 2>/dev/null && pwd) || _root= _dir= for _b in "${CLAUDE_PROJECT_DIR:-}" "$_root"; do [ -n "$_b" ] && [ -d "$_b/system-dev" ] && { _dir="$_b/system-dev/gate-log"; break; } done [ -n "$_dir" ] || _dir="$HOME/.claude/isep-gate-log" LEDGER="$_dir/gate-log.jsonl" fi [ "$LEDGER" = /dev/null ] && exit 0 # 明示停用 TS=$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || printf '') GATE_NAME=$(printf '%s' "$GATE_NAME" | tr -cd 'A-Za-z0-9_.-' | cut -c1-80) LINE=$(printf '{"ts":"%s","gate":"%s","event":"%s","tool":"%s","verdict":"%s","bypass":"%s","sid":"%s","child":%s}' \ "$TS" "$GATE_NAME" "$EVENT" "$TOOL" "$VERDICT" "$BYPASS" "$SID" "$CHILD") mkdir -p -- "$(dirname -- "$LEDGER")" 2>/dev/null || true # 單一 printf 的短行 append(<4KB)在 O_APPEND 下是原子的 ⇒ 併發的多支閘不會互相截斷。 printf '%s\n' "$LINE" >> "$LEDGER" 2>/dev/null || true ) >/dev/null 2>&1 || true exit $rc