0-1. 学习目标与一句话

权限系统是 tool runtime 的安全闸门:工具调用 ctx.ask,权限服务按 ruleset 和已批准记录决定 allow、deny 或发布 permission.asked 等用户回复。

来源:packages/opencode/src/session/tools.ts:64-72packages/opencode/src/permission/index.ts:161-196packages/opencode/src/permission/evaluate.ts:9-15

2-5. 它的位置与最小源码路径

源码路径卡片

  1. permission/index.ts:19-45:Action、Rule、Request。
  2. permission/evaluate.ts:9-15:最后匹配规则决定 action,默认 ask。
  3. permission/index.ts:161-196:ask 创建 pending request 并等待 Deferred。
  4. permission/index.ts:198-254:reply 处理 once/always/reject。
  5. session/tools.ts:64-72:tool context 接入权限服务。
  6. agent/agent.ts:103-160:默认 agent 和 plan agent 权限。
  7. cli/cmd/run.ts:736-755:非交互 CLI 默认 reject。

6. 用户输入到 agent 行动的整体链路

model emits tool-call
  -> tool execute(params, ctx)
  -> ctx.ask({ permission, patterns })
  -> Permission.ask
  -> evaluate ruleset + approved
     - allow: continue
     - deny: throw DeniedError
     - ask: publish permission.asked and wait
  -> UI/CLI replies once/always/reject
  -> Deferred succeed/fail
  -> tool continues or fails

Java 类比

Rule 像 Spring Security 的配置属性;evaluate 像 voter;Permission.ask 像可异步等待用户批准的 AccessDecisionManager;DeferredCompletableFuture

7. 核心源码逐段讲解

权限类型

export const Action = Schema.Literals(["allow", "deny", "ask"])

export const Rule = Schema.Struct({
  permission: Schema.String,
  pattern: Schema.String,
  action: Action,
})

export class Request extends Schema.Class<Request>("PermissionRequest")({
  id: PermissionID,
  sessionID: SessionID,
  permission: Schema.String,
  patterns: Schema.Array(Schema.String),
  metadata: Schema.Record(Schema.String, Schema.Unknown),
  always: Schema.Array(Schema.String),
  tool: Schema.optional(Schema.Struct({ messageID: MessageID, callID: Schema.String })),
}) {}

路径:packages/opencode/src/permission/index.ts:19-45

规则计算

const match = rules.findLast(
  (rule) => Wildcard.match(permission, rule.permission) && Wildcard.match(pattern, rule.pattern),
)
return match ?? { action: "ask", permission, pattern: "*" }

路径:packages/opencode/src/permission/evaluate.ts:11-15。后写规则优先,默认 ask。

ask

for (const pattern of request.patterns) {
  const rule = evaluate(request.permission, pattern, ruleset, approved)
  if (rule.action === "deny") return yield* new DeniedError(...)
  if (rule.action === "allow") continue
  needsAsk = true
}

if (!needsAsk) return
const deferred = yield* Deferred.make<void, RejectedError | CorrectedError>()
pending.set(id, { info, deferred })
yield* bus.publish(Event.Asked, info)
return yield* Deferred.await(deferred)

路径:packages/opencode/src/permission/index.ts:161-196

reply

if (input.reply === "reject") {
  yield* Deferred.fail(existing.deferred, new RejectedError())
  for (const [id, item] of pending.entries()) {
    if (item.info.sessionID !== existing.info.sessionID) continue
    pending.delete(id)
    yield* Deferred.fail(item.deferred, new RejectedError())
  }
  return
}

yield* Deferred.succeed(existing.deferred, undefined)
if (input.reply === "once") return

路径:packages/opencode/src/permission/index.ts:210-230

默认权限

const defaults = Permission.fromConfig({
  "*": "allow",
  doom_loop: "ask",
  external_directory: { "*": "ask", ...whitelisted },
  question: "deny",
  plan_enter: "deny",
  plan_exit: "deny",
  repo_clone: "deny",
  read: {
    "*": "allow",
    "*.env": "ask",
    "*.env.*": "ask",
    "*.env.example": "allow",
  },
})

路径:packages/opencode/src/agent/agent.ts:103-122

8. 关键 TypeScript 语法复习

  • Schema.Literals(["allow", "deny", "ask"]):类似 Java enum。
  • Schema.Class:运行时 schema + TS 类型 + class。
  • Schema.optional:可选字段,运行时可能不存在。
  • const { ruleset, ...request } = input:对象 rest。
  • findLast:后面的规则优先。
  • { -readonly [K in keyof _Info]: _Info[K] }:mapped type 去 readonly。

9-10. 架构思想与模块协作

  • Policy engine:evaluate 计算 action。
  • Event bus:permission.asked/replied 解耦 UI 和工具执行。
  • Async gate:Deferred.await 暂停 tool execution。
  • Layered ruleset:agent/session/approved 多层规则合并。
  • Fail closed:默认 ask,非交互 CLI 默认 reject。

11. mini agent 对应代码

async function ask(input) {
  for (const pattern of input.patterns) {
    const rule = evaluate(input.permission, pattern, input.ruleset)
    if (rule.action === "deny") throw new Error("permission denied")
    if (rule.action === "ask") {
      const request = createPendingRequest(input)
      eventBus.emit("permission.asked", request)
      return await request.deferred
    }
  }
}

12. 费曼复述区

  1. allow、deny、ask 对 tool 执行有什么影响?
  2. 为什么 evaluate 用 findLast?
  3. ctx.ask 和 Permission.ask 的边界是什么?
  4. always 除了放行当前请求,还会影响什么?

13-15. 练习与自测

  • 入门:画出 Action/Rule/Request 字段表。
  • 进阶:解释 reject 为什么拒绝同 session 的其它 pending request。
  • 源码追踪:从 EditTool.execute 追到 Permission.ask
  • 小实现:写一个支持 once/always/reject 的 mini permission service。
  • 自测:为什么不能只靠 prompt 告诉模型不要乱操作?

16. 下一步阅读建议

继续读 “LSP / 诊断 / 上下文增强”。权限负责能不能做,LSP 负责做完以后如何把代码反馈带回 agent。