Files
ISEP/hooks/leo21c-mcp-guard.sh
T
Leo b8060ca263 新增 leo21c-mcp-guard.sh:擋下打向 leo 真庫的 Arcrun/KBDB MCP 呼叫(ISEP#69)
leo 2026-08-27:「我從頭的指令就叫你去打 youlin,stage 環境是它你不知道嗎?
為什麼打算用我的真庫測試?」

實況(總管當天實查):這台機器同時掛兩條 Arcrun MCP 連線,工具名是兩串 UUID——
mcp__00402d88-...__* → logged_in_as: leo21c(真庫),mcp__d0eaf65b-...__* →
logged_in_as: youlin(測試場)。總管整天用第一條,測試資料卻寫在第二條,
造成三次假結論(「wiki 沒送上去」「排在 1691 筆後面」「ingest 沒寫進去」——
全是查錯庫),而總管自己每張派工單都寫著「測試一律用 youlin」,卻一次都沒套用。

判準:任何打向 leo21c 座標的 kbdb_*/arcrun_* MCP 呼叫一律擋,除非明說
(touch /tmp/.leo21c-mcp-ok,單次 15 分鐘、會留痕到 .claude/hooks/
leo21c-mcp-guard.log)。讀寫都擋,但訊息不同:唯讀提示「你可能查錯地方」,
寫入提示「這會汙染真資料」。arcrun_whoami 本身永遠放行(唯一能確認身份的
工具,擋它會造成死結)。

已知限制(查過但查不到穩定識別法,不是猜測):MCP server 的 UUID 是這次連線
分配的,不是穩定識別碼,重裝/換 session 會換掉;~/.claude.json 等本機設定檔
裡找不到這兩條連線的來源設定,判斷是雲端 connector 層動態配的。因此這兩個
UUID 是寫死的(2026-08-27 由 arcrun_whoami 實測得到),且會過期。設計上不是
「表格以外的一律放行」,而是「不在表格裡的 UUID 一樣擋,且訊息明講清單可能
過期」——大聲失敗,不靜默放行到身份不明的目標(fail-closed)。

測試:hooks/tests/leo21c-mcp-guard.test.sh 19/19 通過,涵蓋擋(真庫讀/寫)、
放行(測試場讀/寫)、不擋(whoami 三種座標)、不擋(無關工具)、擋(未知UUID)、
戳記單次放行+留痕。既有測試套件(ask-user-question/dispatch-format/
factory-idle/main-and-prod-push/prod-write/stage-before-prod/sdd-guard)
全部重跑一遍,均維持原本通過狀態,未受本次改動影響。

hooks.json 新增一條 PreToolUse matcher:.*mcp__[0-9a-fA-F-]+__(kbdb_|arcrun_).*
docs/hooks-inventory.md 補上新閘一行,並記錄目前實際磁碟數(49 檔/60 條)
跟這份表既有列表(47/57)之間還有既存落差(isep-presence-beacon.sh/
milestone-due-guard.sh 疑似兩支舊閘未收錄)——不是本票動的閘,未一併查證,
留給總管或後續票核實。

plugin.json 0.5.0 → 0.5.1(改完不升版沒人吃得到;check-version-consistency.sh
在本分支照慣例是紅的,tag 於 merge 時打)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 15:01:29 +08:00

203 lines
9.5 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/sh
# leo21c-mcp-guard.sh — PreToolUse:擋下打向 leo21c 真庫的 Arcrun/KBDB MCP 呼叫
# leo 2026-08-27 立,ISEP#69
#
# leo 原話:「我從頭的指令就叫你去打 youlin,stage 環境是它你不知道嗎?
# 為什麼打算用我的真庫測試?」
#
# 🔴 立這道閘的來由(2026-08-27 總管實查):
# 這台機器同時掛著兩條 Arcrun MCP 連線,工具名是兩串亂碼 UUID:
# mcp__00402d88-5e2f-4125-b971-8bdac50d7456__* → logged_in_as: leo21c 🔴 leo 的真庫
# mcp__d0eaf65b-583e-464e-aacc-00527ac05c4b__* → logged_in_as: youlin ✅ 測試場
# 總管一整天用的是第一條,所有測試資料卻寫在 youlin。範圍講清楚:
# 打在 leo21c 上的**全是唯讀查詢**kbdb_searchkbdb_get_mapwhoami),
# 沒有寫入、沒有 ingest。但唯讀也不該發生,而且它造成了三次假結論:
# ①「wiki 沒送上去」——其實上去了,在 youlin
# ②「檔案排在 1691 筆佇列後面」——查錯庫
# ③「ingest 回 200 但一個半小時還沒寫進去」——換 youlin 那條查,四塊內容全在
# 最難看的一格:總管每一份派工單都親手寫著「測試一律用 youlin,絕對不要碰 leo21c」
# ——規則寫給別人,自己一次都沒套用。
#
# 判準(封動作,不封措辭 —— 同 empty-handed-stop-guardleo21c-write-guard 的哲學):
# 任何打向 leo21c 座標的 Arcrun/KBDB MCP 呼叫 ⇒ 擋,除非明說要打它(見下方戳記)。
# 讀寫都擋,但訊息不同:唯讀提示「你可能查錯地方」,寫入提示「這會汙染真資料」。
# `arcrun_whoami` 本身永遠放行——那是唯一能確認身份的工具,擋它會造成死結。
#
# 🔴 已知限制(紅線要求「查不到就明講」,這裡就是查不到的那部分):
# MCP server 的 UUID 是這台機器這次連線分配的,**不是穩定不變的識別碼**
# ——查過 ~/.claude.json/本機常見設定檔,這兩條 Arcrun 連線都不是本地
# mcpServers 設定出來的(找不到來源設定檔),判斷是雲端 connector 這層
# 動態配的,UUID 會在重裝/換 session 時换掉。沒有找到官方文件證實這點,
# 這裡寫的是「查過但查不到穩定識別法」,不是猜測的答案。
# ⇒ 下面兩個 UUID 是寫死的(2026-08-27 由 leo 實測 arcrun_whoami 得到),
# **這份對照表會過期**。因此設計上不是「表格以外的一律放行」,
# 而是「不在表格裡的 UUID 一樣擋,且訊息明講『清單可能過期』」——
# 大聲失敗,不要靜默放行到一個身份不明的目標(fail-closed)。
set -eu
LEO21C_UUID="00402d88-5e2f-4125-b971-8bdac50d7456"
YOULIN_UUID="d0eaf65b-583e-464e-aacc-00527ac05c4b"
STAMP="/tmp/.leo21c-mcp-ok"
PROJ="${CLAUDE_PROJECT_DIR:-$(pwd)}"
LOGDIR="$PROJ/.claude/hooks"
trace() {
# 留痕:擋下與放行都記,"明說放行" 也要留下痕跡讓 leo 事後查得到
# inkstone/InkStoneCo#48 同一個理由:閘只記「擋下」,分母未知答不出「它有沒有在動」)
T=$(date "+%Y-%m-%d %H:%M:%S" 2>/dev/null || echo "?")
if [ -d "$LOGDIR" ]; then
printf '| %s | %s | %s | %s |\n' "$T" "$1" "${UUID:-?}" "${TOOL:-?}" \
>> "$LOGDIR/leo21c-mcp-guard.log" 2>/dev/null || true
fi
}
stamp_ok() {
# 單次、15 分鐘失效 —— 同 prod-write-guard.sh 的戳記設計,用完即丟
# (多條 subagent 並行時「時間窗」本身就是漏洞,單次消耗把窗口壓到最小)
[ -f "$STAMP" ] || return 1
NOW=$(date +%s 2>/dev/null || echo 0)
MT=$(stat -f %m "$STAMP" 2>/dev/null || stat -c %Y "$STAMP" 2>/dev/null || echo 0)
case "$NOW$MT" in *[!0-9]*) return 1 ;; esac
[ "$NOW" -gt 0 ] && [ "$MT" -gt 0 ] || return 1
[ $((NOW - MT)) -lt 900 ] || return 1
rm -f "$STAMP" 2>/dev/null || true
return 0
}
INPUT="$(cat)"
TOOL=$(printf '%s' "$INPUT" | python3 -c '
import json,sys
try: print(json.load(sys.stdin).get("tool_name","") or "")
except Exception: print("__PYFAIL__")
' 2>/dev/null || printf '__PYFAIL__')
# ── fail-closed:解析壞了不准變成靜默放行 ────────────────────────────
# 這裡拿不到 tool_name,就退回對原始 payload 做字串比對;
# 命中真庫座標就照「未知/不確定」等級擋,而不是直接 exit 0。
if [ "$TOOL" = "__PYFAIL__" ] || [ -z "$TOOL" ]; then
if printf '%s' "$INPUT" | grep -q "$LEO21C_UUID"; then
UUID="$LEO21C_UUID"; TOOL="(解析失敗,字串比對命中真庫座標)"
stamp_ok && { trace "✅ 明說放行(解析失敗)"; exit 0; }
trace "⛔ 擋下(解析失敗仍命中真庫)"
cat >&2 <<MSG
🚫 leo21c-mcp-guard 解析這次呼叫的 tool_name 失敗,
但原始內容裡看得到 leo21c 的座標 —— fail-closed,先擋。
要放行這一次:touch /tmp/.leo21c-mcp-ok
MSG
exit 2
fi
exit 0
fi
# 只管「mcp__<uuid>__(kbdb_|arcrun_)*」這個形狀,其餘一律放行
case "$TOOL" in
mcp__*__kbdb_*|mcp__*__arcrun_*) ;;
*) exit 0 ;;
esac
UUID=$(printf '%s' "$TOOL" | sed -E 's/^mcp__([0-9a-fA-F-]+)__.*/\1/')
SUFFIX=$(printf '%s' "$TOOL" | sed -E 's/^mcp__[0-9a-fA-F-]+__//')
# 🔴 紅線:arcrun_whoami 本身永遠不擋——它是唯一能確認身份的工具,
# 擋它會造成死結(想查是誰、卻連查都查不了)。
case "$SUFFIX" in
arcrun_whoami) exit 0 ;;
esac
# youlin 測試場:預設放行,不留痕(每次都記會把 log 灌爆,而這是預期的正常路徑)
[ "$UUID" = "$YOULIN_UUID" ] && exit 0
# 明說要打(leo 有時真的需要查自己的庫):單次戳記放行,留痕
if stamp_ok; then
trace "✅ 明說放行"
exit 0
fi
# 判斷讀寫,訊息不同(兩者都擋,但傷害的說法不一樣)
case "$SUFFIX" in
*create_record*|*create_template*|*push_workflow*|*delete_workflow*|\
*recipe_push*|*recipe_delete*|*create_tag*|*delete_tag*|\
*tag_resource*|*untag_resource*|*run_workflow*|*report_feedback*|*recipe_submit*)
KIND="write" ;;
*)
KIND="read" ;;
esac
if [ "$UUID" = "$LEO21C_UUID" ]; then
trace "⛔ 擋下(真庫/$KIND)"
if [ "$KIND" = "write" ]; then
cat >&2 <<MSG
🚫 這個寫入打向 leo21c —— leo 本人的真庫,不是測試場(leo 2026-08-27 立,ISEP#69
leo 原話:「我從頭的指令就叫你去打 youlin,stage 環境是它你不知道嗎?
為什麼打算用我的真庫測試?」
你剛剛呼叫的是:
mcp__${LEO21C_UUID}__${SUFFIX}
→ logged_in_as: leo21c 🔴 真庫
這不是「查錯地方」——這是**把測試資料寫進 leo 自己在用的知識庫**,會汙染他的真實資料。
你要的測試場是另一條線(同樣的動作,換一個 server 前綴):
mcp__${YOULIN_UUID}__${SUFFIX}
→ logged_in_as: youlin.hsieh.dev ✅ 測試場,隨便寫
改法:換那條線再打一次。
⚠️ 若這次真的是 leo 要你動他的真庫(而不是測試):
touch /tmp/.leo21c-mcp-ok # 只放行「下一個」呼叫,15 分鐘內有效、用完即消失
會被記錄到 .claude/hooks/leo21c-mcp-guard.log。
MSG
else
cat >&2 <<MSG
🚫 這個查詢打向 leo21c —— leo 本人的真庫,不是測試場(leo 2026-08-27 立,ISEP#69
leo 原話:「我從頭的指令就叫你去打 youlin,stage 環境是它你不知道嗎?
為什麼打算用我的真庫測試?」
你剛剛呼叫的是:
mcp__${LEO21C_UUID}__${SUFFIX}
→ logged_in_as: leo21c 🔴 真庫
即使只是唯讀查詢,打錯庫依然有害——**你可能正在查錯地方**:
真庫的資料跟測試場的資料是兩份,用真庫的查詢結果去判斷測試進度,
會做出「wiki 沒送上去」「排在 1691 筆後面」這類看起來像結論、其實是查錯庫的假結論
2026-08-27 總管本人今天就連中三次)。
你要的測試場是另一條線(同樣的查詢,換一個 server 前綴):
mcp__${YOULIN_UUID}__${SUFFIX}
→ logged_in_as: youlin.hsieh.dev ✅ 測試場
改法:換那條線再查一次。
⚠️ 若這次真的是要查 leo 自己的庫(他本人有時真的需要):
touch /tmp/.leo21c-mcp-ok # 只放行「下一個」呼叫,15 分鐘內有效、用完即消失
會被記錄到 .claude/hooks/leo21c-mcp-guard.log。
MSG
fi
exit 2
fi
# ── 走到這裡:UUID 兩個都不是 —— 對照表可能過期,不要靜默放行 ─────────
# 這正是檔頭說的「大聲失敗」:某條連線被重裝、UUID 換了,我們認不出它是誰,
# 與其猜測放行、不如逼一次 arcrun_whoami 或人工更新這份表。
trace "⛔ 擋下(未知UUID,清單可能過期)"
cat >&2 <<MSG
🚫 這個 MCP server 的 id 不在 leo21c-mcp-guard.sh 的已知清單裡
已知清單(2026-08-27 由 arcrun_whoami 實測寫死,見本檔檔頭):
${LEO21C_UUID} → leo21c(真庫,預設擋)
${YOULIN_UUID} → youlin(測試場,預設放)
這次打的是:${UUID}
兩個都不是 —— 代表某條 MCP 連線被重裝過、UUID 換了,這份對照表可能已經過期。
寧可先擋、不要靜默放行到一個身份不明的目標。怎麼辦:
1. 呼叫 arcrun_whoami(這支工具本身不會被擋)確認這個 server 真正的身份
2. 如果它是新的 youlin id → 回報 isep-guard 更新 hooks/leo21c-mcp-guard.sh 的對照表
3. 現在就是要放行這一次 → touch /tmp/.leo21c-mcp-ok15 分鐘內有效、用完即消失)
MSG
exit 2