7de1ad6be6
擋的是**證據的出處**,不是措辭。
判準(兩個條件同時成立才響):
① 要送出去的那份東西(票上的留言/派工單)裡,貼了一個值
——entry id 或 `kb://` 來源位址——而它這個 session 只在
`kbdb_search` 的回應裡出現過
② 這個 session 從沒用產品檢索路徑(graph/wiki 內容/query)拿過那筆
為什麼不比對措辭:驗收條件第 4 條寫死不准,而 leo 2026-08-17 已經證偽過
文字層——那天 8 次誤攔、0 次正確攔截,且方向穩定:紅線寫得越細,命中
關鍵字的機率越高 ⇒ 那些閘在懲罰謹慎。值跟動作一樣有限且可枚舉,措辭不是。
🔴 `kbdb_search` 一點都沒有變難用:查 wiki、找 record_id、看某筆在不在,
全部照放。它只在「把搜尋輸出貼出去當產品檢索壞掉的證據」那一刻才響。
實測 31 條,A 群(不該擋)11 條、B 群(該擋)5 條、C 群訊息 6 條、
D 群登記處 9 條。真跡重演=inkstone/Arcrun#167 comment 4865 那則退回,
原文照貼會被擋;走過一次真路徑之後同一份留言就放行。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
144 lines
7.3 KiB
Bash
Executable File
144 lines
7.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# 管什麼: 記下「這個 session 看到的每一筆 KBDB 資料,是從哪一條路拿到的」——搜尋出來的,還是走產品檢索路徑拿到的。
|
||
# 為什麼: 2026-08-27 inkstone/Arcrun#167 comment 4865,總管把 kbdb_search 的輸出當成「產品檢索壞了」的證據,退回一張已經做完的票、重派一條線。錯的不是那句話,是那條路。
|
||
# 誤觸時怎麼關: 這支不擋任何東西,只留檔。要停用就從 hooks.json 拿掉它(配套的 search-is-not-proof-guard 會跟著失效,見該檔)。
|
||
#
|
||
# kbdb-evidence-stamp.sh — 證據的出處登記處(PostToolUse: 各 kbdb_* 工具)
|
||
#
|
||
# ── 為什麼是「出處」而不是「措辭」───────────────────────────────────
|
||
# leo 2026-08-17 證偽過文字層:那天在文字層封路的閘 **8 次誤攔、0 次正確攔截**,
|
||
# 且方向穩定——紅線寫得越細,命中關鍵字的機率越高 ⇒ **那些閘在懲罰謹慎**。
|
||
# leo 原話:「自然語言的變體是無限的,blacklist 永遠追不完。
|
||
# 封路哲學之所以有效,是因為它封的是**動作**——動作有限且可枚舉。」
|
||
#
|
||
# ⇒ 所以本閘不看你怎麼說,看你**引用的那筆資料是從哪支工具掉出來的**。
|
||
# 資料值跟動作一樣是有限且可枚舉的:一個 entry id、一個 `kb://` 來源位址,
|
||
# 它要嘛出現在 kbdb_search 的回應裡,要嘛沒有。**這是事實,不是判讀。**
|
||
#
|
||
# ── 兩條路的分別(D81,leo 2026-08-13 親自裁定)────────────────────
|
||
# `system-dev/docs/2-architecture/decisions/D81-vector-only-for-entity-normalize.md`
|
||
# Graph(三元組/entities) → **找到該找的東西** ← 產品檢索路徑
|
||
# LLM Wiki(卡片文本) → **拉到對的內容** ← 產品檢索路徑
|
||
# 向量/關鍵字搜尋 → **協助 entities normalize**,**輔助,不是檢索主路徑**
|
||
#
|
||
# 🔴 **`kbdb_search` 本身完全合法**,這支不會讓它變難用——
|
||
# subagent-wiki-guard.sh 還明文要求「查任何東西之前先用它查 wiki」。
|
||
# leo 的話是「不能拿它當**產品檢索路徑**」,不是「不准呼叫」。
|
||
# ⇒ 本檔只登記出處,判斷留給 search-is-not-proof-guard.sh,而那支的觸發條件
|
||
# 窄到「你把搜尋結果的原始值貼進票或派工單裡當證據」才會響。
|
||
#
|
||
# ── 平台依據(查證過,不是推測)─────────────────────────────────────
|
||
# PostToolUse 的 stdin payload 帶得到工具的**輸出**:
|
||
# `@anthropic-ai/claude-code/sdk.d.ts:54-59`
|
||
# export type PostToolUseHookInput = BaseHookInput & {
|
||
# hook_event_name: 'PostToolUse'; tool_name: string;
|
||
# tool_input: unknown; tool_response: unknown; };
|
||
# 實跑的那顆(Caskroom 2.1.220)內建說明同樣寫著
|
||
# `"tool_response": { "success": true } // PostToolUse only`。
|
||
#
|
||
# ── fail-closed:這支失效的時候要出聲,不准靜默 ──────────────────────
|
||
# `kbdb-asked-stamp.sh` 檔頭記著 leo 2026-08-08 的原話:
|
||
# 「你自己寫了 hook,結果**一次次改都沒產生用途**,問題太大」。
|
||
# ⇒ 所以 payload 裡**根本沒有 `tool_response` 這個欄位**時(=平台換了介面),
|
||
# 本檔走 exit 2 把話講到 stderr 上(PostToolUse 的 exit 2 不會擋工具,
|
||
# 只會把訊息交給 Claude 看)。**寧可吵一次,也不要變成一支活著卻沒作用的閘。**
|
||
# 「回應讀得到但裡面沒有指紋」是正常情況(搜尋 0 筆),不出聲。
|
||
set -uo pipefail
|
||
|
||
INPUT=$(cat)
|
||
|
||
OUT=$(printf '%s' "$INPUT" | python3 -c '
|
||
import json, os, re, sys
|
||
|
||
try:
|
||
d = json.load(sys.stdin)
|
||
except Exception:
|
||
print("SKIP"); raise SystemExit
|
||
|
||
tool = d.get("tool_name") or ""
|
||
sid = d.get("session_id") or "nosid"
|
||
|
||
# 這一族才登記;別的工具直接走人
|
||
if "kbdb_" not in tool:
|
||
print("SKIP"); raise SystemExit
|
||
|
||
# 🔴 D81 的兩條產品檢索路徑 vs 輔助工具。判斷只看工具名,不看內容。
|
||
WALKED = ("kbdb_get_record", "kbdb_graph_neighbors", "kbdb_query", "kbdb_get_map")
|
||
if any(w in tool for w in WALKED):
|
||
kind = "walked"
|
||
elif "kbdb_search" in tool:
|
||
kind = "search"
|
||
else:
|
||
print("SKIP"); raise SystemExit # 寫入類工具(create_*)不是證據來源
|
||
|
||
if "tool_response" not in d: # ← 平台換介面了,本閘等於死了
|
||
print("NOFIELD"); raise SystemExit
|
||
|
||
# 回應可能是 dict、list、字串、或 MCP 的 content blocks——一律壓成字串再找指紋。
|
||
# 這樣就不必猜它今天是哪一種包裝。
|
||
def flatten(x, acc):
|
||
if isinstance(x, str):
|
||
acc.append(x)
|
||
elif isinstance(x, dict):
|
||
for v in x.values(): flatten(v, acc)
|
||
elif isinstance(x, (list, tuple)):
|
||
for v in x: flatten(v, acc)
|
||
elif x is not None:
|
||
acc.append(str(x))
|
||
|
||
parts = []
|
||
flatten(d.get("tool_response"), parts)
|
||
try:
|
||
parts.append(json.dumps(d.get("tool_response"), ensure_ascii=False))
|
||
except Exception:
|
||
pass
|
||
blob = "\n".join(parts)
|
||
|
||
# ── 指紋只取兩類,兩類都是「值」不是「措辭」───────────────────────
|
||
# ① entry id:`e_<uuid>`。36 字元的 UUID,實質上不會誤撞。
|
||
# ② 來源位址:`kb://…`。它在 metadata_json 那串裡,而**跳脫前後長得一樣**
|
||
# (貼進 markdown 會變成 \"…\",但 kb:// 那段本身不變)⇒ 貼過去也認得出來。
|
||
# 兩類都是可枚舉的資料值,正好對上 leo 8-17「動作有限且可枚舉,文字不是」。
|
||
ids = set(re.findall(r"\be_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b", blob))
|
||
srcs = set(re.findall(r"kb://[^\"\\\s,}\]]{6,}", blob))
|
||
fps = sorted(ids | srcs)
|
||
|
||
path = "/tmp/.kbdb-evidence-%s-%s" % (kind, sid)
|
||
if fps:
|
||
try:
|
||
with open(path, "a", encoding="utf-8") as f:
|
||
for x in fps:
|
||
f.write(x + "\n")
|
||
os.chmod(path, 0o600)
|
||
except Exception:
|
||
print("WRITEFAIL"); raise SystemExit
|
||
print("OK %s %d" % (kind, len(fps)))
|
||
' 2>/dev/null) || OUT="PYFAIL"
|
||
|
||
case "${OUT:-}" in
|
||
NOFIELD)
|
||
cat >&2 <<'EOF'
|
||
⚠️ kbdb-evidence-stamp 失效了:這次的 PostToolUse payload **沒有 `tool_response` 欄位**。
|
||
|
||
它靠這個欄位登記「這筆資料是從哪條路拿到的」,欄位不見 =
|
||
`search-is-not-proof-guard.sh` 從現在起**永遠不會響**(而且不會有人發現)。
|
||
|
||
依據(本閘寫的時候查證過):
|
||
@anthropic-ai/claude-code/sdk.d.ts:54-59 PostToolUseHookInput 帶 `tool_response`
|
||
|
||
⇒ 平台介面變了。開一張票去修這兩支,不要當作沒看到——
|
||
一支活著卻沒作用的閘,比沒有閘更糟:它會讓人以為這一格有人守。
|
||
EOF
|
||
exit 2 ;;
|
||
PYFAIL|WRITEFAIL)
|
||
cat >&2 <<'EOF'
|
||
⚠️ kbdb-evidence-stamp 這次沒登記成功(python3 或 /tmp 寫入失敗)。
|
||
|
||
後果:這一次的 kbdb 呼叫不會被 `search-is-not-proof-guard.sh` 看見。
|
||
不影響你手上的工作,但**這一格暫時沒人守**——請自己確認一次
|
||
「我等一下要下的結論,走的是不是我實際查過的那條路」。
|
||
EOF
|
||
exit 2 ;;
|
||
esac
|
||
exit 0
|