1
00:00:00,100 --> 00:00:06,432
本内容改编自小山学堂《学 AI 产品，从入门到精通》，为二次演绎配音版。

2
00:00:06,382 --> 00:00:21,590
这是 Codex 系列的第十二集，讲三件连在一起的事：命令策略怎么自带测试、审批弹窗到底由哪两颗旋钮决定、以及当弹窗多到人开始无脑点同意时，系统怎么把判断权交给另一个模型。

3
00:00:21,590 --> 00:00:24,499
先从一个很常见的场景说起。

4
00:00:24,499 --> 00:00:30,845
你给团队写一条禁令，pattern 写成 git 加 reset，本意是拦掉 hard 那个参数。

5
00:00:30,845 --> 00:00:33,837
过了一周有人报，keep 也被拦了。

6
00:00:33,837 --> 00:00:37,263
前缀一短，后面跟什么都命中同一条。

7
00:00:37,263 --> 00:00:40,580
再过一周，模型用 bash -lc 包了一层。

8
00:00:40,580 --> 00:00:46,518
禁令按字面 argv 去匹配 bash，第一个词对不上，命令直接进了启发式通道。

9
00:00:46,518 --> 00:00:49,727
所以每条白名单都在赌两件事。

10
00:00:49,727 --> 00:00:55,232
一是 pattern 刚好覆盖你想拦的，二是它不会误伤你想放的。

11
00:00:55,232 --> 00:01:02,876
这两件事通常要等线上才知道，而下一次改 pattern 的人，看不到作者当初怕误伤哪条命令。

12
00:01:02,876 --> 00:01:04,835
一句话先放在这儿。

13
00:01:04,835 --> 00:01:08,140
把以为写成例子，机器就可以反对你。

14
00:01:08,140 --> 00:01:10,508
这三节其实是一条链。

15
00:01:10,508 --> 00:01:24,310
第一节讲策略文件怎么把误伤钉死在加载期；第二节讲弹窗什么时候出现，以及点过一次之后系统记住了什么；第三节讲当弹窗多到失去意义时，判断权怎么移交给另一个模型。

16
00:01:24,310 --> 00:01:29,526
三节加起来，就是从规则到审批再到审查的完整闭环。

17
00:01:29,670 --> 00:01:33,345
Codex 的第一招是把正反例写进规则本身。

18
00:01:33,295 --> 00:01:35,315
规则里有两个字段。

19
00:01:35,315 --> 00:01:42,166
match 里放的是这条规则必须命中的命令，not_match 里放的是它必须放过的命令。

20
00:01:42,166 --> 00:01:49,570
注意时序，prefix_rule 并不当场跑例子，它先把规则和例子推进一个待校验队列。

21
00:01:49,570 --> 00:01:53,440
整份 Starlark 求值完之后，parse 再统一校验。

22
00:01:53,440 --> 00:01:55,627
校验顺序是固定的。

23
00:01:55,627 --> 00:01:59,041
先跑反例，命中了就报 ExampleDidMatch。

24
00:01:59,041 --> 00:02:03,175
再跑正例，一个都没命中就报 ExampleDidNotMatch。

25
00:02:03,175 --> 00:02:08,295
两种错都让 parse 返回失败，这份策略根本不会被 build 出去。

26
00:02:08,295 --> 00:02:16,060
出处是 execpolicy 下 parser.rs 的第五十七到七十八行，以及第一百三十三到一百五十一行。

27
00:02:16,060 --> 00:02:19,281
crate 的 README 把这件事写成一句话。

28
00:02:19,281 --> 00:02:23,704
它们是加载期校验的示例调用，可以当成单元测试。

29
00:02:23,704 --> 00:02:31,240
字符串和 token 数组两种写法进解析器后都会变成 token 列表，字符串走 shlex 拆词。

30
00:02:31,240 --> 00:02:39,113
校验时启发式回调传空，也就是说正例必须靠真实的前缀规则命中，不能靠启发式凑数。

31
00:02:39,113 --> 00:02:40,904
为什么这样成立。

32
00:02:40,904 --> 00:02:45,411
白名单的典型失败，是作者以为 pattern 是这个意思。

