permission-model.mdx 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. ---
  2. title: "权限模型 - Allow/Ask/Deny 三级权限体系"
  3. description: "详解 Claude Code 的三级权限模型实现:基于 src/utils/permissions/permissions.ts 的规则匹配引擎、五层规则来源优先级、工具名/命令/路径三维度匹配、Denial Tracking 死循环防护、权限模式切换机制。"
  4. keywords: ["权限模型", "Allow Ask Deny", "PermissionRule", "checkPermissions", "Denial Tracking", "权限规则"]
  5. ---
  6. {/* 本章目标:基于源码揭示权限系统的完整实现 */}
  7. ## 三种权限行为
  8. 每一次工具调用,系统都会做出三种裁决之一:
  9. | 行为 | 含义 | 返回类型 | 典型场景 |
  10. |------|------|---------|---------|
  11. | **Allow** | 自动放行,用户无感知 | `{ behavior: 'allow', updatedInput, decisionReason }` | Read 读取项目内文件 |
  12. | **Ask** | 弹出确认对话框 | `{ behavior: 'ask', message, suggestions, metadata }` | Bash 执行未知命令 |
  13. | **Deny** | 直接拒绝 | `{ behavior: 'deny', message, decisionReason }` | 尝试执行被禁止的命令 |
  14. 这些行为由 `PermissionResult` 类型定义(`src/utils/permissions/PermissionResult.ts`)。
  15. ## 权限规则的五层来源
  16. 规则从 5 个来源汇聚(`PERMISSION_RULE_SOURCES`,`permissions.ts:109`),优先级从高到低:
  17. ```
  18. 1. session — 用户在当前对话中手动授权("Always allow")
  19. 2. cliArg — 命令行 --allow/--deny 参数
  20. 3. command — Skill 工具的 allowedTools 白名单
  21. 4. projectSettings — .claude/settings.json(团队共享)
  22. 5. userSettings — ~/.claude/settings.json(跨项目)
  23. 6. policySettings — 企业管理员下发的策略(用户不可覆盖)
  24. ```
  25. 每个来源维护三个数组:`alwaysAllowRules[source]`、`alwaysAskRules[source]`、`alwaysDenyRules[source]`。
  26. 规则数据结构为 `PermissionRule`:
  27. ```typescript
  28. {
  29. source: PermissionRuleSource // 来自哪个层级
  30. ruleBehavior: 'allow' | 'ask' | 'deny'
  31. ruleValue: {
  32. toolName: string // 如 "Bash"、"mcp__server1"
  33. ruleContent?: string // 如 "git *"、"src/**"
  34. }
  35. }
  36. ```
  37. ## 规则匹配引擎
  38. ### 三维度匹配
  39. `permissions.ts` 实现了三种匹配维度:
  40. **1. 工具名匹配**(`toolMatchesRule()`,第 238 行)
  41. 匹配整个工具,仅当规则没有 `ruleContent`:
  42. ```typescript
  43. // 精确匹配
  44. rule "Bash" → 匹配 BashTool
  45. rule "mcp__server1" → 匹配该 MCP Server 的所有工具(server 级别)
  46. rule "mcp__server1__*" → 通配符匹配(同上)
  47. ```
  48. MCP 工具使用 `getToolNameForPermissionCheck()` 获取匹配名称,支持有前缀(`mcp__server__tool`)和无前缀模式。
  49. **2. 命令模式匹配**(BashTool 的 `checkPermissions()`)
  50. BashTool 通过 `preparePermissionMatcher()`(`Tool.ts:514`)解析命令模式:
  51. ```json
  52. {"tool": "Bash", "ruleContent": "git *"} → 匹配 "git commit -m 'fix'"
  53. ```
  54. 命令通过 AST 解析(`readOnlyValidation.ts` 使用 tree-sitter bash),提取第一个子命令进行匹配。
  55. **3. 路径匹配**(文件工具的 `checkPermissions()`)
  56. Read/Edit/Write 工具通过 `getPath()` 提取文件路径,与 `ruleContent` 中的 glob 模式匹配:
  57. ```json
  58. {"tool": "Edit", "ruleContent": "src/**"} → 匹配 "src/utils/foo.ts"
  59. ```
  60. ### 权限检查的完整流程
  61. 每次工具调用的权限检查(`canUseTool()` → `checkPermissions()`)经过以下步骤:
  62. ```
  63. 1a. Blanket deny 检查
  64. getDenyRuleForTool() → 工具名完全匹配 deny 规则?
  65. ↓ 命中 → deny(工具在 getTools() 阶段就被过滤掉)
  66. 1b. Blanket allow 检查
  67. toolAlwaysAllowedRule() → 工具名完全匹配 allow 规则?
  68. ↓ 命中 → allow
  69. 2. 工具自身 checkPermissions()
  70. 各工具有自定义逻辑:
  71. - BashTool: readOnlyValidation → sandbox 判定 → AST 解析 → 模式匹配
  72. - FileEditTool: 路径白名单检查
  73. - SkillTool: safe properties 白名单 + 精确/前缀匹配
  74. ↓ 返回 PermissionResult
  75. 3. Hook 系统
  76. executePermissionRequestHooks() → PreToolUse hook 可以 override
  77. ↓ hook 返回 deny → deny
  78. ↓ hook 返回 ask → 升级为 ask
  79. 4. Ask 规则检查
  80. getAskRules() → 命中 → ask
  81. 5. 默认行为
  82. 根据当前 permissionMode 决定默认行为
  83. - 'default': 大部分工具 ask
  84. - 'plan': 写操作 deny,读操作 allow
  85. - 'bypass': 全部 allow
  86. ```
  87. ## 权限模式
  88. | 模式 | `PermissionMode` 值 | 适用场景 | 行为 |
  89. |------|---------------------|---------|------|
  90. | **Default** | `'default'` | 日常使用 | 敏感操作逐一确认 |
  91. | **Plan Mode** | `'plan'` | 探索阶段 | 只能读不能写(`isReadOnly()` 检查) |
  92. | **Auto** | `'auto'` | 信任 AI | 通过 transcript classifier 自动决策 |
  93. | **Bypass** | `'bypassPermissions'` | 完全信任 | 所有操作自动放行(需显式 `--dangerously-skip-permissions`) |
  94. Plan Mode 切换由 `EnterPlanModeTool.call()` 触发:
  95. ```typescript
  96. // EnterPlanModeTool.ts:88
  97. context.setAppState(prev => ({
  98. ...prev,
  99. toolPermissionContext: applyPermissionUpdate(
  100. prepareContextForPlanMode(prev.toolPermissionContext),
  101. { type: 'setMode', mode: 'plan', destination: 'session' },
  102. ),
  103. }))
  104. ```
  105. 退出时由 `ExitPlanModeV2Tool` 恢复为之前的模式。
  106. ## Denial Tracking:死循环防护
  107. `src/utils/permissions/denialTracking.ts` 实现了拒绝追踪机制:
  108. ```typescript
  109. const DENIAL_LIMITS = {
  110. maxDenialsPerTool: 3, // 同一工具连续拒绝上限
  111. cooldownPeriodMs: 30_000, // 冷却期 30 秒
  112. }
  113. ```
  114. 当 AI 被连续拒绝同一类操作达到上限时:
  115. 1. `recordDenial()` 记录拒绝,增加计数
  116. 2. `shouldFallbackToPrompting()` 检测到连续拒绝,返回 true
  117. 3. 系统向 AI 注入消息:"Your previous tool call was rejected..."
  118. 4. AI 被迫改变策略,避免"反复请求同一个被拒操作"的死循环
  119. 操作成功时调用 `recordSuccess()` 重置计数。
  120. ## 规则的运行时更新
  121. 权限规则可以在运行时动态更新(`applyPermissionUpdate()`,`PermissionUpdate.ts`):
  122. ```typescript
  123. type PermissionUpdate =
  124. | { type: 'addRule', behavior, rule, destination }
  125. | { type: 'removeRule', behavior, rule, destination }
  126. | { type: 'setMode', mode, destination }
  127. ```
  128. 当用户在 Ask 对话框中选择 "Always allow",系统调用 `persistPermissionUpdates()` 将规则写入对应层级的 settings 文件(project/user/managed),同时更新内存中的 `toolPermissionContext`。