Harness Engineering
第14章 Agent 权限模型设计
第14章 Agent 权限模型设计
“The model doesn’t know that
rmandlsare 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 工具的开发者,都经历过”它差点干了一件蠢事”的时刻。
模型的盲区
模型的推理能力在统计意义上很强,但它不具备”这件事做错了后果不可逆”的风险意识。它对 rm 和 ls 的调用,在它看来没有本质区别——都只是完成任务的一个步骤。
这就是权限系统存在的根本理由:
在模型的能力和它被允许使用的能力之间,建立一道由工程系统维护的屏障。
与传统 RBAC 的四个差异
和传统 RBAC(基于角色的访问控制)不同,Agent 权限系统面临几个独特挑战:
| 维度 | 传统 RBAC | Agent 权限 |
|---|---|---|
| 行为可预测性 | 高(代码写死) | 低(模型推理) |
| 主体意图 | 明确(程序执行) | 需要推断(模型意图) |
| 粒度需求 | 角色级 | 极细(单次操作) |
| 决策时机 | 部署时 | 运行时 |
| 用户参与 | 管理员配置 | 每次操作都可能需要 |
- 行为不可完全预测:你不知道模型下一步要调用什么工具、传什么参数
- 意图需要推断:模型说”我要删除这个文件是为了重建它”,你如何判断这是合理操作还是幻觉?
- 粒度极细:不是”能不能访问文件系统”的问题,而是”能不能访问 /etc 目录下的文件”的问题
- 交互式决策:某些权限需要实时向用户确认,这引入了 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 -rf、DROP TABLE、curl | 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 工具却不区分 ls 和 rm -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-list | Deny-list |
|---|---|---|
| 默认安全性 | 高(未知=禁止) | 低(未知=允许) |
| 可用性 | 低(要列出所有允许) | 高(默认能用) |
| 维护成本 | 高(持续扩充) | 中(遇到坏东西才加) |
| 适用场景 | 安全关键 | 便利性优先 |
| 对新威胁 | 天然防御 | 需要更新 |
Claude Code 的混合策略
Claude Code 采用了混合策略——宏观上是 Allow-list,微观上用 Deny-list 补充。
宏观层面,每个工具默认需要用户确认才能执行,这本质上是 Allow-list:没有明确授权的操作不会自动执行。
但在自动模式下,它切换为 Deny-list 策略:默认允许执行,但维护一个明确的禁止列表。
为什么选混合策略
-
Allow-list 的问题是枚举不完
- 合法的 Bash 命令有无限种,你不可能列出所有允许的命令
- 用户总会遇到 Allow-list 没覆盖的合理场景
- 过严的 Allow-list 导致用户关掉权限或疯狂 approve
-
Deny-list 的问题是遗漏致命
- 漏掉一个危险命令,后果可能不可逆
- 攻击者会找 Deny-list 的缝隙
-
所以最佳实践是:在安全模式下用 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 的做法是:
- 解析命令——先拆分管道、识别子 shell、展开别名
- 提取原子命令——每个原子命令单独检查
- 参数分析——识别
-rf这样的危险参数组合 - 路径规范化——检查最终操作的路径
- 上下文感知——考虑 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
确认流程是权限系统的”用户界面”。设计得好,用户感觉安全又高效;设计得差,用户要么被烦死,要么盲目点”允许”——两种情况都违背了权限系统的初衷。
好的确认提示的三要素
一个好的确认提示需要回答三个问题:
- Agent 要做什么? 工具名称 + 参数的清晰展示
- 为什么要做? Agent 的推理过程(上下文)
- 风险是什么? 操作的潜在后果
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. 对危险操作使用视觉警告
当命令匹配高危模式时(如包含 rm、chmod 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 Code | Cursor | Devin |
|---|---|---|---|
| 控制粒度 | 工具/动作/资源 三层 | 功能区域级 | 环境级 |
| 用户参与 | 高(可配置为低) | 中等 | 低 |
| 安全上限 | 最高 | 中等 | 取决于沙箱质量 |
| 自主性 | 低→高可调 | 中等 | 高 |
| 配置成本 | 高 | 低 | 低(由平台承担) |
| 学习曲线 | 陡 | 平 | 平 |
| 适合场景 | 本地开发 | IDE 集成 | 云端长任务 |
没有银弹
没有”最好”的权限模型,只有最适合特定场景的模型:
- 本地开发工具 更适合 Claude Code 的用户主权型——开发者需要精细控制
- 云端自动化平台 更适合 Devin 的沙箱型——隔离环境比操作级审批更高效
- IDE 集成场景 更适合 Cursor 的场景隔离型——利用已有的安全基础设施
14.11 设计你自己的权限模型
如果你正在构建一个 Agent 系统,以下是我总结的权限模型设计清单:
8 条设计清单
- 确定安全底线:哪些操作在任何情况下都不允许?把它们放入全局 deny-list
- 按工具分类风险:读取类工具通常安全,可以默认放行;写入类需要确认;执行类需要更严格的控制
- 设计确认流程时考虑疲劳:如果用户每分钟需要确认超过 3 次,你的默认权限太严格了
- 提供”记住”机制:每次确认都应该提供持久化选项,避免重复劳动
- deny 优先于 allow:在优先级冲突时,永远选择更安全的决策
- 记录一切:审计日志是优化权限策略的唯一可靠数据来源
- 提供合理的预设:不要让用户从零开始配置权限。根据典型使用场景提供预设模板
- 权限配置本身需要保护:修改权限规则的操作,应该有独立的确认机制
设计实施路径
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 设计、是一种伦理选择——技术上可以诱导用户、但选择不诱导——这是成熟产品团队的自律。