Harness Engineering

第14章 Agent 权限模型设计

作者 杨艺韬 · 7,483 字

第14章 Agent 权限模型设计

“The model doesn’t know that rm and ls are different. It’s your harness’s job to know.” —— 杨艺韬

“权限是 Agent 时代唯一能让用户睡得着觉的东西。” —— 杨艺韬

本章要点

  • Agent 权限保护的是一个不完全可预测的推理引擎对物理世界的影响
  • 五级权限模型:Plan / Default / Auto-Edit / Full-Auto / Custom
  • 三层粒度:per-tool / per-action / per-resource
  • Allow-list vs Deny-list:宏观白名单,微观黑名单——混合最优
  • 动态权限升级:单向阶梯,只能紧不能松
  • 确认疲劳是反向效应——过多确认反而降低安全性
  • 三层持久化:Session / Project / Global
  • deny 优先 + 范围小优先——安全约束只能收紧

Agent 能执行代码、修改文件、访问网络——这些能力让它强大,也让它危险。本章从最小权限原则出发,拆解 Claude Code 的五级权限模型,探讨权限粒度、确认流程、动态升级、持久化策略和审计日志等核心设计决策。

14.1 为什么 Agent 需要权限系统

传统软件的权限模型保护的是”能访问什么数据”。Agent 的权限问题本质不同——它保护的是”一个不完全可预测的推理引擎,能对物理世界造成多大影响”。

血泪案例

一个没有权限约束的 Agent 意味着什么?让我们看几个真实场景:

场景 1: rm 变体
  模型幻觉认为要"清理临时文件"
  → 执行 `rm -rf /var/tmp/*`
  → 一个环境变量展开错误 → 实际执行了 `rm -rf /`

场景 2: 恶意 npm 包
  模型推荐安装一个"lodash-utils"包
  → 这个包名是它幻觉的,真实的是"lodash-util"
  → 真实被安装的可能是攻击者注册的同名恶意包
  → 包的 post-install 脚本执行任意代码

场景 3: API Key 泄漏
  模型调试网络问题时
  → 读取 .env 文件
  → 以"调试"名义把内容发给外部日志服务
  → AWS Key 泄漏到第三方

场景 4: 不可逆的"优化"
  模型为"优化性能"
  → 修改了生产环境的 postgresql.conf
  → shared_buffers 调到 99% 物理内存
  → 数据库启动失败 → 数据丢失

这些不是假设。任何使用过 Agent 工具的开发者,都经历过”它差点干了一件蠢事”的时刻。

模型的盲区

模型的推理能力在统计意义上很强,但它不具备”这件事做错了后果不可逆”的风险意识。它对 rmls 的调用,在它看来没有本质区别——都只是完成任务的一个步骤。

这就是权限系统存在的根本理由:

在模型的能力和它被允许使用的能力之间,建立一道由工程系统维护的屏障。

与传统 RBAC 的四个差异

和传统 RBAC(基于角色的访问控制)不同,Agent 权限系统面临几个独特挑战:

维度传统 RBACAgent 权限
行为可预测性高(代码写死)低(模型推理)
主体意图明确(程序执行)需要推断(模型意图)
粒度需求角色级极细(单次操作)
决策时机部署时运行时
用户参与管理员配置每次操作都可能需要
  1. 行为不可完全预测:你不知道模型下一步要调用什么工具、传什么参数
  2. 意图需要推断:模型说”我要删除这个文件是为了重建它”,你如何判断这是合理操作还是幻觉?
  3. 粒度极细:不是”能不能访问文件系统”的问题,而是”能不能访问 /etc 目录下的文件”的问题
  4. 交互式决策:某些权限需要实时向用户确认,这引入了 UX 设计的考量

14.2 Claude Code 的五级权限模型

Claude Code 设计了一个五级权限模型,从最严格到最宽松:

graph LR
    subgraph Modes["权限模式 (从严到松)"]
        M1[🔒 Plan Mode<br/>只读分析] --> M2[🔐 Default Mode<br/>逐个确认]
        M2 --> M3[🔓 Auto-Edit Mode<br/>文件自动放行]
        M3 --> M4[🟢 Full-Auto Mode<br/>全自动执行]
        M4 --> M5[⚙️ Custom Rules<br/>精细化控制]
    end

    Trust[用户信任度] -.->|"从低到高"| Modes

    style M1 fill:#fee2e2,stroke:#ef4444
    style M2 fill:#fef3c7,stroke:#f59e0b
    style M3 fill:#fef9c3,stroke:#eab308
    style M4 fill:#dcfce7,stroke:#22c55e
    style M5 fill:#dbeafe,stroke:#3b82f6

Plan Mode(只读规划)

最安全的模式。模型只能读取信息、分析代码、给出建议,但不能执行任何修改操作。

适用场景

  • 代码审查
  • 架构讨论
  • 新项目评估
  • 处理敏感生产环境

实现简洁:Harness 层只需不注册写入类工具即可。

典型工作流

用户: "分析这个项目的架构有什么问题"
Agent: Read / Grep / Glob 组合调查
Agent: 生成详细的分析报告
Agent: 不会执行任何修改

Default Mode(逐个确认)

大多数用户的日常模式。模型可以调用所有工具,但每次工具调用前都会暂停循环,展示工具名称和参数,等待用户确认。

