逐日AI
第 1 周 · D5约 4 小时

权限与审批:三态规则、按工具与路径匹配,以及自动模式的边界

给 Agent 装一道审批门:把每次工具调用交给一套 allow 与 ask 与 deny 三态规则判定,规则按工具名与路径模式匹配,审批结果能记住,并想清楚全自动模式在什么条件下才是负责任的。

今日目标 0/3

登录后可以勾选并保存进度。

今日目标

  1. 能设计一套按工具与路径匹配的三态权限规则,并说清匹配优先级
  2. 能把审批做成不阻塞循环的状态机,拒绝时让模型换一条路而不是卡死
  3. 能说出自动模式的三个前置条件,以及少了哪一个就不该开

昨天给了它写权限,今天给它一道门。读完回到页面顶部把三条目标勾掉。

小白版讲解

定谁能拍板:哪些事新人自己做,哪些必须先问一句

新人有了提交权限之后,你们之间一定会有一次对话,内容大概是:「跑测试、看日志、改你自己那个模块,这些不用问我;动数据库迁移、改 CI 配置、发布,先来问一句。」

这次对话的产物不是一份权限清单,而是一条判断标准。这一点很重要,因为你不可能穷举所有操作。你真正传达的是:这件事出错的话,代价是可逆的还是不可逆的?影响的是你自己还是别人?

Agent 的审批门就是把这条标准写成代码。但它有一个新人没有的难处:你没法跟它「对话」,只能给它规则。而规则一旦定错,会向两个方向失效:

  • 定太严:几乎每一步都要问。用户点了三十次同意之后,就开始不看内容直接按同意——这时候门已经不存在了,只剩仪式感。
  • 定太松:它自己就把事情做完了,包括你不希望它做的那件。而且你只会在事后发现。

所以这道门的设计目标不是「拦得多」,而是把「值得问」的那几次问出来,其余的别打扰人。今天要做的三件事都服务于这个目标:把判定分成三态而不是两态、把规则按具体度而不是顺序匹配、把审批做成循环里的一次暂停而不是一次异常。

三态不是两态:ask 是给「要看情况」留的位置

先说为什么必须有三种状态。

只有允许和禁止的系统,会把所有「要看情况」的操作硬塞进其中一边。塞进允许,那些操作就永远不经过人;塞进禁止,用户会发现 Agent 做不了正事,于是他做的第一件事就是把规则放宽——很可能宽到底。

第三态 ask 的作用是给「上下文决定」这件事一个位置。改 src 下的文件该不该允许?这取决于改的是哪个文件、改成什么、当时在做什么任务。这些判断规则表答不了,但人看一眼就能答。

三态各自的语义要写清,特别是第三个:

判定语义什么时候用
allow自己做,不打扰人可逆、低风险、高频。例如跑测试、看状态
ask停下来问一句要看情况的操作,占绝大多数写操作
deny不许做,而且不要再问不可逆或明确不该做的事。问了也不该同意

denyask 的差别不只是严格程度,而是要不要打扰人。「改测试文件」这件事你不希望 Agent 做,但也不希望它每次都来问你一遍——那等于把判断成本转嫁给你。deny 的正确语义是「这条路封了,你自己换一条」。

还有一条口径今天终于用上了:只读工具默认放行。这就是第三天在工具定义里加的那个只读标记的用处——审批门不需要认识具体工具,只需要问它「你会改东西吗」。

匹配什么:工具名、路径模式、命令前缀,谁的优先级更高

