职责概述

解决的问题:AI 可以读写文件、执行命令、访问网络,但用户不可能每次都手动审批。需要一个权限系统自动判断"这个操作安不安全",安全的放行,可疑的询问,危险的拒绝。

应用场景:① 用户首次允许读取 src/ 目录后,后续读取自动放行 ② 模型尝试执行 rm -rf 时弹窗确认 ③ 项目级 .claude/settings.json 预设允许规则,免交互 ④ Hook 脚本在工具执行前后拦截和修改行为。

一句话理解:就像手机的权限管理——App 请求摄像头,第一次弹窗问你,你同意后以后就不再问了。但如果你设置里关了,它就永远用不了。

架构设计

权限模式层(PermissionMode)
default — 每次工具调用都需要确认
acceptEdits — 自动接受文件编辑
auto — AI 分类器自动决策
plan — 计划模式(受限执行)
bypassPermissions — 跳过所有权限
dontAsk — 拒绝所有需要确认的操作
规则引擎层(permissions.ts)
hasPermissionsToUseTool() — 13 步决策链
checkRuleBasedPermissions() — 规则子集检查
ToolPermissionContext — allow/deny/ask 规则集
permissionSetup.ts — 模式初始化与转换
Hook 拦截层(PermissionContext.ts)
createPermissionContext() — 交互式权限上下文
runHooks() — PreToolUse Hook 执行
tryClassifier() — AI 分类器异步检查
interactiveHandler / coordinatorHandler / swarmWorkerHandler
沙箱隔离层(sandbox-adapter.ts)
SandboxManager — 文件系统 + 网络隔离
convertToSandboxRuntimeConfig() — 配置转换
wrapWithSandbox() — 命令沙箱包装
autoAllowBashIfSandboxed — 沙箱内自动放行

核心数据流

权限决策流程(13 步链)

Step 1a: 全局 deny 规则
getDenyRuleForTool() 检查工具是否被 blanket deny。匹配到则直接返回 deny。来源:permissions.ts:1079
Step 1b: 全局 ask 规则
getAskRuleForTool() 检查工具是否配置了 "每次询问"。沙箱启用且命令可沙箱化时跳过。来源:permissions.ts:1092
Step 1c: 工具级权限检查
tool.checkPermissions(parsedInput, context) 调用工具自身的权限逻辑(如 BashTool 的子命令规则匹配)。来源:permissions.ts:1114-1126
Step 1d: 工具实现拒绝
如果 tool.checkPermissions() 返回 deny,直接返回拒绝。
Step 1e-1g: 特殊检查
1e: requiresUserInteraction 强制交互 | 1f: 内容级 ask 规则(如 Bash(npm publish:*))| 1g: 安全路径检查(.git/, .claude/ 等)——bypass 免疫。
Step 2a: 模式级放行
检查 bypassPermissions 模式或 plan+bypass 组合。直接返回 allow。来源:permissions.ts:1268
Step 2b: 全局 allow 规则
toolAlwaysAllowedRule() 检查工具是否有 blanket allow。匹配到则返回 allow。
Step 3: passthrough → ask 转换
如果前面步骤返回 passthrough(无明确决策),转换为 ask——需要用户交互确认。
Step 4: 模式后处理
dontAsk 模式将 ask 转为 deny。auto 模式运行 AI 分类器(acceptEdits 快路径 → classifier API 调用)。连续拒绝追踪防止循环。来源:permissions.ts:505-622

安全层协作图

LLM 返回 tool_use
模型请求调用工具
hasPermissionsToUseTool()
permissions.ts:473 — 13 步规则引擎
createPermissionContext()
PermissionContext.ts:96 — Hook + 交互
SandboxManager
sandbox-adapter.ts — 进程隔离
allow
规则放行 / bypass / allow 规则
deny
deny 规则 / 安全检查 / dontAsk
ask
交互式确认 / Hook 审批 / 分类器

关键类型与接口

PermissionMode 类型(types/permissions.ts:29)

// 外部可见的模式
type ExternalPermissionMode = 'default' | 'plan' | 'acceptEdits' | 'bypassPermissions' | 'dontAsk'
// 内部扩展模式(ant-only)
type InternalPermissionMode = ExternalPermissionMode | 'auto' | 'bubble'
type PermissionMode = InternalPermissionMode

ToolPermissionContext 类型(Tool.ts:123-138)

export type ToolPermissionContext = DeepImmutable<{
  mode: PermissionMode
  additionalWorkingDirectories: Map<string, AdditionalWorkingDirectory>
  alwaysAllowRules: ToolPermissionRulesBySource    // 按来源分组的 allow 规则
  alwaysDenyRules: ToolPermissionRulesBySource     // 按来源分组的 deny 规则
  alwaysAskRules: ToolPermissionRulesBySource      // 按来源分组的 ask 规则
  isBypassPermissionsModeAvailable: boolean
  isAutoModeAvailable?: boolean
  strippedDangerousRules?: ToolPermissionRulesBySource  // auto 模式剥离的危险规则
  shouldAvoidPermissionPrompts?: boolean           // 后台 agent 无 UI 时自动拒绝
  awaitAutomatedChecksBeforeDialog?: boolean       // coordinator worker 先等自动化检查
  prePlanMode?: PermissionMode                     // plan 模式前的原始模式
}>