用户可以选择:

  • 允许(本次执行)
  • 拒绝(取消本次执行)
  • 允许并记住(加入 session 白名单)

这个模式在安全性和效率之间取得了合理的平衡:

  • 简单的读取操作:用户快速按下回车
  • 危险操作:用户有机会审查

Auto-Edit Mode(文件自动放行)

解除了文件编辑的确认要求。模型可以自由地读写文件,但执行 Bash 命令等高风险操作仍需确认。

设计逻辑

  • 文件编辑有 Git 兜底——最坏情况 git checkout 就能恢复
  • 命令执行的后果可能无法撤销——rm -rfDROP TABLEcurl | bash 都是单向操作

Full-Auto Mode(全自动)

取消所有确认,模型完全自主运行。

适用场景

  • CI/CD 环境中的自动化流水线
  • 长时间任务(用户离开,让 Agent 自行工作)
  • 沙箱环境(隔离后危害可控)

Claude Code 在启用此模式时会显示醒目的警告。

Custom Rules(自定义)

最灵活的模式。用户可以定义精细化的规则:

允许所有文件读取
允许编辑 src/ 目录下的文件
禁止编辑 config/ 目录
Bash 命令需要确认
但 npm test 自动放行
npm publish 永远禁止

这是真正的生产级配置。

渐进式信任阶梯

这五个级别的设计思路值得深挖。它不是一个简单的”开关”,而是一个渐进式信任阶梯

graph TD
    New[新用户] --> D[Default Mode<br/>每次确认]
    D -->|建立认知| D2[对 Edit 操作产生信任]
    D2 --> AE[Auto-Edit Mode<br/>文件放行]
    AE -->|进一步信任| AE2[对特定 Bash 命令产生信任]
    AE2 --> C[Custom Rules<br/>精细规则]
    C -->|特殊场景| FA[Full-Auto Mode<br/>CI/CD]

    style New fill:#dbeafe,stroke:#3b82f6
    style C fill:#dcfce7,stroke:#22c55e

新用户从 Default Mode 开始,每一次交互都在建立对 Agent 行为模式的认知。当用户确认了 100 次 Edit 操作后,自然会想”能不能自动放行这些”,于是升级到 Auto-Edit Mode。

最终,高度信任 Agent 的用户会使用 Custom Rules 来精确控制权限边界。

14.3 权限粒度:工具、动作、资源三层模型

权限系统的核心设计决策是粒度。粒度太粗,安全形同虚设;粒度太细,用户被确认弹窗淹没。

我把 Agent 权限粒度分为三层:

第一层:per-tool(按工具)

最基础的粒度。允许或禁止某个工具的使用:

interface ToolPermission {
  tool: string       // "Bash" | "Edit" | "Read" | ...
  allowed: boolean
}

这一层实现简单,但远远不够。允许使用 Bash 工具却不区分 lsrm -rf,就像给人一把万能钥匙然后说”只许开你自己的门”

第二层:per-action(按动作)

在工具内部区分具体动作。对于 Bash 工具,区分不同的命令;对于文件操作工具,区分读、写、创建、删除。

interface ActionPermission {
  tool: string
  action: string     // "read" | "write" | "execute" | "delete"
  allowed: boolean
}

Claude Code 在 Bash 工具上做了精细的动作级控制。它维护了一个命令分类体系:

const commandCategories = {
  safe: ["ls", "cat", "grep", "find", "echo", "pwd", "wc", "head", "tail"],
  moderate: ["npm test", "npm run build", "git status", "git diff", "git log"],
  dangerous: ["rm", "chmod", "chown", "curl | bash", "eval", "dd"],
  blocked: [
    "rm -rf /",
    ":(){ :|:& };:",   // fork bomb
    "mkfs",
    "dd if=/dev/zero of=/dev/sd",
    "> /dev/sda",
  ]
}
类别处理
safe自动放行
moderate宽松模式下放行
dangerous始终需要确认
blocked直接拒绝,无论权限模式

这种分类不是静态配置,而是通过模式匹配动态判断的。

第三层:per-resource(按资源)

最精细的粒度,控制工具可以操作的具体资源:

interface ResourcePermission {
  tool: string
  action: string
  resource: string   // 支持 glob 模式: "src/**/*.ts", "!**/secrets/**"
  allowed: boolean
}

路径模式匹配是资源级权限最常见的实现方式:

permissions:
  - tool: Edit
    allow:
      - "src/**"
      - "tests/**"
      - "docs/**"
    deny:
      - "**/credentials*"
      - "**/.env*"
      - "**/secrets/**"
      - "**/*.key"
      - "**/*.pem"

  - tool: Bash
    allow_commands:
      - "npm test*"
      - "npm run *"
      - "git status"
      - "git diff*"
      - "git log*"
    deny_commands:
      - "npm publish*"
      - "git push --force*"
      - "curl * | bash"
      - "sudo *"
      - "rm -rf *"

粒度的使用原则

三层粒度的选择不是”越细越好”。每增加一层粒度,用户的配置成本和系统的判断开销都在增加。实践中的经验是:

工具层做准入控制,动作层做分类管理,资源层只用于高风险场景。

