← 返回文章

SOURCE NOTE · PERMISSIONS

Lesson 4: Permissions 子系统

工具执行前的统一闸门:先硬拦敏感路径与 deny 列表,再按模式决定放行、弹确认还是直接拒绝。

上一课跟着模型返回的 tool_use 走完了 Tools 子系统,知道了 is_read_only 是工具递给权限系统的一条信号。这一课拆开闸门本身:工具执行前,权限系统按什么顺序裁决?三种模式分别拦什么?硬拦截和「需要确认」有何不同?魔改时该改配置还是改 checker.py

一句话概括

PermissionChecker.evaluate() 是工具执行前的统一闸门:先硬拦敏感路径与 deny 列表,再按模式决定「放行 / 弹确认 / 直接拒绝」。不要在每个工具里散落 if——策略集中在一处。

PRE_TOOL_USE hook
        │
        ▼
registry.get + model_validate
        │
        ▼
抽取 file_path / command
        │
        ▼
PermissionChecker.evaluate(...)
        │
   ┌────┼────────┐
   │    │        │
  放行  确认     硬拦
   │    │        │
   ▼    ▼        ▼
execute prompt  错误 ToolResult
        │
   用户同意? → execute / 拒绝

下面这个交互演示把九步决策瀑布跑一遍:选一个场景,看裁决发生在第几步、出口是哪一个。

INTERACTIVE TRACE · DECISION WATERFALL

9 CHECKS

read_file 读普通文件:前 6 步全部未命中,第 7 步 is_read_only 放行,不弹确认。

  1. 01

    SENSITIVE_PATH_PATTERNS

    命中内置敏感路径

    排队
  2. 02

    denied_tools

    工具名被全局禁用

    排队
  3. 03

    allowed_tools

    工具名被显式放行

    排队
  4. 04

    path_rules · allow=false

    路径规则拒绝

    排队
  5. 05

    denied_commands

    命令命中禁用模式

    排队
  6. 06

    mode == full_auto

    全自动模式放行剩余

    排队
  7. 07

    is_read_only == True

    只读工具免确认

    排队
  8. 08

    mode == plan

    规划模式拦截写操作

    排队
  9. 09

    mode == default

    写操作需人工确认

    排队

核心文件

三种模式

切换方式:TUI 里 /permissions default|plan|full_auto,或 /plan on|off;CLI 可用 --permission-mode

PermissionDecision:三种出口

permission-decision.py · simplifiedpython
@dataclass(frozen=True)
class PermissionDecision:
  allowed: bool
  requires_confirmation: bool = False
  reason: str = ""

evaluate() 决策瀑布:顺序即安全策略

顺序写死在 checker.pyevaluate()。越靠前优先级越高:

  1. 敏感路径 SENSITIVE_PATH_PATTERNS → 硬拦(全模式)
  2. tool 在 denied_tools → 硬拦
  3. tool 在 allowed_tools → 放行(仍躲不过第 1 步)
  4. path_rules 匹配且 allow=False → 硬拦
  5. command 匹配 denied_commands → 硬拦
  6. mode == full_auto → 放行剩余
  7. is_read_only == True → 放行
  8. mode == plan → 硬拦 mutating
  9. mode == default → 需确认(mutating)

每一步的职责:

引擎如何消费决策

_execute_tool_callengine/query.py)在 validate 之后:

execute-tool-call.py · simplifiedpython
_file_path = _resolve_permission_file_path(cwd, tool_input, parsed)
_command   = _extract_permission_command(tool_input, parsed)

decision = permission_checker.evaluate(
  tool_name,
  is_read_only=tool.is_read_only(parsed),
  file_path=_file_path,
  command=_command,
)

if not decision.allowed:
  if decision.requires_confirmation and permission_prompt:
      confirmed = await permission_prompt(tool_name, decision.reason)
      if not confirmed:
          return ToolResultBlock(..., is_error=True)
  else:
      return ToolResultBlock(..., is_error=True)  # 硬拦

# 通过 → execute(...)

路径抽取会统一 file_path / path / glob·grep 的目录根,保证 path_rules 对内置工具一致生效。

配置面:PermissionSettings

permission-settings.py · simplifiedpython
class PermissionSettings(BaseModel):
  mode: PermissionMode = PermissionMode.DEFAULT
  allowed_tools: list[str] = []
  denied_tools: list[str] = []
  path_rules: list[PathRuleConfig] = []   # pattern + allow
  denied_commands: list[str] = []

挂在全局 settings 的 permission 字段。会话启动时 ui/runtime.py 用它构造 PermissionChecker;用户改模式后会 set_permission_checker 换新实例。

常见诉求对应的优先手段:

与 is_read_only 的契约

工具自己声明「这次调用是否只读」:

魔改关键点

要加一条「永远不能做」的规则 → 插在瀑布靠前(或扩展 SENSITIVE_PATH_PATTERNS)。要改「日常是否弹窗」→ 改 mode 或 is_read_only,别到处写 if

源码精读