PermissionDecision 类型(types/permissions.ts:241-246)

export type PermissionDecision<
  Input extends Record<string, unknown> = Record<string, unknown>,
> = PermissionAllowDecision<Input>
  | PermissionAskDecision<Input>
  | PermissionDenyDecision

PermissionDecisionReason 联合类型(types/permissions.ts:271-324)

export type PermissionDecisionReason =
  | { type: 'rule'; rule: PermissionRule }            // 规则匹配
  | { type: 'mode'; mode: PermissionMode }            // 模式级决策
  | { type: 'safetyCheck'; classifierApprovable?: boolean }  // 安全路径检查
  | { type: 'user' }                                   // 用户手动决策
  | { type: 'hook'; hookOutput: HookOutput }           // Hook 拦截
  | { type: 'classifier'; ... }                        // AI 分类器
  | { type: 'autoAccept' }                             // acceptEdits 快路径
  | { type: 'asyncAgent'; reason: string }             // 后台 agent 无 UI
  | { type: 'denialLimit'; ... }                       // 连续拒绝上限

核心权限检查函数(permissions.ts:473)

export const hasPermissionsToUseTool: CanUseToolFn = async (
  tool, input, context, assistantMessage, toolUseID,
): Promise<PermissionDecision> => {
  const result = await hasPermissionsToUseToolInner(tool, input, context)

  // 重置连续拒绝计数
  if (result.behavior === 'allow') { /* ... */ return result }

  // dontAsk 模式: ask → deny
  if (result.behavior === 'ask') {
    if (appState.toolPermissionContext.mode === 'dontAsk') {
      return { behavior: 'deny', decisionReason: { type: 'mode', mode: 'dontAsk' } }
    }
    // auto 模式: 运行 AI 分类器
    if (mode === 'auto' || (mode === 'plan' && isAutoModeActive())) {
      // 安全检查 → acceptEdits 快路径 → classifier API 调用
      // ...
    }
  }
  return result
}

规则子集检查(permissions.ts:1071-1156)

export async function checkRuleBasedPermissions(
  tool: Tool, input, context,
): Promise<PermissionAskDecision | PermissionDenyDecision | null> {
  // 1a. getDenyRuleForTool → blanket deny
  // 1b. getAskRuleForTool → blanket ask (沙箱自动放行例外)
  // 1c. tool.checkPermissions(parsedInput, context) → 工具级检查
  // 1d. deny → 返回拒绝
  // 1f. 内容级 ask 规则
  // 1g. safetyCheck(bypass 免疫)
  return null  // 无规则拒绝
}

沙箱配置转换(sandbox-adapter.ts:172)

export function convertToSandboxRuntimeConfig(
  settings: SettingsJson,
): SandboxRuntimeConfig {
  // 从 settings.permissions 提取网络域名规则
  // 从 settings.sandbox 提取文件系统限制
  // 组装为 SandboxRuntimeConfig {
  //   filesystem: { allowRead, denyRead, allowWrite, denyWrite, ... },
  //   network: { allowDomains, denyDomains, ... },
  //   ...
  // }
}

设计模式与亮点

纵深防御(Defense in Depth)
三层安全机制协同:规则引擎(快速过滤已知模式)→ Hook 拦截(可编程扩展)→ 沙箱(进程级兜底)。每层独立运作,任何单层失效不影响其他层。
安全
13 步决策链
hasPermissionsToUseToolInner() 实现 13 步顺序决策,从 deny 规则(最严格)到 allow 规则(最宽松),中间包含工具级检查、安全路径检查、模式级放行。每步可短路返回。
核心
Bypass 免疫机制
安全路径检查(.git/, .claude/ 等)和内容级 ask 规则即使在 bypassPermissions 模式下也必须提示用户。Step 1f/1g 在 Step 2a(bypass 放行)之前执行,确保不可绕过。
安全

更多亮点

开发者实践指南

为新工具添加权限控制

最简方式:无需任何操作。buildTool() 默认提供 checkPermissions: () => allow,将权限决策委托给通用系统。
如果工具执行有风险的操作(如网络请求、文件写入),必须覆盖 checkPermissions() 提供工具级权限逻辑。参考 BashTool 的子命令匹配模式。
// 工具级权限检查示例
async checkPermissions(input, context): Promise<PermissionResult> {
  const appState = context.getAppState()
  const permissionCtx = appState.toolPermissionContext

  // 检查是否有内容级 allow 规则
  const rules = getRuleByContentsForTool(permissionCtx, this, 'allow')
  for (const [pattern, rule] of rules) {
    if (matchesPattern(input.command, pattern)) {
      return { behavior: 'allow', updatedInput: input }
    }
  }

  return { behavior: 'ask', message: `Allow ${this.name}?` }
}

权限模式切换

权限规则格式

// 工具级规则
"Bash"                          // 匹配整个工具
"Edit"                          // 匹配 FileEditTool

