RSS

第 3 章 / 共 21 章

三道权限闸门:把危险挡在执行之前

约 6 分钟 更新于

上一章留下的问题:现在模型手里有 bash。你说一句”帮我清理一下这个项目”,它完全可能生成 rm -rf /——不是因为它有恶意,而是因为它对”这个项目”的边界理解和你不一样。文件工具有 safe_path 挡着,bash 什么都没有。

很多人第一反应是在系统提示词里写”禁止执行危险命令”。这不管用,原因很简单:**提示词是请求,不是约束。**模型可能听,也可能在长上下文里把它忘了,还可能被输入里的内容带偏。安全必须由运行时的代码来强制。

3.1 三道闸门

把管控拆成三级,从最硬到最软:

闸门判断什么命中后
一:硬拒绝命令里有永不允许的模式(rm -rf /、sudo、mkfs、dd if=、shutdown……)直接拒绝,不给任何人商量的余地
二:规则匹配上下文相关的敏感操作(往工作目录外写文件、命令里含 rm 、chmod 777……)交给第三道闸门
三:人工确认由人来判断用户输入 y / n 决定放行还是拒绝

三道都没命中的操作,直接执行。这个”deny → ask → allow”的渐进结构,是在安全和可用之间取平衡:全部都问,人会疲劳到闭眼按 y;全部不问,等于没装。

第3章:权限的三道闸门
图 3.1:一次工具调用要过的三道判定。要看的是分叉的顺序——硬拒绝在最前面,它不给”人工确认”留任何机会,因为有些操作不应该由疲惫的人在深夜按一下 y 就放行。

3.2 三段代码

DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if=", "> /dev/sda"]

def check_deny_list(command: str) -> str | None:
    for pattern in DENY_LIST:
        if pattern in command:
            return f"已阻止:'{pattern}' 在硬拒绝名单中"
    return None

PERMISSION_RULES = [
    {
        "tools": ["write_file", "edit_file"],
        "check": lambda args: not (WORKDIR / args.get("path", "")).resolve().is_relative_to(WORKDIR),
        "message": "正在向工作目录之外写入",
    },
    {
        "tools": ["bash"],
        "check": lambda args: any(kw in args.get("command", "") for kw in ["rm ", "> /etc/", "chmod 777"]),
        "message": "可能具有破坏性的命令",
    },
]

def check_rules(tool_name: str, args: dict) -> str | None:
    for rule in PERMISSION_RULES:
        if tool_name in rule["tools"] and rule["check"](args):
            return rule["message"]
    return None

def ask_user(tool_name: str, args: dict, reason: str) -> str:
    print(f"\n⚠  {reason}")
    print(f"   工具:{tool_name}({args})")
    choice = input("   允许执行吗?[y/N] ").strip().lower()
    return "allow" if choice in ("y", "yes") else "deny"

串成一个判定函数,插进循环:

def check_permission(block) -> bool:
    if block.name == "bash":                                  # 闸门一
        reason = check_deny_list(block.input.get("command", ""))
        if reason:
            print(f"\n⛔ {reason}")
            return False

    reason = check_rules(block.name, block.input)             # 闸门二
    if reason:
        if ask_user(block.name, block.input, reason) == "deny":   # 闸门三
            return False
    return True

# agent_loop 里:
for block in response.content:
    if block.type == "tool_use":
        if not check_permission(block):
            results.append({"type": "tool_result", "tool_use_id": block.id,
                            "content": "用户拒绝了此操作。"})
            continue
        output = TOOL_HANDLERS[block.name](**block.input)
        results.append({"type": "tool_result", "tool_use_id": block.id, "content": output})

**注意被拒绝时那条 tool_result。**很多人会直接 continue 掉,结果 tool_use 没有配对的结果,API 直接报错。更重要的是:把”用户拒绝了”这句话回给模型,它才知道要换个方案,而不是傻等或重试同一条命令。拒绝也是一种反馈。

3.3 走一遍:一次被拦下的清理

任务:“帮我清理一下这个项目里的临时文件。”

轮次模型的动作闸门判定回给模型的内容
1bash: find . -name '*.tmp'三道全过12 个文件路径
2bash: rm -rf ./闸门一命中?否(没有 rm -rf / 字面串)→ 闸门二命中(含 rm )→ 问人用户按 n → “用户拒绝了此操作。”
3bash: rm ./cache/a.tmp ./cache/b.tmp ...闸门二命中 → 问人 → 用户按 y执行成功

第 2 轮是关键:模型的第一反应过于粗暴,闸门二把它交给了人,人拦下了;模型看到拒绝之后,第 3 轮自己改成了逐个删除具体文件。这就是”拒绝作为反馈”的实际效果。

同时也暴露了一个真实缺陷:rm -rf ./ 并没有命中硬拒绝名单,因为名单里存的是 rm -rf / 这个字面串。下一节就说这件事。

3.4 工程现实:字符串匹配远远不够

教学版的黑名单是朴素的子串匹配,很容易被绕开:rm -rf / 可以写成 rm -fr /、rm -rf /(多个空格)、rm -rf $HOME/..、bash -c 'rm -rf /',甚至 base64 编码后再 eval。真实系统需要解析命令结构、识别管道与子 shell、对路径做规范化,而不是找字符串。

还有一条更重要的工程原则:**hook 或规则给出的”允许”,不能覆盖配置层面的”禁止”。**也就是说,权限系统要有明确的优先级——配置里写死禁用的工具,任何运行时逻辑都不能把它放行。否则你的安全边界就成了”最宽松的那条规则说了算”。

真正的隔离靠的还是容器、只读挂载、独立用户、网络白名单这些操作系统级手段。权限闸门是在正确的环境里减少误伤,不是在错误的环境里制造安全感。

3.5 常见坑

3.6 设计你自己的闸门规则

3.7 画出你自己的三级分界线