0cdb6f2c05
inkstone/InkStoneCo#23、#56 同一個病:閘對整條指令字串做關鍵字掃描, 把「檔案內容/留言引用裡剛好提到某個關鍵字」當成「真的在執行」, 同時放過包一層讀取指令、或藏在 heredoc body 裡的真動作。 - 新增共用輔助 hooks/lib/strip_heredoc.py:heredoc body 是資料不是指令, 四支閘(github-contact / main-and-prod-push / stage-before-prod / kbdb-api-wall 的 Bash 分支)呼叫前一律先拿掉 body 再比對。 - main-and-prod-push-guard.sh:修掉跟 release-tag-guard.sh 同款的 「開頭是讀取工具就整條放行」前綴繞過洞;git push 的偵測改成指令位置比對; main/master 目標改用單字邊界,不再誤中 "domain" 這種子字串。 - github-contact-guard.sh:拿掉 gh CLI/git push 判準裡「前面隨便一個空白 就算數」的鬆散邊界,只認真正的指令分隔符。 - kbdb-api-wall-guard.sh:Bash 分支原本引用不存在的 kbdb_cmd_check.py, python3 找不到檔案就吃掉錯誤印 "OK",該分支形同虛設——任何 `wrangler d1 execute` 直打 kbdb 都會被放行。邏輯搬進新檔 hooks/lib/kbdb_cmd_check.py(shlex 分詞、quote-aware),把 .sh 的 參照路徑改過去,補回 Bash 分支的 kbdb-sql-ok 逃生口。 四支各補 InkStoneCo#40 §1 要求的三行中文檔頭。 新增四支可重跑測試(scripts/test-*.sh),共 69 條斷言全過, 含 #23/#56 票上實撞的原始形狀(寫 docs/TESTING.md 的 heredoc、 貼引用 #56 敘述的留言、`grep git push`)。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
232 lines
14 KiB
Bash
Executable File
232 lines
14 KiB
Bash
Executable File
#!/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 <<EOF
|
||
🧱 kbdb-api-wall-guard:這次寫入違反 KBDB 的兩條鐵律。
|
||
$VIOLATION
|
||
|
||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
【鐵律全文】KBDB = API-as-Wall
|
||
· **零 SQL**:一律走 kbdb 的 HTTP API,不准直接碰它的 D1
|
||
· **永不加表**:只有 **entries / templates** 兩張;新結構用 template + record
|
||
(`entry_values` 2026-08-15 廢除,見上)
|
||
|
||
【為什麼這條特別容易被繞過】(2026-08-07 實錄,總管與 subagent 兩層都失守)
|
||
想事情時用的是**基礎設施的詞**(D1/資料表/migration/schema),
|
||
而鐵律掛在**領域的詞**(KBDB)上 ⇒ 詞對不上,skill 就不會被載入。
|
||
⇒ **碰到「D1」時的第一個動作,是先問「這顆 D1 是誰的」**,
|
||
不是先想「要建什麼表」。三個 binding 全指向 arcrun-kbdb。
|
||
|
||
【真的有例外】該行尾加 \`kbdb-sql-ok\`(留痕),並在 commit 訊息說明理由。
|
||
EOF
|
||
exit 2
|