// 内容级规则(工具特定)
"Bash(git *)"                   // Bash: git 前缀命令
"Bash(npm run *)"               // Bash: npm run 前缀
"Edit(/path/to/dir/**)"         // Edit: 路径 glob 匹配
"WebFetch(domain.com)"          // WebFetch: 域名匹配
"mcp__server__tool"             // MCP 工具名匹配
"Agent(my-agent-type)"          // Agent 类型匹配

架构师决策指南

设计权衡

13 步顺序链 vs 责任链模式
采用固定的 13 步顺序检查而非可组合的责任链。优势:行为可预测,调试简单(每步有明确注释),性能好(无递归)。代价:新步骤需要修改核心函数,不够灵活。这是一个合理的权衡——权限逻辑需要确定性而非灵活性。
DeepImmutable 权限上下文
ToolPermissionContext 使用 DeepImmutable 包装,确保工具实现无法意外修改权限上下文。修改必须通过 applyPermissionUpdates()setToolPermissionContext。这防止了权限状态的不一致性。

扩展性考量

性能考量

安全考量

Fail-closed 原则:所有默认值采用"最严格"原则。buildTool() 默认 isReadOnly: falseisConcurrencySafe: false;权限检查默认返回 ask(需用户确认)。只有在明确规则或模式下才允许自动放行。Bypass 免疫机制确保安全路径检查不可绕过。

可视化处理拓扑图

每次工具调用首先进入 7 步规则漏斗(Phase 1),前 4 步快速查找 O(1),后 3 步是安全免疫锚点。通过规则层后进入模式路由(Phase 2),根据 bypass/auto/default 等模式分派到不同决策路径。Auto 模式执行三层渐进决策(Phase 3),90%+ 的调用在前两层完成。

工具调用请求tool + input + context
1a. 全局 Deny 规则查找getDenyRuleForTool() — permissions.ts:1079
命中
DENY最高优先级 · 立即返回
无 deny 规则 ↓
1b. 全局 Ask 规则查找getAskRuleForTool() — permissions.ts:1092
命中
ASK沙箱自动放行例外
无 ask 规则 ↓
1c. 工具自身权限检查tool.checkPermissions(parsedInput, context) — permissions.ts:1114BashTool: 解析命令→匹配子命令规则→检查沙箱
ALLOW
DENY
ASK
PASS
allow/deny/ask 直接返回,passthrough 继续检查 ↓
1d. 工具 Deny工具定义的拒绝条件
1e. 交互型工具需要用户交互
1f. 内容级 Ask基于参数的规则
1g. 安全路径检查 — BYPASS-IMMUNE.git/ · .claude/ · .vscode/ · shell 配置即使 --dangerously-skip-permissions 也必须提示用户
匹配安全路径
强制 ASK人类必须确认
通过 ↓
↓ Phase 1 全部通过,进入 Phase 2 ↓
Phase 2 — 模式路由
2a. 当前是否 Bypass 模式?--dangerously-skip-permissions / plan+bypass — permissions.ts:1268
ALLOW1g 已过滤危险路径
否 ↓
2b. 工具在 Allow 白名单中?toolAlwaysAllowedRule() — permissions.ts:1284
ALLOW
否 ↓
权限模式路由toolPermissionContext.mode
passthrough → ASKdefault / acceptEdits / planpermissions.ts:1299-1318
↓ Phase 3 ↓Auto 模式分层决策
Phase 3 — Auto 模式三层渐进决策
Auto 模式激活permissions.ts:520-525
1
acceptEdits 模拟 — 本地计算 · 0ms
创建临时 acceptEdits 上下文,假设工具在该模式下是否放行。排除 Agent/REPL(会无条件放行)。permissions.ts:600-656
→ ALLOW
模拟结果为 allow → 直接放行,否则继续 ↓
2
安全工具白名单 — 预定义列表 · 0ms
isAutoModeAllowlistedTool() 检查只读工具(Read、Glob 等)。permissions.ts:660
→ ALLOW
不在白名单中 → 继续 ↓
3
AI 分类器 — Haiku 调用 · ~1000ms
classifyYoloAction(messages, action, ...) 独立 Haiku 模型分析工具名+参数安全性。permissions.ts:688-702
安全 → ALLOW
危险 → DENY
不确定 → ASK
连续拒绝追踪 — 自适应安全升级
permissions.ts:483-501 + denialTracking.ts
① 分类器 DENY → consecutiveDenials++
② 超过阈值 → 升级到用户确认(不再自动)
③ 一次成功执行 → recordSuccess() 重置计数器
④ 恢复正常 auto 模式
防止提示注入攻击下的危险操作链
旁路 — Hook 专用快速路径
完整路径
hasPermissionsToUseTool()
Phase 1 · 1a-1g
Phase 2 · 模式判断
Phase 3 · Auto 决策
最终决策
Hook 快速路径
checkRuleBasedPermissions()
Phase 1 · 仅 1a-1g
直接返回
跳过 Phase 2+3
零 AI 调用
漏斗设计:从最廉价的全局 Map 查找(0ms)到较重的工具自检,最后到安全免疫锚点。1g 的 bypass-immune 是整个系统最关键的安全保证——即使 --dangerously-skip-permissions 也无法绕过。bypass 检查(2a)在安全路径检查(1g)之后执行,如果反序则攻击者可通过 bypass 跳过所有安全检查。

