Layer 3: 权限系统 —— Agent 自主性与安全性的工程权衡
一个可以执行 shell 命令、编辑文件、调用外部 API 的 Agent,如何在「让用户尽量少操心」和「不做危险的事」之间找到平衡?
这是所有 Agent 框架设计中最关键的工程问题之一。Claude Code 的权限系统是目前业界最精细的 Agent 权限实现,值得深入研究。
1. 架构总览
Section titled “1. 架构总览” ┌───────────────────────────┐ │ hasPermissionsToUseTool │ ← 权限决策总入口 │ permissions.ts:473 │ └──────────┬────────────────┘ │ ┌────────────────────┼────────────────────┐ ▼ ▼ ▼ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │ 规则引擎 │ │ 模式引擎 │ │ 分类器引擎 │ │ Rule-based │ │ Mode-based │ │ Classifier │ └─────────────┘ └──────────────┘ └──────────────┘ │ │ │ ▼ ▼ ▼ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │ allow/deny/ │ │ bypass/plan/ │ │ ML 分类器 │ │ ask 规则 │ │ acceptEdits │ │ auto 模式 │ └─────────────┘ └──────────────┘ └──────────────┘| 文件 | 职责 | 行数 |
|---|---|---|
types/permissions.ts | 纯类型定义(打破循环依赖) | ~440 |
utils/permissions/permissions.ts | 权限决策主逻辑 | ~1200 |
utils/permissions/PermissionMode.ts | 模式配置与元数据 | ~140 |
utils/permissions/PermissionRule.ts | 规则类型定义(Zod schema) | ~40 |
utils/permissions/denialTracking.ts | 拒绝追踪与熔断 | ~45 |
utils/permissions/classifierDecision.ts | ML 分类器决策 | feature-gated |
utils/permissions/yoloClassifier.ts | Auto 模式分类器 | feature-gated |
2. 三大权限原语
Section titled “2. 三大权限原语”2.1 Permission Behavior(权限行为)
Section titled “2.1 Permission Behavior(权限行为)”// types/permissions.ts:44type PermissionBehavior = 'allow' | 'deny' | 'ask'所有权限决策最终归结为三种行为:
- allow:静默放行,用户无感知
- deny:静默拒绝,告诉模型「权限不足」
- ask:弹出交互式对话框,让用户决定
★ 设计洞察:注意没有
warn或log这类「软性」行为。每个决策都是硬性的、有确定结果的。这避免了安全系统中常见的「告警疲劳」问题。
2.2 Permission Mode(权限模式)
Section titled “2.2 Permission Mode(权限模式)”// types/permissions.ts:28-29type InternalPermissionMode = ExternalPermissionMode | 'auto' | 'bubble'type PermissionMode = InternalPermissionMode
// types/permissions.ts:16-22const EXTERNAL_PERMISSION_MODES = [ 'acceptEdits', // 自动批准文件编辑,其他操作询问 'bypassPermissions', // 跳过所有权限检查(除了安全检查) 'default', // 标准模式,遇到需要权限的操作就询问 'dontAsk', // 不询问,直接拒绝需要权限的操作 'plan', // 计划模式,只允许读操作] as const模式之间存在层级关系,从最严格到最宽松:
plan (最严格) → dontAsk → default → acceptEdits → auto → bypassPermissions (最宽松)★ 设计洞察:
auto和bubble是内部模式(InternalPermissionMode),不暴露给外部用户。auto使用 ML 分类器自动决策,通过feature('TRANSCRIPT_CLASSIFIER')编译时条件编译控制。这说明该功能仍在实验阶段,通过 feature gate 进行灰度发布。
2.3 Permission Rule(权限规则)
Section titled “2.3 Permission Rule(权限规则)”// types/permissions.ts:67-79type PermissionRuleValue = { toolName: string // 例如 "Bash"、"mcp__server1" ruleContent?: string // 例如 "prefix:git *"、"npm publish:*"}
type PermissionRule = { source: PermissionRuleSource // 规则来源 ruleBehavior: PermissionBehavior // allow | deny | ask ruleValue: PermissionRuleValue}规则的来源层级(优先级从高到低):
// permissions.ts:109-114const PERMISSION_RULE_SOURCES = [ ...SETTING_SOURCES, // userSettings, projectSettings, localSettings, flagSettings, policySettings 'cliArg', // 命令行参数 'command', // 运行时命令 'session', // 当前会话临时授权] as const★ 设计洞察:规则来源区分了 8 个层级,这不是过度设计——它解决的是「谁说了算」的问题。企业管理员的
policySettings可以覆盖用户的userSettings,但用户的session临时授权又可以在当前会话中生效。这是一个典型的分层配置系统(类似 CSS 的特异性)。
3. 权限决策管线(核心算法)
Section titled “3. 权限决策管线(核心算法)”hasPermissionsToUseToolInner() 是核心决策函数(permissions.ts:1158),执行一个精心设计的多步管线:
Step 1: 规则检查(硬性边界) ├── 1a. 整个工具被 deny 规则拒绝? → DENY(不可覆盖) ├── 1b. 整个工具有 ask 规则? → ASK(除非沙箱可以自动放行) ├── 1c. 调用 tool.checkPermissions()(工具自身的权限逻辑) ├── 1d. 工具实现返回 deny? → DENY ├── 1e. 工具需要用户交互? → ASK(即使 bypass 模式也必须询问) ├── 1f. 内容级别的 ask 规则? → ASK(如 Bash(npm publish:*)) └── 1g. 安全检查(.git/、.claude/ 等)? → ASK(bypass 免疫)
Step 2: 模式检查(全局策略) ├── 2a. bypassPermissions 模式? → ALLOW └── 2b. 工具在全局 allow 规则中? → ALLOW
Step 3: 最终处理 ├── passthrough → 转换为 ask ├── dontAsk 模式?ask → DENY(静默拒绝) ├── auto 模式?→ 进入 ML 分类器流程 └── 无头模式?→ 尝试 Hook 决策,否则 DENY关键代码路径
Section titled “关键代码路径”// permissions.ts:1158-1318async function hasPermissionsToUseToolInner( tool: Tool, input: { [key: string]: unknown }, context: ToolUseContext,): Promise<PermissionDecision> { // 1a. 整个工具被 deny const denyRule = getDenyRuleForTool(appState.toolPermissionContext, tool) if (denyRule) { return { behavior: 'deny', ... } }
// 1c. 工具自身的权限检查 const parsedInput = tool.inputSchema.parse(input) toolPermissionResult = await tool.checkPermissions(parsedInput, context)
// 1g. 安全检查 —— bypass 免疫! if (toolPermissionResult?.behavior === 'ask' && toolPermissionResult.decisionReason?.type === 'safetyCheck') { return toolPermissionResult // 即使 bypass 模式也必须询问 }
// 2a. bypass 模式放行 if (shouldBypassPermissions) { return { behavior: 'allow', ... } }
// 3. passthrough → ask const result = toolPermissionResult.behavior === 'passthrough' ? { ...toolPermissionResult, behavior: 'ask' } : toolPermissionResult
return result}★ 设计洞察:注意步骤的顺序至关重要。
deny规则(1a/1d)在bypass模式(2a)之前检查,而安全检查(1g)被标记为「bypass 免疫」。这意味着即使用户开启了bypassPermissions,对.git/、.claude/等关键路径的操作仍然需要确认。这是一个「纵深防御」的设计思想。
4. Auto 模式与 ML 分类器
Section titled “4. Auto 模式与 ML 分类器”当用户选择 auto 模式时(或 Plan 模式下激活了 auto),系统使用一个两阶段 ML 分类器来自动决策:
权限检查返回 ask │ ├── 安全检查(safetyCheck 且 !classifierApprovable)?→ 直接 ASK ├── 工具需要用户交互?→ 直接 ASK │ ├── 快速路径 1: acceptEdits 模拟 │ └── 模拟 acceptEdits 模式下的 checkPermissions │ 如果会放行 → ALLOW(跳过分类器,省 API 调用) │ ├── 快速路径 2: 安全工具白名单 │ └── isAutoModeAllowlistedTool(tool.name) │ 如果在白名单 → ALLOW(跳过分类器) │ └── 完整分类器路径 └── classifyYoloAction(messages, action, tools, context, signal) ├── shouldBlock = true → DENY + 追踪拒绝 ├── shouldBlock = false → ALLOW └── unavailable → fail-closed 或 fail-open(由 feature flag 控制)分类器的工程精妙之处
Section titled “分类器的工程精妙之处”1. 快速路径优化:在调用昂贵的 ML 分类器之前,先检查两条快速路径。如果在 acceptEdits 模式下就会被放行(如安全的文件编辑),直接放行。如果工具在安全白名单中(如 Grep、Glob),直接放行。这极大减少了分类器的调用次数。
// permissions.ts:600-649 — acceptEdits 模拟快速路径if (result.behavior === 'ask' && tool.name !== AGENT_TOOL_NAME && tool.name !== REPL_TOOL_NAME) { const acceptEditsResult = await tool.checkPermissions(parsedInput, { ...context, getAppState: () => ({ ...state, toolPermissionContext: { ...state.toolPermissionContext, mode: 'acceptEdits' as const, // 模拟 acceptEdits 模式 }, }), }) if (acceptEditsResult.behavior === 'allow') { return { behavior: 'allow', ... } // 跳过分类器 }}2. 拒绝追踪与熔断:
const DENIAL_LIMITS = { maxConsecutive: 3, // 连续 3 次拒绝 maxTotal: 20, // 累计 20 次拒绝}
function shouldFallbackToPrompting(state: DenialTrackingState): boolean { return ( state.consecutiveDenials >= DENIAL_LIMITS.maxConsecutive || state.totalDenials >= DENIAL_LIMITS.maxTotal )}★ 设计洞察:拒绝追踪解决了一个关键问题——如果分类器反复拒绝 Agent 的操作,Agent 会陷入死循环。当连续被拒绝 3 次或累计被拒绝 20 次,系统自动「回退到人工审批」模式,让用户来决定。这是一个工程上的「熔断器」模式。同时,任何一次成功的操作都会重置连续拒绝计数器(
recordSuccess),避免误触发。
3. 容错设计:分类器不可用时的处理策略由 feature flag tengu_iron_gate_closed 控制:
- fail-closed(默认):分类器挂了就拒绝,宁可卡住也不放过
- fail-open:分类器挂了就回退到正常的交互式审批
// permissions.ts:846-876if (classifierResult.unavailable) { if (getFeatureValue_CACHED_WITH_REFRESH('tengu_iron_gate_closed', true, 30 * 60 * 1000)) { return { behavior: 'deny', ... } // fail-closed } return result // fail-open:回退到正常流程}5. 规则匹配机制
Section titled “5. 规则匹配机制”5.1 工具级匹配
Section titled “5.1 工具级匹配”// permissions.ts:238-269function toolMatchesRule(tool, rule): boolean { // 规则不能有 content(content 用于子命令匹配) if (rule.ruleValue.ruleContent !== undefined) return false
// 直接名称匹配:规则 "Bash" 匹配工具 "Bash" if (rule.ruleValue.toolName === nameForRuleMatch) return true
// MCP 服务器级匹配:规则 "mcp__server1" 匹配 "mcp__server1__tool1" // 通配符支持:规则 "mcp__server1__*" 匹配该服务器所有工具 return ruleInfo?.serverName === toolInfo?.serverName && (ruleInfo?.toolName === undefined || ruleInfo?.toolName === '*')}5.2 内容级匹配
Section titled “5.2 内容级匹配”内容级匹配由每个工具自己的 checkPermissions() 实现。例如 BashTool 解析命令前缀:
规则 "Bash(prefix:git *)" → 匹配所有 git 开头的命令规则 "Bash(npm publish:*)" → 匹配 npm publish 相关命令规则 "Agent(Explore)" → 匹配 Explore 类型的子 Agent5.3 PermissionContext 数据结构
Section titled “5.3 PermissionContext 数据结构”// Tool.ts:123-138type ToolPermissionContext = DeepImmutable<{ mode: PermissionMode additionalWorkingDirectories: Map<string, AdditionalWorkingDirectory> alwaysAllowRules: ToolPermissionRulesBySource // { userSettings: ["Bash(prefix:git *)"], ... } alwaysDenyRules: ToolPermissionRulesBySource alwaysAskRules: ToolPermissionRulesBySource isBypassPermissionsModeAvailable: boolean isAutoModeAvailable?: boolean shouldAvoidPermissionPrompts?: boolean // 无头模式(后台 Agent) awaitAutomatedChecksBeforeDialog?: boolean // 先跑自动化检查再弹框 prePlanMode?: PermissionMode // Plan 模式前的原始模式}>★ 设计洞察:
DeepImmutable<>包装意味着权限上下文是不可变的。修改权限状态需要创建新的 context 对象,这保证了在并发 Agent 场景下的线程安全。
6. 无头 Agent 的权限处理
Section titled “6. 无头 Agent 的权限处理”后台运行的 Agent(如子 Agent、远程 Agent)无法弹出交互式对话框。系统提供了特殊处理:
// permissions.ts:400-471async function runPermissionRequestHooksForHeadlessAgent(...) { // 1. 先给 Hook 一个机会来决策 for await (const hookResult of executePermissionRequestHooks(...)) { if (decision.behavior === 'allow') return allowDecision if (decision.behavior === 'deny') return denyDecision } // 2. 没有 Hook 决策 → 返回 null,调用方会 auto-deny return null}
// permissions.ts:929-952if (appState.toolPermissionContext.shouldAvoidPermissionPrompts) { const hookDecision = await runPermissionRequestHooksForHeadlessAgent(...) if (hookDecision) return hookDecision return { behavior: 'deny', message: AUTO_REJECT_MESSAGE(tool.name) }}★ 设计洞察:这是 Hook 系统与权限系统的关键交互点。企业用户可以注册
PermissionRequestHook,在无头 Agent 模式下自动审批特定操作。这让自动化 CI/CD 管线成为可能——Agent 在后台运行,Hook 脚本根据策略自动决策。
7. 权限更新与持久化
Section titled “7. 权限更新与持久化”用户在交互式对话框中做出的选择可以持久化为规则:
// types/permissions.ts:98-131type PermissionUpdate = | { type: 'addRules'; destination: PermissionUpdateDestination; rules: ...; behavior: ... } | { type: 'replaceRules'; ... } | { type: 'removeRules'; ... } | { type: 'setMode'; destination: ...; mode: ... } | { type: 'addDirectories'; ... } | { type: 'removeDirectories'; ... }
type PermissionUpdateDestination = | 'userSettings' // 全局用户设置 | 'projectSettings' // 项目级设置 | 'localSettings' // 本地设置 | 'session' // 仅当前会话 | 'cliArg' // CLI 参数级别用户的选择对应不同的持久化层级:
- 「Allow Once」→ session(会话结束即失效)
- 「Always Allow」→ userSettings(永久生效)
- 「Deny」→ 同上
8. Agent 工程实践 Takeaway
Section titled “8. Agent 工程实践 Takeaway”8.1 分层决策是 Agent 权限的最佳实践
Section titled “8.1 分层决策是 Agent 权限的最佳实践”不要设计单一的 allow/deny 开关。工业级 Agent 需要:
- 多个决策源(规则、模式、分类器、Hook)
- 明确的优先级链(deny 优先于 allow,安全检查免疫 bypass)
- 多级持久化(session < user < project < policy)
8.2 安全检查需要「bypass 免疫」
Section titled “8.2 安全检查需要「bypass 免疫」”某些操作(修改 .git/、.claude/、shell 配置文件)即使在最宽松的模式下也必须询问用户。这是纵深防御的关键层。
8.3 ML 分类器需要工程保护
Section titled “8.3 ML 分类器需要工程保护”Claude Code 的 auto 模式展示了如何正确使用 ML 分类器做权限决策:
- 快速路径减少调用:先用确定性规则过滤
- 熔断机制:连续拒绝 3 次或累计 20 次就回退到人工
- 容错设计:分类器不可用时有明确的 fail-closed/fail-open 策略
- 可观测性:每次分类器决策都有详细的遥测数据
8.4 不可变状态保证并发安全
Section titled “8.4 不可变状态保证并发安全”DeepImmutable<ToolPermissionContext> 确保多个 Agent 可以安全地并发读取权限上下文,修改需要创建新对象。
与其他层的交互
Section titled “与其他层的交互”- → Tool 系统(L2):每个工具实现
checkPermissions()方法,提供工具特定的权限逻辑 - → Hook 系统(L4):
PermissionRequestHook 可以拦截并自动决策 - → Sub-agent(L6):无头 Agent 使用
shouldAvoidPermissionPrompts+ Hook 决策 - → Agent Loop(L1):权限检查在工具执行管线中,位于
runToolUse()的前置步骤