#!/bin/bash # 管什麼: Bash 分支擋「繞過 API 牆、直接對那顆資料庫執行 SQL」的指令;Write/Edit 分支擋新增 DDL/原生 SQL/新 D1 binding。 # 為什麼: leo 2026-08-07 立 D38——KBDB 只能走 API,零 SQL、永不加表;規則掛在「KBDB」這個詞上,容易在想著「D1/建表」時漏載入。 # 誤觸時怎麼關: 該行尾加 kbdb-sql-ok(留痕,commit 說明理由);Bash 分支的判準邏輯在 hooks/lib/kbdb_cmd_check.py,誤攔先查那支。 # PreToolUse hook — KBDB 是 API-as-Wall:零 SQL、永不加表(L3 硬攔截) # # 【leo 2026-08-07 立】原話: # 「確認執行記錄在 D1 是用 template 不是建表?」 # 「**KBDB 本來就只能用 API,如果他用 SQL 就是錯,現在居然還能建表,如何鎖住?**」 # 「它應該要被攔,**你也應該要被攔**,你要思考怎麼做不會再有錯誤指令。」 # # 為什麼存在(2026-08-07 事故實錄,兩層都失守): # ① **總管派工的指令本身違規**——寫成「新增 D1 migration 建執行紀錄表」。 # 病根:總管用**基礎設施的詞(D1)**思考,而鐵律掛在**領域的詞(KBDB)**上 # ⇒ `Skill(kbdb)` 的觸發詞對不上 ⇒ 整條鐵律沒被載入。 # 而且查了 Cloudflare 官方的 D1 額度(外部事實),**卻沒查自家那顆 D1 是誰** # ——三個 binding(CREDENTIALS_DB/ANALYTICS_DB/kbdb 的 DB)全指向同一顆 `arcrun-kbdb`。 # ② **subagent 照著做,而且更進一步直接 `ANALYTICS_DB.prepare()` 下原生 SQL**。 # 它沒建表(對),但繞過 API 牆讀寫 KBDB 的 D1,同樣違規。 # ⇒ 靠「我記得有這條鐵律」會失守,因為**想不到要載入它的時候,它就不存在**。 # 這個 hook 不看你記不記得,只看你寫了什麼。 # # 攔三種違規: # A. 對 KBDB 下 DDL(CREATE/ALTER/DROP TABLE)——「永不加表」 # B. 對綁在 arcrun-kbdb 的 D1 binding 下原生 SQL(.prepare/.exec/.batch)——「零 SQL」 # C. 新增一個 database_name = arcrun-kbdb 的 [[d1_databases]] binding——「別再多開一道門」 # # 豁免:該行尾加 `kbdb-sql-ok`(留痕,commit 說明理由)。 # 既存違規(credentials.ts / auth-dispatcher.ts 等)不在本 hook 的射程內—— # 它只擋**這次寫進去的內容**,不回頭掃全 repo。既存的收拾另案處理。 # # 誠實限制:regex 偵測有偽陰/偽陽,擋得住「照常識寫」的違規,擋不住刻意改名混淆。 # 價值是「讓正解成為阻力最小的路 + 留痕可審」,不是技術防偽。絕不聲稱不可能繞過。 set -euo pipefail INPUT=$(cat) # ── Bash 分支:擋「繞過檔案、直接用命令列對 KBDB 下 DDL/SQL」────────── # 2026-08-07 leo 第 1 條指令「封鎖所有寫表的路」——只擋 Write/Edit 是不夠的, # `wrangler d1 execute arcrun-kbdb --command "CREATE TABLE ..."` 一樣建得出表。 if command -v jq >/dev/null 2>&1; then BASH_CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty') else BASH_CMD="" fi if [ -n "${BASH_CMD:-}" ]; then # 逃生口跟 Write/Edit 分支同一個標記(留痕,commit 說明理由)——加了這支才會真的擋, # 之前 Bash 分支引用的檔案不存在、永遠印 "OK",逃生口自然也沒人需要,現在補上。 case "$BASH_CMD" in *kbdb-sql-ok*) exit 0 ;; esac # 🔴 2026-08-07 修正:第一版做「整條命令字串比對」,結果**第四次誤擋總管**—— # 誤擋的是 `git commit -m "...引用了那個命令字面..."`:那是在**描述**這件事, # 不是在執行它。同一天已被自己的閘誤擋三次(Task 分支),這是第四次。 # ⇒ 只在**命令位置**才算數:字串出現在開頭,或緊接在 && ; | 換行之後。 # 引號內、heredoc 內、commit message 內的同樣字面一律不算。 # 判斷邏輯住在獨立檔(見該檔開頭:內嵌逃逸讓這道閘改一次壞一次) BASH_VERDICT=$(printf '%s' "$BASH_CMD" \ | python3 "$(dirname "$0")/lib/kbdb_cmd_check.py" 2>/dev/null || echo "OK") if [ "$BASH_VERDICT" = "BAD" ]; then cat >&2 <<'EOB' 🧱 kbdb-api-wall-guard(Bash):偵測到繞過 API 牆、直接對那顆資料庫執行 SQL 的命令。 🔴 D38(leo 2026-08-07):**任何東西禁止用 SQL 語句存取資料,一律 API。** KBDB = API-as-Wall(鐵律全文在 cypher-executor/src/routes/kbdb-proxy.ts 檔頭, leo 2026-06-14 立)。命令列直打繞過牆,**這條路 2026-08-07 起封鎖**。 ✅ 正確做法 · 新資料類型 → seed 一列 template (範本:kbdb/migrations/0003_library_map.sql) 🔴 0004_execution_log_template.sql 是**反例**,不要照抄——它是 D91 影子表本人 · 讀寫資料 → 走 kbdb HTTP API(慣例見 cypher-executor/src/routes/kbdb-proxy.ts) 🔴 **不加表不等於合規**:資料要寫成完整 record 展開, **不准打包進 metadata_json**(D91;seed 一列 template 只是半套) 真有例外:命令裡加註 `kbdb-sql-ok`(留痕),並在 commit 說明理由。 EOB exit 2 fi exit 0 fi if command -v jq >/dev/null 2>&1; then TASK_PROMPT=$(printf '%s' "$INPUT" | jq -r '.tool_input.prompt // empty') else TASK_PROMPT="" fi if [ -n "${TASK_PROMPT:-}" ]; then # 🔴 2026-08-07 第三次修正:Task 分支從「硬擋」改成「注入提示」。 # # 為什麼改(實錄):這條分支上線後**連續三次誤擋總管正確的派工 prompt**—— # ① 「不准另開表」(禁止語境)② 「回到三張核心表」(驗法敘述) # ③ 引用 leo 的警告原話(在講「不要建」,卻被當成「在叫人建」) # # ⇒ 判準(總管當天得出,寫下來免得又忘): # **自然語言的閘容易兩頭失準,硬擋會擋住正確的工作。 # 機械可判的是「產物」(SQL/DDL/binding),不是「指令的措辭」。** # ⇒ **指令層用提示(本分支,exit 0 注入 context),產物層用硬擋 # (Write/Edit 分支與 Bash 分支,exit 2)。** 兩層合起來防線才完整。 # # 提示仍有價值,因為**時機對**——它在派工送出的當下出現, # 而不是像 subagent-wiki-guard 那樣「指令已經寫完才附在工具結果裡」。 if command -v jq >/dev/null 2>&1; then jq -n --arg g "$(cat <<'EOG' 🧱 你正在派工。送出前對照資料層的兩條鐵律(2026-08-07 總管親手違反過): 【D38(leo 2026-08-07)】**任何東西禁止用 SQL 語句存取資料,一律 API。** 唯一例外是資料層 worker 自己(`kbdb/src/`、`kbdb/migrations/`)——它就是那面牆。 KBDB 只有 entries / templates **兩張**核心表,**永遠不加新的**; 新資料類型 = seed 一列 template。 🪦 **`entry_values` 已於 2026-08-15 廢除**(migration `0007`,leo:「我希望它不見, 這對我來說是恥辱柱」)。指標改用 entries 上的 `src_id`/`rel_id`/`dst_id` 欄位。 還沒更新的實例身上可能仍有那張表——**看到它=那台還沒跟上,不是它還合法**。 現成範本:`kbdb/migrations/0003_library_map.sql` 🔴 `0004_execution_log_template.sql` 是**反例**,不要照抄——它是 D91 影子表本人 🔴 **不加表不等於合規**:資料要寫成完整 record 展開,**不准打包進 metadata_json** (D91/D93;seed 一列 template 只是半套,而三道防線都只在數表,這個病從它們中間走過去) 【派工鐵律】**寫目的,不寫做法**(頂層 CLAUDE.md 規則三點五) ❌ 直接指定實作(例如叫人去做某種 schema 異動) ——2026-08-07 總管真的這樣寫過,一句話同時違反兩條鐵律,subagent 照做,兩層一起錯。 ✅ 寫「要達成什麼 + 怎麼驗 + 紅線」,把實作留給那個 repo 的人格自己查。 理由:**指令越具體,收工方越不會質疑**——你不是那個 repo 的專家, 別替它決定實作,那等於把自己的無知固化成命令。 【leo 原話】「先前測試 Haiku 就可以成功寫 Arcrun 工作流, **但你這個 Opus 卻下錯指令,問題在你這裏**。」 ——差別不在模型能力,在 haiku 每次被強制注入「先看 skill 清單」,而你沒有。 📌 產物層另有硬閘(本 hook 的 Write/Edit 與 Bash 分支)會擋 DDL、牆外原生 SQL、 繞過 API 直打那顆資料庫的命令。這裡只提醒,不阻擋。 EOG )" '{"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":$g}}' fi exit 0 fi 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 # 只管 arcrun / arcrun-rag 這兩個會碰到 KBDB 的地方(其他 repo 沒有 KBDB,別誤擋) case "$FILE_PATH" in *matrix/arcrun/*|*products/arcrun-rag/*|*kbdb*) ;; *) exit 0 ;; esac # 去掉帶豁免標記的行後再驗 SCAN=$(printf '%s' "$CONTENT" | grep -v 'kbdb-sql-ok' || true) [ -z "${SCAN:-}" ] && exit 0 VIOLATION="" # ── A. DDL:永不加表 ──────────────────────────────────────────────── if printf '%s' "$SCAN" | grep -qiE '(CREATE|ALTER|DROP)[[:space:]]+TABLE'; then VIOLATION="${VIOLATION} ── A. 偵測到 DDL(CREATE/ALTER/DROP TABLE) KBDB 只有兩張核心表:**entries / templates**,**永遠不加新 table**。 (`entry_values` 已於 2026-08-15 廢除,migration `0007`;指標改走 entries 上的 `src_id`/`rel_id`/`dst_id`。舊實例身上還看得到它=那台沒跟上,不是它還合法。) (權威來源:kbdb/migrations/0001_base.sql。**注意別把概念名當表名**—— Block/Template/Slot 是「三層概念」;entries 是萬用主表,靠 entry_type 區分 block/template/slot/project/workflow/recipe_stat…; 2026-08-07 本 hook 初版把概念名寫成表名,會害人去找不存在的表。) 所有資料結構都用 Block + Template + Slot 三層概念實現,落在那三張表上。 ✅ 正確做法:建一個 template,每筆資料是它的 record。 去讀既有的 template(\`triplet\` 是現成範本),照它的形狀做,不要新增第二套。" fi # ── B. D38 零 SQL:牆外任何地方都不准下原生 SQL ────────────────────── # 【leo 2026-08-07 立 D38】「**任何東西禁止用 SQL 語句存取資料,一律 API。**」 # 範圍不只 KBDB——唯一的例外是「資料層 worker 自己」(kbdb 就是那面牆,牆內當然用 SQL)。 # # 🔴 2026-08-07 補漏:舊版只比對 `CREDENTIALS_DB.prepare(`,**抓不到別名**—— # 而 `auth-dispatcher.ts:66,70` 正是 `const db = env.CREDENTIALS_DB;` 然後 `db.prepare(...)` # ⇒ 舊規則對真實的違規現場完全無效。改成「牆外任何 .prepare/.exec/.batch 都擋」。 IS_WALL_INTERNAL=0 case "$FILE_PATH" in *matrix/arcrun/kbdb/src/*|*matrix/arcrun/kbdb/migrations/*) IS_WALL_INTERNAL=1 ;; esac if [ "$IS_WALL_INTERNAL" -eq 0 ] \ && printf '%s' "$SCAN" | grep -qE '\.[[:space:]]*(prepare|exec|batch)[[:space:]]*\('; then VIOLATION="${VIOLATION} ── B. 偵測到原生 SQL(.prepare / .exec / .batch),而這個檔在「牆外」 🔴 **D38(leo 2026-08-07):任何東西禁止用 SQL 語句存取資料,一律走 API。** 唯一例外是資料層 worker 自己(\`kbdb/src/\`、\`kbdb/migrations/\`)——它就是那面牆。 ✅ 正確做法:走 HTTP API。 先讀 \`cypher-executor/src/routes/kbdb-proxy.ts\` 看既有呼叫慣例 (認證怎麼帶、owner_id 怎麼注入),照著走,不要新增第二套。 📌 別名也算:\`const db = env.XXX_DB; db.prepare(...)\` 與直接寫一樣是違規 (2026-08-07 舊版規則就是漏在這裡)。" fi # ── C. 別再多開一道門:新增指向 arcrun-kbdb 的 D1 binding ──────────── if printf '%s' "$SCAN" | grep -q 'd1_databases' && printf '%s' "$SCAN" | grep -q 'arcrun-kbdb'; then VIOLATION="${VIOLATION} ── C. 偵測到新增一個指向 arcrun-kbdb 的 [[d1_databases]] binding 🔴 有 D1 binding 就能下任意 SQL——**這正是 2026-08-07 那次能建表的原因**。 現況已經有三個 binding 指向同一顆(CREDENTIALS_DB/ANALYTICS_DB/kbdb 的 DB), **不要再多開第四個**。 ✅ 正確做法:走 kbdb HTTP API;真的需要獨立儲存 → 開一顆**不叫 arcrun-kbdb** 的 D1, 並在 pending-changes 提案說明為什麼(那是架構決策,要負責人裁)。" fi [ -z "$VIOLATION" ] && exit 0 cat >&2 <