Harness Engineering

第14章 Agent 权限模型设计

作者 杨艺韬 · 7,951 字

本章要点

  • Agent 权限保护的是一个不完全可预测的推理引擎对数字世界系统状态的影响
  • 四档权限模式:Plan / Default / Auto-Edit / Full-Auto(官方标识符 plan / default / acceptEdits / bypassPermissions
  • 一层规则配置:Custom Rules 在任意模式下细化边界
  • 三层粒度:per-tool / per-action / per-resource
  • Allow-list vs Deny-list:宏观白名单,微观黑名单——混合最优
  • 动态权限升级:单向阶梯,只能紧不能松
  • 确认疲劳是反向效应——过多确认反而降低安全性
  • 三层持久化:Session / Project / Global
  • deny 优先 + 范围小优先——安全约束只能收紧

Agent 能执行代码、修改文件、访问网络——这些能力让它强大,也让它危险。本章从最小权限原则出发,以 Claude Code 类产品为例,拆解其四档权限模式与 Custom Rules 规则配置层,探讨权限粒度、确认流程、动态升级、持久化策略和审计日志等核心设计决策。

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 权限系统面临几个独特挑战:

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

14.2 Claude Code 的四档权限模式

Claude Code 采用四档权限模式,从最严格到最宽松。除此以外,还可以通过 Custom Rules 这层规则配置对任意模式做细化约束:

graph LR
    subgraph Modes["权限模式 (从严到松)"]
        M1[🔒 Plan Mode<br/>plan · 只读分析] --> M2[🔐 Default Mode<br/>default · 逐个确认]
        M2 --> M3[🔓 Auto-Edit Mode<br/>acceptEdits · 文件自动放行]
        M3 --> M4[🟢 Full-Auto Mode<br/>bypassPermissions · 全自动执行]
    end
    R[⚙️ Custom Rules<br/>跨模式规则配置] -.->|"叠加到任意模式"| Modes
    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 R fill:#dbeafe,stroke:#3b82f6

Plan Mode(只读规划)

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

适用场景

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

一种最简实现思路:Harness 层不注册写入类工具即可。不过 Claude Code 实际的做法更完整——工具照常注册,由权限层拒绝写入类调用,并配合"退出计划模式需用户审批计划"的流程。

典型工作流

用户: "分析这个项目的架构有什么问题"
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:跨模式的规则配置层

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 --> FA[Full-Auto Mode<br/>CI/CD 等特殊场景]
    C[Custom Rules<br/>精细规则] -.->|"可在任意阶段叠加"| D
    C -.-> AE
    C -.-> FA
    style New fill:#dbeafe,stroke:#3b82f6
    style C fill:#dbeafe,stroke:#3b82f6

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

在任意阶段,用户都可以使用 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 工具上做了精细的动作级控制——源码中有专门的命令安全分析与危险模式识别模块(如 bashClassifierdangerousPatterns)。下面用一个四档分类体系(示意,非其真实源码)来说明这类动作级控制的思路:

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-list Deny-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 等环境
// 伪代码:示意命令语义分析流程,parseBashCommand / CommandAnalysis / AtomicCommand /
// Risk / checkDangerousPatterns 等需用真实解析器与类型定义替换
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/**)"
    ]
  }
}

这里的规则 DSL 按工具分两类:Bash 规则用 Bash(命令前缀:*)——冒号前是命令前缀、冒号后的 * 匹配任意后续参数,例如 Bash(npm test:*) 放行所有以 npm test 开头的命令;文件类工具(Edit / Read / Write)用 gitignore 风格的路径 glob,例如 Edit(src/**) 放行对 src/ 目录下文件的编辑、Read(~/.ssh/**) 匹配主目录下的 .ssh——文件规则没有冒号语法。更完整的工程实现与 defaultMode 字段说明见第17章《Human-in-the-Loop》。

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/settings.json 约定入库、团队共享,.claude/settings.local.json 存放个人覆盖、由工具自动加入 .gitignore。在此之上还有一层企业托管策略(managed settings)——由管理员统一下发、优先级最高、个人无法覆盖。

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 Code Cursor Devin
控制粒度 工具/动作/资源 三层 功能区域级 环境级
用户参与 高(可配置为低) 中等
安全上限 最高 中等 取决于沙箱质量
自主性 低→高可调 中等
配置成本 低(由平台承担)
学习曲线
适合场景 本地开发 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 的沙箱隔离——这是权限模型的物理执行层。