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

2
00:00:06,382 --> 00:00:13,701
这一集讲沙箱，讲的是一个很容易被忽略的分工，安全策略到底是写在哪一层的。

3
00:00:13,701 --> 00:00:19,326
素材来自 OpenAI 的 Codex 仓库，我会顺着沙箱管理器的调用链讲。

4
00:00:19,326 --> 00:00:21,490
先给一个具体场景。

5
00:00:21,490 --> 00:00:27,560
同一份仓库，三台机器，模型发出同一条查看仓库状态的命令。

6
00:00:27,560 --> 00:00:32,788
配置里写的是同一份权限档案，工作区可写，网络收紧。

7
00:00:32,788 --> 00:00:36,610
但三条命令实际跑起来的形态完全不同。

8
00:00:36,610 --> 00:00:41,057
苹果机器上，命令外面套了一层系统的沙箱执行器。

9
00:00:41,057 --> 00:00:45,973
Linux 上，档案被序列化成一个参数交给一个辅助程序。

10
00:00:45,973 --> 00:00:49,891
Windows 上，命令可能原样出门，什么都没套。

11
00:00:49,891 --> 00:00:53,125
为什么同一份档案会有三种命运。

12
00:00:53,125 --> 00:00:56,694
因为从档案到命令之间，隔着一次编译。

13
00:00:56,694 --> 00:01:04,375
而这次编译要先回答两个问题，这台机器要不要上沙箱，以及这台机器有没有可用的后端。

14
00:01:04,375 --> 00:01:08,016
这两个问题的答案不一样，产物就不一样。

15
00:01:08,016 --> 00:01:11,033
排障的时候最容易在这里走歪。

16
00:01:11,033 --> 00:01:20,709
日志里档案还在，模型上下文里那份环境说明也还在，看起来一切正常，只是平台这一层没有把档案编译成包装命令。

17
00:01:20,709 --> 00:01:25,336
于是你以为沙箱失效了，实际上它从来没被装上过。

18
00:01:25,336 --> 00:01:32,800
这一集的下半场会再往下钻一层，讲苹果机器上那层沙箱的策略文本是怎么拼出来的。

19
00:01:32,800 --> 00:01:40,492
你会发现同样的原则在更细的粒度上又出现了一次，意图和能力分开，正文和参数分开。

20
00:01:40,492 --> 00:01:46,899
上半场讲调度，下半场讲拼装，两半场是同一套思路的两个尺度。

21
00:01:47,040 --> 00:01:53,997
Codex 把这件事拆成两个函数，我认为这是整个沙箱设计里最值得抄的一笔。

22
00:01:53,947 --> 00:01:58,117
第一个函数只返回一个布尔，要不要上沙箱。

23
00:01:58,117 --> 00:02:00,581
它读权限档案里的三态。

24
00:02:00,581 --> 00:02:06,855
禁用这一档恒为假，强制这一档恒为真，自动这一档要看档案的形状。

25
00:02:06,855 --> 00:02:09,572
有托管网络要求，必须上。

26
00:02:09,572 --> 00:02:15,377
网络收紧的时候，除了调用方自己管文件系统这种情况，都要上。

27
00:02:15,377 --> 00:02:19,800
只有网络放开并且文件系统不受限，才跳过。

28
00:02:19,800 --> 00:02:23,742
第二个函数按操作系统给出一个后端类型。

29
00:02:23,742 --> 00:02:30,028
苹果机器给一种，Linux 给另一种，Windows 多一个开关，开关关着就是空。

30
00:02:30,028 --> 00:02:35,677
注意这里空的语义，它不是不需要，是这台主机没有可以派发的实现。

31
00:02:35,677 --> 00:02:40,377
然后调度函数先问第一个，再把第二个的空收成无沙箱。

32
00:02:40,377 --> 00:02:43,442
这句话是本集最关键的一句。

33
00:02:43,442 --> 00:02:47,721
档案说需要，主机给不出，最终类型仍然是无。

34
00:02:47,721 --> 00:02:49,427
为什么不报错。

35
00:02:49,427 --> 00:02:55,197
因为要不要是策略问题，有没有是能力问题，两者的失败后果不同。