核心处理流程详解

权限系统是 Claude Code 安全模型的核心。每次工具调用都经过一条 13 步的权限检查管线,从规则匹配到模式判断再到 AI 分类器,层层过滤。hasPermissionsToUseTool 是外部入口,hasPermissionsToUseToolInner 执行核心逻辑,checkRuleBasedPermissions 提供规则子集的快速路径供 hooks 使用。

1. hasPermissionsToUseTool — 外部入口
permissions.ts:473-479 接收工具、输入和上下文。L480 调用 hasPermissionsToUseToolInner() 获取核心决策。如果结果是 allow(L486-501),检查并重置连续拒绝计数器(auto 模式下,一次成功执行就打断拒绝连击)。如果结果是 ask,进入模式转换逻辑。
2. hasPermissionsToUseToolInner — 步骤 1a: 全局拒绝规则
permissions.ts:1171-1181 首先检查工具是否被全局拒绝规则(deny rule)匹配。getDenyRuleForTool()(L287-292)在 toolPermissionContext 的拒绝规则列表中查找匹配项。如果命中,立即返回 deny——这是最高优先级的判断,不可被任何后续步骤覆盖。
3. 步骤 1b: 全局询问规则
permissions.ts:1184-1206 检查工具是否有全局 ask rule。getAskRuleForTool()(L297-302)查找匹配的"总是询问"规则。例外:当 Bash 工具启用了沙箱且沙箱自动放行开启时(L1189-1193),跳过 ask 规则,让后续的 Bash checkPermissions 按命令粒度处理。
4. 步骤 1c: 工具自身权限检查
permissions.ts:1210-1223 调用 tool.checkPermissions(parsedInput, context)。先用 Zod schema 解析输入(L1215),然后委托给工具实现的具体检查逻辑。BashTool 会解析命令、匹配子命令规则、检查沙箱;EditTool 会检查文件路径安全性。返回的 toolPermissionResult 可能是 allow/deny/ask/passthrough。
5. 步骤 1d-1g: 安全免疫检查
permissions.ts:1226-1260 执行一系列"免疫检查":工具实现的 deny(1d)、需要用户交互的工具(1e)、内容级 ask 规则(1f)和安全路径检查(1g)。安全路径检查(.git/、.claude/、.vscode/、shell 配置)是 bypass-immune 的——即使在 --dangerously-skip-permissions 模式下也必须提示用户。
6. 步骤 2a: 模式判断 — bypass 检查
permissions.ts:1268-1281 检查当前权限模式:bypassPermissions 模式直接放行(但安全检查已在步骤 1g 过滤了危险路径)。plan 模式如果初始启动时带有 bypass,也放行。步骤 2b(L1284-1297)检查工具级 allow 规则——如果工具在白名单中,直接放行。
7. Auto 模式处理 — AI 分类器决策
permissions.ts:520-686 在 auto 模式下(L520-525),执行多层快速路径:先尝试 acceptEdits 模拟(L600-656,检查工具在 acceptEdits 模式下是否会被放行),再查安全工具白名单(L660),最后运行 AI 分类器(L688-702)。分类器使用独立的 Haiku 模型调用,分析工具名和参数判断安全性。分类器结果受连续拒绝计数器影响——拒绝过多会升级到用户确认。
8. 步骤 3: 最终决策 — passthrough 转 ask
permissions.ts:1299-1318 将工具返回的 passthrough 行为转换为 ask(L1301-1309),表示"没有规则匹配,需要用户确认"。最终返回 PermissionDecision,包含 behavior(allow/deny/ask)、可选的 updatedInput(权限系统可能修改输入)和 decisionReason(决策原因用于审计和日志)。
权限管线的核心原则是"fail-closed":默认采用最严格策略。TOOL_DEFAULTScheckPermissions 返回 allow(因为未实现自定义检查的工具按默认放行),但 hasPermissionsToUseToolInner 在无规则匹配时将 passthrough 转为 ask。安全检查(1g)是 bypass-immune 的——即使在 --dangerously-skip-permissions 模式下,修改 .git/ 或 shell 配置文件仍然会触发用户确认。

设计精华

1. 纵深防御 — 13 步管线中的不可绕过层

权限检查不是单一的"yes/no"判断,而是一条 13 步管线,其中某些步骤是不可绕过的"锚点"。步骤 1a(全局 deny)在任何模式之前执行,步骤 1g(安全路径检查)在 bypass 模式之后仍然生效。这种"洋葱模型"确保即使外层被绕过,内层仍然保护关键资源。

