subagent-first-guard 補三個洞:閘不管 .sh、派過工變免死金牌、警報響一次就啞掉
leo 2026-08-27(inkstone/ISEP#66):「你禁止寫 code,為什麼你改?為什麼你可以執行?」 總管親手改了 no-ticket-no-dispatch.sh,這道閘一聲不響。 三個洞,一次補: 1. `.sh` 從沒進過受管副檔名清單——這個 repo 自己的閘全是 .sh,等於這道閘 從第一行 case 就直接放行,路徑白名單根本沒機會判斷。這才是真正原因, 不是路徑豁免。修法:閘本身(`.claude/hooks/*.sh`、`hooks/*.sh`)獨立收進 受管清單;同時拿掉 `*/.claude/hooks/*` 整目錄豁免——能改閘的人等於能關掉 所有其他閘,這句話本身就是拿掉它的理由。 2. 「這個 session 派過一次工」曾被當永久放行條件,但那混淆了「這件任務有沒有 交出去」跟「下一次任意的手改合不合理」——整個拿掉,改成純資訊(留在被擋下 時的訊息裡,告訴你上次派工是幾分鐘前)。 3. 「同一 session 只擋一次」讓警報響過一次就對餘生啞掉。改成寬限期(預設 15 分鐘,可用 SUBAGENT_GUARD_NAG_TTL 覆寫做測試):寬限期內不重複鬼打牆, 過了就再響——不選邊犧牲「有效」或「不煩人」。 放行只剩三種:CLAUDE_CODE_CHILD_SESSION=1(subagent 本人,它就是被派來寫的)/ solo-ok 非空(寫了理由,不是 touch 空檔)/檔案本身不歸這道閘管(測試檔/ system-dev 的 wiki/非受管副檔名)。 新增 scripts/test-subagent-first-guard.sh,20/20 通過:涵蓋三個洞各自的重演、 CHILD_SESSION 放行、測試檔/wiki 放行、solo-ok 空檔不算數、寬限期到期再響。 plugin.json 0.5.0 → 0.5.1;docs/hooks-inventory.md 同步更新兩支閘的說明。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Executable
+96
@@ -0,0 +1,96 @@
|
||||
#!/bin/bash
|
||||
# subagent-first-guard 的測試(inkstone/ISEP#66)
|
||||
#
|
||||
# 補的三個洞:
|
||||
# 洞一:.sh 從沒進過受管副檔名清單 ⇒ 這個 repo 自己的閘(全是 .sh)從沒被這道閘管過。
|
||||
# 洞二:「這個 session 派過一次工」被當永久放行——拿掉,不再是放行條件。
|
||||
# 洞三:「同一 session 只擋一次」讓警報響過一次就對餘生啞掉——改成寬限期,過了再響。
|
||||
#
|
||||
# 判準:派過工不再免死金牌/閘本身(.sh)進受管清單/寬限期內不鬼打牆、過了要再響/
|
||||
# CHILD_SESSION=subagent 本人放行/測試檔・system-dev wiki 放行/
|
||||
# solo-ok 要非空(寫理由)才放行,單純 touch 不算。
|
||||
cd "$(dirname "$0")/.." || exit 1
|
||||
H=hooks/subagent-first-guard.sh
|
||||
PASS=0; FAIL=0
|
||||
|
||||
run(){ # $1=want_exit $2=file_path $3=sid $4=child(0/1)
|
||||
local want="$1" fp="$2" sid="$3" child="${4:-0}"
|
||||
local payload
|
||||
payload=$(python3 -c 'import json,sys;print(json.dumps({"tool_name":"Edit","tool_input":{"file_path":sys.argv[1]},"session_id":sys.argv[2]}))' "$fp" "$sid")
|
||||
if [ "$child" = "1" ]; then
|
||||
got=$(CLAUDE_CODE_CHILD_SESSION=1 SUBAGENT_GUARD_NAG_TTL="${TTL:-900}" bash -c 'printf "%s" "$1" | bash "$2" >/dev/null 2>&1; echo $?' _ "$payload" "$H")
|
||||
else
|
||||
got=$(env -u CLAUDE_CODE_CHILD_SESSION SUBAGENT_GUARD_NAG_TTL="${TTL:-900}" bash -c 'printf "%s" "$1" | bash "$2" >/dev/null 2>&1; echo $?' _ "$payload" "$H")
|
||||
fi
|
||||
if [ "$got" = "$want" ]; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi
|
||||
printf 'want=%s got=%s sid=%s %.70s\n' "$want" "$got" "$sid" "$fp"
|
||||
}
|
||||
|
||||
clean(){ rm -f "/tmp/.subagent-spawned-$1" "/tmp/.solo-ok-$1" "/tmp/.subagent-guard-warned-$1"; }
|
||||
|
||||
R="/Users/youlinhsieh/Documents/tech_projects/InkStoneCo"
|
||||
ISEP="/Users/youlinhsieh/Documents/tech_projects/ISEP"
|
||||
|
||||
echo "── 洞一:閘本身是 .sh,也要被這道閘管(重演 2026-08-27 那次)──"
|
||||
S=t66-hole1-a; clean "$S"
|
||||
run 2 "$R/.claude/hooks/no-ticket-no-dispatch.sh" "$S" 0
|
||||
S=t66-hole1-b; clean "$S"
|
||||
run 2 "$ISEP/hooks/subagent-first-guard.sh" "$S" 0
|
||||
|
||||
echo "── 洞二:派過工不是免死金牌——就算剛派、就算派了很多次,直接手改一樣要擋 ──"
|
||||
S=t66-hole2-a; clean "$S"
|
||||
date +%s > "/tmp/.subagent-spawned-$S" # 剛派過工(此刻)
|
||||
run 2 "$R/.claude/hooks/no-ticket-no-dispatch.sh" "$S" 0
|
||||
S=t66-hole2-b; clean "$S"
|
||||
echo $(( $(date +%s) - 3600 )) > "/tmp/.subagent-spawned-$S" # 一小時前派過(今天真實情境的重演)
|
||||
run 2 "$R/.claude/hooks/no-ticket-no-dispatch.sh" "$S" 0
|
||||
|
||||
echo "── 洞三:警報不能響一次就對餘生啞掉——寬限期內不鬼打牆,過了要再響 ──"
|
||||
S=t66-hole3; clean "$S"; TTL=2
|
||||
run 2 "$R/matrix/arcrun/foo.py" "$S" 0 # 第一次:沒見過 → 擋,順便留下 warned 時戳
|
||||
run 0 "$R/matrix/arcrun/foo.py" "$S" 0 # 馬上重試:寬限期內 → 不鬼打牆,放行
|
||||
sleep 3
|
||||
run 2 "$R/matrix/arcrun/foo.py" "$S" 0 # 寬限期過了 → 再響
|
||||
TTL=900
|
||||
|
||||
echo "── 不該擋:subagent 本人(CHILD_SESSION=1)自己寫它被派來寫的 code ──"
|
||||
S=t66-child; clean "$S"
|
||||
run 0 "$R/.claude/hooks/no-ticket-no-dispatch.sh" "$S" 1
|
||||
run 0 "$R/matrix/arcrun/foo.py" "$S" 1
|
||||
|
||||
echo "── 不該擋:測試檔 ──"
|
||||
S=t66-test; clean "$S"
|
||||
run 0 "$R/matrix/arcrun/foo_test.py" "$S" 0
|
||||
run 0 "$R/matrix/arcrun/foo.test.ts" "$S" 0
|
||||
run 0 "$ISEP/tests/foo.spec.js" "$S" 0
|
||||
|
||||
echo "── 不該擋:system-dev/ 的 wiki(即使副檔名受管)──"
|
||||
S=t66-wiki; clean "$S"
|
||||
run 0 "$R/system-dev/wiki/status.md" "$S" 0
|
||||
run 0 "$R/system-dev/docs/some_script.py" "$S" 0
|
||||
|
||||
echo "── 不該擋:非受管副檔名(一般 .sh 不在 hooks 目錄/.md/.json)──"
|
||||
S=t66-nonguard; clean "$S"
|
||||
run 0 "$R/scripts/deploy.sh" "$S" 0
|
||||
run 0 "$R/README.md" "$S" 0
|
||||
run 0 "$R/package.json" "$S" 0
|
||||
|
||||
echo "── solo-ok:要非空(寫理由)才放行,單純 touch 不算 ──"
|
||||
S=t66-soloempty; clean "$S"
|
||||
touch "/tmp/.solo-ok-$S"
|
||||
run 2 "$R/matrix/arcrun/foo.py" "$S" 0
|
||||
clean "$S"
|
||||
echo "理由:單行修,緊急止血" > "/tmp/.solo-ok-$S"
|
||||
run 0 "$R/matrix/arcrun/foo.py" "$S" 0
|
||||
clean "$S"
|
||||
|
||||
echo "── 真違規不能因為這次改動漏擋:全新 session 直接改一般 code ──"
|
||||
S=t66-fresh; clean "$S"
|
||||
run 2 "$R/matrix/arcrun/graph-executor.ts" "$S" 0
|
||||
|
||||
# 清理
|
||||
for s in t66-hole1-a t66-hole1-b t66-hole2-a t66-hole2-b t66-hole3 t66-child t66-test t66-wiki t66-nonguard t66-soloempty t66-fresh; do clean "$s"; done
|
||||
|
||||
echo
|
||||
echo "$PASS/$((PASS+FAIL)) 通過"
|
||||
[ "$FAIL" -eq 0 ]
|
||||
Reference in New Issue
Block a user