跳转到内容

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~18PermissionID 类型定义
src/permission/arity.ts~163BashArity 字典:Bash 命令的参数数量映射
src/session/session.sql.tsPermissionTable 定义,SQLite 存储 Ruleset
src/util/wildcard.tsWildcard.match / Wildcard.all,通配符模式匹配引擎

类型体系

Action — 权限操作三态

Permission 模块用 Zod 枚举定义了三种操作级别:

const Action = z.enum(["allow", "deny", "ask"]).meta({ ref: "PermissionAction" });
  • allow:自动放行,Agent 无需等待,工具直接执行
  • ask:暂停执行,通过 Deferred 异步等待用户明确批准
  • deny:直接拒绝,抛出 DeniedError,Agent 收到错误信号

注意:是 ask 而非 confirmask 强调”提问”语义,与下文的 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:工具名称。内置工具有 readeditbashglobgrep 等;MCP 工具使用 server.tool 格式(如 github.create_issue
  • pattern:匹配范围。可以是文件路径("src/**")、命令模式("rm *")、或通配符("*" 匹配所有)
  • action:命中规则后的操作

Ruleset — 规则集合

RulesetRule[] 的类型别名。权限评估时,所有规则按声明顺序逐一匹配,最后一条匹配的规则生效(last-match-wins)。

type Ruleset = Rule[]

Ruleset 有两个来源:

  1. configRules:从 Config 文件加载的初始规则
  2. 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: "*" }
}

关键设计点:

  1. 多 Ruleset 展平:接受可变参数 ...rulesets,通常传入 [configRules, approvedRules],展开为一个大数组
  2. 双重匹配:每条规则需要 permissionpattern 两个维度都匹配才算命中
  3. findLast 语义:最后一条匹配的规则胜出。这允许”先 deny 所有,再 allow 特定路径”的声明模式
  4. 默认 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 系统上的规则行为一致
纯函数 evaluateevaluate() 是无副作用的纯函数,便于测试和理解。所有副作用(持久化、事件广播)都在 ask()/reply() 中处理
catchall 默认 askConfig 中 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 的 Permission Schema 定义了 14+ 种操作的独立规则,catchall 确保新工具安全
  • CLI / TUI:订阅 Event.Asked 渲染确认界面;用户操作触发 Permission.reply()。TUI 也可以通过 Permission.list() 展示当前的权限规则
  • MCP:MCP 工具调用同样经过 Permission 管控,permission 字段使用 server.tool 格式(如 github.create_issue
  • SessionPermissionTable 定义在 session.sql.ts,与 Session 共享 SQLite 数据库。ask()sessionID 参数用于级联拒绝和批量批准的分组依据
  • Storage:持久化的 approved Ruleset 通过 Storage 层写入 SQLite 的 PermissionTable
  • BusEvent.AskedEvent.Replied 通过 Bus 广播,所有 UI 层(CLI、TUI、API)都可以订阅这些事件
  • Wildcard(util)evaluate() 依赖 Wildcard.match() 进行通配符模式匹配,是 Permission 决策的基础能力