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

2
00:00:06,382 --> 00:00:10,937
今天这一集，我想追踪一次工具调用的完整生命周期。

3
00:00:10,937 --> 00:00:21,490
你让 Agent 跑一条命令，从它提出请求，到这条命令真的在操作系统里跑起来，中间到底过了几道关，每一道关是谁在做决定。

4
00:00:21,490 --> 00:00:27,319
这个问题之所以重要，是因为很多关于 Agent 安全的讨论其实停得很浅。

5
00:00:27,319 --> 00:00:34,098
有人以为安全就是判断这是个什么工具，有人以为弹个窗让用户点一下就算安全了。

6
00:00:34,098 --> 00:00:43,846
但你去看 Grok Build 的源码，会发现它是一条六段的链路，而且最关键的一条认知是，授权和约束是两层完全不同的东西。

7
00:00:43,846 --> 00:00:45,661
这一集分两半。

8
00:00:45,661 --> 00:00:51,021
前半段走完那条六段授权链，重点看它为什么不能被简化。

9
00:00:51,021 --> 00:01:00,793
后半段讲钩子，也就是挂在事件上的可编程检查点，核心结论只有一句，明确拒绝才阻断，钩子自己挂了则放行。

10
00:01:00,793 --> 00:01:04,170
先预告一句收尾时会回到的观点。

11
00:01:04,170 --> 00:01:14,098
真正可靠的系统，不会把安全寄托在某一个组件上，而是让每一层都清楚自己负责什么，以及自己失效时会发生什么。

12
00:01:14,098 --> 00:01:18,449
这两层如果混在一起谈，你会得出错误的结论。

13
00:01:18,449 --> 00:01:25,805
比如以为权限允许就等于这次执行是安全的，或者以为有了沙箱就不必再做授权判断。

14
00:01:25,805 --> 00:01:31,646
前者忽略了执行时的能力约束，后者把能力边界当成了意图审查。

15
00:01:31,646 --> 00:01:35,000
这一集就是把它们掰开，各归各位。

16
00:01:35,140 --> 00:01:36,832
第一段叫解析。

17
00:01:36,782 --> 00:01:52,419
工具输入会被映射成具体的访问类型，可能是读文件、改文件、跑命令、搜索、调用外部工具、抓取网页或者搜索网页，同时还会带上路径、命令、域名或者外部工具名称这些细节。

18
00:01:52,419 --> 00:01:54,703
为什么要强调这一步。

19
00:01:54,703 --> 00:01:58,874
因为决策的输入不是工具类型，而是访问语义。

20
00:01:58,874 --> 00:02:06,626
同样一个工具，操作的是仓库内的源码，还是家目录下的密钥文件，是完全不同的两件事。

21
00:02:06,626 --> 00:02:11,758
如果只按工具类型判断，你就把最关键的那部分信息丢掉了。

22
00:02:11,758 --> 00:02:17,852
所以第一条原则就是，决策输入比工具类型更具体，别只读工具名。

23
00:02:17,852 --> 00:02:19,931
第二段是计划模式。

24
00:02:19,931 --> 00:02:25,881
计划模式的编辑门可以在发出权限请求之前直接拒绝修改动作。

25
00:02:25,881 --> 00:02:31,109
也就是说，有些请求根本走不到后面几关，在这里就被拦下了。

26
00:02:31,109 --> 00:02:35,100
而计划文件本身有单独的自动批准路径。

27
00:02:35,100 --> 00:02:36,939
第三段是钩子。

28
00:02:36,939 --> 00:02:40,148
工具执行前的钩子可以显式阻断。

29
00:02:40,148 --> 00:02:45,689
匹配上的钩子按配置顺序运行，显式的拒绝会立即停止。

30
00:02:45,689 --> 00:02:49,162
这里埋一个伏笔，本集后半段会展开。

31
00:02:49,162 --> 00:02:57,179
超时、崩溃或者输出格式错误，按当前实现是失败放行的，并且会记录到界面和日志里。

32
00:02:57,179 --> 00:02:59,871
随后还可以再运行客户端钩子。

33
00:02:59,871 --> 00:03:01,590
这里还有个细节。

34
00:03:01,590 --> 00:03:07,984
计划模式下修改动作被拦在权限请求之前，用户根本不会看到弹窗。