36
00:02:55,197 --> 00:03:06,302
策略说要但能力给不出，在某些部署里是可以接受的降级，决策权交给后面的策略层，而不是让沙箱这一层直接把命令掐死。

37
00:03:06,302 --> 00:03:09,656
这里还要分清三态各自钉死的是什么。

38
00:03:09,656 --> 00:03:15,870
禁用这一档钉死的是不需要外层沙箱，强制这一档钉死的是需要外层沙箱。

39
00:03:15,870 --> 00:03:19,031
注意它们钉死的是意图，不是能力。

40
00:03:19,031 --> 00:03:24,608
强制这一档写得再硬，也钉不出一台机器上有没有可用的后端。

41
00:03:24,608 --> 00:03:30,822
这就是两个函数必须分开的根本原因，意图可以被钉死，能力只能被查询。

42
00:03:30,822 --> 00:03:34,932
这个拆分带来的最大好处是排障路径清晰。

43
00:03:34,932 --> 00:03:41,591
以后遇到沙箱没生效，你先问两件事，配置里写的是不是要，这台机器上有没有。

44
00:03:41,591 --> 00:03:45,617
两个答案一对，问题在哪一层立刻就清楚了。

45
00:03:45,617 --> 00:03:50,738
如果这两个问题混在一个函数里，你只能靠打日志猜。

46
00:03:50,738 --> 00:03:53,105
还有一层收益在产品侧。

47
00:03:53,105 --> 00:04:02,360
状态栏、文档、模型说明，如果都去读配置里希望启用的那一侧，用户看到的就是意图而不是事实。

48
00:04:02,360 --> 00:04:07,949
排障会从这里开始歪，因为所有人都在对着一份没有落地的配置讨论。

49
00:04:07,949 --> 00:04:15,052
正确的做法是让这些展示读最终类型，也就是那个已经把能力收进去的结果。

50
00:04:15,190 --> 00:04:17,447
再看一份档案的去向。

51
00:04:17,397 --> 00:04:22,205
它其实有两个出口，一个喂给包装命令，一个喂给模型。

52
00:04:22,205 --> 00:04:27,133
权限档案的三个变体，写的是谁负责建外面那层沙箱。

53
00:04:27,133 --> 00:04:30,582
托管这一档由系统自己拼包装命令。

54
00:04:30,582 --> 00:04:32,938
禁用这一档不要外层。

55
00:04:32,938 --> 00:04:38,479
外部这一档文件系统由调用方负责，网络仍然可能归系统管。

56
00:04:38,479 --> 00:04:41,147
转换函数按类型分派。

57
00:04:41,147 --> 00:04:50,474
无沙箱这一档原样传出用户的命令参数，连当前工作目录都不准备，因为未沙箱的请求可能带着别的机器的路径。

58
00:04:50,474 --> 00:05:00,126
苹果机器只信任系统里那个固定路径的执行器，策略走一个参数，路径走另一个参数，用户命令放在双短横线后面。

59
00:05:00,126 --> 00:05:08,755
Linux 把整份档案序列化进一个参数交给辅助程序，辅助程序缺失直接报错，不会按无沙箱降级。

60
00:05:08,755 --> 00:05:16,388
Windows 第一拍只做校验，命令参数不动，包装器就是当前的可执行文件，套壳推迟到后面那一步。

61
00:05:16,388 --> 00:05:19,537
同一个档案的另一个出口是模型。

62
00:05:19,537 --> 00:05:25,642
它由另一个组件收成内部枚举，渲染进模型看得见的环境上下文。

63
00:05:25,642 --> 00:05:30,270
禁用这一档会写成类型是禁用，文件系统是不受限。

64
00:05:30,270 --> 00:05:33,347
这里有个很重要的边界要说清楚。

65
00:05:33,347 --> 00:05:38,058
模型可见的文本挡不住越权，它只减少无效尝试。

66
00:05:38,058 --> 00:05:45,053
也就是说，这份渲染进上下文的说明，作用是让模型别去撞墙，不是墙本身。

67
00:05:45,053 --> 00:05:47,938
真正的墙在子进程入口。

68
00:05:47,938 --> 00:05:54,008
把这两件事分清楚，你才知道哪些改动是安全的，哪些是自欺欺人。

