ee085c1854
inkstone/ISEP#122:leo 三次要部署被 prod-write-guard.sh 恆擋,蓋了戳記 也放不了行。根因不在 prod-write-guard.sh 本身,而在 InkStoneCo/.claude/settings.json 還登記著一份 2026-08-20 立 ISEP plugin 之前的舊 hook,跟 plugin 自己的 hooks.json 同時掛在同一個事件上——每個 PreToolUse 都跑了兩次,一次性戳記被第一份消耗掉,第二份永遠看到空戳記。 實查(在這台機器的樹上實數):InkStoneCo 專案版登記著 38 支 hook, 34 支跟 plugin 完全同名同用途(純殘骸)、3 支是已退役機制 (claim-verify-police.sh/subagent-claim-worksheet.sh/sdd-guard.sh, ISEP#60/#91 早已裁定退役但專案版沒跟著退)、1 支(kv-write-guard.sh) 是專案版有、plugin 當時沒有的真閘。 這次: - kv-write-guard.sh 原樣搬進本 repo(B 組,PreToolUse Write|Edit|MultiEdit), 補齊那個真的缺口,附 8 條迴歸測試 - 新增 duplicate-hook-registration-guard.sh(E 組,SessionStart): 往後任何專案的 settings.json 又跟 plugin 長出同名登記,開場就點名, 不必再靠「戳記莫名其妙失效」才發現,附 8 條迴歸測試 - 兩支新閘:61→63 支、84→86 條註冊(在自己的樹上實數,不是加減推) InkStoneCo 那邊的清理(37 支殘骸從 settings.json 與 .claude/hooks/ 移除, kv-write-guard.sh 保留至本次升版並 /plugin update 之後)另外commit, 不在本 repo 範圍內。 驗證:重演過修好前的雙掛載(蓋一次戳記→專案版先吃掉→plugin 版恆擋, EXIT=2),也驗過修好後單次執行放行(EXIT=0)與不蓋戳記仍被擋(EXIT=2)。 待總管定版。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
116 lines
6.7 KiB
Bash
Executable File
116 lines
6.7 KiB
Bash
Executable File
#!/bin/bash
|
||
# PreToolUse hook(Write|Edit|MultiEdit)— 長效資料不准寫進 KV,一律走 KBDB API
|
||
#
|
||
# 【portage 記錄,inkstone/ISEP#122】本檔 2026-08-25 立於
|
||
# `InkStoneCo/.claude/hooks/kv-write-guard.sh`(那時 ISEP 還沒吸收它),
|
||
# 2026-09-02 原樣搬進 ISEP——它是那次盤點揪出來的**唯一一支**「專案版有、
|
||
# plugin 沒有」的真閘(另外 3 支同名殘骸是已退役機制,直接刪掉,不搬)。
|
||
# 搬完之後 `InkStoneCo/.claude/hooks/kv-write-guard.sh` 就是廢棄複本,
|
||
# 待這裡升版、`/plugin update` 裝上之後即可從專案版與 settings.json 一併移除。
|
||
#
|
||
# 【leo 2026-08-25 立】原話(三句,一句比一句尖):
|
||
# 「已經禁止寫入 KV 了,這是太貴的資源,應該都去寫入 KBDB,到底寫入 KV 做什麼?」
|
||
# 「上次刪掉一堆東西,現在又寫入,**你沒有規範嗎**」
|
||
# 「**我強制禁止寫 SQL,要用 KBDB API,你就讓它繞過寫 KV 代替?**」
|
||
#
|
||
# ── 為什麼存在(2026-08-25 實查,這是本閘的正身)─────────────────────────
|
||
# 規則早就在:`system-dev/wiki/decisions-summary.md:133`
|
||
# 「**KV =暫存(cache / transient),不是長期真相源。**」
|
||
# 而且打過一場「KV 退休戰」(同檔 254 行,SUBMISSIONS_KV 併入那一役)。
|
||
#
|
||
# 🔴 **但 `.claude/hooks/` 底下跟 KV 有關的閘:0 支。**
|
||
# ⇒ 規則被讀到了,卻沒有任何機制驗證有沒有照做——與 2026-08-25 一整天挖出來的
|
||
# 四層 migration 缺陷、以及 08-10 抓到的 history-first/KBDB-first/stage-first
|
||
# **完全同一個形狀**。
|
||
#
|
||
# 🔴 **更糟的是 leo 指出的因果**(總管用 git 查證屬實):
|
||
# 禁 SQL 的牆 2026-08-07
|
||
# app-system.ts 誕生 2026-08-24 ← 17 天之後,**從第一行就在寫 KV**
|
||
# 而 `kbdbFetch()` 裡的 KV 字樣是 **0** ⇒ 走 KBDB API 根本不碰 KV
|
||
# ⇒ 那些 KV 寫入**每一處都是繞過**,沒有一處經過知識庫。
|
||
# **牆立起來了,水從旁邊流走了。**
|
||
#
|
||
# ── 判準(封動作,不封措辭)──────────────────────────────────────────
|
||
# 新增一行「往 KV 寫入」的程式碼 ⇒ 擋一次,要求說明它為什麼不是長效資料
|
||
# 只是讀(.get/.list) ⇒ 放行
|
||
# 刪除既有的 KV 寫入(退休戰) ⇒ 放行(那正是我們要的方向)
|
||
#
|
||
# ── 豁免(留痕,不是後門)────────────────────────────────────────────
|
||
# 那一行尾端加 `kv-ok: <理由>`。
|
||
# 合法的理由只有一種形狀:**這筆資料本來就是短命的**(session、一次性 nonce、
|
||
# 有 TTL 的快取)。寫「暫時先這樣」「之後再搬」都不算——
|
||
# 那是把債留給下一個人,而 2026-08-25 證明了沒人會回來還。
|
||
set -uo pipefail
|
||
|
||
payload=$(cat)
|
||
read -r tool content file <<<"$(printf '%s' "$payload" | python3 -c "
|
||
import json,sys
|
||
try:
|
||
d=json.load(sys.stdin); ti=d.get('tool_input') or {}
|
||
# Write 用 content;Edit 用 new_string;MultiEdit 把每筆 new_string 串起來
|
||
parts=[ti.get('content') or '', ti.get('new_string') or '']
|
||
for e in (ti.get('edits') or []): parts.append(e.get('new_string') or '')
|
||
body='\n'.join(p for p in parts if p)
|
||
print(d.get('tool_name','') or 'x', len(body), ti.get('file_path','') or 'x')
|
||
sys.stderr.write(body)
|
||
except Exception:
|
||
print('x 0 x')
|
||
" 2>/tmp/.kv-guard-body)" || exit 0
|
||
|
||
body=$(cat /tmp/.kv-guard-body 2>/dev/null)
|
||
rm -f /tmp/.kv-guard-body
|
||
[ -z "$body" ] && exit 0
|
||
|
||
# 只管程式碼檔;文件、設定、測試裡提到 KV 不算違規
|
||
case "$file" in
|
||
*.ts|*.js|*.mjs|*.tsx|*.jsx) ;;
|
||
*) exit 0 ;;
|
||
esac
|
||
# 閘自己、與退休戰的測試檔放行(否則永遠改不動它)
|
||
case "$file" in
|
||
*kv-write-guard*|*.test.*|*/tests/*) exit 0 ;;
|
||
esac
|
||
|
||
# 命中的行:往 KV 綁定寫入,且**該行沒有 kv-ok 豁免**
|
||
# 判準是形狀不是名字:**全大寫的 binding 後面接 .put(/.delete(**。
|
||
# 為什麼不列名字清單(第一版就是這樣寫壞的):名字會長出新的(下一個 KV 綁定叫什麼沒人知道),
|
||
# 而清單只認得已知的那幾個 ⇒ 新的 KV 從清單的縫隙走過去,本閘就變成裝飾。
|
||
# 全大寫識別字 + .put/.delete 這個形狀,在 Workers 裡就是 KV(D1 沒有 .put,JS 物件用 .set)。
|
||
hits=$(printf '%s' "$body" | grep -nE '\b[A-Z][A-Z0-9_]{1,}\.(put|delete)\(' \
|
||
| grep -v 'kv-ok' || true)
|
||
[ -z "$hits" ] && exit 0
|
||
|
||
cat >&2 <<EOF
|
||
🚫 長效資料不准寫進 KV——一律走 KBDB API(leo 2026-08-25 立)
|
||
|
||
leo 原話:
|
||
「**我強制禁止寫 SQL,要用 KBDB API,你就讓它繞過寫 KV 代替?**」
|
||
「上次刪掉一堆東西,現在又寫入,**你沒有規範嗎**」
|
||
|
||
━━ 你這次要寫的 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
$(printf '%s' "$hits" | head -6 | sed 's/^/ /')
|
||
|
||
━━ 為什麼擋 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
規則早就在(\`system-dev/wiki/decisions-summary.md:133\`):
|
||
**KV =暫存(cache / transient),不是長期真相源。**
|
||
而 2026-08-25 實查:\`.claude/hooks/\` 底下跟 KV 有關的閘 **0 支**
|
||
⇒ 規則被讀到了,卻沒有機制驗證有沒有照做。本閘就是補那一格。
|
||
|
||
🔴 **並且**:\`kbdbFetch()\` 裡的 KV 字樣是 **0**——走 KBDB API 根本不碰 KV。
|
||
所以「寫 KV」在這個系統裡**必定意味著繞過知識庫**,不是一種實作選擇。
|
||
禁 SQL 的牆 2026-08-07 立,而 app-system 2026-08-24 誕生就在寫 KV
|
||
——**牆立起來了,水從旁邊流走了。**
|
||
|
||
━━ 怎麼過 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
① **本來就該走 KBDB**(絕大多數)→ 改成 KBDB API。
|
||
新資料類型=seed 一列 template,資料展開成完整 record,
|
||
**不准打包進 metadata_json**(D91/D93)。讀 \`arcrun-kbdb-guardrails\` skill。
|
||
② **這筆資料真的是短命的**(session/一次性 nonce/有 TTL 的快取)
|
||
→ 那一行尾端加 \`kv-ok: <為什麼它是短命的>\`,留痕放行。
|
||
③ **你在做的是退休戰(刪掉 KV 寫入)** → 本閘不擋刪除,只擋新增。
|
||
|
||
🔴 \`kv-ok: 暫時先這樣\` / \`之後再搬\` **不算理由**——那是把債留給下一個人,
|
||
而 2026-08-25 已經證明了沒人會回來還。
|
||
EOF
|
||
exit 2
|