Defense-in-Depth Permission Pipeline
13 步检查中的关键锚点
// permissions.ts:1158-1319 — 管线关键锚点
async function hasPermissionsToUseToolInner(tool, input, context) {
  // 1a. 全局 deny — 最高优先级,不可覆盖
  const denyRule = getDenyRuleForTool(context, tool) // L1171
  if (denyRule) return { behavior: 'deny' }

  // 1g. 安全路径 — bypass-immune
  if (toolPermissionResult?.behavior === 'ask'
      && toolPermissionResult.decisionReason?.type === 'safetyCheck') {
    return toolPermissionResult // 即使 bypass 模式也必须提示
  }

  // 2a. bypass 模式 — 在安全检查之后
  if (shouldBypassPermissions) return { behavior: 'allow' }

  // 3. 无匹配规则 → ask(fail-closed)
  return toolPermissionResult.behavior === 'passthrough'
    ? { ...toolPermissionResult, behavior: 'ask' }
    : toolPermissionResult
}
洋葱模型的关键约束是顺序:如果 bypass 检查(2a)在安全路径检查(1g)之前执行,攻击者就可以通过 --dangerously-skip-permissions 修改 .git/config 执行恶意代码。当前顺序保证了安全检查的不可绕过性——这是代码注释中反复强调"bypass-immune"的原因。

2. Auto 模式分类器 — 渐进式自动化

Auto 模式不是简单的"自动放行",而是一个多层次的决策系统。在调用昂贵的 AI 分类器之前,先检查三个快速路径:acceptEdits 模拟(permissions.ts:600-656,本地检查零延迟)、安全工具白名单(L660,预定义列表)和连续拒绝计数器(防止分类器被反复滥用)。只有快速路径都未命中时,才调用分类器。

Tiered Auto-Mode Decision
从廉价到昂贵的渐进式决策
// permissions.ts:600-686 — Auto 模式决策层级
// Tier 1: acceptEdits 模拟(本地计算,零延迟)
if (tool.name !== AGENT_TOOL_NAME && tool.name !== REPL_TOOL_NAME) {
  const acceptEditsResult = await tool.checkPermissions(parsedInput, {
    ...context,
    getAppState: () => ({
      ...state,
      toolPermissionContext: { ...state.toolPermissionContext, mode: 'acceptEdits' }
    }),
  })
  if (acceptEditsResult.behavior === 'allow') return allow // 快速放行
}

// Tier 2: 安全工具白名单(预定义列表,零 API 调用)
if (isAutoModeAllowlistedTool(tool.name)) return allow

// Tier 3: AI 分类器(Haiku 调用,~1s 延迟)
const classifierResult = await classifyYoloAction(messages, action, ...)
acceptEdits 模拟的精妙之处在于"假设检验":创建一个临时的 acceptEdits 模式上下文来检查工具权限,但不修改真实的 AppState。如果工具在 acceptEdits 模式下被放行(意味着是安全的文件编辑操作),就不需要调用分类器。Agent 和 REPL 工具被排除,因为它们的 checkPermissions 在 acceptEdits 模式下会无条件放行——这会绕过分类器对代码执行的审查。

3. 连续拒绝追踪 — 自适应安全升级

permissions.ts:483-501denialTracking.ts 实现了连续拒绝计数器。在 auto 模式下,如果分类器连续拒绝多个工具调用(表示模型可能在尝试危险操作链),系统会升级到用户确认模式。一次成功的工具执行(L486-501)就重置计数器,恢复正常自动模式。

Consecutive Denial Tracking
自适应安全升级,防止自动化滥用
// permissions.ts:483-501 — 成功执行重置计数器
if (result.behavior === 'allow') {
  const currentDenialState =
    context.localDenialTracking ?? appState.denialTracking
  if (appState.toolPermissionContext.mode === 'auto'
      && currentDenialState
      && currentDenialState.consecutiveDenials > 0) {
    const newDenialState = recordSuccess(currentDenialState)
    persistDenialState(context, newDenialState)
  }
  return result
}
连续拒绝追踪解决了一个微妙的安全问题:分类器可能在大多数情况下正确放行,但在模型被提示注入攻击后开始执行危险操作链时,每个单独的调用可能看起来都"不太危险"。连续拒绝计数器捕捉这种模式——当模型连续多次被拒绝时,说明整体意图可能有风险,应该让人类介入。

4. checkRuleBasedPermissions — Hook 专用快速路径

permissions.ts:1071-1156checkRuleBasedPermissions() 是权限管线的子集,只执行步骤 1a-1g 的规则匹配部分,不涉及模式判断(bypass/auto/dontAsk)和 AI 分类器。这个快速路径供 PreToolUse hooks 使用——hooks 需要知道工具是否被规则允许或拒绝,但不应触发完整的模式逻辑和分类器调用。

Rule-Based Fast Path for Hooks
Hooks 使用规则子集,避免触发完整管线
// permissions.ts:1071-1076
export async function checkRuleBasedPermissions(
  tool: Tool, input, context
): Promise {
  // 只执行步骤 1a-1g,不涉及模式/分类器
  // 1a. 全局 deny
  // 1b. 全局 ask
  // 1c. 工具自身 checkPermissions
  // 1d-1g. 安全免疫检查
  return null // 无规则反对 → 返回 null(交给调用方决定)
}
将权限管线拆分为"规则层"和"模式层"是可扩展性的关键。新的权限模式(如 future 的"learning mode")只需在模式层添加逻辑,不影响规则层的稳定性。hooks 通过 checkRuleBasedPermissions 获得一致的规则视图,而不关心当前是什么模式在生效。