33
00:02:45,411 --> 00:02:49,353
把以为写成例子，机器就能在加载期反对你。

34
00:02:49,353 --> 00:02:57,298
而且规则和例子写在同一处，策略文件被拷进用户目录或者被下发的时候，例子跟着一起走。

35
00:02:57,298 --> 00:03:03,560
漏写例子等于漏写测试，加载器不会替你编例子，写了就会强制执行。

36
00:03:03,560 --> 00:03:06,757
再把这个机制放回开头那个例子。

37
00:03:06,757 --> 00:03:11,853
你想拦 hard，就把 pattern 写成 git 加 reset，反例里放上 keep。

38
00:03:11,853 --> 00:03:18,368
加载时反例命中，说明这条规则把不想拦的命令也拦了，parse 直接失败。

39
00:03:18,368 --> 00:03:22,346
你要么把 pattern 补全成三段，要么换个写法。

40
00:03:22,346 --> 00:03:27,887
无论哪一种，错误都停在加载期，而不是等到有人来报障。

41
00:03:28,040 --> 00:03:30,045
第二招是匹配方式。

42
00:03:29,995 --> 00:03:36,425
规则本体就是一段前缀，匹配是精确的字符串相等，没有 glob，也没有正则。

43
00:03:36,425 --> 00:03:39,358
这条设计带来两个直接后果。

44
00:03:39,358 --> 00:03:45,908
git reset --hard 能吃掉后面再跟一个分支名的命令，因为多出来的 token 不参与比较。

45
00:03:45,908 --> 00:03:50,584
但它吃不了中间插了 --config 的写法，因为第二个 token 对不上。

46
00:03:50,584 --> 00:03:55,560
出处是 execpolicy 下 rule.rs 的第四十六到五十九行。

47
00:03:55,560 --> 00:04:01,473
查找的时候，规则按第一个 token 放进桶，先按 argv 的第零项精确取。

48
00:04:01,473 --> 00:04:05,235
取不到，再考虑把绝对路径收成 basename。

49
00:04:05,235 --> 00:04:08,889
命中多条规则怎么办，对 decision 取 max。

50
00:04:08,889 --> 00:04:16,365
Decision 这个枚举派生了 Ord，变体的书写顺序就是严重程度，Allow 小于 Prompt 小于 Forbidden。

51
00:04:16,365 --> 00:04:18,396
组合命令也一样。

52
00:04:18,396 --> 00:04:21,425
先拆段再摊平，再取一次 max。

53
00:04:21,425 --> 00:04:27,867
一段 git status 是 Prompt，一段 git commit 是 Forbidden，整条管道最终仍是 Forbidden。

54
00:04:27,867 --> 00:04:32,026
出处是 policy.rs 的第四百零二到四百一十一行。

55
00:04:32,026 --> 00:04:34,226
这里有个很好的性质。

56
00:04:34,226 --> 00:04:37,122
叠加只能加严，不能放宽。

57
00:04:37,122 --> 00:04:42,663
这个序写在枚举变体上，运行时没有另一张优先级表可以写错。

58
00:04:42,663 --> 00:04:46,665
用户层一条宽松的 allow 盖不住系统层的 forbidden。

59
00:04:46,665 --> 00:04:48,240
代价也要说。

60
00:04:48,240 --> 00:04:57,254
前缀强迫你把意图落成 token 序列，插在中间的参数会让规则失效，作者必须写更短的前缀或者另写一条。

61
00:04:57,254 --> 00:05:03,528
而正反例就是用来接住这个代价的，写短了，反例会在加载期先崩。

62
00:05:03,680 --> 00:05:05,685
第三招处理包装。

63
00:05:05,635 --> 00:05:10,070
禁令写的是 git reset --hard，模型写成 bash -lc 包一层。

64
00:05:10,070 --> 00:05:14,120
按字面 argv 去匹，第一个词是 bash，禁令不亮。

65
00:05:14,120 --> 00:05:17,101
判定之前先走一个解析函数。