graph TD
    G1[per-tool 准入]
    G1 -->|所有操作| G2[per-action 分类]
    G2 -->|高频使用| Auto[自动放行或询问]
    G2 -->|高风险 Bash| G3[per-resource 精细]
    G3 -->|.env/ssh| Deny[强制拒绝]
    G3 -->|src/| Allow[白名单放行]

    style Auto fill:#dcfce7,stroke:#22c55e
    style Deny fill:#fee2e2,stroke:#ef4444

14.4 Allow-list 与 Deny-list:两种策略的博弈

权限系统有两种基本策略:

  • Allow-list(白名单):默认禁止一切,只放行明确允许的操作
  • Deny-list(黑名单):默认允许一切,只阻止明确禁止的操作

两种策略的利弊

维度Allow-listDeny-list
默认安全性高(未知=禁止)低(未知=允许)
可用性低(要列出所有允许)高(默认能用)
维护成本高(持续扩充)中(遇到坏东西才加)
适用场景安全关键便利性优先
对新威胁天然防御需要更新

Claude Code 的混合策略

Claude Code 采用了混合策略——宏观上是 Allow-list,微观上用 Deny-list 补充

宏观层面,每个工具默认需要用户确认才能执行,这本质上是 Allow-list:没有明确授权的操作不会自动执行。

但在自动模式下,它切换为 Deny-list 策略:默认允许执行,但维护一个明确的禁止列表。

为什么选混合策略

  1. Allow-list 的问题是枚举不完

    • 合法的 Bash 命令有无限种,你不可能列出所有允许的命令
    • 用户总会遇到 Allow-list 没覆盖的合理场景
    • 过严的 Allow-list 导致用户关掉权限或疯狂 approve
  2. Deny-list 的问题是遗漏致命

    • 漏掉一个危险命令,后果可能不可逆
    • 攻击者会找 Deny-list 的缝隙
  3. 所以最佳实践是:在安全模式下用 Allow-list 保底,在自动模式下用 Deny-list 兜底

Deny-list 的陷阱

Deny-list 的设计需要特别谨慎。一个常见的陷阱是只匹配命令前缀

// ❌ 错误:只检查命令开头
const blocked = ["rm -rf /", "mkfs"]
function isBlocked(cmd: string) {
  return blocked.some(b => cmd.startsWith(b))
}

// 攻击者的绕过方式:
// "cd / && rm -rf ."       ← 不以 rm 开头
// "bash -c 'rm -rf /'"     ← 套一层 bash
// "find / -delete"         ← 换了工具
// "$(echo rm) -rf /"       ← 命令替换
// "rm$'\x20'-rf /"         ← 特殊空格
// "rm  -rf /"              ← 两个空格

健壮的 Deny-list 实现

健壮的 Deny-list 需要理解命令的语义,而不是简单的字符串匹配。Claude Code 的做法是:

  1. 解析命令——先拆分管道、识别子 shell、展开别名
  2. 提取原子命令——每个原子命令单独检查
  3. 参数分析——识别 -rf 这样的危险参数组合
  4. 路径规范化——检查最终操作的路径
  5. 上下文感知——考虑 cwd 等环境
function analyzeCommand(cmd: string): CommandAnalysis {
  const parsed = parseBashCommand(cmd);  // 使用真正的 bash 解析器
  const atomicCommands: AtomicCommand[] = [];

  for (const node of parsed.walk()) {
    if (node.type === 'simple_command') {
      atomicCommands.push({
        program: node.name,
        args: node.args,
        redirections: node.redirections,
      });
    }
    // 处理 pipes, subshells, command substitution, etc.
  }

  const risks: Risk[] = [];
  for (const atomic of atomicCommands) {
    risks.push(...checkDangerousPatterns(atomic));
  }

  return { atomicCommands, risks };
}

14.5 动态权限升级:从怀疑到信任

静态权限配置无法覆盖所有场景。一个更自然的模式是动态权限升级:Agent 从最小权限开始,根据行为表现和用户反馈逐步获得更多权限。

14.5.1 单次会话内的升级

用户:"帮我重构 auth 模块"

第 1 轮:Agent 使用 Read/Grep 分析代码结构
       → 读取类操作自动放行

第 2 轮:Agent 想用 Edit 修改 src/auth/login.ts
       → Default Mode 请求确认
       用户:允许,并记住"允许编辑 src/auth/**"

第 3 轮:Agent 编辑 src/auth/session.ts
       → 匹配 "src/auth/**" 规则,自动放行

第 4 轮:Agent 想运行 npm test
       → 请求确认
       用户:允许,并记住"允许 npm test"

第 5 轮:Agent 修改另一个 auth 文件并运行测试
       → 全部自动放行

这就是”渐进式信任”的实际体验。用户不需要提前配置所有权限,也不需要每次都确认相同类型的操作。系统会学习用户的授权模式。

14.5.2 跨会话的信任积累

用户在项目级配置文件(如 .claude/settings.json)中记录的权限规则会持久化。经过多次会话后,这些规则逐渐形成一个针对特定项目的权限画像:

{
  "permissions": {
    "allow": [
      "Edit(src/**)",
      "Edit(tests/**)",
      "Bash(npm test*)",
      "Bash(npm run build)",
      "Bash(git status)",
      "Bash(git diff*)"
    ],
    "deny": [
      "Bash(npm publish*)",
      "Bash(git push*)",
      "Edit(**/.env*)",
      "Edit(**/config/production*)"
    ]
  }
}

14.5.3 单向阶梯原则

