fix: 白名單認的是「哪個 repo」不是路徑前綴(inkstone/ISEP#117)

照 `line-needs-own-worktree.sh` 印的下一步開 worktree(`<repo>-wt-<票號>/`),
接著在裡面寫檔會被 `guard-cross-project.sh` 擋——它的白名單是路徑前綴,
`matrix/arcrun-wt-195/…` 對不上 `matrix/arcrun/*`。
⇒ 照規矩做的人被擋,留在共用目錄裡的人反而過得了(inkstone/Arcrun#195 實撞,
工人只好把 worktree 開到 InkStoneCo 樹之外躲開它)。

判準改成 git 自己算得出來的事實,跟 line-needs-own-worktree 同一個形狀:
`git worktree list --porcelain` 第一筆永遠是主工作目錄 ⇒ 主目錄與它所有的
worktree 回同一個答案。`-wt-` 這個命名慣例一個字都沒有進判準。
名單上沒有的 repo、它的 worktree、根本不是 repo 的目錄,全部照舊擋。

順帶修掉一個獨立的缺陷(實測證據見檔內註解與 wiki):
這支閘在 plugin 裡**從來沒生效過**。它用 `<hook 檔>/../..` 當頂層 repo 根,
搬進 plugin 之後那個路徑指到 plugin cache 的上一層 ⇒ 每一次都靜默放行。
同一份 payload:plugin 那份 exit 0、InkStoneCo 那份 exit 2,而兩個檔案 diff 一字不差。
改用 `$CLAUDE_PROJECT_DIR`(README 路徑規約本來就這樣寫)。
🔴 這也代表:本輪的修法要生效,`InkStoneCo/.claude/hooks/guard-cross-project.sh`
那份現役副本必須退場(刪檔+從 settings.json 取消註冊)——那是頂層的樹,
不在本 PR 裡,交回總管裁。

測試:scripts/test-guard-cross-project.sh(docs/TESTING.md A25)21 條,
真的 git repo + 真的 worktree 當道具,放行 12 條/該擋 9 條兩個方向都驗。
不新增第三支閘。

版本號待總管定版。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-01 14:46:35 +08:00
parent 4b91cd5d48
commit 7af30e97b0
6 changed files with 251 additions and 12 deletions
+88
View File
@@ -0,0 +1,88 @@
#!/bin/bash
# 跨專案越界閘的測試(docs/TESTING.md A25)— inkstone/ISEP#117
#
# 判準:白名單放行的是「**哪個 repo**」,不是「哪個路徑前綴」。
# ⇒ `matrix/arcrun` 的 worktree(照 line-needs-own-worktree.sh 印的那行開出來的
# `matrix/arcrun-wt-<票號>/`)要跟主工作目錄一樣放行;
# ⇒ 名單上沒有的子 repo、它的 worktree、根本不是 repo 的目錄,全部照舊擋。
#
# 這支測試用**真的 git repo 真的 worktree**當道具(不是假路徑字串)——
# 因為被測的判準就是 `git worktree list` 的第一筆,用假路徑等於沒驗到判準本體。
#
# ⚠️ 誠實邊界:本測試用 `CLAUDE_CODE_CHILD_SESSION=0` 代表「總管」。
# 真實環境裡那個變數不可靠(Claude Desktop 的主 session 也是 1
# 見 InkStoneCo/system-dev/wiki/ops-facts.md)。這裡驗的是**閘的分支邏輯**,
# 不是「總管在真機上一定會被擋」——後者是另一件事,本輪沒動。
set -u
cd "$(dirname "$0")/.." || exit 1
H="$PWD/hooks/guard-cross-project.sh"
FIX="$(mktemp -d "${TMPDIR:-/tmp}/isep117.XXXXXX")"
trap 'rm -rf "$FIX"' EXIT
g() { git -C "$1" -c user.email=t@t -c user.name=t "${@:2}"; }
mkrepo() { # mkrepo <路徑>
mkdir -p "$1" && git -C "$1" init -q 2>/dev/null
: > "$1/seed"; g "$1" add -A >/dev/null 2>&1; g "$1" commit -qm seed >/dev/null 2>&1
}
ROOT="$FIX/InkStoneCo"
mkrepo "$ROOT" # 頂層樹本身也是一個 repo
mkrepo "$ROOT/matrix/arcrun" # 白名單上的子 repo
mkrepo "$ROOT/products/arcrun-rag" # 白名單上的子 repo
mkrepo "$ROOT/matrix/other" # **不在**白名單上的子 repo
mkdir -p "$ROOT/matrix/plain-dir" # 根本不是 repo
mkdir -p "$ROOT/_archive/old"
# 照 line-needs-own-worktree.sh 印出來的那一行原樣開(<NAME>-wt-<票號>,兄弟目錄)
g "$ROOT/matrix/arcrun" worktree add -q "$ROOT/matrix/arcrun-wt-117" -b wt117 >/dev/null 2>&1
g "$ROOT/products/arcrun-rag" worktree add -q "$ROOT/products/arcrun-rag-wt-99" -b wt99 >/dev/null 2>&1
g "$ROOT/matrix/other" worktree add -q "$ROOT/matrix/other-wt-1" -b wt1 >/dev/null 2>&1
# 名字長得像 worktree、實際上只是個空目錄
mkdir -p "$ROOT/matrix/arcrun-wt-fake"
# macOS 的 $TMPDIR 是 /var/...symlink),git 回的是 /private/var/... ⇒ 兩種形式都要驗
ROOT_PHYS="$(cd "$ROOT" && pwd -P)"
PASS=0; FAIL=0
run() { # run <want> <CHILD_SESSION> <file_path> <說明>
local want=$1 child=$2 fp=$3 note=$4 got
printf '%s' "{\"tool_input\":{\"file_path\":$(python3 -c 'import json,sys;print(json.dumps(sys.argv[1]))' "$fp")}}" \
| CLAUDE_PROJECT_DIR="$ROOT" CLAUDE_CODE_CHILD_SESSION="$child" bash "$H" >/dev/null 2>&1
got=$?
if [ "$got" = "$want" ]; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi
printf 'want=%s got=%s child=%s %s\n' "$want" "$got" "$child" "$note"
}
echo "── 該放行:subagent 在白名單 repo 裡(含照閘 A 開出來的 worktree)──"
run 0 1 "matrix/arcrun/src/foo.ts" "主工作目錄(迴歸:本來就該過)"
run 0 1 "matrix/arcrun-wt-117/src/foo.ts" "★ 本票的病:照閘 A 開的 worktree"
run 0 1 "$ROOT/matrix/arcrun-wt-117/src/foo.ts" "★ 同上,絕對路徑"
run 0 1 "matrix/arcrun-wt-117/a/b/c/還沒建的檔.ts" "worktree 裡的新檔(目錄還不存在)"
run 0 1 "products/arcrun-rag-wt-99/main.go" "另一個白名單 repo 的 worktree"
run 0 1 "products/arcrun-rag/main.go" "主工作目錄(迴歸)"
run 0 1 "$ROOT_PHYS/matrix/arcrun-wt-117/src/foo.ts" "★ 絕對路徑是實體形式(/private/var vs /var"
echo "── 該放行:跟白名單無關的既有規則(迴歸,不能被這次改動弄壞)──"
run 0 0 "matrix/arcrun/docs/HANDOFF-x.md" ".md 一律放行(leo 2026-06-15 拍板)"
run 0 0 "matrix/other/README.md" "非白名單 repo 的 .md 也放行"
run 0 0 "system-dev/wiki/tool.py" "頂層自己的檔"
run 0 0 ".claude/hooks/x.sh" "頂層自己的檔"
run 0 1 "/tmp/somewhere-else/x.ts" "頂層樹之外(本來就不管)"
echo "── 該擋:總管(非 subagent)寫實作進子 repo ──"
run 2 0 "matrix/arcrun/src/foo.ts" "白名單只對 subagent 開,總管照擋"
run 2 0 "matrix/arcrun-wt-117/src/foo.ts" "★ worktree 不是總管的後門"
run 2 0 "products/arcrun-rag/main.go" "同上,另一個 repo"
run 2 0 "_archive/old/x.py" "_archive 也在管轄內"
echo "── 該擋:subagent,但那個 repo 不在白名單上 ──"
run 2 1 "matrix/other/src/foo.ts" "不在名單的子 repo"
run 2 1 "matrix/other-wt-1/src/foo.ts" "★ 不在名單的 repo,它的 worktree 一樣擋"
run 2 1 "matrix/plain-dir/foo.ts" "根本不是 repogit 往上找到頂層樹)"
run 2 1 "matrix/arcrun-wt-fake/foo.ts" "★ 名字像 worktree、其實是空目錄 ⇒ 名字騙不過去"
run 2 1 "_archive/old/x.py" "_archive 不在白名單上"
echo
echo "通過 $PASS 條,失敗 $FAIL"
[ "$FAIL" -eq 0 ]