Agent 实践借鉴 — 垂直 Agent 权限设计

设计目标:当你在构建客服 agent、运维 agent、财务 agent 等垂直场景时,agent 需要替用户执行业务操作——查订单、退单、改地址、发优惠券、修改会员等级。这些操作有不同程度的风险。本节从 Claude Code 的权限系统中提取可复用的设计模式,指导你在自己的垂直 agent 中落地权限控制。

一、场景映射:客服 Agent 遇到的权限问题

你的客服 agent 有这些工具:查询订单、查询物流、修改地址、退单/退款、发放优惠券、修改会员等级、查看客户隐私信息。不同坐席有不同权限:初级客服只能查询,高级客服可以退单,主管可以改会员等级。你需要回答:

二、借鉴 CC:精简版权限管线(5 步而非 13 步)

Claude Code 用了 13 步管线,因为它面对的是通用开发环境——任意工具、任意命令、不可预测的用户意图。你的垂直 agent 场景更收敛,5 步管线足够覆盖:

客服 Agent 权限管线:deny → 角色检查 → 操作等级 → 免疫锚点 → 放行
// 借鉴 CC 的 hasPermissionsToUseToolInner,精简为 5 步
// CC 用 13 步是因为通用场景,客服场景 5 步即可

async function checkPermission(
  action: AgentAction,   // 如 { tool: 'refund', orderId: 'xxx', amount: 5000 }
  context: AgentContext,  // 包含坐席角色、当前会话、客户信息
): Promise<PermDecision> {

  // Step 1: 全局禁止规则(对应 CC Step 1a)
  // 场景:公司规定 VIP 客户不能被某个渠道的 agent 退款
  const denyRule = getDenyRule(context.tenant, action.tool);
  if (denyRule) return { behavior: 'deny', reason: 'business_rule_deny' };

  // Step 2: 角色权限检查(对应 CC Step 1c 工具自身检查)
  // 场景:初级坐席不能退单,高级坐席可以
  if (!context.role.permissions.includes(action.tool)) {
    return { behavior: 'deny', reason: 'role_insufficient' };
  }

  // Step 3: 操作风险分级(借鉴 CC auto 模式的快速路径思路)
  // 场景:退款 < 200 元自动通过,200-2000 需确认,>2000 需主管审批
  const riskLevel = evaluateRisk(action, context);
  if (riskLevel === 'auto') return { behavior: 'allow', reason: 'low_risk' };
  if (riskLevel === 'confirm') return { behavior: 'ask', reason: 'medium_risk' };
  if (riskLevel === 'escalate') {
    return { behavior: 'ask', reason: 'high_risk',
             escalateTo: 'supervisor' };  // 升级到主管
  }

  // Step 4: ★ 免疫锚点(借鉴 CC bypass-immune 思路)★
  // 场景:删除客户数据、修改系统配置——无论角色和风险级别,必须人工确认
  const IMMUNE_OPERATIONS = [
    'delete_customer',      // 删除客户数据
    'modify_vip_level',     // 修改 VIP 等级(防内部舞弊)
    'export_customer_data', // 导出客户隐私数据
    'system_config_change', // 系统配置变更
  ];
  if (IMMUNE_OPERATIONS.includes(action.tool)) {
    return { behavior: 'ask', reason: 'immune_checkpoint',
             bypassImmune: true,  // 即使开启了自动模式也必须确认
             escalateTo: 'admin' };
  }

  // Step 5: 默认放行(借鉴 CC 的 allow 白名单)
  // 查询类操作、低风险通知类操作直接通过
  return { behavior: 'allow', reason: 'default_allow' };
}
为什么是 5 步而不是 13 步:Claude Code 的 13 步是因为它面对的场景高度不确定——任意文件路径、任意 shell 命令、多种权限模式(plan/bypass/auto/dontAsk)。你的垂直 agent 操作类型是可穷举的(查询/退单/改地址/发券...),所以不需要那么多中间层。但 免疫锚点这个概念必须保留——这是 CC 权限系统最有价值的设计思想。

三、借鉴 CC:免疫锚点 — 安全的最后一道防线

CC 的 bypass-immune 设计(L1252-1260)是这个权限系统最有价值的模式。核心思想:某些操作的安全检查在"模式路由"之前执行,即使管理员开启了"自动模式"也绕不过去。

CC 的做法
安全路径(.git/、.claude/)的检查在 Step 1g 执行。bypass 模式的放行在 Step 2a。因为 1g 在 2a 之前,所以即使 --dangerously-skip-permissions 也改不了 .git/config。
客服 Agent 怎么用
定义"免疫操作表"——无论坐席角色多高、是否开启了自动模式,这些操作必须经过二次确认。比如修改 VIP 等级、导出客户数据、删除工单。免疫检查在"自动放行"判断之前。
// 免疫锚点实现 — 借鉴 CC permissions.ts:1252-1260 的思路
// 关键:immune 检查在 auto-allow 之前,顺序保证安全