35
00:03:07,984 --> 00:03:15,460
这条路径让计划模式的约束比普通授权更硬，因为它绕过了用户确认而不是依赖它。

36
00:03:15,460 --> 00:03:23,381
这跟直觉相反，通常我们觉得让用户点一下最保险，但在这里，不问用户反而是更强的约束。

37
00:03:23,381 --> 00:03:27,479
把前三段合起来看，你会发现一个有意思的结构。

38
00:03:27,479 --> 00:03:34,979
前面这几关都在做同一件事，能早拒绝就早拒绝，尽量不把判断压力推给用户。

39
00:03:34,979 --> 00:03:39,066
换句话说，链路的设计意图是逐层收窄。

40
00:03:39,066 --> 00:03:42,636
能由系统确定的，就不要问用户。

41
00:03:42,636 --> 00:03:45,965
必须由用户决定的，才放到最后。

42
00:03:45,965 --> 00:03:49,138
这里还有一条工程判断值得记住。

43
00:03:49,138 --> 00:03:56,686
用户的确认是稀缺资源，一个每次都弹窗的系统，用户很快就会养成无脑点同意的习惯。

44
00:03:56,686 --> 00:04:02,335
减少不必要的确认，本身就是在保护确认这个机制的有效性。

45
00:04:02,476 --> 00:04:04,301
第四段是策略。

46
00:04:04,251 --> 00:04:16,053
权限解析模块要合并好几路来源，包括请求自带的要求、托管设置、托管配置、Grok 自身的配置，以及一份来自另一款产品的设置作为兜底。

47
00:04:16,053 --> 00:04:18,950
这里有一条特别值得记住的规则。

48
00:04:18,950 --> 00:04:24,671
规则评估与来源顺序无关，优先级是拒绝大于询问大于允许。

49
00:04:24,671 --> 00:04:35,056
这句话的意思是，不管一份允许规则写在多前面、来自多高优先级的配置文件，只要存在一条拒绝规则命中，拒绝就赢。

50
00:04:35,056 --> 00:04:40,392
这是一个非常明确的方向性设计，合并规则是取严不取宽。

51
00:04:40,392 --> 00:04:47,376
我们在上一集讲沙箱配置合并的时候见过同样的思路，全局优先、项目只能新增。

52
00:04:47,376 --> 00:04:51,258
这其实是贯穿整个系统的一条设计原则。

53
00:04:51,258 --> 00:04:55,933
凡是涉及安全的配置合并，方向必须是从严不从宽。

54
00:04:55,933 --> 00:04:58,385
为什么要反复强调这条。

55
00:04:58,385 --> 00:05:04,202
因为一个系统里最危险的不是没有规则，而是规则之间会互相覆盖。

56
00:05:04,202 --> 00:05:10,609
只要你允许后来的配置覆盖前面的配置，攻击者就一定会去找那个覆盖点。

57
00:05:10,609 --> 00:05:15,404
再往深想一层，这套优先级还解决了一个现实问题。

58
00:05:15,404 --> 00:05:18,361
不同来源的配置由不同的人写。

59
00:05:18,361 --> 00:05:25,056
托管设置来自组织，项目配置来自仓库，请求自带的要求则来自模型本身。

60
00:05:25,056 --> 00:05:32,255
如果按来源顺序决定胜负，那么谁能控制顺序，谁就能控制安全，这显然是错的。

61
00:05:32,255 --> 00:05:37,532
所以规则评估与来源顺序无关，这是一条抗操纵的设计。

62
00:05:37,684 --> 00:05:42,201
第五段是决策，也是整条链路里分支最多的一段。

63
00:05:42,151 --> 00:05:45,336
最先短路的是托管策略的拒绝。

64
00:05:45,336 --> 00:05:48,305
只要命中，后面全都不用看了。

65
00:05:48,305 --> 00:05:50,636
随后依次考虑这些路径。

66
00:05:50,636 --> 00:06:01,959
优先锁定的放行、会话级授权、自动模式的快速路径与分类器、沙箱内命令的自动放行、只读安全项、外部工具与域名授权。

67
00:06:01,959 --> 00:06:06,983
只有当这些都没能给出结论的时候，才进入提示，去问用户。

