The permission system: Allow, block, approve
Before the code: why you never let an AI run rm -rf without asking
Your assistant now has tools, which means it can do real work. The same power means it can also delete your home directory if the model gets creative. The fix is a safety layer that sits between the model and your shell. Some commands are obviously safe and run instantly. Some are obviously dangerous and never run. The rest pause and ask you for explicit permission once, then remember the answer. This module also gives the same brain three ways to reach you: terminal, web, and Telegram, all routing into the same assistant.
Safety in one picture: allow, block, ask
Quiz: Quiz
Loading practice…
You gave the agent run_command. That is also the line where most engineers get nervous. The right answer is not to take it away. The right answer is a classifier that knows which commands are obviously safe, which are obviously not, and which need a human to look once.
SAFE_COMMANDS = {
"ls", "cat", "head", "tail", "wc", "grep", "find", "echo",
"pwd", "whoami", "date", "cal", "uname", "which", "file",
"python", "python3", "node", "pip", "npm",
}
DANGEROUS_PATTERNS = [
r"\brm\s+-rf\s+/", # rm -rf /
r"\bmkfs\b", # Format filesystem
r"\bdd\s+if=", # Raw disk write
r">\s*/dev/sd", # Write to disk device
r"\bshutdown\b", # Shutdown system
r"\breboot\b", # Reboot system
r"\bcurl\b.*\|\s*bash", # Pipe curl to bash
r"\bwget\b.*\|\s*bash", # Pipe wget to bash
]An allow-list of commands that read data but never destroy it. Auto-allow these. The block-list is regex patterns that match commands you never want to run.
def check_command_safety(command: str) -> str:
"""
Tier 1: matches dangerous patterns -> always reject
Tier 2: first word is in SAFE_COMMANDS -> always allow
Tier 3: check persistent approvals, else request approval
"""
for pattern in DANGEROUS_PATTERNS:
if re.search(pattern, command):
return "blocked"
first_word = command.strip().split()[0] if command.strip() else ""
if first_word in SAFE_COMMANDS:
return "safe"
if command in load_approvals():
return "approved"
return "needs_approval"The classifier. Block-list wins over allow-list (dangerous patterns are checked first). Anything that does not match either tier falls into needs_approval, which we cache in a file.
How a command flows through the classifier
Because a single safe-looking command can carry a dangerous tail. echo hello && rm -rf / starts with echo, which is in the allow-list. If allow ran first, you would let that through. Checking dangerous patterns first means the regex catches the tail before the allow-list ever runs.
def execute_tool(name: str, args: dict) -> str:
if name == "run_command":
command = args["command"]
safety = check_command_safety(command)
if safety == "blocked":
return f"BLOCKED: Command '{command}' matches a dangerous pattern."
if safety == "needs_approval":
# In a real Telegram bot, you'd pop an inline-keyboard approval.
# For this workshop, we auto-approve and save for next time.
save_approval(command)
try:
result = subprocess.run(
command, shell=True, capture_output=True, text=True, timeout=30,
)
return (result.stdout + result.stderr)[:10000] or "Command completed with no output."
except subprocess.TimeoutExpired:
return "Error: Command timed out after 30 seconds."
except Exception as e:
return f"Error: {e}"
# ... other tools unchangedexecute_tool routes run_command through the classifier. blocked returns an error string back to the model. needs_approval auto-saves for this workshop (in production it would pop a Telegram inline keyboard).
Quiz: Quiz
Loading practice…
AI prompt: Try it: probe the safety boundary
Loading practice…