第 5 章 · 02 permission 权限批准与 checkpoint 检查点


文档摘要

第 5 章 · 02 permission 权限批准与 checkpoint 检查点 本节摘要:本节精读主循环上的两道核心闸门—— 权限批准与 checkpoint 检查点。permission 把"是否允许这个工具调用"建模成一个纯函数 Policy(规则评估,零 I/O):输入工具名 + 参数 subject,输出三值 (Allow / Ask / Deny);外层 包一个可选的交互式 。Rule 支持 、 通配、 字面量三种形式。三种批准模式(Ask 逐个问 / Auto 自动批普通项 / Yolo 跳过普通提示)是 UI 层对 Policy 的"模式化"包装。

第 5 章 · 02 permission 权限批准与 checkpoint 检查点

本节摘要:本节精读主循环上的两道核心闸门——internal/permission 权限批准与 checkpoint 检查点。permission 把"是否允许这个工具调用"建模成一个纯函数 Policy(规则评估,零 I/O):输入工具名 + 参数 subject,输出三值 Decision(Allow / Ask / Deny);外层 Gate 包一个可选的交互式 Approver。Rule 支持 ToolNameToolName(glob) 通配、ToolName=literal 字面量三种形式。三种批准模式(Ask 逐个问 / Auto 自动批普通项 / Yolo 跳过普通提示)是 UI 层对 Policy 的"模式化"包装。checkpoint 则是每 turn 一个文件快照:在 execute_one 执行非只读工具前,用 tool.Previewer 拿到"改哪个文件",把编辑前内容存进 <session-id>.ckpt/,支持回放代码 / 对话 / 两者 / 从此分叉。这是 agent 安全性(权限)与可恢复性(checkpoint)的基石。

内容来源:原项目源码 internal/permission/permission.gointernal/control/checkpoint.godocs/TOOL_APPROVAL_MODES.mddocs/CHECKPOINTS.mddocs/RECOVERY.md,精读并套用体系化模板。

⚠️ 注意:permission 的 Policy 是纯函数(规则评估无 I/O),把"问用户"这件事推迟到外层 Gate 的 Approver;checkpoint 是文件快照(不是 git),bash 副作用不纳入快照(和 Claude Code 对齐)。

学习目标

阅读完本节,你应当能够:

  1. 说出 Decision 的三个值(Allow / Ask / Deny)各自的语义,以及 Policy 评估的默认兜底。
  2. 区分 Rule 的三种书写形式:ToolNameToolName(glob)ToolName=literal
  3. 解释为什么 Policy 设计成纯函数(无 I/O),而把交互式批准推迟到 Gate
  4. 说清 Ask / Auto / Yolo 三种批准模式各自放行什么、仍然拦截什么。
  5. 读懂 checkpoint 的"每 turn 一个快照"模型:FileSnap / Checkpoint / <session-id>.ckpt/
  6. 解释为什么 bash 副作用不纳入快照,以及 Preview 与 checkpoint 是同一道缝。
  7. 区分 RewindScope 的三种值(Code / Conversation / Both)与"从此分叉"。

一、permission 的定位:纯函数 Policy + 外层 Gate

internal/permission 的包注释把设计意图讲得很直接:

// Package permission decides, per tool call, whether to allow it, deny it, or // ask the user first. The core is a pure Policy (rule evaluation, no I/O); a // Gate wraps a Policy with an optional interactive Approver and is what the // agent consults at execute time. Keeping rule evaluation pure makes it // trivially testable and keeps the agent independent of how "ask" is resolved.

翻译:permission 包对每个工具调用做判断——允许 / 拒绝 / 先问用户。核心是一个纯 Policy(只评估规则,没有任何 I/O);外层 Gate 在 Policy 外面包一个可选的交互式 Approver,agent 在 execute 时咨询的是 Gate。把规则评估做成纯函数有两个好处:(1) 极易测试(不依赖任何 I/O,纯输入输出);(2) agent 不依赖"问用户"是怎么实现的(终端 TUI 弹卡片、桌面弹窗、headless 直接失败,各前端自己解决 Ask)。

这套"纯内核 + 可插拔外壳"是 Go 里处理"有副作用的决策"的经典套路,和第 4 章 OmsEngine 的"被动订阅 + 字典缓存"异曲同工:把可测试的纯逻辑和不可测试的 I/O 分层。

二、Decision:三值决策

源码定义了三值:

// Decision is the outcome of evaluating a tool call against a Policy. type Decision int const ( Allow Decision = iota // 直接放行,不问 Ask // 交给交互式 Approver;没有 Approver 时退化为 Allow Deny // 任何模式下都拦截 ) // ParseDecision maps a config string to a Decision. Unknown / empty input // defaults to Ask — the conservative posture for a writer fallback. func ParseDecision(s string) Decision { switch strings.ToLower(strings.TrimSpace(s)) { case "allow": return Allow case "deny": return Deny default: return Ask } }