69
00:05:54,008 --> 00:05:57,638
再看外部这一档，它最容易引起误解。

70
00:05:57,638 --> 00:06:02,301
它的意思是文件系统由调用方负责，不是什么都不管。

71
00:06:02,301 --> 00:06:04,777
网络仍然可能归系统管。

72
00:06:04,777 --> 00:06:12,974
也就是说，即使你把文件系统这一摊交给了外面的调用方，网络那一段的策略还是这套代码在编译。

73
00:06:12,974 --> 00:06:20,642
理解这一点很重要，因为很多人以为选了外部就等于全权交给调用方，实际上只是分工不同。

74
00:06:20,642 --> 00:06:26,989
顺带一句，平台给不出实现的时候，渲染给模型的那份说明不会改口。

75
00:06:26,989 --> 00:06:31,063
它仍然按档案描述意图，不按主机能力撒谎。

76
00:06:31,063 --> 00:06:37,181
这个选择是对的，因为模型规划下一步用的是意图，主机能力是执行层的事。

77
00:06:37,181 --> 00:06:48,936
反过来想一下，如果它按能力改口，模型就会以为沙箱没装是配置允许的结果，于是放心去尝试那些本该被挡住的操作，撞墙次数反而变多。

78
00:06:49,080 --> 00:06:52,996
单条命令还可以再带一份临时叠加的权限。

79
00:06:52,946 --> 00:06:57,982
这里的合并规则很讲究，加宽和批准用的是两种运算。

80
00:06:57,982 --> 00:06:59,617
合并走并集。

81
00:06:59,617 --> 00:07:02,898
任意一侧打开网络，结果就是开网。

82
00:07:02,898 --> 00:07:05,855
文件系统条目首尾相接去重。

83
00:07:05,855 --> 00:07:10,626
这条规则回答的是，这条命令本次要开多大的口。

84
00:07:10,626 --> 00:07:12,321
批准走求交。

85
00:07:12,321 --> 00:07:18,919
网络必须两侧都开才留下，文件系统只保留落在请求范围内的已批准条目。

86
00:07:18,919 --> 00:07:23,967
这条规则回答的是，人批下来的东西有没有宽过请求本身。

87
00:07:23,967 --> 00:07:26,407
为什么批准必须用求交。

88
00:07:26,407 --> 00:07:33,619
因为如果人批的时候也用并集，一次误批就能把请求里根本没有的路径写进会话授权。

89
00:07:33,619 --> 00:07:40,674
开口是这次的事，批准会沉淀成后续每一轮的默认，两者的后果量级完全不同。

90
00:07:40,674 --> 00:07:42,705
还有一个细节值得记。

91
00:07:42,705 --> 00:07:47,429
求交结果为空，会话不记账，基座档案继续生效。

92
00:07:47,429 --> 00:07:52,249
这里空集的含义是，这次额外开口没留下任何加宽。

93
00:07:52,249 --> 00:07:57,345
不要把它解释成改成了最严，也不要解释成整条命令被禁止。

94
00:07:57,345 --> 00:08:02,802
命令还能不能跑，要看后面的策略和沙箱，不在这一个返回值里。

95
00:08:02,802 --> 00:08:05,794
再补一个容易被做错的细节。

96
00:08:05,794 --> 00:08:14,629
求交结果为空的时候，会话不记账，意思是这次批准没有沉淀成任何长期授权，基座档案继续生效。

97
00:08:14,629 --> 00:08:25,049
如果你把它实现成清空会话授权，那就错了，因为原有的授权是用户之前明确批准过的，不该被一次失败的额外请求连带清掉。

98
00:08:25,049 --> 00:08:28,943
空集只表示这次没加宽，不表示回滚。

99
00:08:28,943 --> 00:08:36,239
这种把加宽和批准分开做运算的做法，本质上是在区分临时授权和长期授权。

100
00:08:36,239 --> 00:08:44,424
任何涉及审批的系统都会遇到这道题，而且通常都是在出了一次误批事故之后才想起来补。

101
00:08:44,570 --> 00:08:48,678
下半场讲苹果机器上那层沙箱具体怎么拼。

102
00:08:48,628 --> 00:08:52,114
这里有个非常漂亮的设计，值得单独讲。