68
00:06:06,983 --> 00:06:10,672
素材里给了一段真实源码，我把重点念一下。

69
00:06:10,672 --> 00:06:15,396
托管策略运行在自动放行和沙箱快速路径之前。

70
00:06:15,396 --> 00:06:28,641
而且沙箱自动放行的条件写得很死，只有在访问类型是命令、沙箱确实应该自动允许、并且没有被策略强制提示、也没有被自动模式强制提示的情况下才放行。

71
00:06:28,641 --> 00:06:30,468
为什么要念这个。

72
00:06:30,468 --> 00:06:36,045
因为有一条流传很广的说法，说沙箱里所有写操作都会自动批准。

73
00:06:36,045 --> 00:06:37,487
这是错的。

74
00:06:37,487 --> 00:06:44,903
源码里的沙箱快速路径专门只检查命令这一类，而且还受两个强制提示开关的约束。

75
00:06:44,903 --> 00:06:48,966
改文件另有自己的会话授权和编辑策略。

76
00:06:48,966 --> 00:06:52,391
素材里那句关键校正值得抄下来。

77
00:06:52,391 --> 00:06:56,754
沙箱内所有写操作自动批准，并非通用规则。

78
00:06:56,754 --> 00:07:03,437
看到这种流传很广的简化说法，第一反应应该是去翻源码，而不是继续传播它。

79
00:07:03,437 --> 00:07:05,516
还有一点同样重要。

80
00:07:05,516 --> 00:07:10,901
自动模式的分类器给出的判定，也可以被强制提示开关覆盖。

81
00:07:10,901 --> 00:07:17,427
这说明即便是模型自己觉得安全的操作，组织策略依然保留最后否决权。

82
00:07:17,427 --> 00:07:21,718
模型的主观判断永远排在被配置的规则后面。

83
00:07:21,718 --> 00:07:28,124
按这个顺序读下来你会发现，用户确认其实是最后兜底，而不是主要手段。

84
00:07:28,124 --> 00:07:36,790
一个设计良好的授权链，绝大部分请求应该在前面几段就被自动裁决掉，剩下的才需要人来拍板。

85
00:07:36,940 --> 00:07:40,303
第六段是执行，这里要分清两层。

86
00:07:40,253 --> 00:07:45,253
权限层回答的是，这次工具请求是否允许进入执行。

87
00:07:45,253 --> 00:07:52,272
它读取访问类型、目标细节、组织策略、会话授权、自动判定和用户选择。

88
00:07:52,272 --> 00:07:55,770
规则可以要求询问，也可以直接拒绝。

89
00:07:55,770 --> 00:08:01,635
沙箱层回答的是，获准执行的进程实际能访问哪些文件和网络。

90
00:08:01,635 --> 00:08:04,352
这两层的边界必须分开记。

91
00:08:04,352 --> 00:08:07,356
权限允许只放行本次请求。

92
00:08:07,356 --> 00:08:14,604
如果沙箱确实处于激活状态，那么进程仍然要受能力集合和子进程网络策略的约束。

93
00:08:14,604 --> 00:08:21,263
反过来说，沙箱没有应用的时候，你也不能把权限弹窗当成内核级别的隔离。

94
00:08:21,263 --> 00:08:22,861
一句话总结。

95
00:08:22,861 --> 00:08:27,669
授权决定能不能尝试，沙箱限制执行时能做到什么。

96
00:08:27,669 --> 00:08:31,791
两层叠加，才是一次工具执行的真实安全边界。

97
00:08:31,791 --> 00:08:35,998
把这个机制跟前面几段对照起来看会更清楚。

98
00:08:35,998 --> 00:08:42,368
权限层在授权链里决定能不能尝试，沙箱层在执行时决定能做到什么。

99
00:08:42,368 --> 00:08:48,462
即使权限层放行了，沙箱层仍然可能让这次操作写不进去、连不出去。

100
00:08:48,462 --> 00:08:51,876
这不是重复劳动，而是纵深防御。

101
00:08:51,876 --> 00:08:55,902
两层各自独立失效，攻击者也得连破两道。

102
00:08:55,902 --> 00:08:58,414
还要提醒一句常见误解。

