Merge pull request #548 from mameikagou/fix-s03-del-command-448

fix(s03): match Windows del as a command word
This commit is contained in:
Yang Haoran
2026-08-27 00:38:22 +08:00
committed by GitHub
19 changed files with 636 additions and 280 deletions

View File

@@ -57,6 +57,15 @@ def check_deny_list(command: str) -> str | None:
**ゲート 2**:ルールマッチング — 「いつユーザーに聞くべきか」を記述する。各ルールはツールとチェック条件を指定する。 **ゲート 2**:ルールマッチング — 「いつユーザーに聞くべきか」を記述する。各ルールはツールとチェック条件を指定する。
```python ```python
import re
DESTRUCTIVE_COMMAND_WORD = re.compile(
r"(?i)(?:^|[;&|()\n])\s*(?:rm|del)(?=\s|$|[;&|()])"
)
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
PERMISSION_RULES = [ PERMISSION_RULES = [
{ {
"tools": ["read_file", "write_file", "edit_file"], "tools": ["read_file", "write_file", "edit_file"],
@@ -65,7 +74,9 @@ PERMISSION_RULES = [
}, },
{ {
"tools": ["bash"], "tools": ["bash"],
"check": lambda args: any(kw in args.get("command", "") for kw in ["rm ", "> /etc/", "chmod 777"]), "check": lambda args: contains_destructive_command(args.get("command", "")) or any(
kw in args.get("command", "") for kw in ["rm ", "> /etc/", "chmod 777"]
),
"message": "Potentially destructive command", "message": "Potentially destructive command",
}, },
] ]
@@ -141,6 +152,7 @@ python s03_permission/code.py
2. `Delete the file test.txt`bash + rm でゲート 2 が発動) 2. `Delete the file test.txt`bash + rm でゲート 2 が発動)
3. `What files are in the current directory?`(読み取り専用、すべて通過) 3. `What files are in the current directory?`(読み取り専用、すべて通過)
4. `Try to write a file to /etc/something`(作業ディレクトリ外への書き込みでゲート 2 が発動) 4. `Try to write a file to /etc/something`(作業ディレクトリ外への書き込みでゲート 2 が発動)
5. Windows では `del test.txt``DEL test.txt` がゲート 2 を発動し、`model``delimiter``echo del test.txt` は発動しない。
観察のポイント:どの操作がそのまま通過するか? どれに確認が必要か? どれが即座に拒否されるか? 観察のポイント:どの操作がそのまま通過するか? どれに確認が必要か? どれが即座に拒否されるか?

View File