103
00:08:52,114 --> 00:08:54,842
沙箱执行器吃的是一段文本。

104
00:08:54,842 --> 00:08:58,881
文本由静态基线加现拼的读写网段组成。

105
00:08:58,881 --> 00:09:01,332
问题来了，路径怎么办。

106
00:09:01,332 --> 00:09:11,933
路径里有用户的目录名，而目录名里可能出现括号、空格、引号，这些字符在那段文本的语法里全都是语法符号。

107
00:09:11,933 --> 00:09:20,383
有人把工程放在一个带括号和空格的目录里，路径一旦插进策略正文，用户的目录名就变成了语法。

108
00:09:20,383 --> 00:09:25,551
解析失败，模型看到的是包装器报错，不是沙箱拒绝。

109
00:09:25,551 --> 00:09:30,924
这种失败最难查，因为你从错误信息里看不出是路径的问题。

110
00:09:30,924 --> 00:09:33,724
解法是把正文和路径分开。

111
00:09:33,724 --> 00:09:40,515
动态读写规则只往正文里写一个键名，真实路径通过另一组参数传进去。

112
00:09:40,515 --> 00:09:43,484
排除子路径同样走参数表。

113
00:09:43,484 --> 00:09:48,989
策略正文和参数表必须同时出现，测试把这件事锁成了合同。

114
00:09:48,989 --> 00:09:53,376
这条原则跟具体的语法无关，值得单独记住。

115
00:09:53,376 --> 00:09:59,445
安全策略如果最终是一段文本，用户可控的字符串就不要进这段文本。

116
00:09:59,445 --> 00:10:05,503
模板里只放占位符，真实路径、域名、端口走另一份参数表。

117
00:10:05,503 --> 00:10:10,095
你在任何模板拼接的场景里都能用，而且成本极低。

118
00:10:10,095 --> 00:10:14,914
还有一点值得学，就是他们用测试把这件事锁成合同。

119
00:10:14,914 --> 00:10:22,162
正文和参数表必须同时出现，这是一条能被自动检查的规则，不是写在文档里的建议。

120
00:10:22,162 --> 00:10:28,424
任何安全约束如果只靠人记住，它迟早会在某次重构里被悄悄改掉。

121
00:10:28,424 --> 00:10:32,114
能写成测试的，就不要写成注释。

122
00:10:32,260 --> 00:10:36,020
再看这段文本的顺序，它也是有讲究的。

123
00:10:35,970 --> 00:10:40,272
静态基线是四份文件，编译期就编进了二进制。

124
00:10:40,272 --> 00:10:43,157
第一句业务规则是默认拒绝。

125
00:10:43,157 --> 00:10:45,669
没写明的事一律拒绝。

126
00:10:45,669 --> 00:10:52,484
子进程继承这份底稿，被拉起的脚本再分叉出来的子孙，仍然在同一套规则里。

127
00:10:52,484 --> 00:10:56,667
然后是动态段，按这一次的可读可写根现拼。

128
00:10:56,667 --> 00:11:01,739
拼接顺序是固定的，基线、读、写、网络在前。

129
00:11:01,739 --> 00:11:07,063
全盘可读才加偏好设置，受限读才加平台默认路径。

130
00:11:07,063 --> 00:11:16,126
祖先的那条禁止删除链接放在最后，目的是避免前面更宽的允许把改名操作要用的删除权限重新打开。

131
00:11:16,126 --> 00:11:20,633
这里的关键不是顺序本身，而是顺序背后的合同。

132
00:11:20,633 --> 00:11:24,479
更窄的拒绝必须能压住前面更宽的允许。

133
00:11:24,479 --> 00:11:35,801
这句话是所有规则叠加系统里最容易出错的地方，因为它要求你对每一条规则的相对强度有明确判断，而不能靠书写顺序碰运气。

134
00:11:35,801 --> 00:11:40,489
同事把仓库设成可写根，让模型在里面改代码。

135
00:11:40,489 --> 00:11:47,532
模型想在仓库里建一个自己的配置目录，命令立刻失败，错误是操作不允许。

136
00:11:47,532 --> 00:11:50,982
可写根听起来像整棵树，实际不是。