规则要能表达三类条件:工具名(edit_file)、路径模式(test/**)、命令前缀(node --test*)。写起来不难,难的是命中多条时听谁的

最容易想到的做法是按顺序,第一条命中的赢。不要这么做。 它把「规则表的顺序」变成了隐含语义:你在末尾加了一条更具体的规则,却被上面某条宽泛规则吃掉,而报错是「什么也没发生」——这类问题排查起来非常痛苦,尤其是规则来自多个地方(默认表、用户配置、本次会话的临时批准)的时候。

本课的口径只有一条:更具体的规则赢;具体度相同时,更严格的赢。

src/kernel/permissions.ts
/** 条件越多、越不含通配符,就越具体 */
function specificity(rule: Rule): number {
  let score = 0
  if (rule.tool && rule.tool !== '*') score += 2
  if (rule.path) score += rule.path.includes('*') ? 2 : 3
  if (rule.command) score += rule.command.includes('*') ? 2 : 3
  return score
}
 
const STRICTNESS: Record<Decision, number> = { deny: 3, ask: 2, allow: 1 }
 
export function decide(tool: ToolDef, subject: Subject, state: PermissionState): Verdict {
  if (tool.readOnly) return { decision: 'allow', reason: '只读工具默认放行' }
 
  const hits = state.rules.filter((rule) => matches(rule, subject))
  // 写操作没有规则命中时默认 ask,不是默认 allow
  if (hits.length === 0) {
    return { decision: 'ask', reason: '没有命中任何规则,写操作默认需要确认' }
  }
 
  const winner = hits.reduce((best, rule) => {
    const bySpec = specificity(rule) - specificity(best)
    if (bySpec !== 0) return bySpec > 0 ? rule : best
    return STRICTNESS[rule.decision] > STRICTNESS[best.decision] ? rule : best
  })
  return { decision: winner.decision, reason: winner.why ?? '命中规则', rule: winner }
}

两版都在做同一件事,而 Python 版把这条口径写得更直白:排序键就是优先级定义。这种「把策略写成一个可比较的键」的做法,比一串 if 好维护得多——加一类条件只要往键里加一项。

还有一个容易忽略的细节:没有规则命中时,写操作的默认判定必须是 ask。 默认放行的门等于没有门,而且这个错误只会在出事那天暴露。本实验 starter 里的默认实现就是错的(默认 allow),自检会有三项亮叉。

顺带看一眼本课默认规则表长什么样,它是个不错的示例(不是行业标准):

TextText
run_command command=node --test*  → allow   跑测试是只读的验证动作
run_command command=git status*   → allow
edit_file   path=test/**          → deny    测试是验收标准,不能靠改测试让它变绿
edit_file   path=**/*lock*        → deny    锁文件应当由包管理器生成
其余写操作:默认 ask | 只读工具:默认 allow

那条「不许改测试」的规则值得单独说一句:模型确实有「改测试让它变绿」的倾向,因为那是达成「测试全绿」这个目标最短的路。这不是它品行不好,是你把目标写成了「让测试通过」而不是「让代码正确」。规则表在这里替你把这条捷径封死。

审批是循环里的一次暂停,不是一次异常

现在是今天最重要的一个设计决定。审批门放在工具执行之前,它只有两种返回:

  • 放行:循环照常去执行工具。
  • 不放行:返回一条工具结果,内容是「为什么不执行」。

第二种是关键。被拒绝的调用在消息数组里,和「工具执行失败」长得完全一样:一条 assistant 消息带着它请求的调用,一条 tool 消息带着失败原因。于是模型会像处理任何一次失败那样自己改道——你不需要为审批发明任何新的消息类型,也不需要动昨天那个已经能跑的循环

src/kernel/approval.ts
/** 返回 null 表示放行;返回一条结果就等于「这次调用不执行」 */
export async function passGate(
  call: ToolCall,
  tool: ToolDef,
  options: GateOptions
): Promise<ToolResult | null> {
  const subject = subjectOf(tool.name, call.args)
  const verdict = decide(tool, subject, options.state)
 
  if (verdict.decision === 'allow') return null
  if (verdict.decision === 'deny') return denied(tool, verdict.reason)
 
  // 没有提问渠道(CI、管道、别的 Agent 的 shell)时按拒绝处理。
  // 在无人值守环境里自动同意的门,等于没有门。
  if (!options.approve) return denied(tool, '需要人工确认,但当前环境无法提问')
 
  // 这个 await 就是「循环里的一次暂停」:模型在等,工具没执行,什么都没发生
  const answer = await options.approve({ call, tool, subject, verdict })
  if (!answer.approved) return denied(tool, answer.note ?? '用户拒绝了这次操作')
 
  remember(subject, answer.scope, options)
  return null
}

反过来看,把拒绝实现成抛异常会付什么代价:你要在循环外面接住它、判断这一轮还要不要继续、还得补一条工具结果消息(否则下一轮请求不合法,因为每个调用都必须有对应的结果)。复杂度全堆在错误路径上,而错误路径是最难测的地方。 循环里这次改动只有五行,就是因为门被设计成了「返回一条结果」。

对应到实现上还有两条纪律:

  • 看不懂的回答按拒绝处理。 用户敲了个回车、敲了句「好的」、或者输入直接结束,都不能算同意。
  • 审批提示必须说清「为什么问你」。 用户看不到规则表,他判断的全部依据就是终端上那两行。本实验的提示长这样:
TextText
需要确认:edit_file src/calc.js
原因:没有命中任何规则,写操作默认需要确认
允许吗?y 这次可以 / a 本会话都可以 / w 写进配置 / n 不行

拒绝之后:把原因当结果回灌,模型自己会换路

拒绝的文本怎么写,决定了模型接下来干什么。这一条和第四天「失败信息是给模型看的下一步动作」是同一条原则,但在审批这里更明显。

写「permission denied」,模型会原地重试同一个调用(它以为是偶发失败)。写「测试是验收标准,不能靠改测试让它变绿,请改被测代码」,它会换一条路。本实验里这个现象是可以直接看到的——离线剧本故意先走那条捷径:

TextText
最快的办法是把那个用例改掉。
⚙ edit_file({"path":"test/calc.test.js","old_string":"test('divide by zero should throw', ()…)
✘ edit_file 回灌 115 字符 · 这次 edit_file 调用没有被执行:测试是验收标准,不能靠改测试让它变绿。请改被测代码。
不能改测试,那就改被测代码——给 divide 加上除零保护。
⚙ edit_file({"path":"src/calc.js","old_string":"export function divide(a, b) {\n  return a /…)
需要确认:edit_file src/calc.js
原因:没有命中任何规则,写操作默认需要确认

五次工具调用里,一次被 deny 拦下、一次停下来问、三次直接放行。这就是这道门该有的样子:该问的问出来了,不该打扰的没打扰。

一条通用的写法建议:拒绝文本包含三段——被拒的是什么、为什么、可以换哪条路。第三段最容易漏,也最有用。

记住这次选择:本轮、本会话,还是持久

同一个操作被问第五遍的时候,用户就开始无脑按同意了。所以「记住」是审批门的必要功能,而不是锦上添花。三种范围各有各的语义:

范围有效期适合什么
本轮这一次任务里不再问「这次可以」——最常用,也最安全
本会话直到退出一个任务里要反复动同一批文件
持久写下来,下次启动仍然有效稳定的团队约定,比如「这个目录随便改」

三者的区别在记到哪里:本轮记进一个一次性集合,会话结束前每轮开始时清空;本会话往规则表里追加一条允许规则;持久除了追加还要落盘。落盘之后它和默认规则表一起参与匹配,走的还是「更具体的赢」那条口径,不需要额外逻辑。

「本轮」这个范围的清空时机值得留意:它跟着一次用户任务走,不跟着会话走。 用户说「这次可以」,指的是他刚才交代的那件事,不是这个终端接下来的余生。所以清空放在每次开始跑一轮的时候。

至于配置写到哪个文件、多层配置怎么合并,是第二十一天打包发布时的题目,今天只演示「记住」这一层语义。

自动模式的三个前置条件

最后回答一个绕不开的问题:能不能干脆全自动?

能,但要看条件。当且仅当下面三条同时成立时,开全自动才是负责任的,少一条就不该开:

  1. 沙盒。 它能碰到的东西是有边界的:临时目录或容器、有限的文件范围、不该有的网络访问被切断。判据是「最坏情况下它能损坏的东西,你能接受」。
  2. 可回滚。 出错之后你能一键退回。判据不是「有 git 就行」,而是「未提交的改动也保得住」——所以第十四天的快照与回滚,是自动模式真正的前置条件。
  3. 可观测。 它做过什么有完整记录,事后能一条条看。第七天的事件日志就是这一条。

三条一起看,会得出一个也许反直觉的结论:自动模式不是一个开关,而是一整套基础设施的结果。 只把审批门关掉、不做前两条,那不叫自动模式,叫无人监督。

顺带回头看昨天那条命令黑名单:它现在的定位更清楚了——它是防手滑的最后一道,不是隔离手段。真正的隔离靠第一条。

源码导读

动手实验

🧪 D5 实验:三态审批门与三种记忆范围

代码位置:labs/my-coding-agent-21days/day-05-approval-gate

今天六个工具一个都不改,改的全是「工具执行之前那一步」。starter 挖了四个练习点,其中两个(默认判定、没有提问渠道时怎么办)都是「写错了不会报错,只会在出事那天暴露」的地方。原样跑是九项里过三项。

  1. 实现规则表与匹配函数,支持工具名、路径模式、命令前缀三类条件,并按「更具体的赢、同具体度更严格的赢」定优先级。
  2. 把审批门插进工具执行链路:只读工具直接过,写工具与命令工具停下来问;确认没有提问渠道时按拒绝处理。
  3. 实现三种记忆范围,并让审批提示里带上「为什么问你」这一行。
  4. 让被拒绝的调用变成一条工具结果回灌,观察模型改测试被拦下之后自己转去改被测代码。
  5. 跑自检:MOCK=1 SELFTEST=1 pnpm start 应该打印 9/9 通过,其中调用序列与提问次数都是可复现的。

验收看五条勾:自检 9/9 通过;只读工具一次都不触发审批;改测试被 deny 拦下且模型自己改道;回答 y 后改动生效、回答 n 后文件一个字节都没动;三种记忆范围各自的效果都能看到(本轮只问一次、本会话不再问、持久那次写进了文件)。

面试题

今天三道题,考的是权限模型的设计判断,不是安全知识背诵:

  1. 设计一个 Agent 的权限模型,你需要哪几种状态?规则按什么匹配?
  2. 用户拒绝了一次工具调用,循环该怎么继续?为什么不能直接抛错?
  3. 什么情况下可以给 Agent 开全自动模式?你会要求哪些前置条件?

完整的中英题干、分析过程与答题要点见本课面试题库的第五天。第二题是这三道里最有区分度的——它表面在问异常处理,实际在问「你有没有想过被拒的调用在消息数组里长什么样」。

检查清单与明日预告

  • 能说清三态各自的语义,特别是 deny 与 ask 的差别不只是严格程度
  • 能解释为什么规则优先级要按具体度而不是按顺序
  • 知道写操作没有规则命中时默认必须是 ask,以及没有提问渠道时该怎么办
  • 能说清「审批是暂停不是异常」在实现上省掉了什么
  • 三种记忆范围各自记在哪、「本轮」的清空时机跟着什么走
  • 能说出自动模式的三个前置条件,以及它们分别对应课程的哪一天

明天是 D6《错误处理与自纠:失败回灌、退避重试、死循环检测与取消中断》。这五天里我们已经零散地遇到过好几类失败:流断在中间、参数不是合法 JSON、编辑的原文对不上、命令卡死、这次被用户拒绝。明天把它们放在一起做一次分类,并给出一条判据:能让模型自己改的错误就回灌给它,不能的才向用户抬头。 顺便把两件今天欠下的事补上——带抖动的退避重试,以及取消信号从按键一路传到子进程的完整链路。

面试题库

  • 设计一个 Agent 的权限模型,你需要哪几种状态?规则按什么匹配、命中多条时听谁的?Designing an agent's permission model: which states do you need, what do rules match on, and which rule wins when several match?
    国内高频海外高频进阶#permissions#agent-design

    分析过程 · 先想清楚再作答

    1. 这题在筛「有没有把门装在真用的东西上」。答「允许和禁止两种」的人没考虑过第三种情况;区分度在于你能不能说出 ask 存在的理由,以及规则冲突怎么解。
    2. 怎么拆:先问「有没有一类操作,答案取决于当时在干什么」。有,而且它是绝大多数写操作——改某个文件该不该允许,取决于改的是哪个文件、改成什么、当时在做什么任务。规则表答不了这种问题,人看一眼能答,所以必须有第三态 ask。
    3. 三态的语义要说清,尤其是 deny 与 ask 的差别不只是严格程度:deny 是「这条路封了,别再问」,问了等于把判断成本转嫁给用户。少了 deny,系统会向两个极端失效——要么把人问烦(他一路按同意,门就没了),要么被放宽到没有约束。
    4. 然后是匹配维度:工具名不够用,必须还能按路径模式与命令前缀匹配,因为「改代码」和「改测试」是同一个工具。另外要有一个「这个工具会不会改东西」的标记,让只读工具默认放行——这个字段必须在工具协议第一版就留,否则外部接进来的工具(MCP、Skills)要么全放行要么全问一遍。
    5. 最后是冲突解决,这是最容易答错的一半:不要按规则表顺序取第一条命中的。顺序会变成隐含语义,你在末尾加一条更具体的规则却被上面某条宽泛规则吃掉,而现象是「什么也没发生」。正确口径是「更具体的赢,具体度相同时更严格的赢」,实现上就是把优先级写成一个可比较的排序键。还有一条:写操作没有规则命中时默认必须是 ask,默认放行的门等于没有门。
    6. 可预期的追问:规则从哪来?至少三处——内置默认、用户配置、本次会话里的临时批准。三处合成一张表,靠具体度而不是来源决定优先级,这样加一处来源不需要改判定逻辑。

    How to reason about it · think before answering

    1. This screens for having actually gated real tools. Answering allow and deny misses the third case; the signal is justifying ask and explaining conflict resolution.
    2. How to break it down: ask whether some operations depend on what you are doing right now. They do, and they are most write operations — whether editing a file is fine depends on which file, what change, and which task. A rule table cannot answer that but a human glance can, hence a third state.
    3. Spell out the semantics, especially that deny differs from ask in kind, not just degree: deny means the path is closed, stop asking, because asking pushes the decision cost onto the user. Without deny the system fails in both directions: users get prompt fatigue and click through, or the rules get widened until nothing is enforced.
    4. Then matching: a tool name is not enough, because editing source and editing tests are the same tool, so path patterns and command prefixes are first-class conditions. You also need a will-this-mutate flag on every tool so read-only tools pass by default, and that flag must exist in the first version of the tool protocol, or externally loaded tools end up either all allowed or all prompted.
    5. Finally conflicts, the half most people get wrong: do not take the first matching rule in table order, because order becomes implicit semantics and a newly added specific rule gets swallowed by a broad one, with no error at all. The right rule is most specific wins, strictest wins on ties, implemented as a comparable sort key. And with no rule matched, a write must default to ask, because a default-allow gate is not a gate.
    6. Likely follow-up: where do rules come from? At least three places — built-in defaults, user config, and in-session approvals. Merge them into one table and let specificity rather than origin decide, so adding a source needs no change to the decision logic.

    答题要点

    • 三态而不是两态:ask 是给「取决于当时在干什么」的操作留的位置
    • deny 与 ask 的差别是「要不要打扰人」,不只是严格程度
    • 匹配维度:工具名、路径模式、命令前缀,另加一个只读标记让只读工具默认放行
    • 冲突解决按具体度而不是表顺序:更具体的赢、同具体度更严格的赢,写成一个排序键
    • 写操作无规则命中时默认 ask;规则可以来自默认表、配置与本次会话,靠具体度统一裁决

    Key points

    • Three states, not two: ask exists for operations whose answer depends on the current task
    • Deny versus ask is about whether to interrupt the user, not merely strictness
    • Match on tool name, path pattern, and command prefix, plus a read-only flag so reads pass by default
    • Resolve conflicts by specificity rather than table order, most specific then strictest, as a sort key
    • Unmatched writes default to ask, and rules from defaults, config, and the session merge into one table
  • 用户拒绝了一次工具调用,循环该怎么继续?为什么不能直接抛错?The user rejects a tool call. How does the loop continue, and why not just throw?
    国内高频海外高频深入#approval#agent-loop

    分析过程 · 先想清楚再作答

    1. 这题表面在问异常处理,实际在问「你有没有想过被拒的那次调用在消息数组里长什么样」。答「捕获异常、提示用户」的人没意识到问题出在消息结构上。
    2. 怎么拆:先看约束。模型请求调工具时,消息数组里会多一条带调用清单的助手消息;而协议要求**每个调用都必须有且只有一条对应的结果消息**,否则下一轮请求不合法,大多数网关会直接报错。所以「拒绝」这件事必须以某种形式变成一条结果消息,逃不掉。
    3. 结论:把审批门做成「返回一条工具结果就等于不执行」。放行返回空,不放行返回一条 ok 为 false 的结果,内容是为什么不执行。于是被拒的调用和「工具执行失败」在消息数组里长得一模一样,模型会像处理任何一次失败那样自己改道——不需要为审批发明新的消息类型,循环里的改动只有几行。
    4. 反过来算抛异常的代价:你要在循环外面接住它、判断这一轮还要不要继续、还得补一条工具结果消息,复杂度全堆在错误路径上——而错误路径是最难测的地方。而且并行调用时更糟:一个被拒不该让另外两个已经执行完的调用作废。
    5. 还有一条决定行为的细节:拒绝文本要写成模型能改道的样子,三段——被拒的是什么、为什么、可以换哪条路。写「permission denied」它会原地重试同一个调用;写「测试是验收标准,请改被测代码」它会换一条路。工具的失败信息就是模型的行为规范。
    6. 可预期的追问:那审批的等待会不会阻塞循环?会,而且应该会——它就是循环里的一次暂停,模型在等、工具没执行、什么都没发生。要注意的是没有提问渠道的场景(CI、管道、别的 Agent 的 shell):此时必须按拒绝处理,自动同意等于把最危险的环境变成全自动。

    How to reason about it · think before answering

    1. This looks like an exception-handling question but really asks whether you have pictured what a rejected call looks like in the message array. Answering catch it and tell the user misses that the problem is structural.
    2. How to break it down: start from the constraint. When the model requests tools, an assistant message carrying the call list is appended, and the protocol requires exactly one result message per call, or the next request is invalid and most gateways reject it outright. So a rejection must become a result message one way or another.
    3. Conclusion: design the gate so that returning a tool result means the call was not executed. Return nothing to allow, or a failed result explaining why. A rejected call then looks exactly like a tool failure, and the model reroutes as it would after any failure, with no new message type and only a few lines of change in the loop.
    4. Price the alternative: throwing forces you to catch outside the loop, decide whether the turn continues, and still synthesize a result message, piling complexity onto the error path, which is the hardest path to test. With parallel calls it is worse, since one rejection should not void two calls that already succeeded.
    5. One behavioral detail: write the rejection in three parts — what was blocked, why, and which alternative exists. Permission denied makes the model retry the same call; testing is the acceptance criterion, change the code under test makes it reroute. A tool's failure text is the model's behavior spec.
    6. Likely follow-up: does waiting for approval block the loop? Yes, and it should — that is the pause. What matters is the no-channel case, in CI, pipes, or another agent's shell, where you must treat ask as deny, since auto-approving turns the most dangerous environment fully autonomous.

    答题要点

    • 协议约束:每个工具调用必须有且只有一条结果消息,否则下一轮请求不合法
    • 门的形状是「返回一条结果就等于不执行」,放行返回空
    • 于是被拒调用与工具失败同构,模型按处理失败的方式自己改道,循环几乎不用改
    • 抛异常会把复杂度堆到错误路径上,并行调用时还会牵连已经成功的调用
    • 拒绝文本三段:被拒的是什么、为什么、可以换哪条路;没有提问渠道时按拒绝处理

    Key points

    • Protocol constraint: exactly one result message per tool call, or the next request is invalid
    • Shape the gate so returning a result means not executed, and returning nothing means allowed
    • Rejections then look like tool failures, so the model reroutes on its own and the loop barely changes
    • Throwing piles complexity onto the error path and, with parallel calls, voids calls that already succeeded
    • Rejection text has three parts: what was blocked, why, and the alternative; with no channel, treat ask as deny
  • 什么情况下可以给 Agent 开全自动模式?你会要求哪些前置条件?When is it acceptable to run an agent fully autonomously, and what preconditions do you require?
    国内高频海外高频进阶#autonomy#safety

    分析过程 · 先想清楚再作答

    1. 这题在考工程判断力,也在考诚实。答「不能开,太危险」得不到分,答「可以开,我平时都开」更糟。区分度在于你能不能给出可检查的条件,而不是一个态度。
    2. 怎么拆:把「自动」翻译成「出错时谁来兜」。人不在场兜,所以必须让环境兜。三条前置条件正好对应三种兜法:沙盒兜住损失范围、回滚兜住已经造成的改动、日志兜住事后追责。少一条就不该开。
    3. 三条各自的判据要具体。沙盒:最坏情况下它能损坏的东西你能接受——临时目录或容器、有限的文件范围、切断不该有的网络。可回滚:出错后能一键退回,而且判据不是「有版本控制就行」,未提交的改动也要保得住,所以文件快照是必需的。可观测:它做过什么有完整记录、能一条条回放,这就是只追加的事件日志的用处。
    4. 结论要点出一个反直觉的地方:**自动模式不是一个开关,而是一整套基础设施的结果。** 只把审批门关掉、不做前三条,那不叫自动,叫无人监督。反过来,三条都具备时,审批门的价值会自然下降——因为「问一句」的收益本来就来自「人能拦住不可逆的事」。
    5. 生产视角补一条常被忽略的:衡量一道门的好坏不要用「问的次数」。每步都问的门实际安全性往往更低,因为它训练用户不看内容直接同意。正确的两个指标是「不可逆操作有没有漏过去」和「一个典型任务里用户被打断几次」。
    6. 可预期的追问:命令黑名单算不算隔离?不算,它是防手滑的最后一道。正则绕过的办法有一百种,真正的隔离只能靠第一条。这个区分要主动讲,它说明你知道自己那道闸有多厚。

    How to reason about it · think before answering

    1. This tests engineering judgment and honesty. Answering never, too risky scores nothing, and answering sure, I always run it is worse. The signal is offering checkable conditions instead of an attitude.
    2. How to break it down: translate autonomy into who absorbs the mistakes. Nobody is watching, so the environment must absorb them, and three preconditions map to three ways of absorbing: a sandbox bounds the damage, rollback undoes what happened, and logs make it auditable. Missing any one means do not enable it.
    3. Make each criterion concrete. Sandbox: the worst thing it can damage is something you accept losing — throwaway directory or container, limited file scope, network cut where it is not needed. Rollback: one action returns you to the previous state, and having version control is not enough, because uncommitted work must survive too, which is why file snapshots are required. Observability: a complete, replayable record of what it did, which is what an append-only event log provides.
    4. State the counterintuitive conclusion: autonomy is not a switch, it is the output of infrastructure. Turning off the approval gate without the three preconditions is not autonomy, it is unsupervised. Conversely, once all three hold, the marginal value of prompting drops, because prompting existed to let a human stop irreversible actions.
    5. One production point people miss: do not judge a gate by how often it asks. A gate that asks on every step is often less safe, because it trains users to approve without reading. The two right metrics are whether any irreversible action slipped through and how many times a typical task interrupts the user.
    6. Likely follow-up: does a dangerous-command denylist count as isolation? No, it is fat-finger protection. There are a hundred ways around a regex, and real isolation only comes from the sandbox. Volunteering that distinction shows you know how thin that layer is.

    答题要点

    • 把「自动」翻译成「出错时谁来兜」:人不在场,所以环境必须兜
    • 三个前置条件:沙盒(损失范围可接受)、可回滚(含未提交改动)、可观测(完整可回放的记录)
    • 少一条就不该开;只关掉审批门不做这三条,叫无人监督不叫自动
    • 衡量门的好坏用「不可逆操作有没有漏过去」与「典型任务被打断几次」,不用问的次数
    • 命令黑名单是防手滑不是隔离,真正的隔离只能靠沙盒

    Key points

    • Translate autonomy into who absorbs errors: with no human present, the environment must
    • Three preconditions: a sandbox bounding damage, rollback that covers uncommitted work, and replayable observability
    • Missing any one means do not enable it; disabling the gate without them is unsupervised, not autonomous
    • Judge a gate by irreversible actions that slipped through and interruptions per typical task, not prompt count
    • A command denylist is fat-finger protection, not isolation; only the sandbox provides that

评论