0-1. 学习目标与一句话
权限系统是 tool runtime 的安全闸门:工具调用 ctx.ask,权限服务按 ruleset 和已批准记录决定 allow、deny 或发布 permission.asked 等用户回复。
来源:packages/opencode/src/session/tools.ts:64-72、packages/opencode/src/permission/index.ts:161-196、packages/opencode/src/permission/evaluate.ts:9-15。
2-5. 它的位置与最小源码路径
源码路径卡片
permission/index.ts:19-45:Action、Rule、Request。permission/evaluate.ts:9-15:最后匹配规则决定 action,默认 ask。permission/index.ts:161-196:ask 创建 pending request 并等待 Deferred。permission/index.ts:198-254:reply 处理 once/always/reject。session/tools.ts:64-72:tool context 接入权限服务。agent/agent.ts:103-160:默认 agent 和 plan agent 权限。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;Deferred 像 CompletableFuture。
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. 费曼复述区
- allow、deny、ask 对 tool 执行有什么影响?
- 为什么 evaluate 用 findLast?
- ctx.ask 和 Permission.ask 的边界是什么?
- always 除了放行当前请求,还会影响什么?
13-15. 练习与自测
- 入门:画出
Action/Rule/Request字段表。 - 进阶:解释 reject 为什么拒绝同 session 的其它 pending request。
- 源码追踪:从
EditTool.execute追到Permission.ask。 - 小实现:写一个支持 once/always/reject 的 mini permission service。
- 自测:为什么不能只靠 prompt 告诉模型不要乱操作?