动态权限升级的关键设计约束是单向阶梯——权限只能升不能降(在单次会话内)。

为什么?如果系统检测到 Agent 执行了一个可疑操作后自动降低权限,会导致用户体验的混乱:

  • 用户要重新确认本来已授权的操作
  • Agent 不知道自己被降权,重复尝试
  • 交互流程被打断

更好的做法是:可疑操作触发一次确认,但不改变已有的权限授予

graph LR
    L1[初始权限级别]
    L1 -->|用户授权| L2[权限级别上升]
    L2 -->|继续授权| L3[更高级别]

    L3 -.->|❌ 不应该发生| L2
    L2 -.->|❌ 不应该发生| L1

    L3 -->|可疑行为| Warn[触发确认<br/>不降级]
    Warn --> L3

    style Warn fill:#fef3c7,stroke:#f59e0b

14.6 用户确认流程的设计

sequenceDiagram
    participant A as Agent
    participant P as 权限引擎
    participant U as 用户

    A->>P: 请求执行 Bash "rm -rf node_modules"
    P->>P: 匹配规则: Bash 工具需要确认
    P->>U: "Agent 要运行: rm -rf node_modules<br/>这会删除依赖目录(可通过 npm install 恢复)"
    alt 用户批准
        U->>P: "允许"
        P->>A: 执行
    else 用户批准并记住
        U->>P: "允许并记住此命令"
        P->>P: 添加会话级白名单规则
        P->>A: 执行
    else 用户拒绝
        U->>P: "拒绝"
        P->>A: 返回 Permission Denied
    end

确认流程是权限系统的”用户界面”。设计得好,用户感觉安全又高效;设计得差,用户要么被烦死,要么盲目点”允许”——两种情况都违背了权限系统的初衷。

好的确认提示的三要素

一个好的确认提示需要回答三个问题:

  1. Agent 要做什么? 工具名称 + 参数的清晰展示
  2. 为什么要做? Agent 的推理过程(上下文)
  3. 风险是什么? 操作的潜在后果

Claude Code 的确认弹窗遵循这个结构:

╭─────────────────────────────────────────────────╮
│  Claude wants to run: Bash                       │
│                                                  │
│  Command: npm install lodash@4.17.21             │
│                                                  │
│  Context: I need lodash for the utility functions│
│                                                  │
│  [Allow]  [Deny]  [Allow Always for this project]│
╰─────────────────────────────────────────────────╯

UX 设计的五个决策

1. 展示完整命令,而非摘要

用户需要看到 rm -rf ./dist 而不是”删除某个目录”。摘要可能隐藏关键细节。

❌ "Agent wants to delete a directory"
✅ "Agent wants to run: rm -rf ./dist"

2. 提供”记住”选项

“Allow Always”让用户一次授权,永久生效(在项目范围内)。这大幅减少了重复确认的疲劳。

3. 对危险操作使用视觉警告

当命令匹配高危模式时(如包含 rmchmod 777),确认提示应该用红色高亮或额外的警告文本:

╭─────────────────────────────────────────────────╮
│  ⚠️  DANGEROUS COMMAND                            │
│                                                  │
│  Claude wants to run: rm -rf ./node_modules      │
│                                                  │
│  Warning: This is a recursive deletion.          │
│  If the path is wrong, data loss may occur.      │
│                                                  │
│  [Allow]  [Deny]  [Allow Always]                 │
╰─────────────────────────────────────────────────╯

4. 批量确认

当模型在一个循环中连续执行多个同类操作时,提供”允许接下来 N 个同类操作”的选项:

Agent wants to edit 10 files matching "src/auth/**"
[Allow this one]
[Allow all 10]
[Allow this pattern permanently]
[Deny]

5. 对预期操作做摘要,意外操作才突出

当前任务涉及编辑 100 个文件时,把它们摘要显示;但如果其中混入了不在预期范围内的文件,这个异常要醒目展示。

确认疲劳:反向效应

有一个反直觉的观察

确认疲劳比没有确认更危险。

当用户连续确认了 20 个无害操作后,第 21 个危险操作也会被条件反射般地确认。

这就是为什么权限系统不应该对所有操作都弹确认——只对真正需要人类判断的操作弹出,其他的要么自动放行,要么自动拒绝。

graph LR
    A[弹窗数量] --> B[用户关注度]
    B --> C[安全效果]

    subgraph 理想
        A1[少量关键弹窗] --> B1[高关注度] --> C1[高安全]
    end

    subgraph 反效果
        A2[大量无差别弹窗] --> B2[低关注度<br/>点击疲劳] --> C2[低安全]
    end

    style C1 fill:#dcfce7,stroke:#22c55e
    style C2 fill:#fee2e2,stroke:#ef4444

14.7 权限持久化:会话、项目、全局三层

权限规则需要在不同范围内持久化。

三层持久化

graph TD
    User[用户]
    User --> G[🌐 全局级<br/>~/.claude/settings.json<br/>用户的安全底线]
    User --> Pr[📁 项目级<br/>.claude/settings.json<br/>项目特定规则]
    User --> S[💬 会话级<br/>内存中<br/>一次性授权]

    style G fill:#fee2e2,stroke:#ef4444
    style Pr fill:#fef3c7,stroke:#f59e0b
    style S fill:#dbeafe,stroke:#3b82f6

会话级(Session):存在内存中,会话结束即消失。“允许这次编辑 package.json”就是会话级权限。适合一次性任务中的临时授权。

