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

一道闸门:先硬拦,再看模式

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

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

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

图Fig. 九道检查,第一个命中的说了算交互演示 · 决策瀑布 · 9 道检查Interactive · decision waterfall · 9 checks
权限场景Permission scenario

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

正在检查第 1 道,共 9 道Checking 1 of 9

  1. SENSITIVE_PATH_PATTERNS命中内置敏感路径
    排队Queued
  2. denied_tools工具名被全局禁用
    排队Queued
  3. allowed_tools工具名被显式放行
    排队Queued
  4. path_rules · allow=false路径规则拒绝
    排队Queued
  5. denied_commands命令命中禁用模式
    排队Queued
  6. mode == full_auto全自动模式放行剩余
    排队Queued
  7. is_read_only == True只读工具免确认
    排队Queued
  8. mode == plan规划模式拦截写操作
    排队Queued
  9. mode == default写操作需人工确认
    排队Queued

源码地图

  • permissions/modes.py:三种模式枚举: / / 。
  • permissions/checker.py:PermissionChecker、、敏感路径硬拦。
  • config/settings.py::mode、deny/allow 列表、路径与命令规则。
  • engine/query.py: 里调用 evaluate + 处理确认弹窗。
  • commands/registry.py:/permissions、/plan 切换模式并重建 checker。

三种模式各拦什么

  • :只读工具通常直接允许;写 / 执行类工具需要确认()。对应日常交互开发。
  • :只读工具允许;写 / 执行类工具直接拦截(不弹确认)。只规划不改盘。
  • :只读工具允许;写 / 执行类工具直接允许。对应自动化 / 信任环境。
口诀

不是「关闭安全」——敏感路径硬拦、、 在所有模式下仍然生效。

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

裁决只有三种出口

例Example permission-decision.py · simplifiedpython
@dataclass(frozen=True)
class PermissionDecision:
  allowed: bool
  requires_confirmation: bool = False
  reason: str = ""
  • allowed=True:引擎直接 execute。
  • allowed=False + requires_confirmation=True:调 permission_prompt;同意则执行,拒绝则错误返回。
  • allowed=False 且无确认:硬拦,直接错误 ToolResult,不弹窗。
易混点

下 mutating 工具返回的是「未允许 + 需要确认」,不是「已允许」。引擎看到 才会弹窗; 模式 mutating 是硬拦(无确认)。

evaluate() 的九步,从上往下看

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

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

每一步的职责:

  • 敏感路径:*/.ssh/*、*/.aws/credentials、*/.openharness/credentials.json 等;配置与模式都无法覆盖。防 prompt injection 读密钥。
  • denied_tools:按工具名全局禁。
  • allowed_tools:按名放行;但敏感路径已在前面拦死。
  • path_rules:fnmatch 路径规则;当前实现主要执行 allow=False 的 deny。
  • denied_commands:对 bash 类命令做 fnmatch(如拦危险 shell)。
  • 模式分支: / 只读 / / 确认。

引擎拿到裁决之后

(engine/query.py)在 validate 之后:

例Example 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 的目录根,保证 对内置工具一致生效。

PermissionSettings 能配什么

例Example 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 换新实例。

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

  • 会话里少点确认 → /permissions full_auto(仍过硬拦)
  • 只规划不改文件 → /plan on 或 mode=plan
  • 禁用某个危险工具 → 配置 denied_tools: ["bash"]
  • 禁止某类 shell → 写 fnmatch 模式
  • 禁止碰某目录 → path_rules: [{pattern, allow: false}]
  • 永不让读密钥 → 已内置敏感路径;可扩

工具说自己只读,权限系统就信

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

  • True → / 下通常免确认(仍受敏感路径 / deny 约束)
  • False → 要确认; 硬拦; 直接过
安全洞警告

错误地把写工具标成只读 = 安全洞。魔改新工具时,宁可 False 多弹一次,不要谎报只读。

想改规则,该动哪里

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

精读清单

  • src/openharness/permissions/checker.py:evaluate() 全文,顺序即策略。
  • src/openharness/permissions/modes.py:三种模式枚举。
  • src/openharness/config/settings.py 的 。
  • src/openharness/engine/query.py 的 _execute_tool_call() 中 evaluate 与确认弹窗段。