@@ -57,6 +57,15 @@ def check_deny_list(command: str) -> str | None:
**Gate 2**: Rule matching — describes "when to ask the user." Each rule specifies a tool and a check condition. **Gate 2**: Rule matching — describes "when to ask the user." Each rule specifies a tool and a check condition.
```python ```python
import re
DESTRUCTIVE_COMMAND_WORD = re.compile(
r"(?i)(?:^|[;&|()\n])\s*(?:rm|del)(?=\s|$|[;&|()])"
)
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
PERMISSION_RULES = [ PERMISSION_RULES = [
{ {
"tools": ["read_file", "write_file", "edit_file"], "tools": ["read_file", "write_file", "edit_file"],
@@ -65,7 +74,9 @@ PERMISSION_RULES = [
}, },
{ {
"tools": ["bash"], "tools": ["bash"],
"check": lambda args: any(kw in args.get("command", "") for kw in ["rm ", "> /etc/", "chmod 777"]), "check": lambda args: contains_destructive_command(args.get("command", "")) or any(
kw in args.get("command", "") for kw in ["rm ", "> /etc/", "chmod 777"]
),
"message": "Potentially destructive command", "message": "Potentially destructive command",
}, },
] ]
@@ -141,6 +152,7 @@ Try these prompts:
2. `Delete the file test.txt` (bash + rm triggers Gate 2) 2. `Delete the file test.txt` (bash + rm triggers Gate 2)
3. `What files are in the current directory?` (read-only, all pass) 3. `What files are in the current directory?` (read-only, all pass)
4. `Try to write a file to /etc/something` (writing outside workspace triggers Gate 2) 4. `Try to write a file to /etc/something` (writing outside workspace triggers Gate 2)
5. On Windows, `del test.txt` and `DEL test.txt` trigger Gate 2, while `model`, `delimiter`, and `echo del test.txt` do not.
What to watch for: Which operations pass through? Which need your confirmation? Which are denied outright? What to watch for: Which operations pass through? Which need your confirmation? Which are denied outright?

View File

@@ -57,6 +57,15 @@ def check_deny_list(command: str) -> str | None:
**闸门 2**负责规则匹配,用来描述"什么时候需要问用户"。每条规则指定工具和检查条件。 **闸门 2**负责规则匹配,用来描述"什么时候需要问用户"。每条规则指定工具和检查条件。
```python ```python
import re
DESTRUCTIVE_COMMAND_WORD = re.compile(
r"(?i)(?:^|[;&|()\n])\s*(?:rm|del)(?=\s|$|[;&|()])"
)
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
PERMISSION_RULES = [ PERMISSION_RULES = [
{ {
"tools": ["read_file", "write_file", "edit_file"], "tools": ["read_file", "write_file", "edit_file"],
@@ -65,7 +74,9 @@ PERMISSION_RULES = [
}, },
{ {
"tools": ["bash"], "tools": ["bash"],
"check": lambda args: any(kw in args.get("command", "") for kw in ["rm ", "> /etc/", "chmod 777"]), "check": lambda args: contains_destructive_command(args.get("command", "")) or any(
kw in args.get("command", "") for kw in ["rm ", "> /etc/", "chmod 777"]
),
"message": "Potentially destructive command", "message": "Potentially destructive command",
}, },
] ]
@@ -141,6 +152,7 @@ python s03_permission/code.py
2. `Delete the file test.txt`bash + rm 会触发闸门 2 2. `Delete the file test.txt`bash + rm 会触发闸门 2
3. `What files are in the current directory?`(只读,全部通过) 3. `What files are in the current directory?`(只读,全部通过)
4. `Try to write a file to /etc/something`(写工作区外,触发闸门 2 4. `Try to write a file to /etc/something`(写工作区外,触发闸门 2
5. 在 Windows 上,`del test.txt``DEL test.txt` 会触发闸门 2`model``delimiter``echo del test.txt` 不会。
观察重点:哪些操作直接通过?哪些需要你确认?哪些被直接拒绝? 观察重点:哪些操作直接通过?哪些需要你确认?哪些被直接拒绝?

View File

@@ -32,6 +32,7 @@ Builds on s02 (multi-tool). Usage:
""" """
import os import os
import re
import subprocess import subprocess
from pathlib import Path from pathlib import Path
@@ -152,12 +153,22 @@ def check_deny_list(command: str) -> str | None:
# Gate 2: Rule matching - context-dependent checks # Gate 2: Rule matching - context-dependent checks
DESTRUCTIVE_COMMAND_WORD = re.compile(
r"(?i)(?:^|[;&|()\n])\s*(?:rm|del)(?=\s|$|[;&|()])"
)
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
PERMISSION_RULES = [ PERMISSION_RULES = [
{"tools": ["read_file", "write_file", "edit_file"], {"tools": ["read_file", "write_file", "edit_file"],
"check": lambda args: not (WORKDIR / args.get("path", "")).resolve().is_relative_to(WORKDIR), "check": lambda args: not (WORKDIR / args.get("path", "")).resolve().is_relative_to(WORKDIR),
"message": "Writing outside workspace"}, "message": "Writing outside workspace"},
{"tools": ["bash"], {"tools": ["bash"],
"check": lambda args: any(kw in args.get("command", "") for kw in ["rm ", "> /etc/", "chmod 777"]), "check": lambda args: contains_destructive_command(args.get("command", "")) or
any(kw in args.get("command", "") for kw in ["rm ", "> /etc/", "chmod 777"]),
"message": "Potentially destructive command"}, "message": "Potentially destructive command"},
] ]

View File

@@ -21,6 +21,7 @@ Hooks run callbacks at fixed points in the agent loop:
""" """
import os import os
import re
import subprocess import subprocess
from pathlib import Path from pathlib import Path
@@ -139,22 +140,32 @@ def trigger_hooks(event: str, *args):
# s03 permission check logic, now wrapped as a hook # s03 permission check logic, now wrapped as a hook
DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="] DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="]
DESTRUCTIVE_COMMAND_WORD = re.compile(
r"(?i)(?:^|[;&|()\n])\s*(?:rm|del)(?=\s|$|[;&|()])"
)
DESTRUCTIVE = ["rm ", "> /etc/", "chmod 777"] DESTRUCTIVE = ["rm ", "> /etc/", "chmod 777"]
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
def permission_hook(block): def permission_hook(block):
"""PreToolUse: s03 check_permission() logic moved here.""" """PreToolUse: s03 check_permission() logic moved here."""
if block.name == "bash": if block.name == "bash":
command = block.input.get("command", "")
for pattern in DENY_LIST: for pattern in DENY_LIST:
if pattern in block.input.get("command", ""): if pattern in command:
print(f"\n\033[31m[blocked] '{pattern}'\033[0m") print(f"\n\033[31m[blocked] '{pattern}'\033[0m")
return "Permission denied by deny list" return "Permission denied by deny list"
for kw in DESTRUCTIVE: if contains_destructive_command(command) or any(
if kw in block.input.get("command", ""): kw in command for kw in DESTRUCTIVE
print(f"\n\033[33m[permission] Potentially destructive command\033[0m") ):
print(f" Tool: {block.name}({block.input})") print(f"\n\033[33m[permission] Potentially destructive command\033[0m")
choice = input(" Allow? [y/N] ").strip().lower() print(f" Tool: {block.name}({block.input})")
if choice not in ("y", "yes"): choice = input(" Allow? [y/N] ").strip().lower()
return "Permission denied by user" if choice not in ("y", "yes"):
return "Permission denied by user"
if block.name in ("read_file", "write_file", "edit_file"): if block.name in ("read_file", "write_file", "edit_file"):
path = block.input.get("path", "") path = block.input.get("path", "")
if not (WORKDIR / path).resolve().is_relative_to(WORKDIR): if not (WORKDIR / path).resolve().is_relative_to(WORKDIR):

View File

@@ -25,6 +25,7 @@ without an update, the harness adds a reminder alongside the tool results.
import ast import ast
import json import json
import os import os
import re
import subprocess import subprocess
from pathlib import Path from pathlib import Path
@@ -218,8 +219,16 @@ def trigger_hooks(event: str, *args):
return None return None
DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="] DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="]
DESTRUCTIVE_COMMAND_WORD = re.compile(
r"(?i)(?:^|[;&|()\n])\s*(?:rm|del)(?=\s|$|[;&|()])"
)
DESTRUCTIVE = ["rm ", "> /etc/", "chmod 777"] DESTRUCTIVE = ["rm ", "> /etc/", "chmod 777"]
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
def permission_hook(block): def permission_hook(block):
"""PreToolUse: s03 permission logic, registered as an s04 hook.""" """PreToolUse: s03 permission logic, registered as an s04 hook."""
if block.name == "bash": if block.name == "bash":
@@ -228,13 +237,14 @@ def permission_hook(block):
if pattern in command: if pattern in command:
print(f"\n\033[31m[blocked] '{pattern}'\033[0m") print(f"\n\033[31m[blocked] '{pattern}'\033[0m")
return "Permission denied by deny list" return "Permission denied by deny list"
for keyword in DESTRUCTIVE: if contains_destructive_command(command) or any(
if keyword in command: keyword in command for keyword in DESTRUCTIVE
print(f"\n\033[33m[permission] Potentially destructive command\033[0m") ):
print(f" Tool: {block.name}({block.input})") print(f"\n\033[33m[permission] Potentially destructive command\033[0m")
choice = input(" Allow? [y/N] ").strip().lower() print(f" Tool: {block.name}({block.input})")
if choice not in ("y", "yes"): choice = input(" Allow? [y/N] ").strip().lower()
return "Permission denied by user" if choice not in ("y", "yes"):
return "Permission denied by user"
if block.name in ("read_file", "write_file", "edit_file"): if block.name in ("read_file", "write_file", "edit_file"):
path = block.input.get("path", "") path = block.input.get("path", "")
if not (WORKDIR / path).resolve().is_relative_to(WORKDIR): if not (WORKDIR / path).resolve().is_relative_to(WORKDIR):

View File

@@ -19,6 +19,7 @@ The subagent has no task tool, so it cannot delegate again.
""" """
import os import os
import re
import subprocess import subprocess
from pathlib import Path from pathlib import Path
@@ -154,9 +155,16 @@ def trigger_hooks(event: str, *args):
DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="] DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="]
DESTRUCTIVE_COMMAND_WORD = re.compile(
r"(?i)(?:^|[;&|()\n])\s*(?:rm|del)(?=\s|$|[;&|()])"
)
DESTRUCTIVE = ["rm ", "> /etc/", "chmod 777"] DESTRUCTIVE = ["rm ", "> /etc/", "chmod 777"]
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
def permission_hook(block): def permission_hook(block):
"""PreToolUse: block denied operations and ask about risky ones.""" """PreToolUse: block denied operations and ask about risky ones."""
if block.name == "bash": if block.name == "bash":
@@ -165,13 +173,14 @@ def permission_hook(block):
if pattern in command: if pattern in command:
print(f"\n\033[31m[blocked] '{pattern}'\033[0m") print(f"\n\033[31m[blocked] '{pattern}'\033[0m")
return "Permission denied by deny list" return "Permission denied by deny list"
for keyword in DESTRUCTIVE: if contains_destructive_command(command) or any(
if keyword in command: keyword in command for keyword in DESTRUCTIVE
print("\n\033[33m[permission] Potentially destructive command\033[0m") ):
print(f" Tool: {block.name}({block.input})") print("\n\033[33m[permission] Potentially destructive command\033[0m")
choice = input(" Allow? [y/N] ").strip().lower() print(f" Tool: {block.name}({block.input})")
if choice not in ("y", "yes"): choice = input(" Allow? [y/N] ").strip().lower()
return "Permission denied by user" if choice not in ("y", "yes"):
return "Permission denied by user"
if block.name in ("read_file", "write_file", "edit_file"): if block.name in ("read_file", "write_file", "edit_file"):
path = block.input.get("path", "") path = block.input.get("path", "")

View File

@@ -20,6 +20,7 @@ The model loads the full SKILL.md only when it calls load_skill.
""" """
import os import os
import re
import subprocess import subprocess
from pathlib import Path from pathlib import Path
@@ -241,9 +242,16 @@ def trigger_hooks(event: str, *args):
DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="] DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="]
DESTRUCTIVE_COMMAND_WORD = re.compile(
r"(?i)(?:^|[;&|()\n])\s*(?:rm|del)(?=\s|$|[;&|()])"
)
DESTRUCTIVE = ["rm ", "> /etc/", "chmod 777"] DESTRUCTIVE = ["rm ", "> /etc/", "chmod 777"]
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
def permission_hook(block): def permission_hook(block):
"""PreToolUse: block denied operations and ask about risky ones.""" """PreToolUse: block denied operations and ask about risky ones."""
if block.name == "bash": if block.name == "bash":
@@ -252,13 +260,14 @@ def permission_hook(block):
if pattern in command: if pattern in command:
print(f"\n\033[31m[blocked] '{pattern}'\033[0m") print(f"\n\033[31m[blocked] '{pattern}'\033[0m")
return "Permission denied by deny list" return "Permission denied by deny list"
for keyword in DESTRUCTIVE: if contains_destructive_command(command) or any(
if keyword in command: keyword in command for keyword in DESTRUCTIVE
print("\n\033[33m[permission] Potentially destructive command\033[0m") ):
print(f" Tool: {block.name}({block.input})") print("\n\033[33m[permission] Potentially destructive command\033[0m")
choice = input(" Allow? [y/N] ").strip().lower() print(f" Tool: {block.name}({block.input})")
if choice not in ("y", "yes"): choice = input(" Allow? [y/N] ").strip().lower()
return "Permission denied by user" if choice not in ("y", "yes"):
return "Permission denied by user"
if block.name in ("read_file", "write_file", "edit_file"): if block.name in ("read_file", "write_file", "edit_file"):
path = block.input.get("path", "") path = block.input.get("path", "")

