权限系统 — 安全即产品

不加刹车的跑车不是跑车,是凶器

📝 本章目标

读完本章,你将:

  1. 用三个 Prompt 让 AI 帮你构建一套完整的权限系统——从”全部要确认”到”分级管控”再到”规则配置”
  2. 理解为什么 Agent 必须有权限系统——没有刹车的能力是灾难
  3. 掌握风险分级和规则匹配的核心逻辑
  4. 了解 Claude Code 七层权限模型的设计思想

想象你雇了一个特别能干的助手——他会写代码、会跑命令、会改文件,什么都会。第一天你让他”帮我整理一下项目目录”,他打开终端敲了一句 rm -rf /——你整台电脑的数据全没了。

这个助手能力没有任何问题,问题是没人管着他

上一章我们给 Agent 装上了手脚——能读文件、写文件、跑命令、搜代码。但你仔细想想,这等于给了一个刚入职的实习生 root 权限:他可以删除你的代码库、可以 git push --force 覆盖同事的工作、可以执行任意命令。

权限系统就是给这个能力超强的实习生装上”刹车”。装完之后,安全的操作自动放行,危险的操作先问你,绝对不能做的直接拒绝。

动手:用三个 Prompt 给 Agent 装上刹车

确认你在 harness 项目根目录。如果你跟着第 3 章做了工具系统,现在应该有 harness/tools/ 目录。如果没有,去 GitHub 仓库 git checkout ch03-tool-system 获取起点。

打开 Claude Code,跟着走。

Prompt 1:危险操作先问我

现在 Agent 执行任何操作都不需要你同意——读文件、删文件、格式化硬盘,它都直接干。我们先给它加一道基本的关卡。

帮我加一个权限检查,危险操作先问我,安全的直接放行。

等 AI 跑完,试一下:

$ harness
You > 帮我看看 pyproject.toml 里写了什么
Assistant > 我来读一下这个文件。
[tool: read_file("pyproject.toml")]   ← 直接执行,没有确认
这个文件是你的项目配置...

You > 帮我创建一个 test.txt
[permission] write_file("test.txt") — 允许执行?(y/n) y
Assistant > 文件已创建。

You > 跑一下 rm -rf temp/
[permission] bash("rm -rf temp/") — 允许执行?(y/n) n
Assistant > 操作被拒绝了。你可以告诉我你想做什么,我来想其他办法。

现在危险操作会先问你了。但你很快会发现一个问题——所有写操作和所有命令都要确认,写个小文件也要确认,太烦了。

Prompt 2:分等级管控

帮我分等级——读取的直接放行,
写入的确认一下,
特别危险的要醒目警告我。
$ harness
You > 帮我看看 README.md
[tool: read_file("README.md")]   ← 读取:直接放行

You > 帮我改一下 config.json
[confirm] write_file("config.json") — 允许?(y/n) y   ← 写入:普通确认

You > 跑一下 git push --force
[DANGER] bash("git push --force") — 这是高危操作!确认执行?(y/n) n
← 高危:醒目警告

好多了。但每次写文件还是要确认一下,有些操作你完全信任——比如在某个测试目录下写文件。下一个 Prompt 解决这个问题。

Prompt 3:可配置的规则系统

帮我加配置机制——
能设规则比如"这个目录下写文件都放行"
或"永远不许执行 rm -rf"。
规则按优先级匹配。
$ harness
(先在配置里加一条规则:tests/ 目录下写文件自动放行)

You > 帮我在 tests/ 目录下创建一个测试文件
[tool: write_file("tests/test_new.py")]   ← 匹配规则,自动放行

You > 帮我改一下 main.py
[confirm] write_file("main.py") — 允许?(y/n) y   ← 不匹配规则,还是要确认

You > 跑一下 rm -rf /
[DENIED] bash("rm -rf /") — 此操作被规则禁止   ← 匹配禁止规则,直接拒绝

💡 三个 Prompt 做了什么

  • Prompt 1 建立了基本门禁——所有操作都过一道检查
  • Prompt 2 加上了风险分级——不同风险等级不同处理方式
  • Prompt 3 加上了规则引擎——可配置、按优先级匹配、灵活可控

现在你的 Agent 不会随便乱来了。完整代码在 GitHub 仓库,对应 tag ch04-permission-system