66
00:05:17,101 --> 00:05:27,486
它要求脚本只由纯词命令加上双与、双竖线、分号、管道这几种连接组成，拒绝重定向、替换、括号和控制流。

67
00:05:27,486 --> 00:05:31,080
过关之后按命令节点切成多段 argv。

68
00:05:31,080 --> 00:05:36,993
于是 bash -lc 包着的 git reset --hard 会被拆成内层那三段，再拿去匹配前缀。

69
00:05:36,993 --> 00:05:43,171
出处是 core 下 exec_policy.rs 的第八百三十一到八百五十八行。

70
00:05:43,171 --> 00:05:45,046
拆不出来的时候呢。

71
00:05:45,046 --> 00:05:49,793
整段 argv 当一条命令，交给启发式和后续的沙箱。

72
00:05:49,793 --> 00:06:01,741
这里有个细节值得留意，空引号插在参数里还能还原，但空引号插在命令名里，word-only 解析会失败，前缀规则就看不见 git 这个词，命令掉进启发式。

73
00:06:01,741 --> 00:06:05,166
吃不下的脚本，就不要假装已经看懂。

74
00:06:05,166 --> 00:06:07,786
这是 fail closed 的通用形状。

75
00:06:07,786 --> 00:06:15,851
它挡得住解析成功但漏掉危险段的情况，而且拆失败之后，启发式和沙箱仍然在后面接着。

76
00:06:15,990 --> 00:06:17,839
第二部分讲审批。

77
00:06:17,789 --> 00:06:20,252
先说一个反直觉的现象。

78
00:06:20,252 --> 00:06:29,483
你把审批留在 on-request，沙箱从 workspace-write 拧到 danger-full-access，十分钟前那条出沙箱的命令还会弹窗，现在不弹了。

79
00:06:29,483 --> 00:06:31,935
你根本没动审批这颗旋钮。

80
00:06:31,935 --> 00:06:35,637
原因是两颗旋钮在同一个判定函数里。

81
00:06:35,637 --> 00:06:45,577
一颗是审批策略，类型里有四个变体；另一颗是文件系统沙箱种类，默认 Restricted，意思是只读或者只能写工作区。

82
00:06:45,577 --> 00:06:48,690
改其中一颗，另一颗的行为跟着走。

83
00:06:48,690 --> 00:06:57,632
出处是 protocol.rs 的第九百二十四到九百四十七行，以及 permissions.rs 的第二百二十七到二百三十二行。

84
00:06:57,632 --> 00:07:03,546
默认逻辑写在 default_exec_approval_requirement 里。

85
00:07:03,546 --> 00:07:11,058
Never 这一层给 Skip，UnlessTrusted 这一层给 NeedsApproval，OnRequest 和 Granular 只在 Restricted 时才要问。

86
00:07:11,058 --> 00:07:16,887
因为 read-only 和 workspace-write 都是 Restricted，所以这两格的默认结局一样。

87
00:07:16,887 --> 00:07:22,152
danger-full-access 把种类变成 Unrestricted，默认函数直接走 Skip。

88
00:07:22,152 --> 00:07:24,111
两个容易搞错的点。

89
00:07:24,111 --> 00:07:31,490
第一，Never 遇到策略要求提问时，会把提问升级成拒绝，它并不表示什么都准跑。

90
00:07:31,490 --> 00:07:41,899
第二，Granular 多一道闸，需要审批且沙箱审批开关关掉时直接 Forbidden，但文件系统如果已经 Unrestricted，这个分支根本进不去。

91
00:07:41,899 --> 00:07:44,543
还有一个加载期的硬拒绝。

92
00:07:44,543 --> 00:07:53,594
配置里 requirements 不允许 danger-full-access，如果同时写了审批策略为 never，加载器会先把权限档案回落到只读。

93
00:07:53,594 --> 00:08:00,481
只读加上从不提问，等于模型在窄沙箱里还没人可问，这种组合直接判非法。

94
00:08:00,481 --> 00:08:04,315
为什么这条要卡在加载期而不是运行时。

95
00:08:04,315 --> 00:08:12,224
因为这种组合一旦允许启动，模型会在一个很窄的沙箱里反复尝试提权，而每一次都被拒。

