gate-log: 透明記錄層——每一支閘的擋/放行/繞過都留得下(InkStoneCo#48)
在此之前 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
This commit is contained in:
Executable
+116
@@ -0,0 +1,116 @@
|
||||
#!/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/<gate>.sh
|
||||
# 改成
|
||||
# ${CLAUDE_PLUGIN_ROOT}/hooks/lib/gate-log-wrap.sh ${CLAUDE_PLUGIN_ROOT}/hooks/<gate>.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 <json-key> —— 印出第一個同名字串欄位的值(清洗後)
|
||||
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
|
||||
Reference in New Issue
Block a user