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**:ルールマッチング — 「いつユーザーに聞くべきか」を記述する。各ルールはツールとチェック条件を指定する。
```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 = [
{
"tools": ["read_file", "write_file", "edit_file"],
@@ -65,7 +74,9 @@ PERMISSION_RULES = [
},
{
"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",
},
]
@@ -141,6 +152,7 @@ python s03_permission/code.py
2. `Delete the file test.txt`bash + rm でゲート 2 が発動)
3. `What files are in the current directory?`(読み取り専用、すべて通過)
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.
```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 = [
{
"tools": ["read_file", "write_file", "edit_file"],
@@ -65,7 +74,9 @@ PERMISSION_RULES = [
},
{
"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",
},
]
@@ -141,6 +152,7 @@ Try these prompts:
2. `Delete the file test.txt` (bash + rm triggers Gate 2)
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)
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?

View File

@@ -57,6 +57,15 @@ def check_deny_list(command: str) -> str | None:
**闸门 2**负责规则匹配,用来描述"什么时候需要问用户"。每条规则指定工具和检查条件。
```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 = [
{
"tools": ["read_file", "write_file", "edit_file"],
@@ -65,7 +74,9 @@ PERMISSION_RULES = [
},
{
"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",
},
]
@@ -141,6 +152,7 @@ python s03_permission/code.py
2. `Delete the file test.txt`bash + rm 会触发闸门 2
3. `What files are in the current directory?`(只读,全部通过)
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 re
import subprocess
from pathlib import Path
@@ -152,12 +153,22 @@ def check_deny_list(command: str) -> str | None:
# 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 = [
{"tools": ["read_file", "write_file", "edit_file"],
"check": lambda args: not (WORKDIR / args.get("path", "")).resolve().is_relative_to(WORKDIR),
"message": "Writing outside workspace"},
{"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"},
]

View File

@@ -21,6 +21,7 @@ Hooks run callbacks at fixed points in the agent loop:
"""
import os
import re
import subprocess
from pathlib import Path
@@ -139,22 +140,32 @@ def trigger_hooks(event: str, *args):
# s03 permission check logic, now wrapped as a hook
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"]
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
def permission_hook(block):
"""PreToolUse: s03 check_permission() logic moved here."""
if block.name == "bash":
command = block.input.get("command", "")
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")
return "Permission denied by deny list"
for kw in DESTRUCTIVE:
if kw in block.input.get("command", ""):
print(f"\n\033[33m[permission] Potentially destructive command\033[0m")
print(f" Tool: {block.name}({block.input})")
choice = input(" Allow? [y/N] ").strip().lower()
if choice not in ("y", "yes"):
return "Permission denied by user"
if contains_destructive_command(command) or any(
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})")
choice = input(" Allow? [y/N] ").strip().lower()
if choice not in ("y", "yes"):
return "Permission denied by user"
if block.name in ("read_file", "write_file", "edit_file"):
path = block.input.get("path", "")
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 json
import os
import re
import subprocess
from pathlib import Path
@@ -218,8 +219,16 @@ def trigger_hooks(event: str, *args):
return None
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"]
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
def permission_hook(block):
"""PreToolUse: s03 permission logic, registered as an s04 hook."""
if block.name == "bash":
@@ -228,13 +237,14 @@ def permission_hook(block):
if pattern in command:
print(f"\n\033[31m[blocked] '{pattern}'\033[0m")
return "Permission denied by deny list"
for keyword in DESTRUCTIVE:
if keyword in command:
print(f"\n\033[33m[permission] Potentially destructive command\033[0m")
print(f" Tool: {block.name}({block.input})")
choice = input(" Allow? [y/N] ").strip().lower()
if choice not in ("y", "yes"):
return "Permission denied by user"
if contains_destructive_command(command) or any(
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})")
choice = input(" Allow? [y/N] ").strip().lower()
if choice not in ("y", "yes"):
return "Permission denied by user"
if block.name in ("read_file", "write_file", "edit_file"):
path = block.input.get("path", "")
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 re
import subprocess
from pathlib import Path
@@ -154,9 +155,16 @@ def trigger_hooks(event: str, *args):
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"]
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
def permission_hook(block):
"""PreToolUse: block denied operations and ask about risky ones."""
if block.name == "bash":
@@ -165,13 +173,14 @@ def permission_hook(block):
if pattern in command:
print(f"\n\033[31m[blocked] '{pattern}'\033[0m")
return "Permission denied by deny list"
for keyword in DESTRUCTIVE:
if keyword in command:
print("\n\033[33m[permission] Potentially destructive command\033[0m")
print(f" Tool: {block.name}({block.input})")
choice = input(" Allow? [y/N] ").strip().lower()
if choice not in ("y", "yes"):
return "Permission denied by user"
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(f" Tool: {block.name}({block.input})")
choice = input(" Allow? [y/N] ").strip().lower()
if choice not in ("y", "yes"):
return "Permission denied by user"
if block.name in ("read_file", "write_file", "edit_file"):
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 re
import subprocess
from pathlib import Path
@@ -241,9 +242,16 @@ def trigger_hooks(event: str, *args):
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"]
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
def permission_hook(block):
"""PreToolUse: block denied operations and ask about risky ones."""
if block.name == "bash":
@@ -252,13 +260,14 @@ def permission_hook(block):
if pattern in command:
print(f"\n\033[31m[blocked] '{pattern}'\033[0m")
return "Permission denied by deny list"
for keyword in DESTRUCTIVE:
if keyword in command:
print("\n\033[33m[permission] Potentially destructive command\033[0m")
print(f" Tool: {block.name}({block.input})")
choice = input(" Allow? [y/N] ").strip().lower()
if choice not in ("y", "yes"):
return "Permission denied by user"
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(f" Tool: {block.name}({block.input})")
choice = input(" Allow? [y/N] ").strip().lower()
if choice not in ("y", "yes"):
return "Permission denied by user"
if block.name in ("read_file", "write_file", "edit_file"):
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="]
DESTRUCTIVE_COMMAND_WORD = re.compile(
r"(?i)(?:^|[;&|()\n])\s*(?:rm|del)(?=\s|$|[;&|()])"
)
DESTRUCTIVE = ["rm ", "> /etc/", "chmod 777"]
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
def permission_hook(block):
if block.name == "bash":
command = block.input.get("command", "")
for pattern in DENY_LIST:
if pattern in command:
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(f" Tool: {block.name}({block.input})")
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
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"]
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
def permission_hook(block):
if block.name == "bash":
command = block.input.get("command", "")
for pattern in DENY_LIST:
if pattern in command:
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(f" Tool: {block.name}({block.input})")
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="]
DESTRUCTIVE_COMMAND_WORD = re.compile(
r"(?i)(?:^|[;&|()\n])\s*(?:rm|del)(?=\s|$|[;&|()])"
)
DESTRUCTIVE = ["rm ", "> /etc/", "chmod 777"]
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
def permission_hook(block):
if block.name == "bash":
command = block.input.get("command", "")
@@ -454,7 +461,9 @@ def permission_hook(block):
if pattern in command:
print(f"\n\033[31m[blocked] '{pattern}'\033[0m")
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(f" Tool: {block.name}({block.input})")
choice = input(" Allow? [y/N] ").strip().lower()

View File

@@ -14,6 +14,7 @@ s11_background_tasks.py - Background Tasks
import atexit
import glob
import os
import re
import signal
import subprocess
import threading
@@ -225,9 +226,16 @@ def trigger_hooks(event: str, *args):
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"]
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
def permission_hook(block):
if block.name == "bash":
command = block.input.get("command", "")
@@ -235,7 +243,9 @@ def permission_hook(block):
if pattern in command:
print(f"\n\033[31m[blocked] '{pattern}'\033[0m")
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(f" Tool: {block.name}({block.input})")
choice = input(" Allow? [y/N] ").strip().lower()

View File

@@ -16,6 +16,7 @@ s12_cron_scheduler.py - Cron Scheduler
import glob
import json
import os
import re
import secrets
import subprocess
import threading
@@ -173,9 +174,16 @@ def trigger_hooks(event: str, *args):
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"]
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
def request_permission(block, reason: str) -> str | None:
if threading.current_thread() is not threading.main_thread():
return "Permission denied: scheduled turns cannot request interactive approval"
@@ -195,7 +203,9 @@ def permission_hook(block):
if pattern in command:
print(f"\n\033[31m[blocked] '{pattern}'\033[0m")
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")
if block.name in ("read_file", "write_file", "edit_file"):

View File

@@ -1663,9 +1663,16 @@ TOOL_HANDLERS = {
HOOKS = {"UserPromptSubmit": [], "PreToolUse": [], "PostToolUse": [], "Stop": []}
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"]
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
def register_hook(event: str, callback):
HOOKS[event].append(callback)
@@ -1686,7 +1693,9 @@ def check_permission(block, prompt_user: bool = True) -> str | None:
for pattern in DENY_LIST:
if pattern in command:
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:
return "Permission required: ask Lead to run this command."
print(f"\n[permission] {block.name}({block.input})")

View File

@@ -369,9 +369,16 @@ def assemble_system_prompt() -> str:
HOOKS = {"UserPromptSubmit": [], "PreToolUse": [], "PostToolUse": [], "Stop": []}
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"]
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
def register_hook(event: str, callback):
HOOKS[event].append(callback)
@@ -390,7 +397,9 @@ def permission_hook(block):
for pattern in DENY_LIST:
if pattern in command:
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})")
if input("Allow? [y/N] ").strip().lower() not in {"y", "yes"}:
return "Permission denied by user"

View File

@@ -31,6 +31,7 @@ import asyncio
import glob
import json
import os
import re
import subprocess
import sys
import time
@@ -45,9 +46,16 @@ DEFAULT_STOP_HOOK_BLOCK_CAP = 8
MAX_GOAL_LENGTH = 4000
CLEAR_ALIASES = {"clear", "stop", "off", "reset", "none", "cancel"}
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"]
def contains_destructive_command(command: str) -> bool:
return bool(DESTRUCTIVE_COMMAND_WORD.search(command))
class GoalError(Exception):
"""The goal command or evaluator could not be used safely."""
@@ -598,7 +606,9 @@ class AgentSession:
for pattern in DENY_LIST:
if pattern in command:
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})")
if input("Allow? [y/N] ").strip().lower() not in {"y", "yes"}:
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