mirror of
https://github.com/shareAI-lab/analysis_claude_code.git
synced 2026-09-21 12:53:37 +08:00
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:
@@ -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` は発動しない。
|
||||
|
||||
観察のポイント:どの操作がそのまま通過するか? どれに確認が必要か? どれが即座に拒否されるか?
|
||||
|
||||
|
||||
@@ -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?
|
||||
|
||||
|
||||
@@ -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` 不会。
|
||||
|
||||
观察重点:哪些操作直接通过?哪些需要你确认?哪些被直接拒绝?
|
||||
|
||||
|
||||
@@ -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"},
|
||||
]
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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", "")
|
||||
|
||||
@@ -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", "")
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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})")
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
127
tests/test_permission_command_words.py
Normal file
127
tests/test_permission_command_words.py
Normal 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
Reference in New Issue
Block a user