9.4 权限请求的交互流程

模型: claude-opus-4-6 (anthropic/claude-opus-4-6) 生成日期: 2026-02-17


当权限评估结果为 ask 时,OpenCode 需要暂停 Agent 的执行,将决策权交给用户。本节将详细解析这个交互流程——从权限请求的发起、TUI 弹窗展示、到用户响应和后续处理。

9.4.1 PermissionNext.Request 数据结构

权限请求(Request)是 ask 流程的核心数据结构:

export const Request = z.object({
  id: Identifier.schema("permission"),     // 唯一标识符
  sessionID: Identifier.schema("session"), // 所属 Session ID
  permission: z.string(),                  // 权限类型(如 "bash")
  patterns: z.string().array(),            // 需要审批的模式列表
  metadata: z.record(z.string(), z.any()), // 元数据(用于 TUI 展示)
  always: z.string().array(),              // "always" 时应授权的模式
  tool: z.object({                         // 工具调用信息(可选)
    messageID: z.string(),
    callID: z.string(),
  }).optional(),
})

各字段的用途:

  • id:每个权限请求的唯一标识,用于后续回复时定位请求。

  • sessionID:关联到发起请求的 Session,用于在 always 回复时批量审批同 Session 的请求。

  • permission:权限类型名称,如 "bash""edit""read"

  • patterns:需要审批的具体模式列表。例如,一个 Bash 命令可能包含多个模式——命令本身("npm install")和它涉及的外部目录路径。所有模式都必须被批准,操作才能继续。

  • metadata:供 TUI 展示的额外信息,如工具名称、命令内容、文件路径等。不参与权限评估,纯粹用于用户界面。

  • always:当用户选择 always 时,应该被永久授权的模式列表。它通常与 patterns 相同,但也可以不同——例如,某些敏感模式可能只允许"一次性"授权而不能被"永久"授权。

  • tool:可选的工具调用上下文信息,包含消息 ID 和调用 ID,用于在 TUI 中精确定位到触发请求的工具调用。

9.4.2 Permission 请求 → TUI 弹窗 → 用户响应 → 存储决策

完整的权限请求交互流程如下:

第一步:ask() 发起请求

ask() 函数是权限请求的入口点。它接受权限信息和 Ruleset,评估每个模式的权限状态:

关键细节:

  1. 逐模式评估:每个 pattern 都单独评估。只要有一个模式的评估结果是 deny,整个操作就被拒绝。

  2. Promise 暂停:当评估结果为 ask 时,ask() 返回一个 Promise。这个 Promise 的 resolvereject 函数被存储在 pending Map 中,等待 reply() 调用时解开——这是经典的"外部 resolve"模式。

  3. 事件通知:通过 Bus.publish(Event.Asked, ...) 通知 TUI 有新的权限请求等待处理。

  4. 已批准规则参与评估:注意 evaluate() 的第四个参数 s.approved——之前通过 always 批准的规则也参与评估,避免重复询问。

第二步:TUI 展示弹窗

TUI 层(在 cli/cmd/tui/ 中实现)订阅了 permission.asked 事件。当事件到来时,TUI 会弹出一个权限确认对话框,展示:

  • 权限类型(如"Bash 命令执行")

  • 具体的操作内容(如要执行的命令、要修改的文件路径)

  • 三个选项:Once(仅此一次)、Always(始终允许)、Reject(拒绝)

第三步:reply() 处理用户响应

用户的选择通过 reply() 函数传回权限系统:

这里有几个值得关注的设计决策:

级联拒绝:当用户拒绝一个权限请求时,同一 Session 中所有等待中的权限请求都会被自动拒绝。这是因为一旦用户明确拒绝了某类操作,Agent 的执行流程可能已经不再有意义——等待中的请求很可能是同一个操作链中的后续步骤。

CorrectedError vs RejectedError:用户拒绝时可以附带一条消息。如果附带了消息,抛出 CorrectedError(Agent 会看到用户的反馈并尝试调整策略);否则抛出 RejectedError(Agent 只知道操作被拒绝了)。

这个区分很巧妙——CorrectedError 的消息会被 LLM 看到并据此调整后续行为,而 RejectedError 只是简单的中断。

9.4.3 arity.ts——权限粒度控制

当 Bash 工具请求权限时,需要确定"命令"的粒度——用户在 TUI 中看到的到底是什么?是完整的命令 npm install lodash --save,还是简化后的 npm install

BashArity 模块解决了这个问题。它维护了一个庞大的命令元数 (arity) 字典,定义了每个命令的"有意义前缀"长度:

prefix() 函数的工作原理:

  1. 将命令拆分为 token 数组(如 ["npm", "run", "dev", "--port", "3000"])。

  2. 从长到短尝试匹配 ARITY 字典中的前缀。

  3. 找到匹配后,返回对应长度的 token 子数组。

  4. 如果没有匹配,默认返回第一个 token。

匹配示例:

完整命令
匹配前缀
Arity
权限模式

cat /etc/passwd

cat

1

"cat"

git checkout main

git

2

"git checkout"

npm run dev

npm run

3

"npm run dev"

npm install lodash

npm

2

"npm install"

docker compose up -d

docker compose

3

"docker compose up"

python script.py

python

2

"python script.py"

这个设计的精妙之处在于 最长前缀匹配

  • npm 的 arity 是 2,所以 npm install 作为一个整体来审批。

  • npm run 的 arity 是 3,所以 npm run dev 包含了具体的脚本名。

这确保了权限审批的粒度是"人类可理解"的——用户看到的不是模糊的 npm,也不是冗长的完整命令行,而是恰好能表达意图的命令前缀。

ARITY 字典覆盖了 100 多个常见命令,涵盖了包管理器(npm、pip、cargo)、容器工具(docker、podman)、版本控制(git)、云平台 CLI(aws、gcloud、az)等常见工具链。


本节小结

权限请求的完整交互流程是:ask() 评估权限 → Bus.publish(Event.Asked) 通知 TUI → TUI 展示弹窗 → 用户选择 → reply() 处理响应 → resolve/reject Promise 恢复/中断 Agent 执行。reject 会级联拒绝同 Session 的所有等待请求,并通过 CorrectedError/RejectedError 的区分让 Agent 获得不同程度的反馈。BashArity 模块通过命令元数字典控制 Bash 权限的展示粒度,确保用户看到的是"人类可理解"的命令前缀而非完整命令行。

Last updated