View File

@@ -178,16 +178,25 @@ def trigger_hooks(event: str, *args):
DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="] DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="]
DESTRUCTIVE_COMMAND_WORD = re.compile(
r"(?i)(?:^|[;&|()\n])\s*(?:rm|del)(?=\s|$|[;&|()])"
)
DESTRUCTIVE = ["rm ", "> /etc/", "chmod 777"] DESTRUCTIVE = ["rm ", "> /etc/", "chmod 777"]
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
def permission_hook(block): def permission_hook(block):
if block.name == "bash": if block.name == "bash":
command = block.input.get("command", "") command = block.input.get("command", "")
for pattern in DENY_LIST: for pattern in DENY_LIST:
if pattern in command: if pattern in command:
return f"Permission denied by deny list: {pattern}" return f"Permission denied by deny list: {pattern}"
if any(keyword in command for keyword in DESTRUCTIVE): if contains_destructive_command(command) or any(
keyword in command for keyword in DESTRUCTIVE
):
print("\n\033[33m[permission] Potentially destructive command\033[0m") print("\n\033[33m[permission] Potentially destructive command\033[0m")
print(f" Tool: {block.name}({block.input})") print(f" Tool: {block.name}({block.input})")
if input(" Allow? [y/N] ").strip().lower() not in ("y", "yes"): if input(" Allow? [y/N] ").strip().lower() not in ("y", "yes"):