137
00:11:50,982 --> 00:11:54,491
这就是默认拒绝加按需开门的效果。

138
00:11:54,491 --> 00:11:58,950
这里还有个被低估的性质，子进程会继承这份底稿。

139
00:11:58,950 --> 00:12:05,104
沙箱执行器拉起的脚本再分叉出来的子孙进程，仍然在同一套规则里。

140
00:12:05,104 --> 00:12:11,210
也就是说，这道墙不是只挡住第一条命令，它跟着进程树往下传。

141
00:12:11,210 --> 00:12:20,789
这一点决定了沙箱和普通的权限检查根本不是一类东西，权限检查是一次性的，沙箱是一次设定、持续生效。

142
00:12:20,930 --> 00:12:26,312
再看一个更细的点，挖空一条路径的时候要挡住几种绕法。

143
00:12:26,262 --> 00:12:30,721
假设你想在可写根里保护一个目录，不让它被模型改。

144
00:12:30,721 --> 00:12:35,000
直觉是写一条排除规则，排除这个子路径。

145
00:12:35,000 --> 00:12:37,248
但只写一条是不够的。

146
00:12:37,248 --> 00:12:41,298
排除子路径的时候，需要写两条禁止规则。

147
00:12:41,298 --> 00:12:44,880
一条管目录自己，一条管下面的内容。

148
00:12:44,880 --> 00:12:47,741
两条一起放进一个并且关系里。

149
00:12:47,741 --> 00:12:52,212
顺序不敏感，但少写管目录自己那条就会漏。

150
00:12:52,212 --> 00:12:53,462
漏在哪。

151
00:12:53,462 --> 00:12:56,430
漏在第一次创建这个目录的时候。

152
00:12:56,430 --> 00:13:02,019
目录还不存在，管内容的那条规则没有适用对象，创建成功。

153
00:13:02,019 --> 00:13:04,063
还有第三种绕法。

154
00:13:04,063 --> 00:13:12,428
模型先在仓库里写一个普通文件，然后把整个工作区改名挪走，想把这个目录一起挪到沙箱外面。

155
00:13:12,428 --> 00:13:18,305
要挡住这招，可写根目录本身还要再钉一条禁止删除链接的规则。

156
00:13:18,305 --> 00:13:26,106
所以挖空一条路径，实际上要同时挡三件事，节点本身、它的子孙、以及把这个节点搬走。

157
00:13:26,106 --> 00:13:29,916
三条一起生成，并且用测试锁住形状。

158
00:13:29,916 --> 00:13:36,827
可写根默认保护三个顶层名字，版本目录、代理配置目录、还有那个配置目录。

159
00:13:36,827 --> 00:13:44,255
另外，可写根中间如果出现用户可控的符号链接，拼装直接报错，不会跟着链接走。

160
00:13:44,255 --> 00:13:49,159
这条很硬，宁可报错也不跟着一个可能被操纵的路径。

161
00:13:49,159 --> 00:13:56,695
另外，可写根中间如果出现用户可控的符号链接，拼装会直接报错，不会跟着链接走。

162
00:13:56,695 --> 00:14:02,825
这条很硬，宁可让整条命令失败，也不跟着一个可能被操纵的路径走。

163
00:14:02,825 --> 00:14:14,339
对比一下常见的实现方式，很多系统会选择解析链接然后继续，结果就是攻击者只要能建一个链接，就能把受保护目录的写入重定向到别处。

164
00:14:14,339 --> 00:14:17,188
最后提醒一句跨平台的差异。

165
00:14:17,188 --> 00:14:23,065
苹果机器上常见的是操作不允许这句话，能被现有的关键词表接住。

166
00:14:23,065 --> 00:14:29,447
不要假设另外两个平台也是同一句错误文案，错误分类不能只认一种字符串。

167
00:14:29,447 --> 00:14:35,373
做拒绝原因归类的时候，关键词表要按平台维护，不要写死一套。

168
00:14:35,520 --> 00:14:39,496
网络是另一段动态文本，规则也一样朴素。

169
00:14:39,446 --> 00:14:46,477
要走代理却没有可用端口，就返回受限的网络段加网络基线，不给整网放行。