项目级(Project):存在项目目录的配置文件中(如 .claude/settings.json)。“在这个项目中,允许运行 npm test”是项目级权限。它跟随项目仓库,团队成员可以共享。

全局级(Global):存在用户主目录的配置文件中(如 ~/.claude/settings.json)。“在所有项目中,禁止运行 rm -rf”是全局级权限。它代表用户的安全底线

优先级规则

三层之间的优先级遵循一个简单规则:

deny 优先于 allow,范围小的优先于范围大的。

deny 不可被覆盖——这是关键的安全属性。

全局 deny "rm -rf *"     → 最终结果:deny(全局 deny 不可被覆盖)
项目 allow "rm -rf dist" → 被全局 deny 覆盖
会话 allow "rm -rf dist" → 被全局 deny 覆盖

全局 allow "npm test"    → 基础权限
项目 deny "npm test"     → 最终结果:deny(项目级 deny 覆盖全局 allow)

全局默认 (无规则)        → 默认"询问"
项目 allow "git status"  → 最终结果:allow
会话 deny "git status"   → 最终结果:deny(会话级 deny 覆盖)

设计哲学

这个优先级模型的核心思想是:安全约束只能收紧,不能放松

  • 全局设置的 deny 规则是不可被项目或会话覆盖的安全底线
  • 项目可以在全局允许的范围内进一步收紧,但不能突破全局设置的禁区
  • 会话级规则最灵活,但也最短命

版本控制决策

项目级权限的一个重要考量是是否纳入版本控制

选项优势劣势
纳入 Git团队共享,新成员无需配置暴露项目的权限结构
不纳入(gitignore)个人偏好不冲突每人都要配置
模板提交 + 个性化覆盖两者兼顾配置复杂

Claude Code 的做法是将权限配置文件放在 .claude/ 目录下,由团队自行决定是否将其加入 .gitignore

14.8 最小权限原则的 Agent 化

最小权限原则(Principle of Least Privilege)在传统安全领域是常识:每个主体只应获得完成其任务所需的最小权限集。但在 Agent 场景中,这个原则的应用远比传统系统复杂。

与传统系统的根本差异

传统系统中,一个服务的权限在部署时确定,运行期间不变。

Agent 的任务是动态的——同一个 Agent,这一刻在读代码,下一刻在执行测试,再下一刻在修改配置。它需要的权限随任务阶段而变化。

graph LR
    T[Agent 任务生命周期]
    T --> S1[阶段 1: 分析<br/>需要 Read]
    T --> S2[阶段 2: 修改<br/>需要 Read + Edit]
    T --> S3[阶段 3: 验证<br/>需要 Read + Bash<br/>不需要 Edit]
    T --> S4[阶段 4: 报告<br/>只需要输出]

    S1 --> P1[最小权限: 只读]
    S2 --> P2[最小权限: 读+写]
    S3 --> P3[最小权限: 读+执行]
    S4 --> P4[最小权限: 无]

    style P1 fill:#dbeafe,stroke:#3b82f6
    style P2 fill:#fef3c7,stroke:#f59e0b
    style P3 fill:#dcfce7,stroke:#22c55e
    style P4 fill:#f3e8ff,stroke:#a855f7

三个机制

最小权限的 Agent 化实现需要三个机制:

1. 任务感知的权限推断

根据用户的初始请求,推断这个任务可能需要的权限范围:

  • “帮我看看这段代码有什么问题” → 只需要读取权限
  • “帮我修复这个 bug” → 需要读取 + 编辑权限
  • “帮我部署到 staging” → 需要几乎所有权限

2. 阶段性权限授予

不在任务开始时就授予所有可能需要的权限,而是按阶段释放:

  • 分析阶段只给读取权限
  • 修改阶段增加编辑权限
  • 验证阶段增加执行权限

3. 权限自动回收

当一个子任务完成后,为其临时授予的权限应当回收。模型不应该因为在任务 A 中获得了 Bash 执行权限,就在无关的任务 B 中继续持有这个权限。

实践的现状

在实践中,完全自动化的最小权限实现仍然很困难。目前更可行的方式是半自动的——系统基于任务类型提供权限建议,用户一键确认或调整。

14.9 审计日志:每个决策都有据可查

权限系统的最后一环是审计。没有审计的权限系统就像没有监控的防火墙——你知道有规则在运行,但不知道它们是否在起作用。

完整的审计条目

interface PermissionAuditEntry {
  // 基础信息
  timestamp: string
  sessionId: string
  userId: string
  agentId: string

  // 请求内容
  tool: string           // 请求的工具
  action: string         // 请求的动作
  resource: string       // 操作的资源
  parameters: unknown    // 完整参数(脱敏)

  // 决策
  decision: "allow" | "deny" | "ask_user"
  decisionSource: string // "global_deny" | "project_allow" | "user_confirmed" | ...
  matchedRule?: string   // 命中的规则(如果是规则决定)

  // 用户交互
  userResponse?: string  // 如果是 ask_user,用户的响应
  userResponseTimeMs?: number  // 用户响应时间

  // 上下文
  modelReasoning?: string // 模型为什么要执行这个操作
  permissionMode: string  // 当时的权限模式

  // 结果
  executed: boolean      // 最终是否执行
  executionResult?: string  // 执行结果摘要
}