View File

@@ -634,15 +634,25 @@ def trigger_hooks(event: str, *args):
return None return None
DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="] DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="]
DESTRUCTIVE_COMMAND_WORD = re.compile(
r"(?i)(?:^|[;&|()\n])\s*(?:rm|del)(?=\s|$|[;&|()])"
)
DESTRUCTIVE = ["rm ", "> /etc/", "chmod 777"] DESTRUCTIVE = ["rm ", "> /etc/", "chmod 777"]
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
def permission_hook(block): def permission_hook(block):
if block.name == "bash": if block.name == "bash":
command = block.input.get("command", "") command = block.input.get("command", "")
for pattern in DENY_LIST: for pattern in DENY_LIST:
if pattern in command: if pattern in command:
return f"Permission denied by deny list: {pattern}" return f"Permission denied by deny list: {pattern}"
if any(keyword in command for keyword in DESTRUCTIVE): if contains_destructive_command(command) or any(
keyword in command for keyword in DESTRUCTIVE
):
print("\n\033[33m[permission] Potentially destructive command\033[0m") print("\n\033[33m[permission] Potentially destructive command\033[0m")
print(f" Tool: {block.name}({block.input})") print(f" Tool: {block.name}({block.input})")
if input(" Allow? [y/N] ").strip().lower() not in ("y", "yes"): if input(" Allow? [y/N] ").strip().lower() not in ("y", "yes"):

