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

Every shell command the assistant wants to run goes through a three-tier classifier before it actually runs.

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.

05-permission-controls/bot.py
python
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.

05-permission-controls/bot.py
python
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

Order matters. Block-list runs first so even a "safe" command containing a dangerous substring is caught.

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.

05-permission-controls/bot.py
python
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 unchanged

execute_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…