File size: 41,576 Bytes
f468102 96f34e3 f468102 0f215fe | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 | # 安全与权限系统深度分析
> 本文基于 Codev (Claude Code) 源代码,深入分析其安全架构与权限子系统。
> 版本参考:commit `835ff5a` / `cdb3bdd`
---
## 1. 设计哲学
### 1.1 Deny-First 原则
系统中所有权限检查的**默认行为是拒绝**。未在规则中明确允许的操作,最终都会向用户发起询问或直接被拒绝。这一原则贯穿整个权限管道(Authorization Pipeline),体现在:
- `PermissionResult` 的默认行为是 `ask`(询问用户),而非 `allow`
- 工具实现的 `checkPermissions()` 方法若返回 `passthrough`,上层会将其转换为 `ask`(`src/utils/permissions/permissions.ts:1300-1310`)
- `bypassPermissions` 模式是唯一能跳过所有检查的模式,但该模式可以通过 Statsig 门控(`tengu_disable_bypass_permissions_mode`)被完全禁用
### 1.2 人类决策权威
用户始终拥有最终决定权。在任何权限模式下,用户都可以通过终端对话框批准或拒绝操作。即使在 `auto` 模式下,当分类器(Classifier)判定需阻止操作时,用户仍可通过交互式对话框覆盖分类器决定。
系统通过以下机制保障人类决策权威:
- **建议系统**(`generateSuggestions`, `src/utils/permissions/filesystem.ts:1414-1473`):当询问用户时,同时提供可操作的建议(如"允许本次会话的所有编辑"、"添加目录到工作区")
- **权限解释器**(`PermissionExplainer`, `src/utils/permissions/permissionExplainer.ts`):使用 Haiku 模型解释命令的风险等级(LOW/MEDIUM/HIGH)、目的和潜在风险,辅助用户决策
- **渐进式暴露**:只对高风险操作发起询问,低风险操作在适当模式下自动批准
### 1.3 防御纵深
系统采用**多层重叠的安全机制**,而非依赖单一安全边界。这意味着即使某一层被绕过,后续层仍能提供保护。
```
┌─────────────────────────────────────────────────────┐
│ 防御纵深架构 │
├─────────────────────────────────────────────────────┤
│ 1. 规则系统 (deny/allow/ask rules) │
│ 2. PreToolUse Hook (用户自定义拦截) │
│ 3. 路径安全检查 (Dangerous Files, Windows 模式) │
│ 4. 模式驱动的权限处理 (Mode-based Decision) │
│ 5. 自动模式分类器 (Auto Mode Classifier) │
│ 6. 用户确认对话框 (User Confirmation Dialog) │
│ 7. 沙箱 (Sandbox) 容器化执行 │
│ 8. 拒绝跟踪 (Denial Tracking) 防滥用 │
└─────────────────────────────────────────────────────┘
```
### 1.4 渐进信任
系统支持用户通过时间建立信任轨迹。随着用户批准更多操作,系统会逐步提高自动化程度:
- **默认模式**:auto-approve rate 起始约 20%,每一步都需确认
- **auto 模式改进**:系统通过拒绝跟踪(`denialTracking.ts`)记录分类器连续拒绝次数,超过阈值(连续 3 次或总计 20 次)后回退到用户询问模式
- **规则积累**:用户可逐步添加 `alwaysAllow` 规则(如 `Bash(ls:*)`),减少未来对低风险操作的询问
- **acceptEdits 模式**:一旦用户选择此模式,工作目录内的所有文件编辑操作自动批准
---
## 2. 七种权限模式
权限模式定义在 `src/types/permissions.ts:16-36`,运行时配置在 `src/utils/permissions/PermissionMode.ts:42-91`。
### 模式总览
| 模式 | 内部名称 | 符号 | 自动批准 | 需要确认 | 风险等级 |
|------|----------|------|----------|----------|----------|
| 默认 | `default` | — | 无 | 所有操作 | 低(最安全) |
| 接受编辑 | `acceptEdits` | ⏵⏵ | 工作目录内文件编辑 | Bash 命令、目录外写入、MCP | 中低 |
| 计划 | `plan` | ⏸ | 读操作(文件读取、搜索) | 写操作、Bash 命令 | 低 |
| 自动 | `auto` | ⏵⏵ | 分类器批准的 + 安全放行列表 | 分类器阻止的 | 中 |
| 不询问 | `dontAsk` | ⏵⏵ | 无(所有 ask → deny) | 无法操作 | 高 |
| 绕过权限 | `bypassPermissions` | ⏵⏵ | 所有操作(除安全检查外) | 安全检查(.git/ 等) | 最高 |
| Bubble | `bubble` | — | Ant 内部使用 | Ant 内部使用 | — |
### 2.1 Default(默认模式)
**文件**: `src/utils/permissions/PermissionMode.ts:45-50`
```typescript
default: {
title: 'Default',
shortTitle: 'Default',
symbol: '',
color: 'text',
external: 'default',
}
```
- **行为**: 每一步操作都需要用户确认
- **自动批准**: 无
- **询问**: 所有工具调用
- **适用场景**: 新项目、不信任 AI 操作时
- **风险**: 最低,但效率也最低
### 2.2 AcceptEdits(接受编辑模式)
**文件**: `src/utils/permissions/PermissionMode.ts:59-65`
```typescript
acceptEdits: {
title: 'Accept edits',
shortTitle: 'Accept',
symbol: '⏵⏵',
color: 'autoAccept',
external: 'acceptEdits',
}
```
- **行为**: 自动批准工作目录内的文件编辑操作(`FileEditTool`, `FileWriteTool`)
- **自动批准**: 工作目录(`getOriginalCwd()` 已在 `additionalWorkingDirectories` 中的路径)内的文件写入
- **询问**: Bash 命令、工作目录外的文件写入、MCP 工具、网络操作
- **实现参考**: `src/utils/permissions/filesystem.ts:1360-1375` — 当 `mode === 'acceptEdits'` 且路径在工作目录内时,直接返回 `allow`
- **适用场景**: 用户希望 AI 可以直接修改代码,但不想让其执行任意命令
### 2.3 Plan(计划模式)
**文件**: `src/utils/permissions/PermissionMode.ts:52-58`
```typescript
plan: {
title: 'Plan Mode',
shortTitle: 'Plan',
symbol: PAUSE_ICON,
color: 'planMode',
external: 'plan',
}
```
- **行为**: 只读模式 + 计划讨论
- **自动批准**: 所有读操作(文件读取、搜索、列表等)
- **询问**: 文件编辑、Bash 命令、网络操作
- **内部机制**: 当从 auto 模式进入 plan 时,auto 分类器仍在后台运行(`prePlanMode` 记录),退出 plan 时恢复 auto 状态(`src/utils/permissions/permissionSetup.ts:1462-1493`)
- **适用场景**: 探索代码库、制定重构计划、代码审查
**与 Auto 模式的联动**(`permissionSetup.ts:1446-1455`):
当用户已选择加入 auto 模式且 `useAutoModeDuringPlan` 启用时,plan 模式下分类器仍处于激活状态。这通过 `shouldPlanUseAutoMode()` 函数判断。
### 2.4 Auto(自动模式)
**文件**: `src/utils/permissions/PermissionMode.ts:80-90`
```typescript
...(feature('TRANSCRIPT_CLASSIFIER')
? {
auto: {
title: 'Auto mode',
shortTitle: 'Auto',
symbol: '⏵⏵',
color: 'warning',
external: 'default',
},
}
: {}),
```
- **行为**: 使用 AI 分类器自动审批操作
- **内部依赖**: `feature('TRANSCRIPT_CLASSIFIER')` — 编译期 feature flag,外部构建中通过 DCE(死代码消除)完全移除
- **自动批准**:
1. `acceptEdits` 快速路径可批准的(工作目录内编辑)
2. 安全放行列表中的工具(`isAutoModeAllowlistedTool`, `classifierDecision.ts:96-100`)
3. 分类器判定为安全的操作
- **询问**: 分类器判定为需阻止的操作(可被用户覆盖)
- **拒绝限制**: 连续 3 次阻止 → 回退到询问;总计 20 次阻止 → 重置并回退(`denialTracking.ts:12-14`)
### 2.5 DontAsk(不询问模式)
**文件**: `src/utils/permissions/PermissionMode.ts:73-79`
```typescript
dontAsk: {
title: "Don't Ask",
shortTitle: 'DontAsk',
symbol: '⏵⏵',
color: 'error',
external: 'dontAsk',
}
```
- **行为**: 将所有 `ask` 决策转换为 `deny`
- **转换**: `src/utils/permissions/permissions.ts:505-518`
```typescript
if (appState.toolPermissionContext.mode === 'dontAsk') {
return {
behavior: 'deny',
decisionReason: { type: 'mode', mode: 'dontAsk' },
message: DONT_ASK_REJECT_MESSAGE(tool.name),
}
}
```
- **适用场景**: 测试、CI/CD 环境或不想让 AI 执行任何操作的场景
### 2.6 BypassPermissions(绕过权限模式)
**文件**: `src/utils/permissions/PermissionMode.ts:66-72`
- **行为**: 绕过所有权限检查(除安全检查和内容特定 ask 规则外)
- **禁用机制**: 可通过 Statsig 门控 `tengu_disable_bypass_permissions_mode` 或 settings 中 `permissions.disableBypassPermissionsMode` 完全禁用(`permissionSetup.ts:695-711`)
- **例外**: `bypassPermissions` 不影响 `requiresUserInteraction` 检查(`permissions.ts:1231-1236`)、安全检查(`permissions.ts:1255-1260`)和内容特定 ask 规则(`permissions.ts:1244-1250`)
### 2.7 Bubble(气泡模式)
- **用途**: Ant 内部使用模式,外部构建不可见(`PermissionMode.ts:104` 中 `process.env.USER_TYPE !== 'ant'` 时被排除)
- **文档**: 无外部可用信息
---
## 3. 授权管道 (Authorization Pipeline)
核心实现位于 `src/utils/permissions/permissions.ts` 的 `hasPermissionsToUseToolInner()` 函数(第 1158-1319 行)和 `hasPermissionsToUseTool()`(第 473-956 行)。
### 管道全景图
```
用户输入工具调用
│
▼
┌─────────────────────────────────────┐
│ 1. 预过滤 │
│ 1a. 全局 deny 规则检查 │
│ 1b. 全局 ask 规则检查 │
│ 1c. 工具特定 checkPermissions() │
│ 1d. 工具实现 deny │
│ 1e. requiresUserInteraction 检查 │
│ 1f. 内容特定 ask 规则检查 │
│ 1g. 安全检查 (safety check) │
└─────────────────┬───────────────────┘
│ (2a-2b)
▼
┌─────────────────────────────────────┐
│ 2. 权限处理 │
│ 2a. bypassPermissions 模式放行 │
│ 2b. 全局 allow 规则放行 │
│ 2c. passthrough → ask 转换 │
└─────────────────┬───────────────────┘
│ (返回 ask/allow/deny)
▼
┌─────────────────────────────────────┐
│ 3. 模式转换 (在 hasPermissionsToUse) │
│ 3a. dontAsk 模式: ask → deny │
│ 3b. Auto 模式: │
│ ├ 非分类器可批准的安全检查: 返回 │
│ ├ PowerShell 默认拒绝 │
│ ├ acceptEdits 快速路径 │
│ ├ 安全放行列表 │
│ └ YOLO 分类器评估 │
│ 3c. 无提示模式: hooks → auto-deny│
└─────────────────┬───────────────────┘
│
▼
┌─────────────────┐
│ 用户确认对话框 │
│ (useCanUseTool) │
└─────────────────┘
```
### 3.1 第 1 步:规则预过滤
在 `hasPermissionsToUseToolInner()`(第 1158-1260 行)中执行:
**1a. 全局 Deny 规则**(第 1171-1181 行):
```typescript
const denyRule = getDenyRuleForTool(appState.toolPermissionContext, tool)
if (denyRule) { return { behavior: 'deny', message: `Permission to use ${tool.name} has been denied.` } }
```
**1b. 全局 Ask 规则**(第 1184-1206 行):检查工具是否在 alwaysAsk 列表中。例外:如果 sandbox 启用且 `autoAllowBashIfSandboxed` 为 true,sandbox 内运行的 Bash 命令可跳过此检查。
**1c. 工具特定权限检查**(第 1214-1223 行):
```typescript
const parsedInput = tool.inputSchema.parse(input)
toolPermissionResult = await tool.checkPermissions(parsedInput, context)
```
每个工具实现自己的 `checkPermissions()` 方法。例如 `BashTool` 实现命令级规则(前缀匹配、通配符匹配)。
**1d. 工具实现 Deny**(第 1226-1228 行):如果工具的 `checkPermissions` 返回 `deny`,直接返回。
**1e. 用户交互要求**(第 1231-1236 行):如果工具标记为 `requiresUserInteraction()`,即使是 `bypassPermissions` 模式也必须询问。
**1f. 内容特定 Ask 规则**(第 1244-1250 行):如 `Bash(npm publish:*)` 这样的规则,即使是 `bypassPermissions` 也必须尊重。
**1g. 安全检查**(第 1255-1260 行):`checkPathSafetyForAutoEdit` 返回的安全检查(如 `.git/`、`.claude/`、shell 配置文件)是 bypass-immune 的。
### 3.2 第 2 步:权限处理
**2a. bypassPermissions 模式**(第 1268-1281 行):直接放行所有操作(1a-1g 已过滤的危险操作除外)。
**2b. 全局 Allow 规则**(第 1284-1297 行):如果工具在 `alwaysAllow` 列表中,直接放行。
**2c. passthrough → ask 转换**(第 1300-1310 行):如果 `checkPermissions` 返回 `passthrough`,转换为 `ask`。
### 3.3 第 3 步:模式转换
在 `hasPermissionsToUseTool()`(第 473-956 行)中执行:
**3a. dontAsk**: 将所有 `ask` 转换为 `deny`(第 505-518 行)。
**3b. Auto 模式**: 复杂的分类器驱动审批流程(第 520-926 行):
1. 非分类器可批准的安全检查 → 保持询问
2. `requiresUserInteraction()` 的工具 → 保持询问
3. 拒绝跟踪检查
4. PowerShell 默认拒绝(除非 `POWERSHELL_AUTO_MODE` 特性启用)
5. **acceptEdits 快速路径**: 如果当前操作在 acceptEdits 模式下会被批准,直接放行(第 600-655 行)
6. **安全放行列表**: `isAutoModeAllowlistedTool()` 中的工具(第 660-685 行)
7. **分类器评估**: `classifyYoloAction()` 执行完整的安全评估(第 692-926 行)
**3c. 无提示模式**: `shouldAvoidPermissionPrompts` 为 true 时(后台/headless agent),执行 PermissionRequest hooks,若 hook 未决定则 auto-deny(第 932-952 行)。
### 3.4 CLI Hook 整合
在 `src/hooks/useCanUseTool.tsx` 中,结果经过:
1. `hasPermissionsToUseTool()` 管道
2. 如果 `allow` → 立即批准,记录分类器批准信息
3. 如果 `deny` → 拒绝,记录 auto mode denial,发送通知
4. 如果 `ask` → 进入交互式权限处理流程:
- `awaitAutomatedChecksBeforeDialog` → 协调器处理
- Swarm Worker 处理(`handleSwarmWorkerPermission`)
- 竞争性分类器检查(Bash 命令的投机性分类器评估,`peekSpeculativeClassifierCheck`)
- 最终 `handleInteractivePermission` 显示对话框
---
## 4. Auto-Mode 分类器
### 4.1 架构概览
Auto mode 分类器是系统的核心智能审批组件,使用 Claude 模型(默认使用主循环模型,可被 `CLAUDE_CODE_AUTO_MODE_MODEL` 环境变量或 GrowthBook 配置覆盖)对每个待审批的工具调用进行安全评估。
**核心文件**:
- `src/utils/permissions/yoloClassifier.ts` — 分类器主逻辑、2 阶段 XML 分类器
- `src/utils/permissions/classifierDecision.ts` — 安全放行列表
- `src/utils/permissions/classifierShared.ts` — 共享解析工具
- `src/utils/permissions/yolo-classifier-prompts/` — 系统提示模板
### 4.2 两阶段分类器
系统支持三种分类器模式,通过 GrowthBook `tengu_auto_mode_config.twoStageClassifier` 配置:
| 模式 | 说明 | 适用场景 |
|------|------|----------|
| `both` (默认) | Stage 1 (fast) + Stage 2 (thinking) | 平衡速度与准确率 |
| `fast` | 仅 Stage 1,max_tokens=256,最终裁定 | 追求速度 |
| `thinking` | 仅 Stage 2,跳过 Stage 1 | 需要深度推理 |
**Stage 1 - 快速判断**(`yoloClassifier.ts:769-858`):
- `max_tokens=64`(fast-only 模式为 256)
- `stop_sequences=['</block>']` — 一旦输出 `</block>` 立即停止
- 后缀:`"\nErr on the side of blocking. <block> immediately."`
- 如果 stage 1 判定允许(`<block>no</block>`),直接返回
- 如果 stage 1 判定阻止(`<block>yes</block>`),进入 stage 2
**Stage 2 - 深度推理**(`yoloClassifier.ts:861-940`):
- `max_tokens=4096`
- 包含 `xml_s2_t2` 后缀,要求 chain-of-thought 推理
- 可包含 `<thinking>` 标签
- 最终输出 `<block>yes/no</block>` 和可选的 `<reason>...</reason>`
### 4.3 双阶段上下文预算
```
┌──────────────────────────────────────────────────────┐
│ 上下文窗口管理 │
├──────────────────────────────────────────────────────┤
│ System Block (cache_control) ← 稳定缓存前缀 │
│ └ 基础提示 + 权限模板 + 用户 allow/deny 规则 │
│ │
│ CLAUDE.md (cache_control) ← 静态用户指令 │
│ │
│ Transcript Entries │
│ ├ User: "..." │
│ ├ Bash ls │
│ ├ User: "..." │
│ └ ... │
│ │
│ Action (cache_control) ← 正在评估的操作 │
│ └ 只有 system + CLAUDE.md + action 三个 cache_control│
│ block(不超过 API 限制的 4 个) │
└──────────────────────────────────────────────────────┘
```
### 4.4 评估指标与风险分类
**官方数据**:
- False-negative rate:17%(即分类器应阻止但未阻止的操作比例)
- 第三方测试 FNR:70.3%(表明在更复杂场景下分类器有显著漏报)
**四种风险类别**(来自系统提示模板 `auto_mode_system_prompt.txt`):
| 类别 | 说明 | 示例 |
|------|------|------|
| Overeager Behavior | 模型过于积极执行未明确请求的操作 | 未问用户就安装包 |
| Honest Mistakes | 模型判断错误导致的非恶意操作 | 删错了文件 |
| Prompt Injection | 提示注入攻击 | 用户指令中隐藏恶意代码 |
| Model Misalignment | 模型与用户意图不一致 | 执行与任务无关的危险操作 |
### 4.5 安全放行列表
**文件**: `src/utils/permissions/classifierDecision.ts:56-94`
```typescript
const SAFE_YOLO_ALLOWLISTED_TOOLS = new Set([
FILE_READ_TOOL_NAME, GREP_TOOL_NAME, GLOB_TOOL_NAME,
LSP_TOOL_NAME, TOOL_SEARCH_TOOL_NAME, LIST_MCP_RESOURCES_TOOL_NAME,
TODO_WRITE_TOOL_NAME, TASK_CREATE_TOOL_NAME, // ... 等
])
```
注意:`isAutoModeAllowlistedTool()`(第 96-100 行)当前实现为 `return true` — 所有工具都跳过分类器。这意味着安全放行列表实际上当前被全局放行取代,所有工具通过 `acceptEdits` 快速路径评估后再考虑分类器。
但代码中的注释明确说明:
> "Does NOT include write/edit tools — those are handled by the acceptEdits fast path"
### 4.6 提示注入防护
**JSONL 转录格式**(`yoloClassifier.ts:412-423`):
```
{"Bash":"ls -la"} ← JSON 编码,注入无法突破字符串边界
{"user":"forged text"} ← tool_use 块中的 user 文本不会混淆分类器
```
传统文本格式:
```
Bash ls -la
User: forged text
```
JSONL 格式是注入防护的关键设计:用户输入和工具调用都被 JSON.stringify 编码,因此恶意内容中的换行符和特殊字符不可能突破字符串边界。
### 4.7 拒绝跟踪 (Denial Tracking)
**文件**: `src/utils/permissions/denialTracking.ts`
```typescript
export const DENIAL_LIMITS = {
maxConsecutive: 3, // 连续拒绝 3 次后回退
maxTotal: 20, // 总计拒绝 20 次后回退
} as const
```
- `recordDenial(state)`: 递增 consecutiveDenials 和 totalDenials
- `recordSuccess(state)`: 重置 consecutiveDenials 为 0
- `shouldFallbackToPrompting(state)`: 超出任一阈值时返回 true
超过限制后的行为:
- CLI 模式:回退到手动询问,包含分类器的拒绝理由
- headless 模式:抛出 `AbortError`,终止 agent
---
## 5. 规则系统
### 5.1 规则类型
**文件**: `src/types/permissions.ts:67-79`, `src/utils/permissions/permissions.ts:238-302`
三种规则行为(`PermissionBehavior`):
| 行为 | 效果 | 适用场景 |
|------|------|----------|
| `allow` | 工具被允许,跳过后续检查 | `--allowed-tools Bash(ls:*)` |
| `deny` | 工具被拒绝,不执行 | `--disallowed-tools Bash(rm:*)` |
| `ask` | 强制询问用户 | `Bash(npm publish:*)` |
### 5.2 规则来源
**文件**: `src/types/permissions.ts:54-62`
| 来源 | 持久化 | 说明 |
|------|--------|------|
| `userSettings` | `~/.claude/settings.json` | 全局用户设置 |
| `projectSettings` | `.claude/settings.json` | 项目级别,可提交到 git |
| `localSettings` | `.claude/settings.local.json` | 项目级别,gitignored |
| `policySettings` | 企业策略 | 只读,不可删除 |
| `flagSettings` | 编译期标志 | 只读 |
| `cliArg` | CLI 参数 | `--allowed-tools`, `--disallowed-tools` |
| `command` | 斜杠命令 | 命令前导块中的规则 |
| `session` | 内存 | 临时会话规则 |
规则加载流程(`permissionsLoader.ts:120-133`):
1. 如果 `allowManagedPermissionRulesOnly` 为 true → 只加载 `policySettings`
2. 否则加载所有已启用的设置源
### 5.3 路径模式匹配
**文件**: `src/utils/permissions/filesystem.ts:960-1025`
使用 gitignore 风格的 `ignore` 库进行路径匹配:
```typescript
export function matchingRuleForInput(
path: string,
toolPermissionContext: ToolPermissionContext,
toolType: 'edit' | 'read',
behavior: 'allow' | 'deny' | 'ask',
): PermissionRule | null
```
**关键要点**:
- 路径被规范化为 POSIX 格式(`relativePath` 函数,第 170-179 行)
- 双斜杠前缀 `//` 表示相对于根 `/` 的路径
- 波浪号前缀 `~/` 表示相对于用户主目录的路径
- 模式 `/**` 后缀被简化为匹配目录本身及其所有子项
- 大小写标准化(`normalizeCaseForComparison`)防止大小写绕过
### 5.4 路径安全检查
**文件**: `src/utils/permissions/filesystem.ts:620-665`
`checkPathSafetyForAutoEdit()` 检查:
1. **Windows 可疑路径模式**(`hasSuspiciousWindowsPathPattern`, `filesystem.ts:537-602`):
- NTFS Alternate Data Streams(`:` 号,仅 Windows/WSL)
- 8.3 短文件名(`~` 后跟数字)
- 长路径前缀(`\\?\`, `//?/` 等)
- 尾部点和空格(`.git.`, `.claude.`)
- DOS 设备名(`CON`, `PRN`, `AUX` 等)
- 三个连续点(`...`)
- UNC 路径(`\\server\share`)
2. **Claude 配置文件**(`isClaudeConfigFilePath`, `filesystem.ts:225-242`):
- `settings.json`, `settings.local.json`
- `.claude/commands/`, `.claude/agents/`, `.claude/skills/`
3. **危险文件**(`DANGEROUS_FILES`, `filesystem.ts:57-68`):
- `.gitconfig`, `.gitmodules`
- `.bashrc`, `.bash_profile`, `.zshrc`, `.zprofile`, `.profile`
- `.ripgreprc`, `.mcp.json`, `.claude.json`
4. **危险目录**(`DANGEROUS_DIRECTORIES`, `filesystem.ts:74-79`):
- `.git`, `.vscode`, `.idea`, `.claude`
安全检查也适用于解析后的符号链接路径,防止通过符号链接绕过。
### 5.5 危险权限检测
**文件**:
- `src/utils/permissions/permissionSetup.ts:94-285`
- `src/utils/permissions/dangerousPatterns.ts`
进入 auto 模式时,系统自动检测并剥离(strip)危险权限:
**危险 Bash 规则**(`isDangerousBashPermission`):
```typescript
// 完全通配: Bash, Bash(*), Bash() → 允许所有命令 → 危险
// 解释器通配: Bash(python:*) → 允许任意 Python 代码 → 危险
// 包管理器: Bash(npm run:*) → 可执行任意脚本 → 危险
```
匹配模式列表(`dangerousPatterns.ts`):
```typescript
CROSS_PLATFORM_CODE_EXEC = [
'python', 'python3', 'node', 'deno', 'ruby', 'perl', 'php', 'lua',
'npx', 'bunx', 'npm run', 'yarn run', 'pnpm run', 'bun run',
'bash', 'sh', 'ssh',
]
// ant-only: gh, curl, wget, git, kubectl, aws, gcloud, gsutil
```
**危险 PowerShell 规则**(`isDangerousPowerShellPermission`, `permissionSetup.ts:157-233`):
额外包含 `iex`, `invoke-expression`, `start-process`, `add-type`, `new-object` 等。
**危险 Agent 规则**(`isDangerousTaskPermission`):
任何 `Agent` 工具的 allow 规则都会绕过分类器对子 agent 的评估。
### 5.6 影子规则检测
**文件**: `src/utils/permissions/shadowedRuleDetection.ts`
检测 allow 规则是否被 ask/deny 规则"屏蔽"(即永远无法生效):
```typescript
// 示例: allow 规则 Bash(ls:*) 被全局 ask 规则 Bash 屏蔽
// 因为 ask 规则在评估顺序中先于 allow 规则
```
- **Deny 屏蔽**: 工具级别的 deny 规则使该工具的所有 allow 规则不可达
- **Ask 屏蔽**: 工具级别的 ask 规则使带内容的 allow 规则不可达(总是会先询问)
- **沙箱例外**: 如果 sandbox 启用且 `autoAllowBashIfSandboxed` 为 true,个人设置的 ask 规则不屏蔽 allow 规则
---
## 6. Shell 沙箱
### 6.1 容器化执行环境
**文件**: `src/utils/sandbox/sandbox-adapter.ts`
SandboxManager 提供容器化的命令执行环境,支持:
- **文件系统隔离**: `getFsReadConfig()` / `getFsWriteConfig()` 控制可读写的路径
- **网络隔离**: `getNetworkRestrictionConfig()` 控制网络访问
- **命令排除**: `excludedCommands` 列表中的命令不被沙箱化
### 6.2 AutoAllowBashIfSandboxed
**文件**: `src/utils/sandbox/sandbox-adapter.ts:469-472`
```typescript
function isAutoAllowBashIfSandboxed(): boolean {
const settings = getSettings_DEPRECATED()
return settings?.sandbox?.autoAllowBashIfSandboxed ?? true // 默认开启
}
```
当此选项启用时:
1. 所有 Bash 命令在沙箱内运行
2. Bash 命令的权限检查被自动放行(`permissions.ts:1189-1193`)
3. 命令不在沙箱内运行时(`dangerouslyDisableSandbox = true` 或 `excludedCommands`),仍遵循正常权限检查
### 6.3 平台支持
支持通过 `enabledPlatforms` 配置控制哪些平台启用沙箱:
```typescript
// src/entrypoints/sandboxTypes.ts:108-112
// 为了 NVIDIA 企业部署,最初仅 macOS 启用沙箱
// Linux/WSL 沙箱支持较新,在扩展前需要更多验证
```
### 6.4 Sandbox 写允许列表
在路径验证中(`pathValidation.ts:101-123`),当沙箱启用时,沙箱配置的写允许列表作为额外的工作目录:
```typescript
export function isPathInSandboxWriteAllowlist(resolvedPath: string): boolean {
const { allowOnly, denyWithinAllow } = SandboxManager.getFsWriteConfig()
// 检查路径是否在 allowOnly 中且不在 denyWithinAllow 中
}
```
---
## 7. 安全相关实现细节
### 7.1 Protected Paths
**文件**: `src/utils/permissions/filesystem.ts`
系统定义了多层保护路径,防止 AI 修改关键配置:
**内部可编辑路径**(`checkEditableInternalPath`, `filesystem.ts:1479-1605`)——自动允许编辑:
- 当前会话的计划文件(`isSessionPlanFile`)
- 临时目录(`isScratchpadPath`)
- 模板任务目录(`CLAUDE_JOB_DIR`,`feature('TEMPLATES')` 时)
- Agent 记忆目录(`isAgentMemoryPath`)
- 自动记忆目录(`isAutoMemPath`,无覆盖路径时)
- `.claude/launch.json`(桌面预览配置)
**内部可读路径**(`checkReadableInternalPath`, `filesystem.ts:1611-1777`)——自动允许读取:
- 会话记忆目录(`isSessionMemoryPath`)
- 项目目录(`isProjectDirPath`)
- 计划文件
- 工具结果目录(`getToolResultsDir`)
- 临时目录
- Agent 记忆目录
- 任务目录(`.claude/tasks/`)
- 团队目录(`.claude/teams/`)
- 内置技能参考文件(`getBundledSkillsRoot`)
### 7.2 Hook 系统
**文件**: `src/utils/hooks.ts:4157-4192`
#### PreToolUse Hooks
`executePermissionRequestHooks()` 在权限检查流程中调用,提供用户自定义的拦截逻辑:
```typescript
export async function* executePermissionRequestHooks<ToolInput>(
toolName: string,
toolUseID: string,
toolInput: ToolInput,
toolUseContext: ToolUseContext,
permissionMode?: string,
permissionSuggestions?: PermissionUpdate[],
signal?: AbortSignal,
timeoutMs: number = TOOL_HOOK_EXECUTION_TIMEOUT_MS,
// ...
)
```
Hook 可以返回:
- `{ behavior: 'allow', updatedInput, updatedPermissions }` → 放行(含输入修改和权限更新)
- `{ behavior: 'deny', message, interrupt }` → 拒绝(含可选的中断信号)
使用场景:
- headless/async agent 不能显示权限提示框时通过 hook 授权
- CI/CD 环境中的自动化策略执行
#### PostToolUse Hooks
代码中存在 `PostToolUse` 钩子类型(在 hook 类型定义中),用于工具执行后的审计、日志记录和副作用处理。
### 7.3 远程会话权限桥接
**文件**: `src/utils/permissions/permissionSetup.ts:748-758`
当 `CLAUDE_CODE_REMOTE` 环境变量设置时,运行在远程桥接模式,权限模式受限:
```typescript
if (
isEnvTruthy(process.env.CLAUDE_CODE_REMOTE) &&
!['acceptEdits', 'plan', 'default'].includes(settingsMode)
) {
// 只有 acceptEdits、plan、default 模式支持 CCR
logEvent('tengu_ccr_unsupported_default_mode_ignored', { mode: settingsMode })
}
```
这意味着在远程会话中,`bypassPermissions` 和 `auto` 模式被禁用,增强了对远程连接的安全控制。
### 7.4 权限解释器
**文件**: `src/utils/permissions/permissionExplainer.ts`
在询问用户时,系统可调用 Haiku 模型生成操作的风险评估:
```typescript
export async function generatePermissionExplanation({
toolName, toolInput, toolDescription, messages, signal,
}): Promise<PermissionExplanation | null>
```
输出包含:
- `riskLevel`: `LOW` | `MEDIUM` | `HIGH`
- `explanation`: 操作说明(1-2 句话)
- `reasoning`: 执行此操作的原因
- `risk`: 可能的风险(15 字以内)
此功能可通过 `permissionExplainerEnabled` 配置禁用。
### 7.5 权限更新的持久化
**文件**: `src/utils/permissions/PermissionUpdate.ts`, `PermissionUpdateSchema.ts`
权限变更(添加规则、删除规则、更改模式、添加目录)通过 `permissionRuleParser.ts` 进行序列化和反序列化,并写入到对应的设置文件。
```typescript
export type PermissionUpdate =
| { type: 'addRules'; destination: PermissionUpdateDestination; rules: PermissionRuleValue[]; behavior: PermissionBehavior }
| { type: 'replaceRules'; destination: ...; rules: ...; behavior: ... }
| { type: 'removeRules'; destination: ...; rules: ...; behavior: ... }
| { type: 'setMode'; destination: ...; mode: ExternalPermissionMode }
| { type: 'addDirectories'; destination: ...; directories: string[] }
| { type: 'removeDirectories'; destination: ...; directories: string[] }
```
### 7.6 特征标识与死代码消除
整个权限系统大量使用 Bun 编译期的 `feature()` 函数进行条件编译:
| Feature Flag | 控制的特性 | 文件 |
|-------------|-----------|------|
| `TRANSCRIPT_CLASSIFIER` | 整个 auto 模式系统(分类器、状态管理、危险权限剥离) | 多处 |
| `BASH_CLASSIFIER` | Bash 命令提示词分类器 | `bashClassifier.ts` |
| `POWERSHELL_AUTO_MODE` | PowerShell 自动模式 | `yoloClassifier.ts` |
| `TEMPLATES` | 模板任务目录自动编辑 | `filesystem.ts` |
这样,外部构建中所有 auto 模式相关代码被完全消除,减小了二进制体积并简化了安全模型。
### 7.7 拒绝统计与滥用防护
**文件**: `src/utils/permissions/denialTracking.ts`
```typescript
export const DENIAL_LIMITS = {
maxConsecutive: 3, // 连续 3 次拒绝 → 回退到手动
maxTotal: 20, // 总计 20 次拒绝 → 回退到手动
} as const
```
拒绝跟踪统计记录在 `AppState.denialTracking` 中,在 auto 模式下每次分类器判定后更新:
- 成功(allow)→ `recordSuccess()` → 重置连续拒绝计数
- 失败(block)→ `recordDenial()` → 递增两个计数器
- 超出阈值 → `shouldFallbackToPrompting()` → 回退到交互式询问
headless agent 模式下,超出限制直接终止 agent(`permissions.ts:1023-1027`)。
### 7.8 CLI 与 Settings 初始化流程
**文件**: `src/utils/permissions/permissionSetup.ts:872-1033`
`initializeToolPermissionContext()` 的完整流程:
1. 解析 `--allowed-tools`, `--disallowed-tools`, `--base-tools` CLI 参数
2. 检测 `bypassPermissions` 模式是否可用(Statsig 门控 + settings 检查)
3. 从磁盘加载所有权限规则(`loadAllPermissionRulesFromDisk`)
4. 检测危险和过宽的 Shell 权限
5. 应用规则到 `ToolPermissionContext`
6. 处理工作目录的符号链接(`process.env.PWD` 与 `getOriginalCwd()` 的差异)
7. 验证并添加附加目录(`--add-dir`)
---
## 附录 A:关键文件索引
| 文件 | 职责 | 关键行 |
|------|------|--------|
| `src/types/permissions.ts` | 权限类型定义 | 16-36 (模式), 54-62 (规则来源), 75-79 (规则), 271-324 (DecisionReason) |
| `src/utils/permissions/PermissionMode.ts` | 权限模式配置与 UI 展示 | 42-91 (模式配置), 97-105 (外部模式过滤) |
| `src/utils/permissions/permissions.ts` | 核心权限检查管道 | 473-956 (hasPermissionsToUseTool), 1158-1319 (hasPermissionsToUseToolInner), 1060-1156 (checkRuleBasedPermissions) |
| `src/utils/permissions/filesystem.ts` | 文件系统权限检查 | 57-68 (危险文件), 74-79 (危险目录), 620-665 (安全检查), 960-1025 (路径匹配), 1479-1605 (内部可编辑路径), 1611-1777 (内部可读路径) |
| `src/utils/permissions/pathValidation.ts` | 路径验证 | 101-123 (沙箱写允许), 141-263 (isPathAllowed), 331-367 (危险删除路径), 373-485 (validatePath) |
| `src/utils/permissions/yoloClassifier.ts` | Auto 模式分类器 | 711-996 (XML 2 阶段分类器), 1012-1306 (classifyYoloAction), 1484-1495 (formatActionForClassifier) |
| `src/utils/permissions/classifierDecision.ts` | 分类器决策与放行列表 | 56-94 (SAFE_YOLO_ALLOWLISTED_TOOLS), 96-100 (isAutoModeAllowlistedTool) |
| `src/utils/permissions/permissionSetup.ts` | 模式切换、危险权限剥离 | 94-147 (isDangerousBashPermission), 157-233 (isDangerousPowerShellPermission), 510-553 (stripDangerousPermissionsForAutoMode), 597-646 (transitionPermissionMode), 872-1033 (initializeToolPermissionContext) |
| `src/utils/permissions/permissionsLoader.ts` | 规则加载与持久化 | 120-133 (loadAllPermissionRulesFromDisk), 229-296 (addPermissionRulesToSettings) |
| `src/utils/permissions/shellRuleMatching.ts` | Shell 规则解析与匹配 | 43-48 (prefix 提取), 90-153 (通配符匹配) |
| `src/utils/permissions/denialTracking.ts` | 拒绝跟踪 | 12-15 (DENIAL_LIMITS), 40-44 (shouldFallbackToPrompting) |
| `src/utils/permissions/shadowedRuleDetection.ts` | 影子规则检测 | 193-234 (detectUnreachableRules) |
| `src/utils/permissions/dangerousPatterns.ts` | 危险命令模式 | 18-42 (CROSS_PLATFORM_CODE_EXEC), 44-80 (DANGEROUS_BASH_PATTERNS) |
| `src/utils/permissions/permissionExplainer.ts` | 权限解释器 | 147-250 (generatePermissionExplanation) |
| `src/hooks/useCanUseTool.tsx` | React 权限 hook | 28-203 (useCanUseTool) |
| `src/utils/sandbox/sandbox-adapter.ts` | 沙箱适配器 | 469-472 (isAutoAllowBashIfSandboxed) |
| `src/utils/hooks.ts` | Hook 执行系统 | 4157-4192 (executePermissionRequestHooks) |
| `src/utils/permissions/bypassPermissionsKillswitch.ts` | 绕过权限禁用开关 | 19-47 (checkAndDisableBypassPermissionsIfNeeded), 74-117 (checkAndDisableAutoModeIfNeeded) |
## 附录 B:数据流图(权限检查)
```
工具调用请求
│
▼
┌──────────────────────────────────────┐
│ hasPermissionsToUseTool() │
│ src/utils/permissions/permissions.ts│
│ │
│ 1. 规则预过滤段 │
│ ├── 全局 deny 规则 │
│ ├── 全局 ask 规则 (sandbox 快速放行)│
│ ├── tool.checkPermissions() │
│ ├── tool.requiresUserInteraction │
│ ├── 内容 ask 规则 │
│ └── 安全检查 │
│ │
│ 2. 权限处理段 │
│ ├── bypassPermissions / plan │
│ └── 全局 allow 规则 │
└──────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ hasPermissionsToUseTool() — 模式后处理│
│ │
│ 3. dontAsk → ask→deny │
│ 4. Auto 模式: │
│ ├─ acceptEdits 快速路径 │
│ ├─ 放行列表 │
│ └─ YOLO 分类器评估 │
│ 5. Headless → hooks → auto-deny │
└──────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ useCanUseTool (React Hook) │
│ src/hooks/useCanUseTool.tsx │
│ │
│ allow → 立即执行 │
│ deny → 拒绝 + 通知 │
│ ask → 交互式处理 │
│ ├─ 协调器检查 │
│ ├─ Swarm Worker 转发 │
│ ├─ 投机性分类器 (Bash) │
│ └─ 用户确认对话框 │
└──────────────────────────────────────┘
```
---
## 附录:AskUserQuestion 独立问答通道
`AskUserQuestion` 用于向用户提出多选题(澄清需求、在方案间做选择)。它**不再借道权限系统的 `ask` 流程**,而是走一条独立的问答通道,与权限确认队列解耦。
### 与权限 `ask` 的区别
| 维度 | 权限式 `ask`(早期实现) | 独立问答通道(当前实现) |
|------|----------------------|----------------------|
| `checkPermissions` 返回 | `behavior: 'ask'` | `behavior: 'allow'` |
| 阻塞方式 | 权限回调回填 `answers` | `call` 内 `await questionService.ask(...)` |
| 本地渲染 | 进 `toolUseConfirmQueue`,由 `PermissionRequest` 渲染 | 独立 overlay(`QuestionPrompt`),与 `toolPermissionOverlay` 并列 |
| 焦点协调 | `tool-permission` 对话框 | `question` 对话框 + `useRegisterOverlay('question')` |
### 运行时组件
- **`src/services/question/questionService.ts`** — 独立问答通道。`ask(questions)` 存入 `pending: Map<id, ...>` 并返回 Promise;`reply` / `reject` 解析对应 Promise;通过 EventEmitter 广播 `asked` / `replied` / `rejected`。
- **`src/tools/AskUserQuestionTool/AskUserQuestionTool.tsx`** — `call` 内 `await questionService.ask(...)`,再把结果按题映射为 `answers`。
- **`src/components/question/QuestionPrompt.tsx`** — 独立 overlay,复用 `QuestionView` / `SubmitQuestionsView` / `use-multiple-choice-state` 渲染,并用 `useKeybindings`(`Tabs` 上下文)绑定多题切换。
- **`src/screens/REPL.tsx`** — 订阅 `asked` 弹出 overlay;订阅 `replied` / `rejected` 关闭 overlay。
### 桥接 / CCR 远程转发
桥接(`BRIDGE_MODE`)连接时,`asked` 事件会**同时**把问题作为 `can_use_tool` control_request 转发给远程用户(claude.ai),与本地 overlay 竞速:
- 远程 `allow` 且带 `updatedInput.answers` → 按题映射后 `questionService.reply(...)`;通用 `allow`(无 answers)降级为每题选第一个选项。
- 远程 `deny` → `questionService.reject(...)`。
- 任一端先应答,都会清掉本地 overlay 并 `cancelRequest` 另一端的 prompt,避免残留。
> 注:因 `checkPermissions` 现返回 `allow`,AskUserQuestion 不再进入 `handleInteractivePermission`,故不会与桥接路径重复转发。
|