View File

@@ -444,9 +444,16 @@ def trigger_hooks(event: str, *args):
DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="] DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="]
DESTRUCTIVE_COMMAND_WORD = re.compile(
r"(?i)(?:^|[;&|()\n])\s*(?:rm|del)(?=\s|$|[;&|()])"
)
DESTRUCTIVE = ["rm ", "> /etc/", "chmod 777"] DESTRUCTIVE = ["rm ", "> /etc/", "chmod 777"]
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
def permission_hook(block): def permission_hook(block):
if block.name == "bash": if block.name == "bash":
command = block.input.get("command", "") command = block.input.get("command", "")
@@ -454,7 +461,9 @@ def permission_hook(block):
if pattern in command: if pattern in command:
print(f"\n\033[31m[blocked] '{pattern}'\033[0m") print(f"\n\033[31m[blocked] '{pattern}'\033[0m")
return "Permission denied by deny list" return "Permission denied by deny list"
if any(keyword in command for keyword in DESTRUCTIVE): if contains_destructive_command(command) or any(
keyword in command for keyword in DESTRUCTIVE
):
print("\n\033[33m[permission] Potentially destructive command\033[0m") print("\n\033[33m[permission] Potentially destructive command\033[0m")
print(f" Tool: {block.name}({block.input})") print(f" Tool: {block.name}({block.input})")
choice = input(" Allow? [y/N] ").strip().lower() choice = input(" Allow? [y/N] ").strip().lower()