日志驱动的洞察

审计日志的价值不仅在于事后追查,更在于权限规则的优化。分析日志可以发现:

观察可能含义建议动作
某操作频繁被确认后允许用户信任这类操作考虑加入 allow-list
某操作频繁被拒绝潜在危险模式考虑加入 deny-list
确认响应时间 < 1s确认疲劳减少这类弹窗
同一命令多次不同决策用户不一致需要明确规则
Agent 尝试超范围操作prompt 可能有问题调整 system prompt

这些数据驱动的洞察,是持续改进权限策略的基础。

审计日志的保护

审计日志本身也是敏感资源:

  • 不能被 Agent 本身修改(防止掩盖痕迹)
  • 需要定期归档和分析
  • 长期保存(满足合规要求)
  • 包含的命令参数要脱敏(API key 等)

14.10 横向对比:Claude Code、Cursor、Devin

不同的 Agent 产品对权限模型的设计哲学截然不同,反映了它们对”Agent 自主性”的不同定位。

Claude Code:用户主权型

用户对每个操作有最终控制权,系统提供多级灵活配置。

假设:用户是有经验的开发者,愿意也有能力管理权限规则。

优势:安全性上限高 劣势:新用户的上手成本也高

Cursor:场景隔离型

把 Agent 的能力按功能区域隔离——代码编辑在编辑器内完成,终端命令在独立面板中执行,每个区域有自己的权限规则。

优势:利用了 IDE 已有的安全边界(编辑器中的操作天然可撤销) 劣势:跨区域协作时权限管理变得碎片化

Devin:沙箱型

给 Agent 一个完整的隔离环境(虚拟机或容器),Agent 在沙箱内拥有几乎完全的自由。

安全边界不在操作级别,而在环境级别——沙箱内怎么折腾都行,出不了沙箱就行。

优势:Agent 的自主性最高,执行效率最好 劣势:沙箱的构建成本高,且沙箱内的错误仍然需要恢复

对比矩阵

维度Claude CodeCursorDevin
控制粒度工具/动作/资源 三层功能区域级环境级
用户参与高(可配置为低)中等
安全上限最高中等取决于沙箱质量
自主性低→高可调中等
配置成本低(由平台承担)
学习曲线
适合场景本地开发IDE 集成云端长任务

没有银弹

没有”最好”的权限模型,只有最适合特定场景的模型

  • 本地开发工具 更适合 Claude Code 的用户主权型——开发者需要精细控制
  • 云端自动化平台 更适合 Devin 的沙箱型——隔离环境比操作级审批更高效
  • IDE 集成场景 更适合 Cursor 的场景隔离型——利用已有的安全基础设施

14.11 设计你自己的权限模型

如果你正在构建一个 Agent 系统,以下是我总结的权限模型设计清单:

8 条设计清单

  1. 确定安全底线:哪些操作在任何情况下都不允许?把它们放入全局 deny-list
  2. 按工具分类风险:读取类工具通常安全,可以默认放行;写入类需要确认;执行类需要更严格的控制
  3. 设计确认流程时考虑疲劳:如果用户每分钟需要确认超过 3 次,你的默认权限太严格了
  4. 提供”记住”机制:每次确认都应该提供持久化选项,避免重复劳动
  5. deny 优先于 allow:在优先级冲突时,永远选择更安全的决策
  6. 记录一切:审计日志是优化权限策略的唯一可靠数据来源
  7. 提供合理的预设:不要让用户从零开始配置权限。根据典型使用场景提供预设模板
  8. 权限配置本身需要保护:修改权限规则的操作,应该有独立的确认机制

设计实施路径

graph TD
    A[1. 威胁建模<br/>列出所有危险操作] --> B[2. 分级<br/>按风险将操作归类]
    B --> C[3. 默认策略<br/>为每个级别设默认决策]
    C --> D[4. 粒度设计<br/>工具/动作/资源三层]
    D --> E[5. 用户交互<br/>确认流程 UX]
    E --> F[6. 持久化<br/>三层配置]
    F --> G[7. 审计<br/>完整日志]
    G --> H[8. 迭代<br/>基于日志优化]
    H --> A

    style A fill:#fee2e2,stroke:#ef4444
    style H fill:#dcfce7,stroke:#22c55e

14.12 四个反模式

反模式一:一刀切的权限

现象:要么全开(所有操作自动执行),要么全关(每个操作都要确认)。

问题:全开不安全,全关用户疯。

对策:分级权限,按风险决定默认行为。

反模式二:Deny-list 覆盖所有场景

现象:试图用黑名单列出所有危险操作。

问题:永远有漏掉的。攻击方式层出不穷。

对策:混合策略 + 默认询问作为兜底。

反模式三:权限配置无保护

现象:Agent 自己可以修改自己的权限配置。

问题:权限系统被绕过。

对策:修改权限配置是最高级别受保护操作。

反模式四:无审计

现象:权限决策不留痕迹。

问题:出事无法追溯;无法优化规则。

对策:结构化审计日志 + 定期分析。

14.13 本章小结:权限模型的核心信念

权限模型不是一次设计完就不动的。它应该随着用户的使用模式、模型能力的进化、攻击手段的演变而持续迭代

核心信念