接下来我们回过头,理解你刚刚构建的东西。


深入理解

为什么 Agent 需要权限系统

你可能会觉得”我自己用,注意点就行了”。但 Agent 和人类有一个根本区别:它不会犹豫

人类看到 rm -rf / 会本能地停下来想想”这是不是要删全盘”。Agent 不会——你说”帮我清理一下临时文件”,它如果判断错了,就真的会跑出一个危险命令,而且毫不犹豫地执行。

🔴 真实世界的恐怖故事

这些不是假设,是真实发生过的事情:

  • 一个用户让 Agent “清理项目目录”,Agent 执行了 rm -rf *——包括 .git 目录。未推送的代码全丢了
  • 一个用户让 Agent “更新远程仓库”,Agent 执行了 git push --force——覆盖了同事一周的工作
  • 一个用户让 Agent “修复权限问题”,Agent 执行了 chmod -R 777 /——整台服务器的文件权限全乱了
  • 一个用户让 Agent “帮我发布新版本”,Agent 修改了生产环境的配置文件并推送——导致线上服务宕机

这些操作在 Agent 看来都是”合理的执行步骤”。它没有恶意,它只是不懂后果

权限系统不是”锦上添花”——它是安全底线。没有权限系统的 Agent 就像没有刹车的跑车:跑得越快,摔得越惨。

理解权限引擎:规则匹配

打开你刚生成的权限相关代码,找到规则匹配的核心逻辑。它的骨架就是下面这 15 行:

# harness/permissions.py — 核心骨架
def check_permission(tool_name, params, rules):
    """检查一个工具调用是否被允许。

    规则按优先级从高到低排列。
    第一条匹配的规则决定结果。
    没有规则匹配时,按风险等级走默认策略。
    """
    risk = get_risk_level(tool_name, params)

    for rule in rules:            # 按优先级遍历
        if rule.matches(tool_name, params):
            return rule.decision  # ALLOW / CONFIRM / DENY

    # 兜底:按风险等级走默认策略
    return DEFAULT_POLICY[risk]

逻辑非常简单:先算出这个操作的风险等级,然后从优先级最高的规则开始逐条匹配,第一条命中的规则说了算。如果所有规则都不匹配,就按风险等级走默认策略(读取放行、写入确认、高危拒绝)。

这种”第一匹配优先”的模式你可能在别处见过——防火墙规则就是这么工作的。越靠前的规则优先级越高,一旦命中就不再往下看。

规则本身长什么样?就是一个简单的”条件 + 决策”组合:

# "tests/ 目录下写文件自动放行"
Rule(tool="write_file", path_glob="tests/**", decision=ALLOW)

# "永远不许执行 rm -rf"
Rule(tool="bash", command_pattern="rm -rf *", decision=DENY)

