主题
Allow Ask Deny 权限模型
本课交付结果
你将交付 evaluatePermission(request, options):读取默认允许,写入与执行默认询问,网络默认拒绝;可显式开启自动写入,并为每次决定生成包含资源的理由。
岗位问题
布尔值“允许/拒绝”无法表达需要人确认的中间状态,也无法在审计时解释为什么执行。Coding Agent 必须把模型意图转换成稳定决策,且保守默认值不能由自然语言提示绕过。
前置检查
前置知识快照
确认工具合同已声明权限:pnpm --filter @learn-traeai/coding-agent-bootcamp verify:lesson -- 06。区分 capability、resource 与 decision;本课策略不执行任何动作。
能力表示动作类别,例如读、写、执行与联网;资源表示动作目标,例如文件路径、命令摘要或域名;决定表示系统在当前策略下如何处理这次请求。三者不能合成一个含糊布尔值。相同写能力面对 src/a.ts 与 .env 可能产生不同决定,同一资源在交互会话和无人值守任务中也可能采用不同规则。
还要区分 Policy Decision Point 与 Enforcement Point。evaluatePermission 只计算决定和理由,不触碰文件、不启动进程、不弹界面;后续 Sandbox Runtime 才强制执行结果。纯函数让规则可以用表格测试、离线评审和回放,副作用边界也不会因 UI 更换而移动。
原理拆解
allow 用于无副作用且边界明确的读取;ask 用于可能修改状态但可经用户批准的动作;deny 用于当前产品根本不提供的能力。理由文本同时服务审批 UI、Trace 和排障。
mermaid
flowchart LR
A[permission + resource] --> B[输入校验]
B --> C{能力类别}
C -- read --> D[allow]
C -- network --> E[deny]
C -- write --> F{autoApproveWrites?}
F -- 是 --> D
F -- 否 --> G[ask]
C -- execute --> G
D --> H[decision + reason]
E --> H
G --> H1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
三态比布尔值多出的 ask 是产品安全的关键。如果只有允许和拒绝,团队要么默认阻断所有有用写入,要么为了可用性把危险动作自动放行。询问态把“系统无法独立承担风险”表达成一等数据,交给用户基于具体资源确认,而不是在提示词里笼统授予永久权限。
默认策略要从最小权限出发。受工作区边界保护的读取可自动允许;写入会改变状态,默认询问;执行可能间接写文件、启动网络或消耗资源,同样询问;网络在当前课程产品中没有实现受控出口,因此拒绝。未知能力不应落到允许分支,安全默认值必须是 ask 或 deny。
理由不是装饰字符串。审批界面需要告诉用户“什么能力作用于哪个资源”,Trace 需要重建当时为何放行,失败排查需要判断命中哪条规则。生产规则最好再返回稳定 ruleId,把本地化展示文本与机器审计身份分开。文本可以改写,规则身份必须稳定。
选项作用域必须窄。autoApproveWrites 只改变 write,不应因为用户愿意自动保存代码,就顺带允许执行命令或访问网络。权限开关越宽泛,用户越难形成准确心智模型。每个开关都应能用反例测试证明“不影响哪些能力”。
策略函数先拒绝空资源,因为没有目标就无法做有意义审批和审计。资源字符串应来自工具合同的规范化表达:文件使用工作区相对路径,命令使用 executable 与 argv 摘要,网络使用规范化主机。策略不负责解析任意自然语言,否则同一动作可能通过不同描述绕过规则。
代码实验
失败实现
下面实现把未知能力和自动模式都放得过宽:
ts
function unsafe(request: PermissionRequest, automatic: boolean) {
if (automatic) return true;
return request.permission !== "network";
}1
2
3
4
2
3
4
正确实现返回可解释三态,并按明确优先级匹配:
ts
export function evaluatePermission(
request: PermissionRequest,
options: PermissionOptions = {},
): PermissionDecision {
if (!request.resource.trim()) throw new Error("资源不能为空");
if (request.permission === "read") {
return { decision: "allow", reason: `只读访问:${request.resource}` };
}
if (request.permission === "network") {
return { decision: "deny", reason: `禁止网络访问:${request.resource}` };
}
if (request.permission === "write" && options.autoApproveWrites) {
return { decision: "allow", reason: `已启用自动写入:${request.resource}` };
}
return { decision: "ask", reason: `需要确认 ${request.permission}:${request.resource}` };
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
网络拒绝写在自动写入之前,让规则意图一眼可见。即使当前条件不会误匹配,安全规则按“明确拒绝、窄允许、保守兜底”组织,更利于未来增加能力时保持默认安全。
bash
pnpm --dir bootcamps/coding-agent/labs/11-permission-policy/starter test
pnpm --dir bootcamps/coding-agent/labs/11-permission-policy/solution test1
2
2
Starter 声明 实现 execute 权限决策;Solution 应通过 5 项测试。
测试不仅验证五个结果,还要证明理由绑定具体资源:
ts
it("执行默认询问且理由包含资源", () => {
const result = evaluatePermission({ permission: "execute", resource: "pnpm test" });
expect(result.decision).toBe("ask");
expect(result.reason).toContain("pnpm test");
});
it("自动写入不会放开网络", () => {
expect(evaluatePermission(
{ permission: "network", resource: "example.com" },
{ autoApproveWrites: true },
).decision).toBe("deny");
});1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
关键实现讲解
先拒绝空资源,再按权限优先级匹配。网络 deny 必须早于通用 ask;自动写入只影响 write,不能顺带放开 execute 或 network。返回新对象,避免调用方篡改共享决定。
策略输出不应缓存成可变全局对象。每次请求返回新结果,Trace 可以原样快照;若多个调用共享同一个决定对象,界面为了附加展示字段的修改可能污染后续审计。规则配置可以冻结,具体决定应是一次调用的一次事实。
deny 与 ask 的恢复语义不同。deny 表示当前策略没有审批通道,Agent 应换方案或停止;ask 表示等待外部授权,不能自动当作失败重试。上层循环必须保留三态,若转成布尔值再用真假分支,询问态很容易被误当允许或永久拒绝。
运行与验证
真实运行输出
bash
pnpm --filter @learn-traeai/coding-agent-bootcamp verify:lesson -- 11
pnpm --filter @coding-agent/sandbox test1
2
2
检查 read/allow、write/ask、network/deny、auto-write/allow 与 execute/ask,且理由包含目标资源。
实际 Lab 运行中,Starter 的统一练习缺口让五项全部失败;Solution 五项全部通过:
text
starter: 5 failed, 0 passed
solution: 5 passed, 0 failed
矩阵: read/allow · write/ask · network/deny · auto-write/allow · execute/ask1
2
3
2
3
再加入空白资源探针和选项隔离探针。空白资源应在任何决策前失败,开启自动写入后 execute 仍为 ask、network 仍为 deny。只看已有五项绿色不足以证明未来新增选项不会扩散权限。
常见失败与排查
故障案例 1
未知能力被静默允许。
症状:新增 publish 权限后未添加规则,却直接执行,没有审批记录。
根因:策略末尾使用默认 allow,新增枚举绕过了旧安全假设。
定位:枚举所有能力运行表驱动测试,检查没有命中明确规则的分支;若返回 allow,默认值错误。
修复:采用保守兜底并对未知能力拒绝或询问;新增能力必须同时提交规则与测试矩阵。
故障案例 2
自动保存意外放开命令和网络。
症状:用户只开启自动写文件,Agent 却无需确认执行安装或访问外网。
根因:把“自动模式”设计成全局跳过审批,而非 write 专属选项。
定位:开启选项后逐一评估 read、write、execute、network,比较只有 write 是否变化。
修复:在 write 条件内读取选项,为所有不应变化的能力增加负断言;避免含糊的万能开关。
故障案例 3
审批界面有决定却没有上下文。
症状:界面只显示“是否允许?”,用户不知道将修改哪个文件或执行什么命令。
根因:策略只返回布尔值或固定文案,没有把规范化资源纳入理由和审计。
定位:检查决定对象和 Trace,若无法从中还原能力、资源与规则,解释链断裂。
修复:返回三态、理由和稳定规则身份;UI 只展示策略事实,不自行猜测动作目标。
课后作业
增加按路径配置的规则优先级,证明 src/** 可自动写而 .env 永远 deny;为每条规则生成可审计 rule id。
进阶要求是加入会话级审批:用户可允许一次、允许本会话或永久拒绝,但缓存键必须同时包含能力、规范化资源和规则版本。写重放测试证明对 src/a.ts 的批准不能用于 .env,旧规则下的批准在策略升级后自动失效。
验收 Rubric
| 维度 | 通过标准 | 常见扣分 |
|---|---|---|
| 默认值 | read allow、write/execute ask、network deny | 未知能力默认允许 |
| 解释性 | 理由包含权限和资源 | 只有布尔值 |
| 选项边界 | auto-write 只影响写入 | 扩散到执行或网络 |
总项目增量
总项目包:@coding-agent/sandbox
总项目路径:packages/sandbox/src/permission-policy.ts
总项目验证命令:pnpm --filter @coding-agent/sandbox test
策略提供第一层能力判定,下一课会进一步检查 execute 中具体命令的危险性。
延伸阅读
方案对比与工程取舍
布尔 allow/deny 实现最短,却无法表达等待用户;三态策略适合交互 Agent,也能在无人值守环境把 ask 映射成拒绝或外部审批队列。基于角色的权限模型容易管理用户群,却难描述某次具体文件与命令;基于能力的模型把授权绑定到动作与资源,更符合工具调用。复杂产品通常把组织角色、项目策略和调用能力分层组合。
规则可采用首个匹配、最高优先级或“最严格结果获胜”。首个匹配易理解但依赖顺序,最高优先级需要处理同级冲突,最严格合并最保守却可能让窄允许永远无法覆盖宽拒绝。无论选择哪种,都要让命中的 rule id 可见,并用冲突表格测试,而不是把顺序藏在多层条件里。
路径规则需要复用安全文件工具的规范化结果。直接对原始字符串做 glob,src/../.env、斜杠差异或链接都可能绕过。先由文件边界确认真实工作区资源,再让策略比较规范化相对路径;策略不是第二套路径解析器。安全组件之间共享资源身份,组合才不会出现缝隙。
.env、密钥目录、锁文件与 CI 发布配置通常需要比普通源码更严格。即使启用自动写入,也可以对这些路径明确 deny 或 ask。规则应体现风险而不是扩展名迷信:仓库也可能把秘密放在其他文件,所以还需内容扫描、最小宿主权限和评审流程。
审批必须发生在副作用之前,并绑定精确动作。界面显示后命令若还能被模型或调用方修改,就会出现“批准 A、执行 B”。审批请求应包含不可变规范化载荷与哈希,Runtime 执行时重新比对。任何字段变化都要求新审批。
一次性 token 要防重放。批准后原子消费,第二次使用失败;设置短过期时间,并绑定会话、工作区、能力、资源、参数摘要和规则版本。只绑定用户身份的通用 token 等同于永久放行,失去逐动作确认的意义。
会话级记忆可以减少重复弹窗,但粒度决定风险。允许“本会话写入 src/**”比允许“本会话所有动作”更可解释;遇到新能力、敏感路径或策略版本变化仍重新询问。缓存命中也应写入 Trace,让事后知道这次没有弹窗是因为哪次批准。
无人值守 CI 中没有人点击 ask,系统必须预先定义处理方式。可以将 ask 视为 deny,或只接受由管理员签名的策略文件。绝不能因“没人回答”自动允许。交互可用性不能改变核心安全语义。
理由文本应避免泄漏秘密。资源可以显示工作区相对路径或命令摘要,但不要把环境变量值、文件内容或完整宿主路径放进审批消息。内部 Trace 同样遵循最小记录,只保存决策所需元数据与哈希。
策略版本是审计的重要维度。规则调整后,相同请求可能得到不同决定;Trace 若只记录结果,无法解释变化。记录策略版本、rule id、输入摘要和决定,既支持事故复盘,也能用历史调用回放新策略,提前观察会增加多少询问或拒绝。
策略评测不只测安全,还要测摩擦。统计自动允许、询问、拒绝比例,用户拒绝率、批准等待时间和重复询问数。ask 过多会培养用户机械点击,最终削弱安全;可通过更窄且有证据的自动允许减少疲劳,而不是把默认值整体放宽。
读取也并非永远无风险。课程假设文件工具已限制工作区,且读取不会修改状态,所以默认 allow;真实产品对密钥、个人数据或受监管仓库可能把读取设为 ask 或 deny。策略默认值必须建立在明确威胁模型上,而不是把“读”普遍等同于安全。
写入也可能分级。创建临时补丁、修改源码、改锁文件、删除文件和覆盖配置的可恢复性不同。最初合同只有 write,后续可以通过资源规则与操作字段细分,但不要让能力枚举无限膨胀。稳定上层能力配合结构化资源属性,往往比为每个文件动作创造新权限更易维护。
网络默认 deny 是因为本周没有域名 allowlist、请求方法限制、响应上限和凭据隔离。简单地把 URL 显示给用户并不足以安全联网:重定向、DNS 变化和内网地址都可能改变目标。只有建立专门网络工具和策略后,才应把部分网络请求从 deny 升为 ask 或 allow。
测试可以由规则矩阵生成:每行包含能力、资源、选项、预期决定、rule id 和理由关键字段。矩阵同时作为文档和回归套件,新增规则时评审者能看到哪些旧行改变。再增加性质测试,断言开启某个窄选项不会让不相关能力变宽。
总项目中的权限策略应与 Command Guard 分工。权限层判断“执行能力通常需要询问”,Guard 再根据具体 git status、pnpm test 或 force push 细分风险。前者提供通用能力默认值,后者理解命令语义。不要把所有命令字符串硬塞进通用权限函数。
用四次请求走一遍决策链。读取 src/index.ts 时,资源已由文件工具证明在工作区,策略返回 allow,理由明确是只读访问。写同一文件默认返回 ask,界面展示目标并等待用户。即使开启自动写入,执行 pnpm test 仍是 ask,因为开关没有跨能力扩散。访问 example.com 始终 deny,因为当前产品没有安全网络出口。四次结果构成用户可预测的心智模型。
如果用户批准一次写入,策略本身不应突然变成永久 allow。批准是请求实例的外部证据,Runtime 可携带一次性 token 重新进入 Enforcement Point;基础策略仍保持默认 ask。这样清空会话或 token 过期后,安全默认自然恢复,不会因修改全局配置留下隐形授权。
规则组合时可以先计算基础决定,再应用更窄项目规则,最后使用组织级不可覆盖拒绝。例如基础 write 为 ask,项目允许 src/** 自动写,组织规则拒绝 .env。对 src/a.ts 得到 allow,对 .env 得到 deny。关键是明确哪类规则可覆盖哪类规则,不能让项目仓库自行覆盖组织安全底线。
资源匹配需要边界。例如字符串前缀 src 也会匹配 src-secret,glob **/.env* 可能误伤示例文件。规则引擎应使用规范化路径段与经过测试的 glob 库,配置界面展示匹配预览。安全规则错误既可能放行危险目标,也可能因过度拦截让用户关闭整个保护。
对命令资源,理由不宜简单 args.join(" ") 后再用于授权,因为展示字符串无法无歧义还原参数。审批载荷保存结构化 executable 与 argv,界面再进行安全转义展示;批准哈希基于规范序列化结构。两个看起来相似的字符串如果参数边界不同,也必须被视为不同动作。
理由生成应由命中规则负责,而不是调用方随意提供。若模型可以自称“安全读取”来影响文案,用户可能被误导。策略根据可信能力、规范化资源和配置生成说明;模型原始解释可以作为补充,但必须与系统判断视觉区分。
空资源同步抛错属于调用合同错误,不应返回 ask 让用户批准“未知目标”。同理,非法权限或缺少工作区身份应在策略前被拒绝。审批不是数据校验后备方案,用户也不应被迫判断系统无法描述的动作。
批量操作需要展开资源或提供集合摘要。一个“写入十个文件”的请求若只显示第一个路径,批准范围不透明;若生成十次弹窗,又造成疲劳。可以把规范化资源集合哈希绑定到一次批量审批,界面显示数量、路径列表和敏感项,任何集合变化都使 token 失效。
撤销能力同样重要。用户发现批准错误后,应能终止尚未执行的队列、清除会话授权并查看已经发生的动作。策略 Trace 关联执行 Trace,才能区分已批准未执行、正在执行和已完成。只有审批弹窗而没有生命周期管理,无法构成完整控制。
并发请求可能同时等待同一资源批准。简单缓存“最近一次允许”会产生竞态,把批准应用到错误请求。每个审批拥有唯一 id 与精确载荷,完成时原子更新对应请求;相同请求可在 UI 合并展示,但底层 token 仍需逐一消费或明确绑定整组。
当规则配置解析失败时,默认行为应收紧而非忽略。比如项目策略文件语法错误,系统可以停用自动允许并把相关动作降为 ask,同时向用户报告配置问题;绝不能悄悄退回全 allow。配置错误也要进入 Trace,避免用户只看到突然增多的弹窗却不知道原因。
策略热更新需要会话一致性。最简单做法是会话启动时固定版本,重大安全拒绝则立即全局生效并废弃旧 token。更复杂的实时更新要保证一次决策到执行使用同一版本,否则可能在旧规则下批准、新规则下执行却无法解释。版本字段与载荷哈希让这种一致性可以验证。
安全团队常关心“谁允许了什么”,而模型团队关心“为什么任务卡住”。同一结构化记录可以服务两者:用户或服务身份、会话、能力、资源摘要、策略版本、rule id、决定、审批结果和执行关联。展示层按权限选择字段,底层事实保持统一。
最后做单调性检查:打开一个窄允许选项,只应让指定请求从 ask 变 allow,不应让任何 deny 变 allow,也不应影响其他能力。增加更严格组织规则,只应保持或收紧决定。把这种偏序性质写成自动测试,能发现条件重排造成的意外权限扩张。
课程验收时,把规则矩阵交给另一位同学,不让他阅读实现,要求预测每项决定并解释原因。若预测与程序一致,说明策略具有可理解性;若必须追踪复杂条件才能回答,应先重构规则和理由,再考虑增加更多例外。安全策略既要机器正确,也要让人能够监督。
这也是三态模型最终的价值:让自动化速度、用户控制和系统底线同时成为可验证事实。
下一课衔接
本课只知道请求属于 execute,默认只能 ask;它还不知道 git status 与 git push --force 的差异。下一课实现 Command Guard,基于固定 executable 和 argv 按安全优先级分类:只读 Git 与测试可允许,安装和 reset 询问,shell、下载器、删除器与 force push 拒绝。三态模型将落到具体命令语义。
- 最小权限与 default deny。
- Capability-based security。
- Human-in-the-loop 审批的可解释性要求。