103
00:08:58,414 --> 00:09:02,801
有人觉得既然有了沙箱，授权链就可以放松一点。

104
00:09:02,801 --> 00:09:04,748
这是相反的思路。

105
00:09:04,748 --> 00:09:10,746
沙箱在部分环境会退化，一旦没生效，授权链就是唯一剩下的防线。

106
00:09:10,746 --> 00:09:15,037
两层的强度都必须独立成立，不能互相指望。

107
00:09:15,037 --> 00:09:19,448
还有一类情况特别容易出问题，就是混合命令。

108
00:09:19,448 --> 00:09:28,126
源码会用语法解析器把可安全分解的脚本拆成一条条普通命令，识别与、或、分号和管道。

109
00:09:28,126 --> 00:09:33,739
每个非初始化片段都要独立通过安全检查、策略或者授权。

110
00:09:33,739 --> 00:09:41,563
这么做是为了防止那种前面放个无害命令、后面跟一个危险命令的写法借首段蒙混过关。

111
00:09:41,563 --> 00:09:48,847
遇到命令替换、复杂控制流，或者根本无法可靠分解的脚本，系统会做保守处理。

112
00:09:48,847 --> 00:09:57,368
包装器会递归剥离到实际命令，危险前缀包括删除、改权限、改属主、杀进程和推送代码。

113
00:09:57,368 --> 00:10:03,450
实在拆不出来的，就进入保守提示，让用户只为完整脚本确认一次。

114
00:10:03,450 --> 00:10:08,955
从工程角度看，这个分段机制解决的是一个很具体的攻击面。

115
00:10:08,955 --> 00:10:18,114
模型很擅长生成看起来无害的命令行，如果只对整条命令做一次判断，那么拼接技巧就成了现成的绕过手段。

116
00:10:18,114 --> 00:10:25,229
所以判据不是「这条命令像不像坏命令」，而是「每一段各自能不能独立通过检查」。

117
00:10:25,372 --> 00:10:27,329
后半段讲钩子。

118
00:10:27,279 --> 00:10:29,430
先建立心智模型。

119
00:10:29,430 --> 00:10:37,303
把钩子看成挂在事件上的可编程检查点，它不是一个拦截网，而是一组可以在特定时刻插入的检查。

120
00:10:37,303 --> 00:10:39,154
事件面有多大。

121
00:10:39,154 --> 00:10:49,671
主流程有八个检查点，会话开始、会话结束、停止、停止失败、工具执行前、工具执行后、工具执行失败、权限被拒。

122
00:10:49,671 --> 00:10:58,997
扩展的还有七个，用户提示提交、通知、子 Agent 启动、子 Agent 停止、压缩前、压缩后，以及一个兼容别名。

123
00:10:58,997 --> 00:11:01,233
加起来十五个事件名。

124
00:11:01,233 --> 00:11:04,022
但这里有一条极其关键的边界。

125
00:11:04,022 --> 00:11:08,757
十五个事件里，只有工具执行前这一个的阻断能力是真。

126
00:11:08,757 --> 00:11:11,293
原文那句话我原样复述。

127
00:11:11,293 --> 00:11:14,671
事件被触发，不等于能控制主流程。

128
00:11:14,671 --> 00:11:20,416
事件枚举负责定义触发点，阻断能力是另一件事单独声明的。

129
00:11:20,416 --> 00:11:22,916
这条边界经常被忽略。

130
00:11:22,916 --> 00:11:27,952
很多人看到有个事件发生，就默认可以在那儿拦住，其实拦不住。

131
00:11:27,952 --> 00:11:32,892
读事件列表的时候，你要同时追踪结果怎么回到调用方。

132
00:11:32,892 --> 00:11:37,808
这跟上一集的思路是一致的，看名字不够，要看能力。

133
00:11:37,808 --> 00:11:39,959
还有一点值得说清楚。

134
00:11:39,959 --> 00:11:48,300
钩子的输入是事件的信封，通过标准输入传给外部命令，输出则是标准输出上的结构化决策。

135
00:11:48,300 --> 00:11:53,961
这个接口设计决定了钩子是一段外部程序，而不是内联的策略。

136
00:11:53,961 --> 00:12:02,375
外部程序会崩溃、会超时、会被系统杀掉，这就是为什么下一张矩阵里，失败情况占了大半个表。

