c1d80756d7
總管複驗 PR 時發現 subagent 只補了一個空殼,還有第二個同款的:
arcrun-intent-guard.sh → exec 不存在的 .py ⇒ 擋掉全部(吵,PR 已修)
irreversible-dispatch-guard.sh → 同款,但寫法是
python3 <不存在> 2>/dev/null || echo '{"verdict":"OK"}'
⇒ **靜默放行全部**(危險,本 commit 補)
實測同一份派工單「驗過了就把舊分支刪掉」:
InkStoneCo 版(有 .py)exit=2 擋 / ISEP 版(缺 .py)exit=0 放行
補完後:不可逆派工 exit=2、正常派工 exit=0、合規工作流 exit=0。
系統性掃描 ISEP 全部 hooks 引用的同目錄檔案:補完後 0 個缺檔(InkStoneCo 本來就是 0)。
順帶把 InkStoneCo 的 hooks/tests/ 四支既有測試一併帶過來。
🔴 這件事很重要:若先併 InkStoneCo#64(刪掉 .claude/hooks/),
這台機器會失去那支唯一還能用的副本——變成真的沒有那道閘。
101 lines
3.9 KiB
Python
Executable File
101 lines
3.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""判斷一份派工單(Agent/Task 的 prompt)有沒有把「不可逆動作」寫成收工方可以自己執行的選項。
|
||
|
||
stdin: 派工單全文 stdout: JSON {"verdict": "BLOCK"/"OK", "hits": [[行號, 該行, 命中詞], ...]}
|
||
|
||
【事故(Gitea Leo/arcrun-rag#33,2026-08-09)】
|
||
subagent 未經 leo 同意刪掉兩條遠端分支。根因不是它亂來——是派工單寫了
|
||
「作廢就刪掉分支」,等於總管預先授權了一個不可逆動作。刪掉的那條裡還有一件
|
||
它自己標明「等 leo 排序」的工作,一併蒸發。
|
||
|
||
【對照組,同一天同一個總管】#14 的派工單寫
|
||
「🔴 刪資料不可逆。動手前先把清單寫在 issue 留言,等總管回覆確認才執行」
|
||
⇒ 那個 agent 真的停下來等。同一個人一次寫對一次寫錯 ⇒ 證明只能靠機械閘,不能靠自律。
|
||
|
||
【判準】
|
||
- 派工單裡出現「不可逆動作」的動詞+對象(刪分支/drop table/rm -rf/force push…)
|
||
- 且該處**沒有被否定**(不是「不准刪」這種禁令句)
|
||
- 且全文**沒有**「停下來等回覆才執行」這類守門片語
|
||
⇒ 判定為「把不可逆動作寫成可以自己執行的選項」,擋下。
|
||
|
||
同時符合上述前兩點、但全文有守門片語 ⇒ 判定為 #14 那種「先回報、等確認」寫法,放行。
|
||
|
||
豁免:命中那一行尾巴加 `irreversible-ok`(留痕式豁免,比照本目錄其他 guard 的慣例)。
|
||
"""
|
||
import json
|
||
import re
|
||
import sys
|
||
|
||
# 不可逆動作:動詞 + 常見對象(分支/資料/表/repo/檔案/環境…)
|
||
IRREVERSIBLE_RE = re.compile(
|
||
r"("
|
||
r"刪(?:除|掉)?[^\n,。!?、;;()()]{0,12}(?:分支|branch|資料|data|table|表|db|資料庫|repo|檔案|record|entry|遠端|remote|環境|instance|實例)"
|
||
r"|砍(?:掉)?[^\n,。!?、;;]{0,6}(?:分支|branch)"
|
||
r"|洗掉"
|
||
r"|清空"
|
||
r"|格式化"
|
||
r"|(?:硬|真)刪(?:除)?"
|
||
r"|永久(?:刪除|移除)"
|
||
r"|drop\s+table"
|
||
r"|rm\s+-rf"
|
||
r"|reset\s+--hard"
|
||
r"|force[-\s]?push"
|
||
r"|git\s+push[^\n]{0,20}(?:--force|-f\b)"
|
||
r"|git\s+branch\s+-D"
|
||
r"|git\s+push[^\n]{0,20}--delete"
|
||
r"|delete[^\n]{0,12}(?:branch|data|table|repo|record)"
|
||
r")",
|
||
re.IGNORECASE,
|
||
)
|
||
|
||
# 否定:這段話是在「禁止」不可逆動作,不是授權它
|
||
NEGATION_RE = re.compile(
|
||
r"(不准|不可|不得|不要|禁止|勿|別|莫|no\s|never\s|don't\s|do not\s)\s*$",
|
||
re.IGNORECASE,
|
||
)
|
||
|
||
# 守門片語:明確要求「停下來,等人回覆才執行」
|
||
GATE_RE = re.compile(
|
||
r"("
|
||
r"先.{0,25}留言.{0,15}等.{0,12}(?:回覆|確認|同意)"
|
||
r"|等.{0,10}(?:leo|總管|leo21c).{0,15}(?:回覆|確認|同意|批准).{0,10}(?:才|再).{0,12}(?:執行|動手|做|刪|砍)"
|
||
r"|不准動手"
|
||
r"|停下來.{0,10}等"
|
||
r"|動手前.{0,15}(?:先|等待|等)"
|
||
r"|等\s*(?:leo|總管)\s*(?:回覆|確認|同意|拍板)"
|
||
r"|wait\s+for\s+(?:confirmation|approval|leo)"
|
||
r"|before\s+(?:doing so|acting|deleting|executing)[^\n]{0,30}(?:wait|confirm)"
|
||
r")",
|
||
re.IGNORECASE,
|
||
)
|
||
|
||
|
||
def check(text: str):
|
||
lines = text.split("\n")
|
||
gate_found = bool(GATE_RE.search(text))
|
||
|
||
hits = []
|
||
for i, line in enumerate(lines, start=1):
|
||
if "irreversible-ok" in line:
|
||
continue
|
||
for m in IRREVERSIBLE_RE.finditer(line):
|
||
before = line[max(0, m.start() - 8): m.start()]
|
||
if NEGATION_RE.search(before):
|
||
continue
|
||
hits.append([i, line.strip(), m.group()])
|
||
|
||
if not hits:
|
||
return "OK", hits, gate_found
|
||
if gate_found:
|
||
return "OK", hits, gate_found
|
||
return "BLOCK", hits, gate_found
|
||
|
||
|
||
if __name__ == "__main__":
|
||
text = sys.stdin.read()
|
||
verdict, hits, gate_found = check(text)
|
||
print(json.dumps(
|
||
{"verdict": verdict, "hits": hits, "gate_found": gate_found},
|
||
ensure_ascii=False,
|
||
))
|