96
00:08:12,224 --> 00:08:17,392
它看不到任何可以求助的通道，行为会退化成随机试错。

97
00:08:17,392 --> 00:08:21,418
与其让它撞墙，不如在配置阶段就判掉。

98
00:08:21,560 --> 00:08:25,512
弹窗上两条记住长得很像，但差别很大。

99
00:08:25,462 --> 00:08:32,157
一条是本会话记住这条命令，一条是把前缀写进策略文件、跨会话生效。

100
00:08:32,157 --> 00:08:35,197
点错了，后面的边界题就会答反。

101
00:08:35,197 --> 00:08:44,164
有意思的是，常规 exec 弹窗的默认菜单里甚至没有按会话记住这一项，只有带网络上下文时才带上它。

102
00:08:44,164 --> 00:08:48,226
用户最常碰到的记住，其实是前缀那一条。

103
00:08:48,226 --> 00:08:52,794
会话缓存活在一个跟会话同寿命的存储里。

104
00:08:52,794 --> 00:08:58,911
key 是规范化后的完整命令，外加工作目录、环境变量和策略指纹。

105
00:08:58,911 --> 00:09:04,741
所有 key 都必须是按会话批准过才命中，只批准一次不会进这张表。

106
00:09:04,741 --> 00:09:09,452
出处是 sandboxing.rs 的第六十四到一百一十六行。

107
00:09:09,452 --> 00:09:15,630
所以 npm run test 批准进会话，npm run lint 是另一把 key，还要再问一次。

108
00:09:15,630 --> 00:09:19,392
会话缓存认完整命令，不认通配符。

109
00:09:19,392 --> 00:09:23,046
前缀扩张只走策略文件那一条路。

110
00:09:23,046 --> 00:09:25,185
还有一处容易被忽略。

111
00:09:25,185 --> 00:09:30,474
改审批策略不会清空这张表，因为 key 里没有审批策略这一项。

112
00:09:30,474 --> 00:09:35,943
你中途从 OnRequest 改成 UnlessTrusted，已经缓存的批准仍然算数。

113
00:09:35,943 --> 00:09:40,282
只有改策略文件会改指纹，缓存才会失效。

114
00:09:40,282 --> 00:09:43,238
最后一层是给模型的说明书。

115
00:09:43,238 --> 00:09:48,779
权限说明是一段 developer 消息，先选沙箱模板再选审批模板。

116
00:09:48,779 --> 00:09:53,695
Never 那份只有一句，不要再申请沙箱权限，命令会被拒。

117
00:09:53,695 --> 00:10:03,611
说明书和运行时是同一份合同的两面，判定函数改了，说明那一行必须一起改，否则模型会按旧规则反复撞墙。

118
00:10:03,750 --> 00:10:07,377
第三部分讲审批疲劳，以及 Codex 的答法。

119
00:10:07,327 --> 00:10:09,118
想象这个过程。

120
00:10:09,118 --> 00:10:13,758
模型先跑 git status，再读两个文件，然后要 git push。

121
00:10:13,758 --> 00:10:20,885
下一分钟它要 curl，再下一分钟要往临时目录写备忘，再下一分钟要删掉那份备忘。

122
00:10:20,885 --> 00:10:27,015
弹窗叠弹窗，人开始只看命令头两个词，拇指已经形成肌肉记忆。

123
00:10:27,015 --> 00:10:32,784
防线还在，判断已经不在，回路退化成一个总是亮绿灯的按钮。

124
00:10:32,784 --> 00:10:35,284
这时候有两个坑不能踩。

125
00:10:35,284 --> 00:10:42,039
一是把自动放行交给刚写出这条命令的主模型，那就成了申请人兼审批人。

126
00:10:42,039 --> 00:10:47,387
二是失败时放行，那超时和坏 JSON 就变成免费通行证。

127
00:10:47,387 --> 00:10:50,561
Guardian 的做法是开一次专职审查。

128
00:10:50,561 --> 00:10:57,279
它不替代沙箱，也不替代网络代理，只接管一类本来会弹给用户的 on-request 审批。