137
00:12:02,375 --> 00:12:06,666
这也是为什么只有工具执行前这一个事件能阻断。

138
00:12:06,666 --> 00:12:14,995
阻断意味着主流程要等一个外部进程的决定，这个等待是有代价的，系统只在一个地方付这个代价。

139
00:12:15,148 --> 00:12:19,905
再具体看钩子结果的解释规则，这是本集最硬的一段。

140
00:12:19,855 --> 00:12:24,747
钩子返回明确拒绝，结果是拒绝，工具调用被阻断。

141
00:12:24,747 --> 00:12:29,687
没有有效输出但退出码是二，走兜底拒绝，也阻断。

142
00:12:29,687 --> 00:12:35,961
有明确允许输出但退出码是二，输出优先，放行并记录一条冲突警告。

143
00:12:35,961 --> 00:12:41,634
退出码非零且不是二，判定为钩子执行失败，放行并记录警告。

144
00:12:41,634 --> 00:12:46,983
超时或者进程崩溃，同样是执行失败，放行并记录警告。

145
00:12:46,983 --> 00:12:58,509
输出无效或者决策值未知，回退到按退出码解释，或者判定为执行失败，这时候输出本身不阻断，但兜底的退出码二仍然会拒绝。

146
00:12:58,509 --> 00:13:02,439
把这六种情况摆在一起，结论非常清楚。

147
00:13:02,439 --> 00:13:07,608
只有明确的拒绝才阻断，钩子自己出任何问题，都是放行。

148
00:13:07,608 --> 00:13:13,341
源码里那条日志写得很直白，钩子失败，忽略，失败放行。

149
00:13:13,341 --> 00:13:18,485
而且注释里明确要求，钩子故障不能破坏工具的可用性。

150
00:13:18,485 --> 00:13:21,358
这个设计不是疏忽，是取舍。

151
00:13:21,358 --> 00:13:23,701
它的安全含义是这样的。

152
00:13:23,701 --> 00:13:28,882
钩子适合做策略提醒、审计，以及可恢复的前置检查。

153
00:13:28,882 --> 00:13:35,973
但是如果你需要的是强制保证，那就必须另外使用权限层和沙箱，不能指望钩子。

154
00:13:35,973 --> 00:13:42,560
把这张矩阵和上一集的沙箱降级条件放在一起看，你会看到一个共同点。

155
00:13:42,560 --> 00:13:49,507
系统明确地区分了两件事，一件是我决定了不行，另一件是我没能做出决定。

156
00:13:49,507 --> 00:13:52,115
前者阻断，后者放行。

157
00:13:52,115 --> 00:13:57,067
这是一种可用性优先的取舍，代价就是安全不能依赖它。

158
00:13:57,067 --> 00:14:02,692
理解了这个区分，你就能判断一个检查机制到底能承担什么职责。

159
00:14:02,692 --> 00:14:07,271
凡是「必须拦住」的规则，都要放在失败即阻断的那一层。

160
00:14:07,271 --> 00:14:11,201
凡是「最好提醒一下」的规则，才可以放在钩子里。

161
00:14:11,201 --> 00:14:15,648
把强制规则放进钩子，等于把它降级成了建议。

162
00:14:15,796 --> 00:14:17,825
最后是配置的写法。

163
00:14:17,775 --> 00:14:21,837
层级是事件、匹配器组、处理器列表三层。

164
00:14:21,837 --> 00:14:31,561
命令从标准输入接收事件信封，有效的输出决策优先，没有有效输出的时候再按退出码解释，退出码二表达拒绝。

165
00:14:31,561 --> 00:14:40,058
匹配器由正则编译，并且有一层兼容映射，让配置里写的命令类工具名能命中内部的实际工具名。

166
00:14:40,058 --> 00:14:43,147
用户指南的示例用的是工具名。

167
00:14:43,147 --> 00:14:52,823
存放位置方面，全局的在用户目录的 grok 目录下，项目的在项目目录的 grok 目录下，而且项目侧的钩子受目录信任控制。

168
00:14:52,823 --> 00:14:58,280
保留的环境变量会被过滤，未解析的变量会在启动前报错。

