Agent 开发进阶(七):给 AI 加上安全锁,权限系统的设计与实现

6 阅读7分钟

Agent 开发进阶(七):给 AI 加上安全锁,权限系统的设计与实现

本文是「从零构建 Coding Agent」系列的第七篇,适合想让 Agent 安全可靠运行的开发者。

先问一个问题

当你的 Agent 越来越强大时,你是否担心过:

  • 它可能会写错重要文件
  • 它可能会执行危险的系统命令
  • 它可能会在不该动手的时候动手

如果没有安全措施,这些担忧很可能变成现实。

Agent 的「安全隐患」问题

到了这一阶段,你的 Agent 已经具备了多种能力:

  • 读写文件
  • 执行系统命令
  • 制定任务计划
  • 压缩上下文
  • 加载技能

但能力越大,风险也越大。如果让模型的「意图」直接变成「执行」,后果不堪设想。

这就是权限系统要解决的核心问题:在模型提议动作和系统执行动作之间,建立一道安全屏障

权限系统的核心设计:四步管道

用一个图来表示权限系统的工作流程:

tool_call
  |
  v
1. deny rules     -> 命中了就拒绝
  |
  v
2. mode check     -> 根据当前模式决定
  |
  v
3. allow rules    -> 命中了就放行
  |
  v
4. ask user       -> 剩下的交给用户确认

关键点只有两个:

  1. 安全优先:危险操作先被挡住
  2. 分级处理:不同场景有不同的权限策略

几个必须搞懂的概念

权限系统

不是「有没有权限」这样一个简单的布尔值,而是一条管道,用来回答:

  1. 这次调用要不要直接拒绝?
  2. 能不能自动放行?
  3. 剩下的要不要问用户?

权限模式

系统当前的总体风格,例如:

  • default:未命中规则时问用户
  • plan:只允许读,不允许写
  • auto:简单安全的操作自动放行

权限规则

「遇到某种工具调用时,该怎么处理」的小条款,例如:

{
    "tool": "bash",
    "content": "sudo *",
    "behavior": "deny",
}

最小实现

1. 定义权限规则

# 拒绝规则
DENY_RULES = [
    # 危险的 bash 命令
    {"tool": "bash", "content": "sudo *", "behavior": "deny"},
    {"tool": "bash", "content": "rm -rf *", "behavior": "deny"},
    {"tool": "bash", "content": "shutdown *", "behavior": "deny"},
    # 越界路径
    {"tool": "write_file", "path": "/etc/*", "behavior": "deny"},
]

# 允许规则
ALLOW_RULES = [
    # 安全的读取操作
    {"tool": "read_file", "behavior": "allow"},
    {"tool": "bash", "content": "ls *", "behavior": "allow"},
    {"tool": "bash", "content": "git status", "behavior": "allow"},
]

# 工具分类
READ_ONLY_TOOLS = ["read_file", "search_code"]
WRITE_TOOLS = ["write_file", "edit_file", "bash"]

2. 权限检查函数

def check_permission(tool_name: str, tool_input: dict) -> dict:
    """检查工具调用的权限"""
    # 1. 检查拒绝规则
    for rule in DENY_RULES:
        if matches_rule(rule, tool_name, tool_input):
            return {"behavior": "deny", "reason": "匹配到拒绝规则"}

    # 2. 检查权限模式
    if current_mode == "plan" and tool_name in WRITE_TOOLS:
        return {"behavior": "deny", "reason": "plan 模式禁止写操作"}
    if current_mode == "auto" and tool_name in READ_ONLY_TOOLS:
        return {"behavior": "allow", "reason": "auto 模式允许读操作"}

    # 3. 检查允许规则
    for rule in ALLOW_RULES:
        if matches_rule(rule, tool_name, tool_input):
            return {"behavior": "allow", "reason": "匹配到允许规则"}

    # 4. 默认行为:询问用户
    return {"behavior": "ask", "reason": "需要用户确认"}

def matches_rule(rule: dict, tool_name: str, tool_input: dict) -> bool:
    """检查工具调用是否匹配规则"""
    if rule.get("tool") != tool_name:
        return False
    
    # 检查路径
    if "path" in rule and "path" in tool_input:
        import fnmatch
        if not fnmatch.fnmatch(tool_input["path"], rule["path"]):
            return False
    
    # 检查内容
    if "content" in rule and "content" in tool_input:
        import fnmatch
        if not fnmatch.fnmatch(tool_input["content"], rule["content"]):
            return False
    
    return True

3. 集成到工具执行流程