129
00:10:57,279 --> 00:11:05,753
路由很窄，策略必须是 OnRequest 或 Granular，审查员必须设为自动审查，默认审查员仍然是用户。

130
00:11:05,753 --> 00:11:07,255
四步走完。

131
00:11:07,255 --> 00:11:17,051
第一步重建上下文，只留用户原话和近期工具痕迹，合成脚手架和普通 developer 消息丢掉，超长内容打上截断标记。

132
00:11:17,051 --> 00:11:23,049
第二步送进一个锁死的会话，只读、策略设为 Never、MCP 已清空。

133
00:11:23,049 --> 00:11:28,686
第三步要求交回 JSON，先打风险等级和授权情况，再给结论。

134
00:11:28,686 --> 00:11:33,205
第四步应用结果，允许就继续，其余一律停住。

135
00:11:33,205 --> 00:11:37,135
失败怎么收场，是这里最见功力的地方。

136
00:11:37,135 --> 00:11:45,020
等待 JSON 有九十秒上限，超时走超时分支，坏 JSON 走关闸分支，动作都停住。

137
00:11:45,020 --> 00:11:52,748
关键是这两类故障会清零连续拒绝计数，避免把基础设施抖动算成模型连续作恶。

138
00:11:52,748 --> 00:11:58,674
只有明确的拒绝才记入熔断器，连续到阈值就打断当前这一轮。

139
00:11:58,674 --> 00:12:00,597
还有一句话要记住。

140
00:12:00,597 --> 00:12:06,330
拒绝的时候会写上禁止改道的指示，不让模型换个写法再来一次。

141
00:12:06,330 --> 00:12:08,517
为什么要有这条指示。

142
00:12:08,517 --> 00:12:12,351
审查员给出的结论只对这一次请求有效。

143
00:12:12,351 --> 00:12:21,270
如果模型换个包装方式再提交一次，它拿到的其实是一个全新的请求，熔断器的连续计数反而会被打断。

144
00:12:21,270 --> 00:12:26,258
写上禁止改道，是把这次拒绝的效力钉在这一轮里。

145
00:12:26,410 --> 00:12:28,739
收尾，四条原则。

146
00:12:28,689 --> 00:12:32,079
一，把假设写成可执行的例子。

147
00:12:32,079 --> 00:12:38,978
白名单的失败大多不是写错语法，而是作者以为的覆盖范围不等于实际覆盖范围。

148
00:12:38,978 --> 00:12:42,523
把以为写成正反例，加载期就能发现。

149
00:12:42,523 --> 00:12:48,389
这条不依赖任何特定语言，换成 JSON 或者 YAML 同样能抄。

150
00:12:48,389 --> 00:12:51,874
二，聚合只能加严，不能放宽。

151
00:12:51,874 --> 00:12:56,322
把严重程度做成有序枚举，聚合时用一次 max。

152
00:12:56,322 --> 00:13:02,271
这样运行时就没有第二张优先级表可以写错，用户层也盖不住系统层。

153
00:13:02,271 --> 00:13:05,516
三，解析不了就承认解析不了。

154
00:13:05,516 --> 00:13:13,497
包装命令能拆就拆开按内层判，拆不开就整段回退给后面的机制，不要假装已经看懂脚本。

155
00:13:13,497 --> 00:13:16,117
这是 fail closed 的通用形状。

156
00:13:16,117 --> 00:13:18,954
四，失败不要静默放宽。

157
00:13:18,954 --> 00:13:23,497
超时、坏输出、解析失败，一律停住动作。

158
00:13:23,497 --> 00:13:31,538
同时要把基础设施故障和模型的真实意图分开计数，否则熔断器会把抖动误判成作恶。

159
00:13:31,538 --> 00:13:34,230
再补一句关于审批的提醒。

160
00:13:34,230 --> 00:13:41,225
给模型的说明书和运行时判定是同一份合同的两面，改一面就必须改另一面。

161
00:13:41,225 --> 00:13:46,862
很多反复撞墙的行为，根源不在模型，而在说明书还是旧的。

162
00:13:46,862 --> 00:13:48,966
这一集就到这里。