View File

@@ -14,6 +14,7 @@ s11_background_tasks.py - Background Tasks
import atexit import atexit
import glob import glob
import os import os
import re
import signal import signal
import subprocess import subprocess
import threading import threading
@@ -225,9 +226,16 @@ def trigger_hooks(event: str, *args):
DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="] DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="]
DESTRUCTIVE_COMMAND_WORD = re.compile(
r"(?i)(?:^|[;&|()\n])\s*(?:rm|del)(?=\s|$|[;&|()])"
)
DESTRUCTIVE = ["rm ", "> /etc/", "chmod 777"] DESTRUCTIVE = ["rm ", "> /etc/", "chmod 777"]
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
def permission_hook(block): def permission_hook(block):
if block.name == "bash": if block.name == "bash":
command = block.input.get("command", "") command = block.input.get("command", "")
@@ -235,7 +243,9 @@ def permission_hook(block):
if pattern in command: if pattern in command:
print(f"\n\033[31m[blocked] '{pattern}'\033[0m") print(f"\n\033[31m[blocked] '{pattern}'\033[0m")
return "Permission denied by deny list" return "Permission denied by deny list"
if any(keyword in command for keyword in DESTRUCTIVE): if contains_destructive_command(command) or any(
keyword in command for keyword in DESTRUCTIVE
):
print("\n\033[33m[permission] Potentially destructive command\033[0m") print("\n\033[33m[permission] Potentially destructive command\033[0m")
print(f" Tool: {block.name}({block.input})") print(f" Tool: {block.name}({block.input})")
choice = input(" Allow? [y/N] ").strip().lower() choice = input(" Allow? [y/N] ").strip().lower()

View File

@@ -16,6 +16,7 @@ s12_cron_scheduler.py - Cron Scheduler
import glob import glob
import json import json
import os import os
import re
import secrets import secrets
import subprocess import subprocess
import threading import threading
@@ -173,9 +174,16 @@ def trigger_hooks(event: str, *args):
DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="] DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="]
DESTRUCTIVE_COMMAND_WORD = re.compile(
r"(?i)(?:^|[;&|()\n])\s*(?:rm|del)(?=\s|$|[;&|()])"
)
DESTRUCTIVE = ["rm ", "> /etc/", "chmod 777"] DESTRUCTIVE = ["rm ", "> /etc/", "chmod 777"]
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
def request_permission(block, reason: str) -> str | None: def request_permission(block, reason: str) -> str | None:
if threading.current_thread() is not threading.main_thread(): if threading.current_thread() is not threading.main_thread():
return "Permission denied: scheduled turns cannot request interactive approval" return "Permission denied: scheduled turns cannot request interactive approval"
@@ -195,7 +203,9 @@ def permission_hook(block):
if pattern in command: if pattern in command:
print(f"\n\033[31m[blocked] '{pattern}'\033[0m") print(f"\n\033[31m[blocked] '{pattern}'\033[0m")
return "Permission denied by deny list" return "Permission denied by deny list"
if any(keyword in command for keyword in DESTRUCTIVE): if contains_destructive_command(command) or any(
keyword in command for keyword in DESTRUCTIVE
):
return request_permission(block, "Potentially destructive command") return request_permission(block, "Potentially destructive command")
if block.name in ("read_file", "write_file", "edit_file"): if block.name in ("read_file", "write_file", "edit_file"):

View File