两个关键细节。

第一,枚举顺序 Allow=0, Ask=1, Deny=2 不是随便排的。 Decisionint,默认零值是 Allow(0)。但注意 ParseDecision 对"未知/空"输入返回 Ask,返回零值——这是写入侧的保守姿态。读侧(配置缺省)和写侧(解析未知)分别选了不同的默认,这是有意的。

第二,"没有 Approver 时 Ask 退化为 Allow"是个微妙的设计。 它的意思是:纯 Policy 说"这个该问用户",但如果没有 Approver(比如某些自动化场景),那就放行而不是卡死。这避免了"配了 ask 规则但没有交互界面 → 整个 agent 卡住"。但这个退化在 headless 模式下会被改写——reasonix run 没有 Approver,默认 Ask 姿态对 writer fallback 和显式 ask 规则失败关闭(fail closed),而不是退化为 Allow。安全优先。

三、Rule:三种书写形式

Rule 匹配工具调用:

type Rule struct { Tool string // 工具名 Subject string // 非空时约束调用的 subject Literal bool // true 时按字面量精确匹配,false 时按 glob 通配 }

ParseRule 解析三种写法:

写法 含义 示例
ToolName 匹配该工具的所有调用(Subject 空) bash 匹配所有 bash 调用
ToolName(glob) 按通配符匹配 subject(支持 * ?) bash(git push *) 匹配所有 git push
ToolName=literal 按字面量精确匹配(把 * 当普通字符) bash=git push origin main 精确到这一条

注释解释了为什么保留 =literal 旧形式:这是"在 Claude Code 风格的 Tool(specifier) 规则出现之前"的老配置写法,为了兼容已有配置而保留。= 形式(当 = 出现在任何 ( 之前时生效)把剩余字符串原样匹配,不做 glob——这样一条记住的"具体命令"里的 * / ? 不会被当成通配。ok 返回 false 表示格式错误(空工具名),让调用方告警而不是静默装一条匹配不到任何东西的规则。

💡 契约要点:permission 的 Rule 是"工具 + subject"二元组,subject 用 glob 还是 literal 是 Rule 自己的属性(不是全局开关)。这意味着你可以同时有 bash(git *)(通配放行所有 git)和 bash=rm -rf /(字面量拒绝这一条),互不干扰。glob 和 literal 的选择绑在每条 Rule 上,设计得很精细。

四、Gate:纯 Policy 之外的可选 Approver

Policy 是纯函数,但"Ask 怎么落地"需要 I/O。Gate 就是这层外壳:

Gate 在 execute 时被 agent 咨询(上一节 execute_one 流程图里的"permission 检查"那一步就是 Gate)。Gate 持有一个 Policy 和一个可选 Approver,逻辑大致是:调 Policy 拿 Decision → Allow 直接放行 / Deny 直接拦 / Ask 则看有没有 Approver。Approver 是个接口,各前端自己实现:终端 TUI 实现成"弹一个批准卡片等用户按键",桌面实现成"Wails 弹窗",headless 实现成"直接 fail closed"。

"用户的选择能变成新规则" 是这层的另一职责。用户在批准卡片上选"总是允许",前端会把这个工具 + subject 写回配置成为一条 allow 规则——下次同样的调用 Policy 直接 Allow,Gate 都不用问。TOOL_APPROVAL_MODES.md 提到:Ask 模式下"动态 Bash 永不继承更宽的 Bash/前缀/glob 规则",可重用的选择会以 Bash=<literal>(字面量)形式存——正好对应上一节讲的 =literal 形式。

五、三种批准模式:Ask / Auto / Yolo

文档 TOOL_APPROVAL_MODES.md 把"批准模式"和 Policy 分开讲,这点很重要:批准模式不是 Policy 的替代,而是对 Policy 决策的一种"模式化"解释。同一套 Policy 规则在三种模式下行为不同:

模式 普通工具权限 仍然拦截
Ask 受控工具(write/bash 等)都先问 (天然全部走 Approver)
Auto 自动批准普通工具权限 显式 deny / 显式 ask 规则、计划确认、全局/偏好/反馈/更新/重复/敏感/超大 remember、嵌套/间接 Bash、MCP 破坏性调用、ask 提问
Yolo 跳过普通工具权限提示 显式 deny 规则、沙箱、计划确认、ask 提问、强制新鲜批准

三个关键认知。

第一,模式独立于协作模式(collaboration mode)。 文档专门提醒:协作/运行时模式(lightweight/balanced/delivery-first)决定 agent 怎么推进任务;工具权限模式决定受控工具是否等批准。两者正交,可以自由组合(Plan + Auto、Goal + Yolo 等)。初学者容易把这俩混成一回事。

第二,Auto 不是"无脑全放行"。 Auto 仍然尊重一堆东西:显式 deny、显式 ask、计划模式的"开始执行"确认、各种 remember 的敏感操作、嵌套/间接 Bash、MCP 破坏性调用、ask 提问。文档原文:"Auto is designed as a behavior, not another feature to configure." Auto 是个行为,不是又一个要配置的特性——它执行 Policy 允许的操作,只在出现真正该用户拥有的决策(新产品方案、计划选择)时才问。

第三,Yolo 是唯一能绕过"嵌套/间接 Bash 需人工"要求的模式,但绕不过 deny 和沙箱。 Yolo 最大化连续执行,适合"计划已确认 + 工作树可回滚"的批量机械编辑。文档明确警告:Yolo 不适合生产、敏感文件、delete/publish/push、需求不清的场景。

💡 契约要点:三种模式本质是"对 Approver 的不同实现策略"。Policy 还是那个纯 Policy,规则还是那些规则;模式改变的是"普通工具权限的 Approver 默认怎么应答"。Auto 的 Approver 对普通项自动说"允许",但遇到 ask/deny 规则、计划确认等仍然转发给真实用户;Yolo 的 Approver 对普通项连问都不问,但 deny/沙箱/计划确认挡得住它。把 Policy(纯规则)和 Mode(Approver 策略)分开,是这套设计能同时保证"可测试"和"灵活"的关键。

六、checkpoint:每 turn 一个文件快照

讲完权限这道"事前闸门",来看"事后可恢复"的 checkpoint。文档 CHECKPOINTS.md 开宗明义:

Let a user rewind a session to a previous point and restore code, conversation, or both — without touching their git history.

核心机制是文件快照,不是 git:

type FileSnap struct { Path string // 工作区相对路径 Content *string // nil → 该文件在此锚点不存在(回放时删除它) } type Checkpoint struct { Turn int // 锚定的用户消息索引(0-based) Time time.Time Prompt string // 用户消息文本 —— picker 的标签 Files []FileSnap // 这一 turn 内动过的不同文件,turn 开始时的状态 }

三个设计要点。

第一,每 turn 一个 checkpoint,在 turn 开始时打开。 文档:"One checkpoint per user turn. A checkpoint opens when a turn starts (Controller.Send / runTurn), labelled with the user prompt." 也就是说,checkpoint 的粒度是"用户消息",不是"工具调用"。一个 turn 里可能改了 5 个文件,这 5 个文件的"turn 开始时内容"都被记进同一个 Checkpoint。

第二,编辑前快照,通过 Previewer 这道缝集中采集。agent.(*Agent).executeOne,执行一个 ReadOnly() 为 false 且实现了 tool.Previewer 的工具前,调 Preview(args) 拿到 diff.Change{Path, Kind, OldText},把该文件的快照记进当前 checkpoint。文档强调:"tool.Previewer already exists and the file-writers implement it, so this is one centralized seam — no per-tool code." Previewer 接口本来就存在(文件类工具已实现),所以采集快照只需要这一处集中缝,不用每个工具各自写。

第三,去重:每 turn 每路径只存第一次。 "Dedup per path per turn: only the first touch is snapshotted." 因为第一次 touch 时的内容就是"turn 开始时的内容",同一文件在这一 turn 内被改多次,只需记最初的版本。Kind == create(文件原本不存在)存 Content = nil,回放时删除该文件;modify/deleteOldText

与 git 的类比与区别

文档特意强调 checkpoint 与 git 的区别(很多人会以为是 git 回滚):

维度 checkpoint(文件快照) git
是否污染 git 零污染,从不 commit/stage/碰 .git/ 必然动 git
非 git 目录 能工作 不能
跟踪范围 只跟踪可预览的编辑工具(write_file/edit_file/multi_edit) 跟踪所有文件变化
bash 副作用 不跟踪(bash rm 不可回放) 取决于是否 commit
存储 会话边车目录 <session-id>.ckpt/,一个 JSON 一个 checkpoint + 索引 .git/ 对象库

为什么 bash 不跟踪?文档直说:"no way to know what a shell command touched"(没法知道一条 shell 命令碰了什么文件)。这和 Claude Code 完全对齐——有风险的 bash 已经被 permission 闸门管住了,checkpoint 只管"可预览的编辑工具"这一类确定性的文件改动。

存储:会话边车目录

checkpoint 存在会话目录的边车子目录:

<session-dir>/<session-id>.ckpt/ 0001.json # 第 1 个 checkpoint 0002.json # 第 2 个 ... index.json # 小索引

文档解释这个布局:"one JSON per checkpoint plus a small index (v1's layout — cheap delete, a corrupt snapshot only loses itself)." 一个 JSON 一个 checkpoint,删除便宜,单个快照损坏只丢自己。它和会话消息 JSONL(agent.Session.Save)分开存,这样会话格式完全不变。checkpoint 跨会话持久:resume 一个会话会重新加载它的 checkpoint,所以重启后 rewind 照样能用。保留期随会话一起清理(默认约 30 天,可配),用全内容快照所以靠保留期回收来限制磁盘。

七、Controller.Rewind:三前端共用的唯一缝

回放(rewind)操作挂在 control.Controller 上,和 SetPlanMode / Compact / NewSession 放在一起。文档明确:这样终端 TUI、桌面 webview、HTTP/SSE 服务器三个前端驱动 rewind 完全一样,没有一个重新实现它

type RewindScope int // Code | Conversation | Both func (c *Controller) Checkpoints() []CheckpointMeta // 给 picker 列表用 func (c *Controller) Rewind(turn int, scope RewindScope) error

三种 scope 的语义:

  • Code:对从 turn 到最新的每个 checkpoint,取每条路径最早FileSnap,把每个文件恢复到那个内容(nil 就删除)——即撤销 turn 及之后的所有编辑。路径穿越会针对实时工作区根重新检查。
  • Conversation:把 Session.Messages 截断到 turn 的用户消息之前,重新 Save,并把截断后的历史作为事件发出让前端重渲染。该 turn 的 prompt 会被还原进 composer 供重发/编辑(Claude Code 行为)。
  • Both:Code + Conversation。

还有一个"从此分叉"(fork-from-here)——从某个 turn 复制出一条新的会话分支继续。这和上一节提到的 branch.go 子代理分支是同一套机制在用户层面的暴露。

💡 契约要点:checkpoint 和 permission 是 agent 安全网的"一对":permission 是事前闸门(决定工具能不能跑),checkpoint 是事后安全网(跑完能回退)。两者都尽量做到"集中一道缝、各前端共用":permission 把"问用户"推迟到 Gate 的 Approver,checkpoint 把"改哪个文件"集中在 Previewer 这道缝、把"回放"集中在 Controller.Rewind。这是 Reasonix 反复用的设计范式——把可测试的纯逻辑(Policy / 快照采集)和有副作用的外壳(Approver / UI 回放)分层。

本节要点回顾

  1. permission 是纯 Policy + 外层 Gate:Policy 零 I/O 极易测试,Gate 包一个可选 Approver 解决"问用户"的 I/O;agent 在 execute 时只认 Gate。
  2. Decision 三值:Allow(放行)/ Ask(交 Approver,无 Approver 退化为 Allow,headless 则 fail closed)/ Deny(任何模式都拦)。
  3. Rule 三种形式:ToolName(全匹配)/ ToolName(glob)(通配)/ ToolName=literal(字面量,glob 与 literal 是 Rule 自身属性)。
  4. 三种批准模式:Ask(逐个问)/ Auto(自动批普通项,仍拦 ask/deny/计划/敏感 remember/嵌套 Bash/MCP 破坏性)/ Yolo(跳过普通提示,仍拦 deny/沙箱/计划/提问)。模式独立于协作模式。
  5. checkpoint 每 turn 一个文件快照:在 turn 开始打开,通过 Previewer 这道集中缝采集"编辑前内容",每路径每 turn 只存第一次,create 存 nil(回放删)、modify/delete 存 OldText。
  6. checkpoint 不是 git:零 git 污染、能在非 git 目录工作、只跟踪可预览编辑工具、bash 副作用不跟踪;存储在会话边车 <session-id>.ckpt/,跨会话持久,随会话清理。
  7. Controller.Rewind 三 scope:Code(撤销编辑)/ Conversation(截断对话)/ Both,加"从此分叉";三个前端共用这一道缝。

下一节,我们看 agent 主循环的"自愈"能力:工具失败后如何自动重读、回显 schema、循环保护;以及 internal/repair 包的真实职责(Reasonix 自身升级的事务安全网,不是工具重试)。最后讲贯穿全章的 REASONIX.md 项目记忆——它是第 6 章 Cache-first 契约的"源头"。


作者与出处
原作者: 灏天文库
整理: 灏天文库整理
本站整理收录,版权归原作者/开源协议所有;欢迎通过原文链接访问源仓库。
发布者: 作者: 灏天文库 转发
评论区 (0)
U