上一篇跟着模型返回的 tool_use 走完了 Tools 子系统,知道了 是工具递给权限系统的一条信号。这一篇看闸门本身:工具执行前,权限系统按什么顺序裁决?三种模式分别拦什么?硬拦截和「需要确认」有何不同?魔改时该改配置还是改 checker.py?
一道闸门:先硬拦,再看模式
是工具执行前的统一闸门:先硬拦敏感路径与 deny 列表,再按模式决定「放行 / 弹确认 / 直接拒绝」。不要在每个工具里散落 if——策略集中在一处。
PRE_TOOL_USE hook
│
▼
registry.get + model_validate
│
▼
抽取 file_path / command
│
▼
PermissionChecker.evaluate(...)
│
┌────┼────────┐
│ │ │
放行 确认 硬拦
│ │ │
▼ ▼ ▼
execute prompt 错误 ToolResult
│
用户同意? → execute / 拒绝
下面这个交互演示把九步决策瀑布跑一遍:选一个场景,看裁决发生在第几步、出口是哪一个。
read_file 读普通文件:前 6 步全部未命中,第 7 步 is_read_only 放行,不弹确认。
正在检查第 1 道,共 9 道Checking 1 of 9
- SENSITIVE_PATH_PATTERNS命中内置敏感路径排队Queued
- denied_tools工具名被全局禁用排队Queued
- allowed_tools工具名被显式放行排队Queued
- path_rules · allow=false路径规则拒绝排队Queued
- denied_commands命令命中禁用模式排队Queued
- mode == full_auto全自动模式放行剩余排队Queued
- is_read_only == True只读工具免确认排队Queued
- mode == plan规划模式拦截写操作排队Queued
- 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。
裁决只有三种出口
@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()。越靠前优先级越高:
- 敏感路径 → 硬拦(全模式)
- tool 在 → 硬拦
- tool 在 → 放行(仍躲不过第 1 步)
- 匹配且
allow=False→ 硬拦 - command 匹配 → 硬拦
mode == full_auto→ 放行剩余is_read_only == True→ 放行mode == plan→ 硬拦 mutatingmode == 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 之后:
_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 能配什么
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 与确认弹窗段。