function evaluateRisk(
  action: AgentAction, context: AgentContext
): 'auto' | 'confirm' | 'escalate' {

  // ★ 先做免疫检查(在自动放行之前!)
  const IMMUNE_OPERATIONS = new Set([
    'delete_customer', 'modify_vip_level',
    'export_customer_data', 'system_config_change',
  ]);

  if (IMMUNE_OPERATIONS.has(action.tool)) {
    // 免疫操作:无论什么角色/模式,都必须人工确认
    // 即使坐席说"帮我自动处理所有事情",这条也过不去
    return 'escalate';  // 强制升级到管理员确认
  }

  // 然后按业务规则分级
  if (action.tool === 'refund') {
    if (action.amount <= 200) return 'auto';        // 低风险:自动通过
    if (action.amount <= 2000) return 'confirm';    // 中风险:坐席确认
    return 'escalate';                                // 高风险:主管审批
  }

  if (action.tool === 'change_address') return 'auto';  // 低风险
  if (action.tool === 'query_order') return 'auto';     // 只读,无风险
  return 'confirm';  // 未知操作默认需确认(借鉴 CC 的 fail-closed)
}

// 反面教材:如果把免疫检查放在 auto-allow 之后
// riskLevel === 'auto' → allow → 免疫检查永远执行不到
// 坐席开了"自动模式"后,delete_customer 也会被自动执行
// 这就是 CC 把 1g 放在 2a 之前的原因

四、借鉴 CC:渐进式自动决策(三层快速路径)

CC 的 auto 模式用三层快速路径(acceptEdits 模拟 → 白名单 → AI 分类器)实现 90%+ 自动化。你的客服 agent 不需要 AI 分类器——业务规则是可穷举的,静态规则表比 AI 更可控。但 渐进式决策的分层思路 可以直接复用:

// 借鉴 CC auto 模式的三层快速路径
// CC: acceptEdits → 白名单 → AI分类器
// 客服Agent: 规则表 → 风险评分 → 人工升级(不需要AI分类器)

async function autoModeDecision(
  action: AgentAction, context: AgentContext
): Promise<PermDecision> {

  // Tier 1: 静态规则表(对应 CC 的白名单,0ms)
  // 客服场景的操作是可穷举的,静态规则比 AI 更可靠
  const STATIC_RULES: Record<string, AutoRule> = {
    'query_order':    { risk: 0,   auto: true },   // 纯查询
    'query_logistics': { risk: 0,  auto: true },   // 纯查询
    'change_address':  { risk: 10, auto: true },   // 低风险修改
    'send_coupon':     { risk: 20, auto: true, limit: 50 },  // 有额度限制
    'refund':          { risk: 50, auto: false },  // 需要评估
    'modify_vip':      { risk: 100, auto: false }, // 免疫操作
  };

  const rule = STATIC_RULES[action.tool];
  if (rule?.auto) {
    // 额外检查额度限制(如每日发券上限)
    if (rule.limit && context.dailyUsage[action.tool] >= rule.limit) {
      return { behavior: 'ask', reason: 'daily_limit_exceeded' };
    }
    return { behavior: 'allow', reason: 'static_rule_auto' };
  }

  // Tier 2: 风险评分引擎(对应 CC 的 acceptEdits 模拟)
  // 综合考虑:操作类型 × 金额 × 客户等级 × 坐席等级 × 当日累计
  const score = calculateRiskScore(action, context);
  if (score < 30) return { behavior: 'allow', reason: 'low_risk_score' };
  if (score < 70) return { behavior: 'ask', reason: 'medium_risk_score' };
  return { behavior: 'ask', reason: 'high_risk_score', escalateTo: 'supervisor' };

  // 注意:不需要 Tier 3(AI 分类器)
  // CC 需要 AI 分类器是因为它面对不可预测的操作(任意 bash 命令)
  // 客服场景的操作类型是有限的,静态规则 + 风险评分足够
}

function calculateRiskScore(action: AgentAction, ctx: AgentContext): number {
  let score = 0;

  // 因子 1:金额(退款 5000 元比退款 50 元风险高)
  score += Math.min(action.amount / 100, 50);

  // 因子 2:当日累计(已经退了 5 单了,第 6 单要提高警惕)
  const dailyTotal = ctx.dailyRefundTotal + (action.amount ?? 0);
  if (dailyTotal > 10000) score += 30;

  // 因子 3:客户等级(VIP 客户的操作需要更谨慎)
  if (ctx.customer.vipLevel >= 3) score += 15;

  // 因子 4:坐席与客户关联(异常关联可能暗示内部舞弊)
  if (ctx.hasAnomalySignal) score += 20;

  return Math.min(score, 100);
}

五、借鉴 CC:连续拒绝追踪与自动升级

CC 用连续拒绝计数器防止 auto 模式被提示注入攻击绕过(L878-901)。客服场景同样需要:当 agent 在短时间内被连续要求执行高风险操作时,自动升级到人工确认,防止社会工程攻击或 agent 被误导。

// 借鉴 CC denialTracking.ts 的连续拒绝追踪
// CC: 连续拒绝 3 次 → 升级到用户确认
// 客服Agent: 连续被要求执行高风险操作 → 自动升级到主管

