e45faef8a2
背景:2026-09-01 一個 subagent 把分支推上 GitHub 沒經過解保險,同時總管這端擋得住, 看似「subagent 端失效」。查證後: - 官方文件(https://code.claude.com/docs/en/hooks)明講 PreToolUse hook 對 subagent 與主線是同一批,只是 input 多帶 agent_id/agent_type ⇒ 閘無從只擋一端。 - github-contact-guard.sh 從不讀那兩個欄位,所以「一端有效一端失效」不會出在身分判斷上。 - 當天的洞是同一族的 cwd 病(inkstone/ISEP#109/#125):閘拿 hook 自己的 cwd 解 origin。 2026-09-07(49a7145)接上 lib/push_target_dir.py 後,**字面路徑**的形狀已修好, 且已由 scripts/test-github-contact-guard.sh 的 A39 cwd 群覆蓋。 本輪交付(都驗過): 1. 在既有的 scripts/test-github-contact-guard.sh(唯一真相源,不另開平行測試)尾端加 7 條 runsub:同一條指令帶 agent_id(subagent)與不帶(總管)各跑一次,判決必須相同。 涵蓋推 GitHub(含 cd/-C 字面路徑原形狀)擋、推 Gitea/讀取放行。26→33,實測 33/33。 2. 回歸性驗過:把新測試跑舊閘(49a7145^)→ 09-01 的兩條 cd/-C 場景紅(漏擋), 證明這組測試真的抓得住它要抓的病。 3. docs/TESTING.md A39 段更新條數、失敗判讀,並記下下面這個殘留破口。 🔴 查出並實測(含雲端實跑)一個殘留的同形狀破口,未修(見下): push 目標用**未展開 shell 變數**表示時(git -C "$VAR" push/cd "$VAR" && git push), hook 收到的是展開前字串,push_target_dir 對 $VAR 回「不確定」⇒ 退回 session cwd 解 remote。 關鍵不對稱:main-and-prod-push-guard 遇「不確定」退回「要戳記」=擋(fail-safe); github-contact-guard 預設「證明不了 github 就放行」⇒ cwd 是 Gitea 時,真的推 GitHub 的 -C "$VAR" 會漏擋。雲端實跑(本 session 即 CHILD_SESSION=1):git -C "$VAR/gh-repo" push 未被 D20 擋,一路打到 git proxy 才被 403 擋(那是 proxy 不是 D20);本機沒有 proxy 後手。 不修的理由:fail-closed 會誤攔「用變數推 Gitea」,屬「誤攔比漏擋嚴重」的取捨, 應另開票由總管/leo 裁(附建議修法:push_target_dir 區分「無指令」與「指令不可解」, 後者 github-contact 改擋並印『改用字面路徑』的出路——出路走得通、可測)。 同形狀家族盤點(實跑驗過,回應「還有哪些閘是這個形狀」): - github-contact-guard.sh:字面路徑已修;殘留變數形狀破口(見上,另票)。本輪補 subagent 維度測試。 - main-and-prod-push-guard.sh:已修(push_target_dir),且遇不確定 fail-safe,cross-repo 測試 19/19。 - line-needs-own-worktree.sh:cwd 維度已修(checkout_target_dir),測試 110/110。 - not-my-branch-guard.sh:★仍是同形狀破口★——git -C "$PROJ" 讀 hook 自己的目錄; NOT_MY_BRANCH_OK=1 前綴 PreToolUse 收不到(逃生口結構性失效)。實測:subagent cd/-C 進別的 repo 對 held 分支 commit → 放行(該擋)。屬 commit 閘、非本票 D20 範圍, mistakes.md 早標「另報」,建議另開票(需 commit-target 解析器+認得字面前綴)。 僅動測試腳本與文件,未動任何會被載入的檔(hooks/commands/skills/agents/plugin.json 皆未動) ⇒ 不改變任何閘的行為,plugin.json 未動,版本待總管定。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TGxitYq49FzYC7EFkbhzF5
105 lines
7.2 KiB
Bash
Executable File
105 lines
7.2 KiB
Bash
Executable File
#!/bin/bash
|
||
# github-contact-guard 的測試(inkstone/InkStoneCo#23)
|
||
# 判準:真的寫 GitHub(gh CLI 高頻 API、git push/remote add 指向 github.com)要擋;
|
||
# 只是提到(heredoc body、同一行引號內的散文、commit message)不准擋;
|
||
# 讀取(clone/fetch/pull/curl 抓檔)一律放行,不管有沒有帶認證。
|
||
cd "$(dirname "$0")/.." || exit 1
|
||
H=hooks/github-contact-guard.sh
|
||
PASS=0; FAIL=0
|
||
run(){ # $1=want $2=cmd
|
||
printf '%s' "{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":$(python3 -c 'import json,sys;print(json.dumps(sys.argv[1]))' "$2")}}" \
|
||
| bash "$H" >/dev/null 2>&1
|
||
got=$?
|
||
if [ "$got" = "$1" ]; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi
|
||
printf 'want=%s got=%s %.72s\n' "$1" "$got" "$2"
|
||
}
|
||
|
||
echo "── 該擋:真的在寫 GitHub ──"
|
||
run 2 'gh issue create --title x --body y'
|
||
run 2 'gh pr create --title x'
|
||
run 2 'git push https://github.com/example/example.git HEAD:main'
|
||
run 2 'git remote add github https://github.com/example/example.git'
|
||
run 2 'echo start && gh api repos/example/example/issues'
|
||
|
||
echo "── 不該擋:讀取一律放行 ──"
|
||
run 0 'git clone https://github.com/example/example.git'
|
||
run 0 'git fetch github'
|
||
run 0 'curl -sL https://github.com/example/example/releases/latest'
|
||
run 0 'gh --version'
|
||
|
||
echo "── 不該擋:只是提到、heredoc body、同一行引號內的散文 ──"
|
||
run 0 "$(printf 'cat > docs/TESTING.md <<%sEOF%s\nexample: git push https://github.com/example/example.git HEAD:main\nEOF\n' "'" "'")"
|
||
run 0 'git commit -m "docs: explain why gh api calls used to be mis-flagged"'
|
||
run 0 'python3 -c "requests.post(url, json={\"body\": \"quoting: git push origin main to github.com was mis-flagged as a real push\"})"'
|
||
run 0 'grep -n "gh api" hooks/github-contact-guard.sh'
|
||
run 0 'grep -rn "git push" installer/scripts/line-source-repo.mjs'
|
||
|
||
echo "── 判「push 到哪」要看這條指令實際會推的那個 repo,不是 hook 站的 cwd(inkstone/ISEP#109 → comment 6629,2026-09-07 實撞)──"
|
||
# 為什麼這群非有不可:雲端薄殼的 cwd 是 GitHub 那份(origin=github.com),而總管在
|
||
# `InkStoneCo/`(origin=Gitea)推分支。舊版拿 **hook 收到的 cwd** 去解 `origin`,
|
||
# 於是 `git -C <InkStoneCo> push origin x` 被判成「remote origin 指向 GitHub」擋下
|
||
# ——連 `-C` 都沒看。arcrun-hand 同日在 Arcrun#176 comment 6614 撞到同一支。
|
||
# 判準仍是唯一識別碼(remote 解出來的 URL 主機),不是名字;**不是「有 -C 就放行」**——
|
||
# 下面「該擋」那組就是 -C/cd 指到 GitHub 那份時照擋的證據。
|
||
W=$(mktemp -d "${TMPDIR:-/tmp}/isep-ghc.XXXXXX"); trap 'rm -rf "$W"' EXIT
|
||
W=$(cd "$W" && pwd -P)
|
||
mk(){ git init -q -b main "$1" && git -C "$1" remote add origin "$2"; }
|
||
mk "$W/shell" https://github.com/example/shell.git # 薄殼:origin=GitHub
|
||
mk "$W/body" https://git.example.invalid/inkstone/body.git # 真身:origin=Gitea 的形狀(主機不是 github.com)
|
||
mkdir -p "$W/body/sub"
|
||
runc(){ # $1=want $2=cwd $3=cmd —— 跟 run 一樣,只是 payload 帶 cwd(hook 真的會收到它)
|
||
LAST_ERR=$(printf '%s' "{\"tool_name\":\"Bash\",\"cwd\":$(python3 -c 'import json,sys;print(json.dumps(sys.argv[1]))' "$2"),\"tool_input\":{\"command\":$(python3 -c 'import json,sys;print(json.dumps(sys.argv[1]))' "$3")}}" \
|
||
| bash "$H" 2>&1 >/dev/null)
|
||
got=$?
|
||
if [ "$got" = "$1" ]; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi
|
||
printf 'want=%s got=%s cwd=%s %.60s\n' "$1" "$got" "${2#$W/}" "$3"
|
||
}
|
||
has(){ if printf '%s' "$LAST_ERR" | grep -qF -- "$1"; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi; printf '%s\n' "$2"; }
|
||
|
||
echo " · 該擋:這條指令實際推的那個 repo 的 origin 是 github.com"
|
||
runc 2 "$W/shell" 'git push origin x'
|
||
runc 2 "$W/body" "git -C $W/shell push origin x"
|
||
has "$W/shell" " 訊息說得出是哪個目錄的 remote 指向 GitHub(人才知道自己站錯地方還是推錯地方)"
|
||
runc 2 "$W/body" "cd $W/shell && git push origin x"
|
||
runc 2 "$W/shell" "(cd $W/body && true); git push origin x"
|
||
runc 2 "$W/shell" 'cd $SOMEWHERE && git push origin x'
|
||
echo " · 不該擋:這條指令實際推的那個 repo 的 origin 不是 github.com(★=6629 那天被擋的形狀)"
|
||
runc 0 "$W/shell" "git -C $W/body push origin x"
|
||
runc 0 "$W/shell" "git -C $W/body push -u origin feat/x"
|
||
runc 0 "$W/shell" "cd $W/body && git push origin x"
|
||
runc 0 "$W/body/sub" 'git push origin x'
|
||
runc 0 "$W/shell" "cd $W && git -C body push origin x"
|
||
runc 0 "$W/body" "(cd $W/shell && true); git push origin x"
|
||
|
||
echo "── subagent 走同一條判斷:payload 帶 agent_id/agent_type 拿到跟總管一模一樣的判決(inkstone/ISEP#119)──"
|
||
# 為什麼這群非有不可(inkstone/ISEP#119):2026-09-01 一個 subagent 把分支推上 GitHub
|
||
# 沒經過解保險,同時總管這端擋得住——表面像「subagent 端失效」。但官方文件講明:
|
||
# subagent 的 tool 呼叫照樣觸發同一批 PreToolUse hook,只是 input 多帶 agent_id/agent_type
|
||
# (https://code.claude.com/docs/en/hooks)。這支閘從頭到尾不讀那兩個欄位 ⇒ 它**無從**
|
||
# 只擋總管或只擋 subagent。當天真正的洞是同一族的 cwd 病(上面 ISEP#109 那組):
|
||
# subagent 站在薄殼、靠 cd/-C 推它自己那顆 origin=github 的 repo,舊版在錯的目錄解 origin
|
||
# ⇒ 放行。這群把「帶 agent_id 的 payload 判決 == 不帶的判決」釘死,證明沒有 subagent 專屬破口。
|
||
runsub(){ # $1=want $2=cwd $3=cmd —— 同一條指令跑兩次:帶 agent_id(subagent)與不帶(總管),
|
||
# 兩次都必須等於 want,才算「subagent 與總管走同一條判斷、拿同一個結果」。
|
||
local want="$1" cwd="$2" cmd="$3" a b
|
||
a=$(printf '%s' "{\"tool_name\":\"Bash\",\"cwd\":$(python3 -c 'import json,sys;print(json.dumps(sys.argv[1]))' "$cwd"),\"agent_id\":\"agt_119\",\"agent_type\":\"isep-hand\",\"tool_input\":{\"command\":$(python3 -c 'import json,sys;print(json.dumps(sys.argv[1]))' "$cmd")}}" \
|
||
| bash "$H" >/dev/null 2>&1; echo $?)
|
||
b=$(printf '%s' "{\"tool_name\":\"Bash\",\"cwd\":$(python3 -c 'import json,sys;print(json.dumps(sys.argv[1]))' "$cwd"),\"tool_input\":{\"command\":$(python3 -c 'import json,sys;print(json.dumps(sys.argv[1]))' "$cmd")}}" \
|
||
| bash "$H" >/dev/null 2>&1; echo $?)
|
||
if [ "$a" = "$want" ] && [ "$b" = "$want" ]; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi
|
||
printf 'want=%s subagent=%s 總管=%s cwd=%s %.48s\n' "$want" "$a" "$b" "${cwd#$W/}" "$cmd"
|
||
}
|
||
echo " · subagent 推 GitHub(含 09-01 的 cd/-C 原形狀)→ 擋,且與總管同判"
|
||
runsub 2 "$W/shell" 'git push origin x'
|
||
runsub 2 "$W/body" "cd $W/shell && git push origin x"
|
||
runsub 2 "$W/body" "git -C $W/shell push origin x"
|
||
runsub 2 "$W/shell" 'gh api repos/example/example/issues'
|
||
echo " · subagent 推 Gitea/讀取 → 放行,且與總管同判(沒有為了擋 subagent 而擋過頭)"
|
||
runsub 0 "$W/body" 'git push origin x'
|
||
runsub 0 "$W/shell" "git -C $W/body push origin x"
|
||
runsub 0 "$W/shell" 'git clone https://github.com/example/example.git'
|
||
|
||
echo
|
||
echo "$PASS/$((PASS+FAIL)) 通過"
|
||
[ "$FAIL" -eq 0 ]
|