@@ -1663,9 +1663,16 @@ TOOL_HANDLERS = {
HOOKS = {"UserPromptSubmit": [], "PreToolUse": [], "PostToolUse": [], "Stop": []} HOOKS = {"UserPromptSubmit": [], "PreToolUse": [], "PostToolUse": [], "Stop": []}
DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="] DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="]
DESTRUCTIVE_COMMAND_WORD = re.compile(
r"(?i)(?:^|[;&|()\n])\s*(?:rm|del)(?=\s|$|[;&|()])"
)
DESTRUCTIVE = ["rm ", "> /etc/", "chmod 777"] DESTRUCTIVE = ["rm ", "> /etc/", "chmod 777"]
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
def register_hook(event: str, callback): def register_hook(event: str, callback):
HOOKS[event].append(callback) HOOKS[event].append(callback)
@@ -1686,7 +1693,9 @@ def check_permission(block, prompt_user: bool = True) -> str | None:
for pattern in DENY_LIST: for pattern in DENY_LIST:
if pattern in command: if pattern in command:
return f"Permission denied by deny list: {pattern}" return f"Permission denied by deny list: {pattern}"
if any(keyword in command for keyword in DESTRUCTIVE): if contains_destructive_command(command) or any(
keyword in command for keyword in DESTRUCTIVE
):
if not prompt_user: if not prompt_user:
return "Permission required: ask Lead to run this command." return "Permission required: ask Lead to run this command."
print(f"\n[permission] {block.name}({block.input})") print(f"\n[permission] {block.name}({block.input})")

View File

@@ -369,9 +369,16 @@ def assemble_system_prompt() -> str:
HOOKS = {"UserPromptSubmit": [], "PreToolUse": [], "PostToolUse": [], "Stop": []} HOOKS = {"UserPromptSubmit": [], "PreToolUse": [], "PostToolUse": [], "Stop": []}
DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="] DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="]
DESTRUCTIVE_COMMAND_WORD = re.compile(
r"(?i)(?:^|[;&|()\n])\s*(?:rm|del)(?=\s|$|[;&|()])"
)
DESTRUCTIVE = ["rm ", "> /etc/", "chmod 777"] DESTRUCTIVE = ["rm ", "> /etc/", "chmod 777"]
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
def register_hook(event: str, callback): def register_hook(event: str, callback):
HOOKS[event].append(callback) HOOKS[event].append(callback)
@@ -390,7 +397,9 @@ def permission_hook(block):
for pattern in DENY_LIST: for pattern in DENY_LIST:
if pattern in command: if pattern in command:
return f"Permission denied by deny list: {pattern}" return f"Permission denied by deny list: {pattern}"
if any(keyword in command for keyword in DESTRUCTIVE): if contains_destructive_command(command) or any(
keyword in command for keyword in DESTRUCTIVE
):
print(f"\n[permission] {block.name}({block.input})") print(f"\n[permission] {block.name}({block.input})")
if input("Allow? [y/N] ").strip().lower() not in {"y", "yes"}: if input("Allow? [y/N] ").strip().lower() not in {"y", "yes"}:
return "Permission denied by user" return "Permission denied by user"

View File

@@ -31,6 +31,7 @@ import asyncio
import glob import glob
import json import json
import os import os
import re
import subprocess import subprocess
import sys import sys
import time import time
@@ -45,9 +46,16 @@ DEFAULT_STOP_HOOK_BLOCK_CAP = 8
MAX_GOAL_LENGTH = 4000 MAX_GOAL_LENGTH = 4000
CLEAR_ALIASES = {"clear", "stop", "off", "reset", "none", "cancel"} CLEAR_ALIASES = {"clear", "stop", "off", "reset", "none", "cancel"}
DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="] DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="]
DESTRUCTIVE_COMMAND_WORD = re.compile(
r"(?i)(?:^|[;&|()\n])\s*(?:rm|del)(?=\s|$|[;&|()])"
)
DESTRUCTIVE = ["rm ", "> /etc/", "chmod 777"] DESTRUCTIVE = ["rm ", "> /etc/", "chmod 777"]
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
class GoalError(Exception): class GoalError(Exception):
"""The goal command or evaluator could not be used safely.""" """The goal command or evaluator could not be used safely."""
@@ -598,7 +606,9 @@ class AgentSession:
for pattern in DENY_LIST: for pattern in DENY_LIST:
if pattern in command: if pattern in command:
return f"Permission denied by deny list: {pattern}" return f"Permission denied by deny list: {pattern}"
if any(keyword in command for keyword in DESTRUCTIVE): if contains_destructive_command(command) or any(
keyword in command for keyword in DESTRUCTIVE
):
print(f"\n[permission] {name}({arguments})") print(f"\n[permission] {name}({arguments})")
if input("Allow? [y/N] ").strip().lower() not in {"y", "yes"}: if input("Allow? [y/N] ").strip().lower() not in {"y", "yes"}:
return "Permission denied by user" return "Permission denied by user"