path_glob 用通配符匹配路径(tests/** 表示 tests 目录及其所有子目录),command_pattern 用模式匹配命令内容。这让规则既灵活又直观。

风险分级

不是所有操作都一样危险。读一个文件和删掉整个目录,风险天差地别。权限系统的第一步就是分级——把所有工具操作分成几个风险等级,每个等级用不同的默认策略。

💡 核心概念:风险分级

风险分级的本质是一个问题:这个操作如果出错,后果有多严重?

  • 只读操作——出错了也没关系,最多是看到了不该看的东西,不会改变任何状态
  • 写入操作——出错了会修改文件或环境,但通常可以恢复(有版本控制的话)
  • 破坏性操作——出错了可能造成不可逆的损失——删除文件、覆盖远程仓库、修改系统配置

等级越高,默认策略越严格。这符合直觉:看看没关系,改东西要同意,搞破坏直接拦住。

表 4-1:三级风险分类

风险等级典型操作出错后果默认策略
只读(read_only)读文件、搜索代码、列目录无副作用直接放行
写入(write)写文件、创建目录、安装依赖可恢复的修改用户确认
破坏性(destructive)删除文件、强制推送、修改系统配置不可逆损失醒目警告 / 拒绝

每个工具的风险等级不是一成不变的——它取决于参数。同样是执行命令,ls -la 是只读的,echo hello > test.txt 是写入的,rm -rf / 是破坏性的。这就是为什么 get_risk_level() 不只看工具名,还要看参数内容。

Claude Code 的七层权限

我们的权限系统只有一层规则。Claude Code 有七层,像洋葱一样层层嵌套,每一层都能对操作说”不”:

图 4-1:Claude Code 七层权限模型

  1. 企业策略 — 管理后台下发,不可覆盖
  2. 组织策略 — 团队级别,统一标准
  3. 项目配置 — settings.json,团队共享
  4. .claude 文件 — 项目根目录,版本控制
  5. 会话设置 — —allowedTools,单次会话
  6. 工具声明 — 工具自带的风险标记
  7. 运行时判断 — 参数分析,智能检测

(优先级从上到下递减)

这七层从上往下,优先级逐级递减:

  • 企业策略——由公司管理员在后台设置,强制所有员工遵守。比如”任何人都不许直接推送到 main 分支”。这一层不可被任何下层覆盖

  • 组织策略——团队级别的约束。比如”前端团队不许直接操作数据库相关文件”。比企业策略灵活,但也不能违反企业策略

  • 项目配置——写在项目的配置文件里,团队成员共享。比如”本项目的 migrations/ 目录只读,不允许 AI 自动修改数据库迁移”

  • .claude 文件——放在项目根目录的指令文件,会被版本控制追踪。可以写”允许在 tests/ 目录下自动写文件”之类的项目级规则

  • 会话设置——启动 Claude Code 时通过参数指定,只影响当前会话。比如 --allowedTools "bash" 表示本次会话允许执行命令

  • 工具声明——每个工具在注册时会声明自己的默认风险等级。比如读文件工具声明自己是”只读”,写文件工具声明自己是”写入”

  • 运行时判断——最底层,根据实际参数做动态判断。同一个命令执行工具,传入 ls 和传入 rm -rf / 的风险等级完全不同

❗ 七层的意义

为什么要这么多层?因为不同角色关心不同的事:

  • 企业管理员关心合规——“绝对不能泄露生产环境密钥”
  • 团队负责人关心规范——“不要让 AI 自动修改公共 API”
  • 项目开发者关心效率——“我信任这个项目的测试目录”
  • 当次使用者关心灵活——“这次我就想让 AI 帮我跑个脚本”

七层让每个角色都能在自己的范围内设置规则,互不冲突,上层永远压过下层。

权限检查的完整流程

当 Agent 想执行一个操作时,权限系统是这样一步步做决策的:

图 4-2:权限检查的完整流程

  1. 工具调用 — Agent 说「我要执行某个操作」
  2. 获取风险等级 — 根据工具名 + 参数,判断这个操作的危险程度
  3. 规则匹配 — 从最高优先级的规则开始逐条匹配
  4. 做出决策 — 匹配到规则 → 按规则来;没匹配到 → 按风险等级走默认策略
  5. 用户确认 — 如果决策是 CONFIRM,弹窗问用户
  6. 执行或拒绝 — 用户同意 → 执行工具;用户拒绝或规则拒绝 → 把拒绝原因告诉 AI

流程中有一个关键细节:拒绝不是结束。当操作被拒绝时,Agent 会收到一条包含拒绝原因的消息。它可能会换一种更安全的方式来完成同样的目标。

比如你拒绝了 rm -rf temp/,Agent 可能会改用”逐个删除 temp/ 目录下的文件”——一个个来,每个都让你确认。这比一句 rm -rf 安全得多。

决策结果的三种类型

  1. ALLOW(放行)——直接执行,不打扰用户。适用于只读操作或已被规则明确信任的操作
  2. CONFIRM(确认)——暂停执行,把操作详情展示给用户,等用户明确同意后再继续。适用于写入操作或中等风险的操作
  3. DENY(拒绝)——直接拒绝,不给用户选择的机会。适用于被规则明确禁止的操作。把拒绝原因告诉 AI,让它想别的办法

进阶:Bash 命令的智能检测

在所有工具中,命令执行工具是最难管控的——因为一个字符串可以是任何东西ls 是安全的,rm -rf / 是致命的,而 curl https://example.com | bash 可能是任何情况。

怎么判断一个命令是否危险?你的权限系统可能用了最直接的办法——检查命令开头是不是 rm、是不是包含 --force。这叫模式匹配

Claude Code 的做法更精细,它用了启发式检测——不只看命令本身,还看命令的意图和上下文

  1. 关键词检测——检查命令中是否包含已知的危险关键词:rm -rfmkfsdd if=chmod -R 777> /dev/sda
  2. 管道分析——curl url | bashwget url -O- | sh 这类”下载并执行”的模式,风险极高,直接标记为破坏性
  3. 路径敏感度——操作 /etc//usr/~/.ssh/ 等系统关键路径时,自动提升风险等级
  4. 命令组合——&&|| 连接的多个命令,每个都要单独检查,取最高风险等级
  5. 环境变量——设置或导出环境变量(尤其是 PATHLD_PRELOAD)也需要关注

把这五条规则翻译成代码,就是下面这个函数:

# harness/permissions.py — Bash 风险检测
DESTRUCTIVE_PATTERNS = [
    r"rm\s+-[^\s]*r",           # rm -rf, rm -r
    r"mkfs\b", r"dd\s+if=",     # 格式化磁盘
    r"chmod\s+-R\s+777",         # 全开权限
    r">\s*/dev/sd",              # 直写磁盘
    r"git\s+push\s+.*--force",   # 强制推送
]
PIPE_EXEC = r"(curl|wget)\b.*\|\s*(bash|sh|zsh)"
SENSITIVE_PATHS = ["/etc/", "/usr/", "~/.ssh/", "/System/"]

