#!/bin/bash # PreToolUse hook — 金鑰只能走 credential 中心,禁止寫在外面(L3 硬攔截) # # 【leo 2026-07-29 立】原話: # 「那就很簡單,禁止把 secrets 寫在外面,不給 AI 寫入欄位,凡是要 secrets 就要去統一中心拿」 # # 為什麼存在(同日事故實錄): # 同一批 workflow 裡**兩種金鑰寫法並存**—— # {{credential.gemini_api_key}} 8 處(正解,走 credential 中心) # __KBDB_TOKEN__ 17 處 / __GITEA_TOKEN__ 6 處 / __GEMINI_API_KEY__ 1 處(佔位符替換,違規) # 連 Gemini 同一把金鑰都同時有兩種寫法。兩套並存必然漂移: # t145 就是漂移的結果——kbdb 身上舊 token、workflow 帶新 token, # 萃取切出的 5 個三元組全部 401 寫不進去,且「指紋一致→永遠跳過」自我保護、重裝幾次都修不好。 # # 病根不是「這次寫錯」,是「AI 天然偏向新增一種做法,而非找出既有做法」。 # 靠紀律會漂移,靠機械閘不會 ⇒ 讓「繞開 credential」在寫入那一刻就失敗。 # # 掛在 settings.json 的 PreToolUse(matcher: Write|Edit|MultiEdit)。 # stdin 收到 JSON:{ tool_name, tool_input: { file_path, content?, new_string? } } # # 誠實限制(抄 wiki-secret-scan 的自我要求):regex 偵測有偽陰/偽陽。 # 擋的是「明顯的金鑰佔位符被寫進 workflow/設定」,擋不了刻意改名混淆的繞道。 # 價值是「讓正解成為阻力最小的路 + 留痕可審」,不是技術防偽。絕不聲稱不可能繞過。 set -euo pipefail INPUT=$(cat) # ── 解析 file_path 與要寫入的內容(優先 jq,無 jq 退回 grep)───────────── if command -v jq >/dev/null 2>&1; then FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty') CONTENT=$(printf '%s' "$INPUT" | jq -r '[.tool_input.content, .tool_input.new_string] | map(select(. != null)) | join("\n")') else FILE_PATH=$(printf '%s' "$INPUT" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"$/\1/') CONTENT=$(printf '%s' "$INPUT") fi [ -z "${FILE_PATH:-}" ] && exit 0 [ -z "${CONTENT:-}" ] && exit 0 # ── 只管「會被執行的產物」:workflow 定義、安裝器、部署設定 ────────────── # 文件/wiki/落帳談論這些字串是正常的(本檔自己就寫滿了),不能擋。 case "$FILE_PATH" in *.md|*/wiki/*|*/docs/*|*/mistakes*|*/CRITICAL-PATH*) exit 0 ;; esac case "$FILE_PATH" in *workflow*|*.yaml|*.yml|*workflows.json|*installer*|*worker.js|*wrangler*) ;; *) exit 0 ;; esac # ── 豁免:該行標了 credential-ok 就放行(給真有理由的例外,留痕可審)──── SCAN=$(printf '%s' "$CONTENT" | grep -v 'credential-ok' || true) [ -z "$SCAN" ] && exit 0 # ── 違規特徵:金鑰類佔位符(__XXX_TOKEN__ / __XXX_KEY__ / __XXX_SECRET__)── # 非金鑰的佔位符(__KBDB_BASE__、__CODE_URL__、__NAMESPACE__…)是正常的,不擋。 HITS=$(printf '%s' "$SCAN" \ | grep -oE '__[A-Z0-9_]*(TOKEN|KEY|SECRET|PASSWORD|CREDENTIAL)[A-Z0-9_]*__' \ | sort -u || true) # ── 違規特徵②:真身被寫進定義(leo:「只能拿 key,要送出時自動去拉 value」)── # AI 只准碰名字;值在 WASM 執行前由 resolve_credentials 回填(graph-executor.ts:247)。 # 抓「Bearer <一長串>」「api_key: <一長串>」這類把值直接寫死的形態。 # {{credential.X}} 與 ${...} 這類引用不算值,先剔除再掃。 LITERAL=$(printf '%s' "$SCAN" \ | grep -vE '\{\{ *credential\.[a-z0-9_]+ *\}\}' \ | grep -oiE '(bearer|api[-_]?key|token|secret|password)"?[:= ]+"?[A-Za-z0-9_\-]{24,}' \ | sort -u || true) if [ -n "$LITERAL" ]; then { echo "🔒 credential 鐵律攔截②:金鑰真身被寫進定義(leo 2026-07-29 立)" echo "" echo " 偵測到疑似金鑰值(已遮蔽尾段):" printf '%s\n' "$LITERAL" | sed -E 's/(.{20}).*/ \1…(已遮蔽)/' echo " 檔案:$FILE_PATH" echo "" echo "【鐵律】「只能拿 key,要送出時自動去拉 value」" echo " ⇒ 定義裡只准出現**名字**,真身永遠不落在 workflow/設定上。" echo "" echo "【正解】{{credential.<名字>}}——執行前由 resolve_credentials 回填" echo " (graph-executor.ts:247,WASM 執行前才解析)。" echo "" echo "【為什麼】值不落地,AI 讀 workflow 也讀不到真身;" echo " 換金鑰只改中心一處,不必回頭改每個引用點(副本歸零=不可能漂移)。" echo "" echo "【誤判】若這串不是金鑰(如 hash/id),該行尾加 credential-ok 豁免。" } >&2 exit 2 fi if [ -n "$HITS" ]; then { echo "🔒 credential 鐵律攔截(leo 2026-07-29 立)" echo "" echo " 偵測到金鑰被寫在 credential 中心之外:" printf ' %s\n' $HITS echo " 檔案:$FILE_PATH" echo "" echo "【鐵律】「禁止把 secrets 寫在外面,不給 AI 寫入欄位," echo " 凡是要 secrets 就要去統一中心拿」" echo "" echo "【正解】用 credential 引用,執行時才解析真身:" echo " Authorization: \"Bearer {{credential.kbdb_internal_token}}\"" echo " 同批 workflow 已有正解範例可抄:{{credential.gemini_api_key}}" echo "" echo "【為什麼】兩套寫法並存必然漂移。t145 實錄:kbdb 身上舊 token、" echo " workflow 帶新 token ⇒ 萃取切出的 5 個三元組全 401 寫不進去," echo " 且指紋一致→永遠跳過→重裝幾次都修不好(自我保護的錯誤狀態)。" echo "" echo "【真有例外】該行尾加 credential-ok 豁免(會留痕,請在 commit 說明理由)。" } >&2 exit 2 fi exit 0