170
00:14:46,477 --> 00:14:51,008
网络基线还在，只是没有那种无条件的出站允许。

171
00:14:51,008 --> 00:14:57,114
换句话说，宁可让网络不好用，也不要为了让它能用就把整网放开。

172
00:14:57,114 --> 00:15:02,559
这里可以横向对比另外两个项目，看清各自把失败倒向哪一边。

173
00:15:02,559 --> 00:15:10,852
一个项目在缺后端的时候直接拒绝执行，文案写得很直白，拒绝在无约束状态下运行这条命令。

174
00:15:10,852 --> 00:15:16,345
Codex 在 Unix 辅助程序缺失时同样不跑，属于同一类选择。

175
00:15:16,345 --> 00:15:24,025
但 Windows 开关关着的时候，它把给不出实现收成无，交给后面的策略层，这是另一种选择。

176
00:15:24,025 --> 00:15:26,814
还有一个项目的做法更不一样。

177
00:15:26,814 --> 00:15:35,287
它的沙箱管理器作用对象是当前进程，注释写明不可逆，编译产物是一组能力，不包每条命令的参数。

178
00:15:35,287 --> 00:15:43,148
平台不支持或者应用失败，都只打警告然后继续跑，后面的工具调用共享同一份能力。

179
00:15:43,148 --> 00:15:47,835
代价是没法在同一进程里给两条命令两套边界。

180
00:15:47,835 --> 00:15:52,775
三种选择的差别，本质上是把不可逆的决策放在哪一层。

181
00:15:52,775 --> 00:15:59,939
放在进程入口，粒度粗但简单，代价是同一进程里的所有命令共享一套边界。

182
00:15:59,939 --> 00:16:07,859
放在每条命令，粒度细但复杂，代价是每次执行都要重新编译一次策略，还要处理编译失败。

183
00:16:07,859 --> 00:16:11,393
没有标准答案，取决于你的威胁模型。

184
00:16:11,393 --> 00:16:13,508
给你一个判断标准。

185
00:16:13,508 --> 00:16:21,682
如果你的场景里，同一个进程会先后执行信任等级差别很大的命令，那就必须为每条命令编译。

186
00:16:21,682 --> 00:16:28,040
如果整个进程的信任等级是统一的，进程级一次设定就够了，简单可靠。

187
00:16:28,040 --> 00:16:35,480
真正危险的是选了进程级却以为自己有命令级粒度，这种错觉比没有沙箱更糟。

188
00:16:35,620 --> 00:16:39,608
第一条，把要不要和有没有写成两个函数。

189
00:16:39,558 --> 00:16:43,356
一个返回布尔，一个返回可选的后端名。

190
00:16:43,356 --> 00:16:50,255
这条最便宜，收益最大，因为它直接决定了你以后能不能在三分钟内定位沙箱问题。

191
00:16:50,255 --> 00:16:57,611
第二条，一份档案两个出口，模型那一侧只负责减少无效尝试，不负责挡住越权。

192
00:16:57,611 --> 00:17:01,289
别把渲染给模型的说明当成安全边界。

193
00:17:01,289 --> 00:17:04,979
第三条，开口用并集，批准用求交。

194
00:17:04,979 --> 00:17:10,507
临时加宽和长期授权的后果量级不同，运算规则就必须不同。

195
00:17:10,507 --> 00:17:15,075
空集表示没留下加宽，不要解释成最严或禁止。

196
00:17:15,075 --> 00:17:21,469
第四条，安全策略如果是一段文本，用户可控的字符串不要进这段文本。

197
00:17:21,469 --> 00:17:25,135
模板只放占位符，真实值走参数表。

198
00:17:25,135 --> 00:17:28,741
这一条今天就能改，成本接近于零。

199
00:17:28,741 --> 00:17:35,988
第五条，挖空一条路径要挡住节点、子孙和搬家三件事，并且用测试锁住形状。

200
00:17:35,988 --> 00:17:42,503
少一条就漏一种绕法，而这种漏法只有在有人刻意尝试的时候才会暴露。

201
00:17:42,503 --> 00:17:45,520
五条串起来，核心还是那句话。

202
00:17:45,520 --> 00:17:49,534
意图和能力分开问，策略和执行分开写。