def classify_bash_risk(command: str) -> str:
    # 管道执行 → 直接判定为破坏性
    if re.search(PIPE_EXEC, command):
        return "destructive"
    # 破坏性关键词
    for pat in DESTRUCTIVE_PATTERNS:
        if re.search(pat, command):
            return "destructive"
    # 敏感路径
    for path in SENSITIVE_PATHS:
        if path in command:
            return "write"  # 提升到写入级别
    # 命令组合:拆开逐个检查,取最高
    if "&&" in command or "||" in command:
        parts = re.split(r"&&|\|\|", command)
        return max(classify_bash_risk(p.strip())
                   for p in parts,
                   key=["read_only","write","destructive"].index)
    return "read_only"  # 默认只读

这段代码正是 Claude Code 权限引擎中 isSafeCommand 逻辑的简化版。真实实现还会检查更多模式(比如 pkillkillallsystemctl),但核心思路一样:从最危险的模式开始匹配,命中即停

💡 不完美但足够好

启发式检测不可能 100% 准确——你总能构造出一个”看起来安全实际很危险”的命令。但它不需要 100% 准确。

权限系统的设计哲学是:宁可多问一次,不可漏放一次。误报(把安全操作标记为危险)只是让用户多按一次确认键;漏报(把危险操作当作安全)可能让用户丢失数据。

Claude Code 在启发式检测之上还有一层保险:AI 模型本身会判断命令的风险。模型见过大量代码和命令,它对”这个命令会造成什么后果”有相当好的直觉。启发式检测 + 模型判断,双重保险。

特殊场景:用户明确要求的危险操作

有时候用户就是明确要求执行一个危险操作——比如”帮我删掉 build/ 目录下的所有文件”。这时候 Agent 应该怎么做?

答案是:执行,但要走完整的确认流程。用户是老板,Agent 应该尊重用户的意图。但它有义务告知风险——“这个操作会永久删除 build/ 目录下的 47 个文件,确认继续?”

这和人类的行为模式一致:如果老板说”把这个目录删了”,一个好员工不会直接拒绝,但他会确认一下”您确定吗?这里面有上次发布的产物”。

权限与工具系统的集成

权限系统不是独立运转的——它嵌入在查询引擎的工具执行流程中。回顾第 2 章的 execute_tool 函数,权限检查就是在执行前插入的一道关卡:

# harness/engine.py — 权限集成
def execute_tool(tool_name, params, state):
    # 1. 权限检查(本章新增)
    decision = permission_engine.check(tool_name, params)

    if decision == "DENY":
        return {"error": f"操作被拒绝:{tool_name}"}

    if decision == "CONFIRM":
        approved = ask_user_confirmation(tool_name, params)
        if not approved:
            return {"error": "用户拒绝了此操作"}

    # 2. 执行工具(第 3 章)
    result = tool_registry.execute(tool_name, params)
    return result

这段代码的关键在于位置——权限检查在工具执行之前,不在之后。一旦工具执行了,rm -rf 删掉的文件就回不来了。所以权限是一个前置守卫,不是事后审计。