View File

@@ -0,0 +1,127 @@
import importlib.util
import os
import sys
import time
import types
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
PERMISSION_LESSONS = tuple(
ROOT / chapter / "code.py"
for chapter in (
"s03_permission",
"s04_hooks",
"s05_todo_write",
"s06_subagent",
"s07_skill_loading",
"s08_context_compact",
"s09_memory",
"s10_task_system",
"s11_background_tasks",
"s12_cron_scheduler",
"s13_agent_teams",
"s14_mcp_plugin",
"s17_goal_loop",
)
)
def load_lesson(workdir: Path, lesson_path: Path):
fake_anthropic = types.ModuleType("anthropic")
fake_dotenv = types.ModuleType("dotenv")
class FakeAnthropic:
def __init__(self, *args, **kwargs):
self.messages = types.SimpleNamespace(create=None)
fake_anthropic.Anthropic = FakeAnthropic
fake_dotenv.load_dotenv = lambda override=True: None
previous_modules = {
"anthropic": sys.modules.get("anthropic"),
"dotenv": sys.modules.get("dotenv"),
}
previous_cwd = Path.cwd()
previous_model = os.environ.get("MODEL_ID")
module_name = f"permission_words_{lesson_path.parent.name}_{time.time_ns()}"
spec = importlib.util.spec_from_file_location(module_name, lesson_path)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
sys.modules["anthropic"] = fake_anthropic
sys.modules["dotenv"] = fake_dotenv
sys.modules[module_name] = module
try:
os.chdir(workdir)
os.environ["MODEL_ID"] = "test-model"
spec.loader.exec_module(module)
return module
finally:
os.chdir(previous_cwd)
if previous_model is None:
os.environ.pop("MODEL_ID", None)
else:
os.environ["MODEL_ID"] = previous_model
for name, previous in previous_modules.items():
if previous is None:
sys.modules.pop(name, None)
else:
sys.modules[name] = previous
sys.modules.pop(module_name, None)
def permission_result(lesson, block):
if hasattr(lesson, "check_rules"):
return lesson.check_rules(block.name, block.input)
if hasattr(lesson, "permission_hook"):
return lesson.permission_hook(block)
goal = lesson.GoalController(evaluator=None)
session = lesson.AgentSession(
client=None,
model="test-model",
goal=goal,
workdir=Path.cwd(),
)
return session._permission_hook(block)
COMMAND_CASES = (
("rm file.txt", True),
("/usr/bin/rm file.txt", True),
("command rm file.txt", True),
("DEL file.txt", True),
("echo ready; rm file.txt", True),
("echo ready && del file.txt", True),
("echo ready || RM file.txt", True),
("echo ready | del file.txt", True),
("echo ready & rm file.txt", True),
("(del file.txt)", True),
("rm; echo ready", True),
("model list", False),
("delimiter file.txt", False),
("echo del file.txt", False),
("echo; delimiter file.txt", False),
)
@pytest.mark.parametrize(
"lesson_path", PERMISSION_LESSONS, ids=lambda path: path.parent.name
)
@pytest.mark.parametrize("command, expected", COMMAND_CASES)
def test_permission_command_words_cover_position_case_and_boundaries(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
lesson_path: Path,
command: str,
expected: bool,
) -> None:
lesson = load_lesson(tmp_path, lesson_path)
monkeypatch.setattr("builtins.input", lambda _prompt: "n")
block = types.SimpleNamespace(name="bash", input={"command": command})
result = permission_result(lesson, block)
assert bool(result) is expected

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long