def execute_tool(tool_name: str, tool_input: dict) -> str:
    """执行工具,带权限检查"""
    # 检查权限
    decision = check_permission(tool_name, tool_input)
    
    if decision["behavior"] == "deny":
        return f"权限拒绝: {decision['reason']}"
    
    if decision["behavior"] == "ask":
        ok = ask_user_confirmation(tool_name, tool_input)
        if not ok:
            return "用户拒绝执行"
    
    # 执行工具
    handler = TOOL_HANDLERS.get(tool_name)
    if not handler:
        return f"未知工具: {tool_name}"
    
    try:
        result = handler(**tool_input)
        return str(result)
    except Exception as e:
        return f"执行错误: {e}"

4. 权限模式管理

def set_mode(mode: str) -> str:
    """设置权限模式"""
    global current_mode
    if mode not in ["default", "plan", "auto"]:
        return f"无效模式: {mode},可选值: default, plan, auto"
    current_mode = mode
    return f"权限模式已设置为: {mode}"

# 初始模式
current_mode = "default"

为什么顺序是这样

  1. 先检查 deny rules:危险操作应该优先被挡住,不应该交给模式去决定
  2. 再检查 mode:模式决定当前会话的整体策略方向
  3. 再检查 allow rules:安全的操作可以直接放行,提高效率
  4. 最后 ask user:前面都没覆盖的灰区,交给用户做最终决定

Bash 工具的特殊处理

Bash 工具是最危险的,因为它几乎能做任何事。即使是简单的实现,也应该挡住这些明显的危险点:

def check_bash_safety(command: str) -> bool:
    """检查 bash 命令的安全性"""
    dangerous_patterns = [
        "sudo",          # 提权
        "rm -rf",        # 强制递归删除
        "shutdown",      # 关机
        "reboot",        # 重启
        "> /dev/null",   # 丢弃输出
        "| bash",        # 管道到 bash
        "$(",            # 命令替换
        "`",             # 反引号命令替换
    ]
    
    for pattern in dangerous_patterns:
        if pattern in command:
            return False
    
    return True

# 在 matches_rule 中添加 bash 安全检查
def matches_rule(rule: dict, tool_name: str, tool_input: dict) -> bool:
    # ... 现有代码 ...
    
    # 额外检查 bash 安全性
    if tool_name == "bash" and "content" in tool_input:
        if not check_bash_safety(tool_input["content"]):
            return True  # 匹配到危险模式
    
    return False

新手最容易犯的 5 个错

1. 把权限系统做成简单开关

# ❌ 错误
if is_allowed:
    execute_tool()
else:
    return "权限拒绝"

# ✅ 正确
# 实现完整的四步权限管道

权限系统不是简单的开关,而是分级处理的管道。

2. 把 allow 规则放在 deny 规则前面

# ❌ 错误
if matches_allow_rule():
    return "allow"
if matches_deny_rule():
    return "deny"

# ✅ 正确
if matches_deny_rule():
    return "deny"
if matches_allow_rule():
    return "allow"

安全优先,危险操作应该先被挡住。

3. 不特殊处理 bash 工具

Bash 工具几乎能做任何事,应该被当作特殊情况处理。

4. 一开始就做复杂的权限模式

# ❌ 错误
modes = ["default", "plan", "auto", "admin", "readonly", "maintenance"]

# ✅ 正确
# 先实现三种基础模式:default, plan, auto

先把基础模式做稳,再考虑扩展。

5. 直接执行工具调用,跳过权限检查

# ❌ 错误
result = handler(**tool_input)  # 直接执行

# ✅ 正确
decision = check_permission(tool_name, tool_input)  # 先检查权限
if decision["behavior"] == "allow":
    result = handler(**tool_input)

任何工具调用都应该经过权限检查。

为什么这很重要

因为 Agent 之所以「可靠」,不是因为它永远不会犯错。

而是因为系统为它设置了一道安全防线,确保它的行为在可控范围内。

就像人类社会一样:自由不是无边界的,而是在规则约束下的安全自由。

推荐的实现步骤

  1. 第一步:先实现三种基础模式(default、plan、auto)
  2. 第二步:实现 deny 和 allow 两类规则
  3. 第三步:为 bash 工具添加最小安全检查
  4. 第四步:添加拒绝计数,防止 Agent 卡住

权限系统与后续章节的关系

  • s07 权限系统:决定「能不能执行」
  • s08 执行钩子:决定「执行前后还能不能插入额外逻辑」
  • s10 Prompt 管道:会把当前模式和权限说明放进 prompt 组装里

所以权限系统是后面很多机制的安全前提。

下一章预告

有了权限系统,你的 Agent 已经具备了基本的安全保障。下一章我们将探讨执行钩子机制,让你能够在工具执行前后插入自定义逻辑,进一步增强系统的可控性。


一句话总结:权限系统不是为了让 Agent 更笨,而是为了让 Agent 的行动先经过一道可靠的安全判断。


如果觉得有帮助,欢迎关注,我会持续更新「从零构建 Coding Agent」系列文章。