Permission 源码解读
模块概述
Permission 模块是 OpenCode 的安全守门人。每当 Agent 试图调用一个工具——读文件、写文件、执行 Bash 命令——Permission 模块都会介入,做出三级判断:自动放行、拦截禁止、还是暂停并询问用户。在”完全自主”和”安全可控”之间找到平衡,是 Permission 模块的核心设计目标。
核心设计选择:
- Effect Deferred 异步模型:ask/reply 通过
Effect.Deferred实现挂起/恢复,天然支持取消和超时 - last-match-wins 规则评估:配置文件中后面的规则覆盖前面的,更符合”先声明通用、再声明例外”的直觉
- Wildcard 通配符匹配:
*映射.*、?映射.,路径分隔符归一化后匹配 - Bus 事件广播:
Event.Asked/Event.Replied通过 Bus 广播,CLI 和 TUI 都可订阅 - 项目级持久化:用户
always决策写入 SQLite,绑定project_id,避免跨项目权限泄漏 - 级联拒绝与批量批准:拒绝一个请求时级联拒绝同 session 所有 pending 请求;
always批准时自动检查并批准其他已满足的 pending 请求
Permission 基于 Effect.ts 的 ServiceMap.Service 架构实现,通过 Bus 事件系统完成异步的”提问-应答”流程,并通过 SQLite 持久化用户的授权决策。
关键文件
| 文件路径 | 行数 | 职责 |
|---|---|---|
src/permission/index.ts | ~326 | 模块主入口:Service 定义、ask/reply 流程、fromConfig 解析、expand 路径展开、级联逻辑 |
src/permission/evaluate.ts | ~15 | 纯函数:last-match-wins 规则评估,findLast 语义 |
src/permission/schema.ts | ~18 | PermissionID 类型定义 |
src/permission/arity.ts | ~163 | BashArity 字典:Bash 命令的参数数量映射 |
src/session/session.sql.ts | — | PermissionTable 定义,SQLite 存储 Ruleset |
src/util/wildcard.ts | — | Wildcard.match / Wildcard.all,通配符模式匹配引擎 |
类型体系
Action — 权限操作三态
Permission 模块用 Zod 枚举定义了三种操作级别:
const Action = z.enum(["allow", "deny", "ask"]).meta({ ref: "PermissionAction" });
- allow:自动放行,Agent 无需等待,工具直接执行
- ask:暂停执行,通过 Deferred 异步等待用户明确批准
- deny:直接拒绝,抛出
DeniedError,Agent 收到错误信号
注意:是
ask而非confirm。ask强调”提问”语义,与下文的Reply类型对应。
Reply — 用户应答三态
当用户回应询问时,使用 Reply 枚举:
const Reply = z.enum(["once", "always", "reject"]);
- once:仅本次放行,下次同类操作仍需询问
- always:记住决策,写入
approved: Ruleset并持久化到 SQLite,后续自动放行 - reject:本次拒绝,Agent 收到
RejectedError
Rule — 单条权限规则
一条权限规则由三个维度构成:
const Rule = z.object({
permission: z.string(), // 工具/操作名称,如 "read"、"bash"、"mcp.github.create_issue"
pattern: z.string(), // 文件路径或命令模式,如 "src/**"、".env"、"rm *"
action: Action, // allow / deny / ask
});
- permission:工具名称。内置工具有
read、edit、bash、glob、grep等;MCP 工具使用server.tool格式(如github.create_issue) - pattern:匹配范围。可以是文件路径(
"src/**")、命令模式("rm *")、或通配符("*"匹配所有) - action:命中规则后的操作
Ruleset — 规则集合
Ruleset 是 Rule[] 的类型别名。权限评估时,所有规则按声明顺序逐一匹配,最后一条匹配的规则生效(last-match-wins)。
type Ruleset = Rule[]
Ruleset 有两个来源:
- configRules:从 Config 文件加载的初始规则
- approvedRules:用户运行时通过 “always” 决策积累的规则
评估时两者合并为一个大数组:evaluate(permission, pattern, configRules, approvedRules)。
PendingEntry — 异步等待中的请求
type PendingEntry = {
deferred: Effect.Deferred<...> // 异步挂起点
request: PermissionRequest // 原始请求信息
}
当 evaluate() 返回 ask 时,Permission 创建一个 PendingEntry 并存入 pending Map。deferred 是 Effect 的异步原语——类似 Promise 但支持外部 resolve/fail。
EDIT_TOOLS — 编辑工具集合
const EDIT_TOOLS = ["edit", "write", "apply_patch", "multiedit"]
这是一个内置常量,用于区分读操作和写操作。某些逻辑(如默认权限策略)根据工具是否属于 EDIT_TOOLS 来决定初始 action。
核心流程
规则评估:last-match-wins(evaluate.ts)
evaluate() 是整个 Permission 模块的决策核心,一个纯粹的、无副作用的函数:
import { Wildcard } from "@/util/wildcard"
type Rule = {
permission: string
pattern: string
action: "allow" | "deny" | "ask"
}
export function evaluate(permission: string, pattern: string, ...rulesets: Rule[][]): Rule {
const rules = rulesets.flat()
const match = rules.findLast(
(rule) => Wildcard.match(permission, rule.permission) && Wildcard.match(pattern, rule.pattern),
)
return match ?? { action: "ask", permission, pattern: "*" }
}
关键设计点:
- 多 Ruleset 展平:接受可变参数
...rulesets,通常传入[configRules, approvedRules],展开为一个大数组 - 双重匹配:每条规则需要
permission和pattern两个维度都匹配才算命中 - findLast 语义:最后一条匹配的规则胜出。这允许”先 deny 所有,再 allow 特定路径”的声明模式
- 默认 ask:无规则匹配时返回
{ action: "ask" }——安全优先,宁可多问一次
Wildcard 匹配引擎(wildcard.ts)
Wildcard.match() 将 glob 模式转为正则表达式进行匹配:
import { sortBy, pipe } from "remeda"
export namespace Wildcard {
export function match(str: string, pattern: string) {
// 路径分隔符归一化:Windows 反斜杠 → 正斜杠
if (str) str = str.replaceAll("\\", "/")
if (pattern) pattern = pattern.replaceAll("\\", "/")
// glob 转正则:
// 1. 转义正则特殊字符(. + ^ $ { } ( ) | [ ] \)
let escaped = pattern
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
.replace(/\*/g, ".*") // * → .* (匹配任意字符序列)
.replace(/\?/g, ".") // ? → . (匹配单个任意字符)
// 处理尾部的 " .*" 模式,使其可选
// 例:"src" 能匹配 "src" 和 "src/xxx"
if (escaped.endsWith(" .*")) {
escaped = escaped.slice(0, -3) + "( .*)?"
}
// 平台适配:Windows 不区分大小写
const flags = process.platform === "win32" ? "si" : "s"
return new RegExp("^" + escaped + "$", flags).test(str)
}
}
匹配规则详解:
| glob 模式 | 正则表达式 | 匹配示例 |
|---|---|---|
* | ^.*$ | 匹配所有字符串 |
src/** | ^src/.*$ | 匹配 src/ 下所有路径 |
.env | ^\.env$ | 精确匹配 .env |
*.ts | ^.*\.ts$ | 匹配所有 .ts 文件 |
rm * | ^rm .*$ | 匹配所有 rm 开头的命令 |
路径归一化:在匹配前将 Windows 的 \ 替换为 /,确保跨平台一致性。s flag 让 . 也能匹配换行符。
fromConfig — 配置解析
fromConfig() 将 Config 中的权限配置解析为 Ruleset,支持两种格式:
// 格式 1:简写格式(仅声明 action)
const config1 = {
permission: {
bash: "allow", // bash 操作全部放行
edit: "deny", // edit 操作全部拒绝
}
}
// 解析为:
// [
// { permission: "bash", pattern: "*", action: "allow" },
// { permission: "edit", pattern: "*", action: "deny" },
// ]
// 格式 2:对象格式(细粒度控制)
const config2 = {
permission: [
{ permission: "edit", pattern: "src/**", action: "allow" },
{ permission: "edit", pattern: ".env", action: "deny" },
{ permission: "bash", pattern: "git *", action: "allow" },
]
}
简写格式在内部被转换为 pattern: "*" 的规则。对象格式支持路径级别的细粒度控制。
expand — 路径展开
expand() 函数在规则匹配前对路径进行展开:
function expand(path: string): string {
// ~ → 用户主目录(homedir)
// $HOME → 实际路径
// ${VAR} → 环境变量值
return path
.replace(/^~/, homedir())
.replace(/\$HOME/g, homedir())
.replace(/\$\{(\w+)\}/g, (_, varName) => process.env[varName] ?? "")
}
这让用户可以在配置中使用 ~/.ssh/* 或 $HOME/projects/** 等路径,Permission 在匹配时自动展开为实际路径。
ask/reply 异步流程
当 evaluate() 返回 ask 时,Permission 进入异步交互:
Agent 工具调用(如 edit("src/app.ts"))
│
▼
Permission.ask({ permission: "edit", patterns: ["src/app.ts"] })
│
├─ disabled("edit") 检查
│ └─ 若工具被全局 deny → 直接抛出 DeniedError
│
├─ evaluate(configRules + approvedRules, "edit", "src/app.ts")
│ ├─ 返回 { action: "allow" } → 直接放行,返回
│ ├─ 返回 { action: "deny" } → 抛出 DeniedError
│ └─ 返回 { action: "ask" } → 继续下面的异步流程
│
├─ 构建 PendingEntry
│ └─ { deferred: Effect.Deferred.make(), request }
│
├─ pending.set(id, entry) // 存入等待队列
│
├─ Bus.publish(Event.Asked, request) // 广播询问事件
│ └─ CLI / TUI 订阅此事件,渲染确认界面
│
└─ Effect.await(deferred) // 挂起,等待用户回应
│ // Agent 的工具调用在此处暂停
│
▼
用户在 CLI 选择 "once" / "always" / "reject"
│
▼
Permission.reply(id, reply)
│
├─ 从 pending Map 取出 PendingEntry
│
├─ 若 reply = "once":
│ ├─ Deferred.succeed() // 解除挂起,工具继续执行
│ └─ pending.delete(id) // 清理等待队列
│
├─ 若 reply = "always":
│ ├─ approved.add(matchedRule) // 添加到已批准规则集
│ ├─ persistToSQLite(approved) // 持久化到 PermissionTable
│ ├─ checkPendingRequests() // 批量检查其他 pending 请求
│ │ └─ 对同一 session 中已满足的 pending 请求自动批准
│ ├─ Deferred.succeed() // 解除挂起
│ └─ pending.delete(id)
│
├─ 若 reply = "reject":
│ ├─ cascadeReject(sessionID) // 级联拒绝同 session 的所有 pending 请求
│ ├─ Deferred.fail(RejectedError) // 当前请求失败
│ └─ pending.delete(id)
│
└─ Bus.publish(Event.Replied, { id, reply }) // 广播应答事件
级联拒绝(cascadeReject)
当用户拒绝一个权限请求时,Permission 会级联拒绝同一 session 中的所有 pending 请求:
function cascadeReject(sessionID: string) {
// 遍历 pending Map
for (const [id, entry] of pending) {
if (entry.request.sessionID === sessionID) {
// 对同一 session 的所有 pending 请求都执行 reject
Deferred.fail(entry.deferred, new RejectedError(entry.request))
pending.delete(id)
Bus.publish(Event.Replied, { id, reply: "reject" })
}
}
}
设计意图:当用户拒绝一个请求时,通常意味着不再信任 Agent 当前的操作序列。级联拒绝避免了一连串无意义的确认提示,让用户可以快速夺回控制权。
always 批量批准
当用户选择 always 时,Permission 不仅批准当前请求,还会检查同 session 中其他 pending 请求是否因新规则而自动满足:
function checkPendingRequests(sessionID: string, newRule: Rule) {
for (const [id, entry] of pending) {
if (entry.request.sessionID === sessionID) {
// 用新规则重新评估 pending 请求
const result = evaluate(
entry.request.permission,
entry.request.pattern,
[newRule], // 新批准的规则
)
if (result.action === "allow") {
// 新规则使得这个 pending 请求也被满足
Deferred.succeed(entry.deferred)
pending.delete(id)
Bus.publish(Event.Replied, { id, reply: "always" })
}
}
}
}
场景示例:Agent 连续发起 3 个 edit("src/xxx") 请求,第一个触发 ask。用户选择 always,后两个请求因新规则自动满足,无需再次确认。
disabled — 工具全局禁用检查
disabled() 函数在 ask() 的最早期调用,检查工具是否被全局 deny:
function disabled(tool: string): boolean {
// 在所有 ruleset 中查找是否有 { permission: tool, pattern: "*", action: "deny" }
// 这是一种快速拒绝路径,跳过完整的 evaluate 流程
const rules = [...configRules, ...approvedRules]
const result = evaluate(tool, "*", rules)
return result.action === "deny"
}
如果工具被 disabled() 返回 true,ask() 直接抛出 DeniedError,不进入异步等待流程。这是一个性能优化——避免为必定被拒绝的请求创建 Deferred 和 Bus 事件。
BashArity — Bash 命令参数数量
arity.ts 维护了一个 Bash 命令参数数量字典:
const BashArity: Record<string, number> = {
"rm": 1, // rm 至少需要 1 个参数(文件名)
"git": 1, // git 至少需要 1 个参数(子命令)
"npm": 1, // npm 至少需要 1 个参数(子命令)
"node": 1, // node 至少需要 1 个参数(脚本路径)
"cat": 1, // cat 至少需要 1 个参数(文件名)
"echo": 0, // echo 可以无参数
"ls": 0, // ls 可以无参数
// ... 更多命令
}
这个字典用于权限模式匹配时的命令分割——确定 Bash 命令的”工具名”和”参数”分界点。例如 rm -rf /tmp/test 中,rm 是工具名,-rf /tmp/test 是参数。Arity 信息帮助 Permission 正确地将命令分割为 permission 和 pattern 两部分。
用户决策持久化
用户选择 always 后的规则持久化到 SQLite:
// session.sql.ts
const PermissionTable = sqliteTable("permission", {
project_id: text("project_id").primaryKey(),
// ...Timestamps
data: text("mode", { mode: "json" }).$type<Permission.Ruleset>(),
});
- 每个项目(
project_id)独立存储一份Ruleset fromConfig()从配置加载初始规则,approved规则在运行时追加list()返回当前项目的全部持久化规则(config + approved 的合并结果)- 持久化与 Session 共享 SQLite 数据库,通过
project_id关联
错误类型
Permission 模块定义了三种错误类型,分别对应不同场景:
// 用户本次拒绝操作
class RejectedError {
request: PermissionRequest // 被拒绝的原始请求
}
// 用户提供了修正内容
class CorrectedError {
feedback: string // 用户的修正说明
request: PermissionRequest // 原始请求
}
// 操作被规则直接 deny
class DeniedError {
ruleset: Ruleset // 触发 deny 的规则集(供调试)
request: PermissionRequest // 被拒绝的请求
}
- RejectedError:用户主动拒绝。Agent 捕获后可以调整策略或放弃操作
- CorrectedError:用户不仅拒绝,还提供了修正内容。
feedback字段包含用户的具体指示,Agent 可以据此重新规划。这在用户不同意 Agent 的操作方向时特别有用 - DeniedError:配置规则直接 deny。
ruleset字段帮助开发者调试”为什么操作被拒绝”
调用链示例
链路 1:Agent 编辑文件(ask → always)
Agent 发起工具调用 edit(path="src/app.ts")
│
▼
Permission.ask({
permission: "edit",
patterns: ["src/app.ts"],
sessionID: "sess-abc",
})
│
├─ disabled("edit") → false(未被全局禁用)
│
├─ evaluate("edit", "src/app.ts", configRules, approvedRules)
│ ├─ 遍历所有规则(findLast 语义):
│ │ rule: { permission: "read", pattern: "*", action: "allow" } → permission 不匹配 "edit"
│ │ rule: { permission: "edit", pattern: ".env", action: "deny" } → pattern 不匹配 "src/app.ts"
│ │ 无更多匹配规则
│ └─ 返回默认: { action: "ask", permission: "edit", pattern: "*" }
│
├─ action = "ask" → 进入异步流程
│ ├─ Deferred.make() → 创建异步挂起点
│ ├─ pending.set("req-1", { deferred, request })
│ ├─ Bus.publish(Event.Asked, { id: "req-1", permission: "edit", pattern: "src/app.ts" })
│ └─ await(deferred) → Agent 工具调用暂停
│
▼
CLI 渲染确认提示:
"Agent 请求编辑 src/app.ts [允许一次] [始终允许] [拒绝]"
│
▼
用户选择 "始终允许"
│
▼
Permission.reply("req-1", "always")
│
├─ approved.add({ permission: "edit", pattern: "src/app.ts", action: "allow" })
├─ PermissionTable.update(projectID, approved) → SQLite 写入
│
├─ checkPendingRequests("sess-abc", newRule)
│ └─ 遍历 pending Map,检查同 session 是否有其他请求被新规则满足
│ └─ 无其他 pending 请求
│
├─ Deferred.succeed(deferred) → Agent 工具调用恢复
├─ pending.delete("req-1")
└─ Bus.publish(Event.Replied, { id: "req-1", reply: "always" })
链路 2:Bash 命令被 deny 规则拦截
Agent 发起工具调用 bash(command="rm -rf /")
│
▼
Permission.ask({
permission: "bash",
patterns: ["rm -rf /"],
sessionID: "sess-abc",
})
│
├─ disabled("bash") → false
│
├─ evaluate("bash", "rm -rf /", configRules, approvedRules)
│ ├─ configRules 中:
│ │ { permission: "bash", pattern: "rm -rf *", action: "deny" }
│ │ → Wildcard.match("rm -rf /", "rm -rf *") → true ✓
│ │ → Wildcard.match("bash", "bash") → true ✓
│ │ → 匹配!action = "deny"
│ └─ 返回: { permission: "bash", pattern: "rm -rf *", action: "deny" }
│
├─ action = "deny" → 直接抛出 DeniedError
│ └─ 携带触发规则 { permission: "bash", pattern: "rm -rf *", action: "deny" }
│
▼
Agent 捕获 DeniedError → 调整策略,不执行该命令
链路 3:MCP 工具调用(ask → once)
Agent 调用 MCP 工具 github.create_issue
│
▼
Permission.ask({
permission: "github.create_issue",
patterns: ["create issue with title..."],
sessionID: "sess-abc",
})
│
├─ disabled("github.create_issue") → false
│
├─ evaluate("github.create_issue", "...", configRules, approvedRules)
│ └─ 无匹配规则 → 默认 { action: "ask", ... }
│
├─ 异步流程 → Bus.publish(Event.Asked)
│
▼
CLI 渲染确认提示:
"Agent 请求调用 github.create_issue [允许一次] [始终允许] [拒绝]"
│
▼
用户选择 "允许一次"
│
▼
Permission.reply("req-2", "once")
│
├─ Deferred.succeed() → Agent 工具调用恢复
├─ pending.delete("req-2")
└─ Bus.publish(Event.Replied, { id: "req-2", reply: "once" })
│
▼
下次 Agent 调用 github.create_issue → 仍需询问(因为只批准了 "once")
链路 4:级联拒绝场景
Agent 并行发起 3 个工具调用(同一 session):
req-1: edit("src/a.ts") → ask(pending 中)
req-2: edit("src/b.ts") → ask(pending 中)
req-3: bash("npm test") → ask(pending 中)
pending Map:
req-1 → { deferred: d1, request: { sessionID: "sess-abc", ... } }
req-2 → { deferred: d2, request: { sessionID: "sess-abc", ... } }
req-3 → { deferred: d3, request: { sessionID: "sess-abc", ... } }
用户对 req-1 选择 "拒绝"
│
▼
Permission.reply("req-1", "reject")
│
├─ Deferred.fail(d1, RejectedError) → req-1 失败
│
├─ cascadeReject("sess-abc")
│ ├─ req-2 属于同一 session → Deferred.fail(d2, RejectedError)
│ │ └─ Bus.publish(Event.Replied, { id: "req-2", reply: "reject" })
│ └─ req-3 属于同一 session → Deferred.fail(d3, RejectedError)
│ └─ Bus.publish(Event.Replied, { id: "req-3", reply: "reject" })
│
└─ 3 个请求全部被拒绝,Agent 收到 3 个 RejectedError → 停止当前操作序列
链路 5:always 批量批准场景
Agent 连续发起 3 个 edit 请求(同一 session):
req-1: edit("src/a.ts") → ask(pending 中)
req-2: edit("src/b.ts") → ask(pending 中)
req-3: edit("src/c.ts") → ask(pending 中)
用户对 req-1 选择 "始终允许"
│
▼
Permission.reply("req-1", "always")
│
├─ approved.add({ permission: "edit", pattern: "src/a.ts", action: "allow" })
│
├─ checkPendingRequests("sess-abc", newRule)
│ ├─ req-2: evaluate("edit", "src/b.ts", [newRule])
│ │ → pattern "src/a.ts" 不匹配 "src/b.ts" → 不满足
│ └─ req-3: evaluate("edit", "src/c.ts", [newRule])
│ → pattern "src/a.ts" 不匹配 "src/c.ts" → 不满足
│
└─ 只有 req-1 被批准,req-2/req-3 仍需用户确认
--- 对比场景:用户选择 allow "src/**" 模式 ---
假设规则是 { permission: "edit", pattern: "src/**", action: "allow" }
checkPendingRequests("sess-abc", newRule)
├─ req-2: evaluate("edit", "src/b.ts", [newRule])
│ → Wildcard.match("src/b.ts", "src/**") → true ✓
│ → 自动批准!Deferred.succeed(d2)
└─ req-3: evaluate("edit", "src/c.ts", [newRule])
→ Wildcard.match("src/c.ts", "src/**") → true ✓
→ 自动批准!Deferred.succeed(d3)
链路 6:Doom Loop 检测中的 Permission 调用
SessionProcessor 检测到 Agent 连续 3 次调用同一工具(相同参数)
│
▼
Permission.ask({
permission: "doom_loop",
patterns: [toolName], // 如 ["edit"]
sessionID: "sess-abc",
})
│
├─ evaluate("doom_loop", "edit", configRules, approvedRules)
│ └─ 通常无专门规则 → 默认 ask
│
├─ CLI 提示:
│ "Agent 连续 3 次调用 edit(相同参数),是否继续?"
│
▼
用户选择 "始终允许"
│ → approved.add({ permission: "doom_loop", pattern: "edit", action: "allow" })
│ → 后续同一工具的 doom loop 不再询问
设计取舍
| 决策 | 理由 |
|---|---|
| last-match-wins vs first-match-wins | 选择 last-match-wins 是因为配置文件中后面的规则通常更具体(如先 deny *,再 allow src/**),更符合”先声明通用、再声明例外”的直觉。这也与 Apache、Nginx 等成熟系统的规则策略一致 |
| Effect Deferred 异步模型 | 相比回调或 Promise,Effect Deferred 天然支持取消(通过 AbortController)和超时,适合 Agent 长时间运行场景。Deferred 可以从外部 resolve/fail,正好对应 Permission 的 ask/reply 模式 |
| 项目级持久化 | 权限决策绑定 project_id 而非全局,避免跨项目权限泄漏。在项目 A 中 always 的权限不会自动应用到项目 B |
| 级联拒绝 | 拒绝一个请求时级联拒绝同 session 所有 pending 请求,避免 Agent 在被拒绝后继续执行无意义的操作序列 |
| always 批量检查 | always 后自动检查同 session 其他 pending 请求是否满足新规则,减少用户重复确认的次数 |
| Wildcard 正则转换 | 将 glob 模式转为正则表达式而非手写 glob 匹配器,代码简洁且正确性有保障。性能在 Permission 场景下不是瓶颈(规则数量通常少于 100) |
| 路径归一化 | 匹配前将 \ 替换为 /,确保 Windows 和 Unix 系统上的规则行为一致 |
| 纯函数 evaluate | evaluate() 是无副作用的纯函数,便于测试和理解。所有副作用(持久化、事件广播)都在 ask()/reply() 中处理 |
| catchall 默认 ask | Config 中 Permission Schema 的 catchall(PermissionAction.default("ask")) 确保新增工具类型不会意外放行 |
| separate evaluate.ts | 将评估逻辑独立为 15 行的纯函数文件,与 index.ts 的 Service 逻辑解耦,方便单元测试 |
与其他模块的关系
- Agent:Agent 在执行每个工具调用前调用
Permission.ask(),等待放行或拒绝。Agent 捕获RejectedError/DeniedError后可以调整策略。Doom Loop 检测也通过Permission.ask()请求用户确认 - Config:权限规则通过
Permission.fromConfig()从 Config 的permission字段加载。Config 的PermissionSchema 定义了 14+ 种操作的独立规则,catchall确保新工具安全 - CLI / TUI:订阅
Event.Asked渲染确认界面;用户操作触发Permission.reply()。TUI 也可以通过Permission.list()展示当前的权限规则 - MCP:MCP 工具调用同样经过 Permission 管控,
permission字段使用server.tool格式(如github.create_issue) - Session:
PermissionTable定义在session.sql.ts,与 Session 共享 SQLite 数据库。ask()的sessionID参数用于级联拒绝和批量批准的分组依据 - Storage:持久化的
approvedRuleset 通过 Storage 层写入 SQLite 的PermissionTable - Bus:
Event.Asked和Event.Replied通过 Bus 广播,所有 UI 层(CLI、TUI、API)都可以订阅这些事件 - Wildcard(util):
evaluate()依赖Wildcard.match()进行通配符模式匹配,是 Permission 决策的基础能力