interface SessionRiskTracking {
  consecutiveHighRiskRequests: number;  // 连续高风险请求数
  totalActions: number;                 // 本次会话总操作数
  riskScoreSum: number;                 // 累计风险分
  startTime: number;
}

const ESCALATION_THRESHOLD = 3;  // 连续 3 次高风险请求 → 升级

function checkAndRecord(
  action: AgentAction,
  tracking: SessionRiskTracking,
  decision: PermDecision
): PermDecision {
  if (decision.behavior === 'allow') {
    // 成功执行 → 重置连续计数(借鉴 CC recordSuccess)
    return decision;
  }

  // 被拒绝或需要确认 → 记录
  tracking.consecutiveHighRiskRequests++;

  if (tracking.consecutiveHighRiskRequests >= ESCALATION_THRESHOLD) {
    // 连续 3 次高风险 → 切换到"谨慎模式"
    // 接下来所有操作都需要坐席确认,不再自动放行
    return {
      behavior: 'ask',
      reason: 'consecutive_risk_escalation',
      message: '检测到连续高风险操作请求,已自动升级为人工确认模式',
    };
  }

  return decision;
}

// 场景:恶意用户不断要求"帮我退款""换个地址退款""再退一次"
// 每次单独看金额都不高,但连续模式暴露异常意图
// 这正是 CC denial tracking 设计要防御的模式

六、落地清单:从 CC 到你的 Agent

必须抄的(核心价值):
  1. 免疫锚点:定义 3-5 个无论什么角色/模式都必须二次确认的操作,检查放在自动放行之前。这是你权限系统安全的底线。
  2. Fail-closed 默认:未知操作默认 deny/ask,不是 allow。CC 的管线末尾是 ask,你的也应该是。
  3. 渐进式决策:从便宜到昂贵。能用静态规则就不算风险分,能算分就不叫人。但免疫操作例外。
不需要抄的(场景差异):
  1. 不需要 13 步管线:CC 面对任意命令需要多层过滤,你的操作类型是有限的,5 步足够。
  2. 不需要 AI 分类器:CC 用 Haiku 做 YOLO 判断是因为 bash 命令不可穷举。客服操作是可枚举的,静态规则表 + 风险评分更可控、更快、更便宜。
  3. 不需要 bypass 模式:CC 有 --dangerously-skip-permissions 是因为开发者有时要快速试错。客服场景不应该有"跳过所有权限"的开关。
  4. 不需要 dontAsk 模式:CC 在非交互模式下把 ask 转 deny。你的 agent 应该始终有交互通道(坐席/主管)。
常见坑:
  • 免疫检查放在自动放行之后:最常见的致命错误。如果 if (mode === 'auto') return allow 在免疫检查之前执行,那么开启自动模式后删客户数据也会被自动执行。顺序决定安全。
  • 风险评分是静态的:退款 5000 元对大客户是日常操作,对小客户是异常。风险评分必须结合上下文(客户等级、历史行为、当日累计),不能只看金额。
  • 没有连续行为检测:单次退款 200 元自动通过没问题,但连续退 20 笔 200 元就是异常。CC 的 denial tracking 就是为了捕捉这种"单次看正常、连续看异常"的模式。

代码索引

文件行数说明
types/permissions.ts~442所有权限类型定义:PermissionMode, PermissionDecision, PermissionRule, ToolPermissionContext 等
utils/permissions/permissions.ts~1487权限检查核心:hasPermissionsToUseTool (13步), checkRuleBasedPermissions, getDenyRuleForTool 等
utils/permissions/permissionSetup.ts~1533权限模式初始化、转换、危险权限剥离、auto 模式门控
utils/permissions/PermissionMode.ts~142PermissionMode 配置、标题、符号、颜色映射
utils/permissions/PermissionResult.ts~36权限结果类型和帮助函数(re-export)
utils/permissions/bashClassifier.ts~62Bash 命令分类器(外部版为空壳)
utils/permissions/denialTracking.ts~文件连续拒绝追踪状态管理
utils/permissions/yoloClassifier.ts~文件YOLO 模式分类器
utils/permissions/dangerousPatterns.ts~文件危险命令模式检测
utils/permissions/pathValidation.ts~文件路径安全验证(.git/ 等保护路径)
utils/permissions/shellRuleMatching.ts~文件Shell 命令规则匹配逻辑
utils/permissions/permissionRuleParser.ts~文件权限规则字符串解析器
utils/permissions/filesystem.ts~文件文件系统权限检查
hooks/toolPermission/PermissionContext.ts~389交互式权限上下文:Hook 运行、分类器、队列操作
hooks/toolPermission/handlers/interactiveHandler.ts~文件主 agent 交互式权限处理
hooks/toolPermission/handlers/coordinatorHandler.ts~文件协调器工作线程权限处理
hooks/toolPermission/handlers/swarmWorkerHandler.ts~文件Swarm 工作线程权限处理
utils/sandbox/sandbox-adapter.ts~986沙箱适配层:配置转换、初始化、命令包装、平台检测
utils/sandbox/sandbox-ui-utils.ts~文件沙箱相关 UI 工具函数