#!/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, ))