拒绝消息的设计

当操作被拒绝时,返回给 AI 的不是简单的 “error”,而是一条有信息量的拒绝消息。这让 AI 能理解为什么被拒、怎么换一种方式达到目标:

# 构造拒绝消息
def build_denial_message(tool_name, params, reason):
    return (
        f"操作 {tool_name} 被权限系统拒绝。\n"
        f"原因:{reason}\n"
        f"建议:尝试使用更安全的替代方案,"
        f"或者请用户手动执行此操作。"
    )

好的拒绝消息包含三部分:什么被拒了为什么拒可以怎么办。这不是客气——它直接影响 AI 后续的行为质量。

会话级权限记忆

在实际使用中你会遇到一个问题:批量重构时 AI 可能要连续写 20 个文件,每个都弹确认——烦死了

解决方案是会话级权限记忆——用户确认过一次的操作模式,在当前会话内自动放行:

# 会话级权限缓存
class SessionPermissionCache:
    def __init__(self):
        self._approved_patterns = set()

    def remember(self, tool_name, path_pattern):
        self._approved_patterns.add((tool_name, path_pattern))

    def is_approved(self, tool_name, params):
        for tool, pattern in self._approved_patterns:
            if tool == tool_name and matches(params, pattern):
                return True
        return False

当用户确认 write_file("src/utils.py") 时,权限系统记住”本次会话内,写入 src/ 目录下的文件已获批准”。后续写入同一目录的文件自动放行,不再重复询问。

❗ 会话级 vs 持久化

权限记忆只在当前会话有效——关掉 harness 就清零。这是刻意的设计:

  • 会话级——方便批量操作,关掉就忘,每次重新开始都是最小权限
  • 持久化——写入配置文件的规则,永久生效,适合”这个目录我永远信任”

Claude Code 用的也是这种双轨模式:会话内的确认会被记住(同类操作不重复问),但关掉终端就清零;持久化的信任写在 settings.json 里。

审计日志

权限系统还有一个常被忽略的功能——审计日志。每一次权限决策都应该被记录下来:谁调了什么工具、传了什么参数、决策结果是什么、用户确认了还是拒绝了。

# 审计日志
def log_permission_decision(tool_name, params, decision, rule):
    entry = {
        "timestamp": datetime.now().isoformat(),
        "tool": tool_name,
        "params": sanitize(params),  # 脱敏
        "decision": decision,
        "matched_rule": rule.name if rule else "default",
    }
    append_to_log(".harness/permission.log", entry)

审计日志的价值在事后分析——如果 Agent 做了一个意外操作,你可以回溯:它是怎么通过权限检查的?匹配了哪条规则?是用户确认了还是规则自动放行了?

注意 sanitize(params) ——日志里不应该出现完整的文件内容或敏感命令参数。记录”写了什么文件”就够了,不需要记录”写了什么内容”。


延伸思考

在进入下一章之前,回头看看你的权限系统,思考几个问题:

  1. 如果 Agent 需要连续执行 20 个文件写入操作(比如批量重构),每个都要确认,用户体验很差。你会怎么设计”批量授权”机制?
  2. 规则匹配是按优先级从高到低的。如果两条规则冲突(一条说放行,一条说拒绝),“第一匹配优先”是最好的策略吗?有没有其他方案?
  3. 权限系统本身也是代码——如果 Agent 有能力修改权限配置文件,它能不能”给自己开后门”?怎么防?

章节小结

  • 三个 Prompt 构建了一套权限系统:基本门禁 → 风险分级 → 规则引擎
  • Agent 需要权限系统是因为它不会犹豫——有能力但无判断是灾难
  • 风险分三级:只读(直接放行)→ 写入(用户确认)→ 破坏性(醒目警告 / 拒绝)
  • 规则匹配采用”第一匹配优先”策略——像防火墙一样,命中就停
  • Claude Code 有七层权限,从企业策略到运行时判断,层层嵌套,上层压过下层
  • Bash 命令用启发式检测判断风险——关键词、管道、路径、命令组合多维度分析
  • 权限系统的哲学:宁可多问一次,不可漏放一次
  • 下一章我们构建多 Agent 编排——让多个 Agent 协同完成复杂任务
English EN 简体中文 ZH