1. Agent 的能力 ≠ Agent 被允许使用的能力
2. 权限是用户与 Agent 之间的契约
3. 安全与可用性必须平衡——过严不安全,过松不可用
4. 最小权限是方向,不是起点
5. 用户永远有最终决定权

核心口号

Trust, but verify. For agents, verify first, then trust gradually.

相信,但要验证。对 Agent,先验证,再逐步建立信任。

一个好的权限系统,今天看起来略显繁琐,明天可能救你一命。

下一章,我们将讨论 Agent 的沙箱隔离——这是权限模型的物理执行层。


延伸阅读:最小权限原则的 50 年史

最小权限原则”(Principle of Least Privilege、POLP)不是新概念——它在 1975 年就被 Jerry Saltzer 和 Michael Schroeder 在经典论文 The Protection of Information in Computer Systems 里明确提出核心思想朴素——“任何程序、任何用户、任何时候、只应该拥有完成当前任务所需的最小权限这个原则在过去 50 年里被反复验证——遵守它的系统更安全、违反它的系统更容易被攻破

Agent 权限模型、是 POLP 在 AI 时代的具体应用和传统软件不同的是——Agent 的”当前任务”是动态变化的(用户每次请求都不一样)——“最小权限”本身也是动态的这让 Agent 权限模型比传统软件更复杂——不能一次性设定、要根据任务实时调整这也是为什么 Claude Code 的”五级权限模型”不是静态配置、而是运行时可升级可降级的动态系统《OpenClaw 源码》第 13 章、《LangGraph 源码》第 9 章都讨论过类似的动态权限——这是 Agent 时代安全工程的共同挑战

延伸阅读:权限粒度的设计挑战

权限系统设计最难的部分——“粒度太粗——用户要么全部允许、要么全部拒绝、没有灵活性太细——用户面对几十个开关、无从下手、最终要么全开要么全关好的权限系统要找到”粒度的 sweet spot”——不多不少、覆盖真实需求

Claude Code 的五级权限(默认拒绝、每次确认、会话允许、持久允许、完全自动)——是一个精巧的粒度设计它不对具体工具细分(不是”允许读文件”、“允许写文件”)、而是对”确认频率”分级——既覆盖了”完全不信任”到”完全信任”的光谱、又不让用户为每个工具配置一遍这种”按信任度而非按工具”的设计、是一种对用户认知负担友好的创新iOS 的 App 权限也是类似思路——不是让用户配置”每个 API 是否可用”、而是让用户配置”位置、相机、通讯录”等高层类别

延伸阅读:动态权限升级的工程艺术

Agent 权限模型的一个关键能力——“动态升级一开始给 Agent 最小权限、运行过程中如果 Agent 需要更高权限、询问用户、用户同意后升级这种机制让”一次授权、永久使用”和”完全不信任、每次都问”之间有了平滑过渡实现这个机制的关键——“在正确的时机询问用户

询问时机的艺术——“太早问”(Agent 一启动就要求大量权限)会让用户不耐烦;“太晚问”(Agent 已经做了一半、突然说”我还需要这个权限”)会打断流程好的设计是”在明确需要时问、问清楚 why、给用户足够信息做决策Claude Code 的权限确认提示会告诉用户”我想调用 X 工具、参数是 Y、原因是 Z”——这三个要素缺一不可这种”透明的权限请求”、让用户能做出知情决策、而不是盲目点”允许

延伸阅读:审计日志的战略价值

权限系统的最后一道防线——“审计日志即使权限控制完美、也可能有人(或 Agent)绕过或滥用——这时需要回溯”到底发生了什么完整的审计日志能让事后分析成为可能——谁、什么时候、用什么权限、做了什么、成功与否没有审计日志、出问题后只能瞎猜

好的审计日志设计有几个原则——“不可篡改”(日志一旦写入、不能被后续操作改写、最好是 append-only)、“全覆盖”(所有权限相关操作都要记、不能遗漏)、“可查询”(按时间、按用户、按操作类型都能查)、“可脱敏”(敏感数据不直接存、而是哈希或脱敏后存)Claude Code 的审计日志、大部分遵循这些原则这些原则在金融、医疗、政府等强合规领域是法律要求——AI 时代、随着 Agent 承担越来越重要的任务、这些要求也会扩展到 Agent 系统《OpenClaw 源码》第 13 章讨论的”Exec 审批流 + 审计日志”——和 Claude Code 的设计思路高度一致、都是把”合规要求”内置到架构里

延伸阅读:权限系统与用户体验的平衡

权限系统的终极挑战——“既要安全、又要体验过度安全(每个操作都要确认)会让 Agent 失去价值(用户宁愿自己手动做);过度宽松(什么都允许)会让 Agent 变成风险源Claude Code 的五级权限、让用户自己在这两个极端之间选择——新手可以用严格模式(每次都确认)、老手可以用宽松模式(会话级或持久级允许)

这种”把权衡交给用户”的设计、比”框架强制选一个平衡点”更友好——不同用户对风险的容忍度不同、让他们自己选但也有代价——新手可能不知道怎么选、容易犯错(选太宽松被坑、或选太严格用不起来)这时需要好的”默认值 + 引导”——默认选择中等严格度(适合大多数场景)、在关键决策点给提示(“你选择了完全自动、这可能有风险、建议先试几个任务再决定”)这种”尊重用户选择 + 适当引导”的设计、是成熟权限系统的标志——iOS 的权限机制、Android 的运行时权限、浏览器的 site permissions——都在走同样的演化路径