169
00:14:58,280 --> 00:15:03,376
素材最后给了一个很有价值的练习，我把它转成一条测试纪律。

170
00:15:03,376 --> 00:15:07,414
你写好一个钩子之后，至少要验证四条路径。

171
00:15:07,414 --> 00:15:11,958
第一，脚本返回明确拒绝时，工具确实被阻断。

172
00:15:11,958 --> 00:15:19,662
第二，制造退出码一、超时、无效输出这三种故障，验证它们全都放行并且产生告警。

173
00:15:19,662 --> 00:15:26,092
第三，把退出码改成二，再验证无效输出的情况下仍然走明确拒绝。

174
00:15:26,092 --> 00:15:32,859
第四，写一句边界说明，讲清楚哪条策略必须移到权限层或者沙箱去。

175
00:15:32,859 --> 00:15:38,015
这四条里，第二条最容易被跳过，而它恰恰是最该测的。

176
00:15:38,015 --> 00:15:42,775
因为你以为在保护你的那个钩子，可能早就在静默放行了。

177
00:15:42,775 --> 00:15:46,080
顺带说一句这套机制的可测试性。

178
00:15:46,080 --> 00:15:52,895
因为钩子就是一段命令，你可以单独跑它、单独制造故障，不需要启动整个 Agent。

179
00:15:52,895 --> 00:15:57,378
这意味着这些检查点是可以被自动化用例覆盖的。

180
00:15:57,378 --> 00:16:04,722
相比之下，很多系统的安全检查是内联的，你想验证它失效时的行为，几乎无从下手。

181
00:16:04,722 --> 00:16:06,933
再补一条工程建议。

182
00:16:06,933 --> 00:16:13,256
钩子脚本要尽量短、尽量快地返回，它跑在主流程的关键路径上。

183
00:16:13,256 --> 00:16:19,217
给它设一个明确的超时，并确保超时之后的行为是你预期的那样。

184
00:16:19,372 --> 00:16:21,641
收尾，压成五条。

185
00:16:21,591 --> 00:16:25,606
第一条，授权不能简化成工具类型判断。

186
00:16:25,606 --> 00:16:31,808
决策输入是访问语义，带上路径、命令、域名这些细节才有意义。

187
00:16:31,808 --> 00:16:36,159
第二条，安全配置的合并方向是从严不从宽。

188
00:16:36,159 --> 00:16:41,868
策略优先级是拒绝大于询问大于允许，与来源顺序无关。

189
00:16:41,868 --> 00:16:46,122
这一条和上一集的全局优先保护是同一个原则。

190
00:16:46,122 --> 00:16:49,884
第三条，权限层和沙箱层必须分开。

191
00:16:49,884 --> 00:16:54,692
授权决定能不能尝试，沙箱限制执行时能做到什么。

192
00:16:54,692 --> 00:16:59,933
别把弹窗当成隔离，也别以为进了沙箱就什么都能自动批准。

193
00:16:59,933 --> 00:17:03,490
第四条，混合命令必须分段判断。

194
00:17:03,490 --> 00:17:10,149
前面的无害命令不能给后面的危险命令背书，拆不出来的脚本就走保守提示。

195
00:17:10,149 --> 00:17:14,548
第五条，钩子只在你明确拒绝的时候才靠得住。

196
00:17:14,548 --> 00:17:19,247
超时、崩溃、非零退出码，全部失败放行。

197
00:17:19,247 --> 00:17:23,310
所以强制保证一定要落在权限层和沙箱层。

198
00:17:23,310 --> 00:17:27,673
把这五条串起来，你会看到一个一致的设计哲学。

199
00:17:27,673 --> 00:17:37,469
真正可靠的系统不会把安全寄托在某一个组件上，而是让每一层都清楚自己负责什么，以及自己失效时会发生什么。

200
00:17:37,469 --> 00:17:42,276
知道自己失效时的行为，这本身就是可靠性的一部分。

201
00:17:42,276 --> 00:17:44,067
最后留一个问题。

202
00:17:44,067 --> 00:17:50,221
你的系统里有没有哪个安全组件，你从未验证过它失效时会发生什么。

203
00:17:50,221 --> 00:17:53,875
如果有，它大概率正在静默放行。