延伸阅读:Agent 权限与传统 RBAC 的差异

传统软件权限用 RBAC(Role-Based Access Control)——用户被分配角色、角色有固定权限集这种模式在静态场景下很好用——员工的职位决定能访问什么文档、什么系统但 Agent 场景下、传统 RBAC 不够用——Agent 不是”员工”、它的”角色”是根据任务动态变化的

这种差异让”Agent 权限模型”成为一个新的研究领域有几种新思路在探索——“Capability-based Access Control”(能力对象、调用时必须持有能力 token)、“Attribute-based Access Control”(根据请求属性动态判断权限)、“Intent-based Access Control”(根据用户意图授权、而不是根据角色)这些思路哪个最适合 Agent、现在还没有定论——这是 2026 年前沿的研究方向之一对有志于深入安全领域的读者——这是一个充满机会的新赛道

延伸阅读:权限撤销的困境

权限系统设计常被忽略的一环——“撤销授权容易、撤销难——一个 Agent 获得了”读所有文件”的权限、它可能已经读了很多文件、把内容存在某个地方;你撤销权限后、它之前获取的信息还在这个”一次授权、信息永久存在”的问题、在 AI 时代特别棘手——因为 LLM 的 context 里可能已经有了敏感信息、无法从模型里”擦除

缓解这个问题的几种思路——“短期权限”(权限默认很短 TTL、到期自动失效)、“范围限制”(即使授权”读文件”、也限制只能读哪些文件)、“会话隔离”(每个会话独立 context、切换会话就清空信息)Claude Code 的会话级权限就体现了第三种思路——会话结束、权限也随之失效这种”权限与会话绑定”的设计、是为了应对”撤销困境”做的务实选择、虽然不完美、但在当前技术条件下是可行的折中

延伸阅读:权限系统与 AI 合规的未来

2026 年开始、全球多个地区陆续出台 AI 合规法规——欧盟 AI Act(2024 通过、2026 全面生效)、美国各州 AI 相关法律、中国《生成式 AI 服务管理办法》——都对”AI 系统的权限控制”提出要求这意味着 Agent 权限模型不再只是”好习惯”、而是”合规必须

合规要求通常涉及几个方面——“明确的权限边界”(文档化 Agent 能做什么、不能做什么)、“用户知情同意”(用户必须明确知道 Agent 将采取什么行动)、“可审计”(所有权限相关操作留痕)、“可干预”(用户能随时暂停、撤销、修改)Claude Code 的权限模型基本满足这些要求——这是它能被企业客户采用的重要原因对做企业级 Agent 产品的团队——合规不是事后补、而是设计之初就要考虑《MCP 协议源码》第 12 章、《OpenClaw 源码》第 13 章都深入讨论了合规设计——读完能对这个话题有立体认识

延伸阅读:权限系统的教育价值

一个有意思的副作用——权限系统其实也在”教育用户用户使用 Claude Code 时、每次看到权限确认、都在被提醒”这是一个敏感操作、要谨慎这种”内置的安全教育”、比一切事后的安全培训都更有效——因为它发生在实际使用的”教学瞬间”(teachable moment)

随着使用时间增加、用户会培养出”对 Agent 行为的安全直觉”——知道什么操作该警惕、什么操作可以放心、什么授权可以给、什么不能给这种直觉、是 Agent 时代”数字公民素养”的重要组成部分好的权限系统、不只是”保护用户”、更是”训练用户”——这种双重价值、让它成为 Agent 产品最值得精心设计的部分之一——投入精力做好权限体验、回报会在长期显现

延伸阅读:权限系统的对抗性思维

做权限系统的工程师、需要有”对抗性思维”——假设”用户里有恶意者”、假设”Agent 可能被操纵”、假设”权限系统会被尝试绕过”——基于这些假设设计防御这种思维、和传统”用户都是善意”的产品思维完全不同

对抗性思维不是悲观、而是务实大多数用户确实是善意的——但”大多数”不是”所有”——只要 0.1% 是恶意的、在百万用户里就是上千个攻击者权限系统必须为这 0.1% 设计、同时不打扰 99.9%这个”既要对少数恶意防御、又不打扰大多数善意”的双重目标、是权限系统的核心挑战Claude Code 的五级权限、就是在这个双重目标之间的精心平衡——给信任的老用户宽松选项、给不确定的场景严格选项

延伸阅读:权限请求的 UX 设计

权限请求的 UX(用户体验)——是容易被忽略但极其重要的细节点击’允许“看起来简单、实际上涉及大量设计考虑——对话框的措辞(用专业术语还是通俗语言)、按钮顺序(允许在左还是在右)、默认选项(Enter 键默认触发哪个)、视觉层次(允许按钮突出还是拒绝按钮突出)——每个细节都影响用户的决策

好的权限 UX 应该遵循”让用户做出理性决策”的原则——不用暗黑模式(dark patterns)诱导用户同意、不把”允许”做得特别显眼让用户忽略风险、给用户足够信息判断。**有些恶劣的产品用”**XX 已为你配置好、点击’允许’继续”这种诱导话术——这是反面教材、Claude Code 避免了这些陷阱这种”尊重用户的知情权”的 UX 设计、是一种伦理选择——技术上可以诱导用户、但选择不诱导——这是成熟产品团队的自律