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

2
00:00:06,382 --> 00:00:11,298
这一集讲一个跨平台开发里最容易自欺的地方，沙箱。

3
00:00:11,298 --> 00:00:13,401
先看一个真实对照。

4
00:00:13,401 --> 00:00:22,379
同事在 Mac 上跑同一个 Agent，它去碰用户目录里的 SSH 私钥，立刻被拦下，错误写着 Operation not permitted。

5
00:00:22,379 --> 00:00:26,646
同一条读取放到 Windows 上，报错变成 Access is denied。

6
00:00:26,646 --> 00:00:35,937
再换一台没开 Windows 沙箱的机器，这条命令根本走不到权限检查那一层，执行策略先把它判成 Forbidden 或 Prompt。

7
00:00:35,937 --> 00:00:41,935
这三种报错看起来只是文案差别，实际对应三种完全不同的隔离深度。

8
00:00:41,935 --> 00:00:46,766
有进程级沙箱、只有身份和 ACL、以及根本没有后端。

9
00:00:46,766 --> 00:00:50,528
三台机器、同一份意图、三种失败。

10
00:00:50,528 --> 00:00:56,334
麻烦在于，模型看到的下一句话不一样，它下一步的行为就会跟着变。

11
00:00:56,334 --> 00:00:58,882
这才是真正危险的地方。

12
00:00:58,882 --> 00:01:06,814
如果 Windows 上假装有和 Unix 一样的默认沙箱，模型就会按自己被关着去规划，实际命令却在裸跑。

13
00:01:06,814 --> 00:01:12,007
承诺了做不到的墙，模型和用户都会按那道墙做决策。

14
00:01:12,007 --> 00:01:14,795
顺着这个对照再往里看一层。

15
00:01:14,795 --> 00:01:21,899
三种失败之所以危险，不是因为报错不一样，而是因为它们决定了 Agent 的下一个动作。

16
00:01:21,899 --> 00:01:27,355
Mac 上明确拒绝，模型通常会换路径或者请用户授权。

17
00:01:27,355 --> 00:01:33,930
Windows 上变成拒绝访问，模型可能以为只是路径写错了，换个写法再试一次。

18
00:01:33,930 --> 00:01:41,670
而第三台机器上，命令可能压根没走到权限检查，返回的是策略层的拒绝或者请求确认。

19
00:01:41,670 --> 00:01:48,305
同一个意图被解释成三种不同的处境，这就是跨平台沙箱最真实的复杂度。

20
00:01:48,305 --> 00:01:51,514
它不是 API 差异，而是语义差异。

21
00:01:51,514 --> 00:01:54,122
所以这一集的任务很具体。

22
00:01:54,122 --> 00:02:02,439
Windows 上没有 seatbelt，也没有 bubblewrap，那套东西到底是怎么叠出来的，它真正挡住了什么，没挡住什么。

23
00:02:02,439 --> 00:02:03,750
开始。

24
00:02:03,890 --> 00:02:06,039
先看思路上的转向。

25
00:02:05,989 --> 00:02:11,314
macOS 那套限制的是这个进程能对哪些路径做哪些操作。

26
00:02:11,314 --> 00:02:16,854
Linux 那套限制的是这个进程能看见什么、能调用哪些系统调用。

27
00:02:16,854 --> 00:02:19,991
Windows 没有 namespace，也没有 seccomp。

28
00:02:19,991 --> 00:02:22,996
所以不能照抄，必须换一个问题。

29
00:02:22,996 --> 00:02:27,311
换成的这个问题是，这个身份能碰哪些资源。

30
00:02:27,311 --> 00:02:30,544
这里有两种隔离原语要分清楚。

31
00:02:30,544 --> 00:02:34,463
一种是「主体能做什么」，一种是「客体能被谁碰」。

32
00:02:34,463 --> 00:02:36,506
Windows 走的是后者。

33
00:02:36,506 --> 00:02:38,092
具体到实现。

34
00:02:38,092 --> 00:02:39,883
令牌决定你是谁。

35
00:02:39,883 --> 00:02:44,126
RestrictedToken 这一档从当前用户的令牌往下裁。

36
00:02:44,126 --> 00:02:48,777
Elevated 这一档先用专用账户登录，再从那个令牌裁。

37
00:02:48,777 --> 00:02:51,326
两边最后都进同一组标志。

38
00:02:51,326 --> 00:02:53,104
再看包装方式。

39
00:02:53,104 --> 00:02:59,691
需要沙箱的时候，Codex 用自己的可执行文件加上一个隐藏参数来当包装器。

40
00:02:59,691 --> 00:03:05,917
这个思路和 Linux 那边改 arg0 是同一类自我调用，不需要额外的二进制。

41
00:03:05,917 --> 00:03:09,607
这里还要把「令牌是从哪来的」说得更清楚一点。

42
00:03:09,607 --> 00:03:15,544
RestrictedToken 这一档拿的是你现在登录的这个用户的令牌，然后往下裁。

43
00:03:15,544 --> 00:03:21,590
所以它天然继承了这个用户对工作区的大部分可见性，只是写被卡住了。

44
00:03:21,590 --> 00:03:31,638
Elevated 这一档不一样，它先创建了一个独立的专用账户并用那个账户登录，拿到的是那一份令牌，再从这份令牌往下裁。

45
00:03:31,638 --> 00:03:36,530
两档之间的差距不是强度高低，而是「以谁的身份起步」。

46
00:03:36,530 --> 00:03:40,484
这一点决定了后面所有 ACL 到底对谁生效。

47
00:03:40,484 --> 00:03:43,525
还有一条硬事实必须说在前面。

48
00:03:43,525 --> 00:03:45,677
整套东西默认关着。

49
00:03:45,677 --> 00:03:52,095
Windows 开关没打开的时候，平台沙箱返回空，后面不会再包任何包装器。

50
00:03:52,095 --> 00:03:57,900
所以讨论所有细节之前，先确认你这台机器上它到底开着没有。

51
00:03:58,030 --> 00:04:01,549
接着看令牌那一关，也就是第一道关卡。

52
00:04:01,499 --> 00:04:04,841
Codex 用三个标志裁出受限令牌。

53
00:04:04,841 --> 00:04:09,204
第一个是禁用最大特权，把不必要的特权削薄。

54
00:04:09,204 --> 00:04:13,747
第二个是 LUA 标志，也就是标准用户的常规模式。

55
00:04:13,747 --> 00:04:21,583
第三个叫写受限，它要求写操作必须同时通过普通 ACL 检查和 restricting SID 的交叉检查。

56
00:04:21,583 --> 00:04:24,805
记住这三件事各自的管辖范围。

57
00:04:24,805 --> 00:04:28,434
特权被削薄了，写必须过交叉检查。

58
00:04:28,434 --> 00:04:29,636
但读呢。

59
00:04:29,636 --> 00:04:34,576
在 RestrictedToken 这一档，读操作仍然跟着当前用户走。

60
00:04:34,576 --> 00:04:41,607
源码自己写得很清楚，写受限令牌上的那些 capability SID 的拒绝读规则，并不参与读检查。

61
00:04:41,607 --> 00:04:43,639
这句话的含义很重。

62
00:04:43,639 --> 00:04:50,550
它意味着在令牌这一档，读工作区之外的文件会穿过，往外发网络也会穿过。

63
00:04:50,550 --> 00:04:53,951
要拦住它们，必须换到 Elevated 那一档。

64
00:04:53,951 --> 00:04:57,833
所以令牌档的定位非常清晰，它只锁写。

65
00:04:57,833 --> 00:05:05,526
这一档能挡住的是模型误改仓库文件，挡不住模型读出你的密钥，更挡不住它把东西往外传。

66
00:05:05,526 --> 00:05:09,071
这里还有一个平台之间的对照值得记住。

67
00:05:09,071 --> 00:05:13,771
Unix 的网络隔离是给进程换一张网络命名空间视图。

68
00:05:13,771 --> 00:05:21,583
WFP 完全不同，进程还在主机的网络栈上，只是这个身份发出的特定流量被内核滤掉。

69
00:05:21,583 --> 00:05:27,004
进程仍然能看见网卡、能遇到解析失败、能去连别的端口。

70
00:05:27,004 --> 00:05:29,456
别把它们当成等价物。

71
00:05:29,590 --> 00:05:38,746
第二道关卡是两个专用的系统用户，账户名在源码里写死，一个是 CodexSandboxOffline，另一个是 CodexSandboxOnline。

72
00:05:38,696 --> 00:05:40,523
为什么需要两个。

73
00:05:40,523 --> 00:05:43,432
这是这一集最精妙的一处设计。

74
00:05:43,432 --> 00:05:48,780
WFP 是 Windows Filtering Platform，一套内核里的包过滤框架。

75
00:05:48,780 --> 00:05:52,458
关键是，它和防火墙规则都按 SID 匹配。

76
00:05:52,458 --> 00:05:55,691
而一个 SID 只能对应一套网络策略。

77
00:05:55,691 --> 00:06:03,576
换句话说，同一个身份没法同时表示完全没网，也没法同时表示可以走代理或者自由出网。

78
00:06:03,576 --> 00:06:05,523
身份是一对一的。

79
00:06:05,523 --> 00:06:07,879
那就只能增加一个维度。

80
00:06:07,879 --> 00:06:13,552
系统里需要几个不同强度的网络策略档位，就得准备几个不同的身份。

81
00:06:13,552 --> 00:06:21,965
所以选账户的函数在两种情况下会选离线身份，一是强制走代理，二是网络策略没有启用。

82
00:06:21,965 --> 00:06:34,706
离线身份会装出站阻断、代理端口白名单，再加十二条 WFP 过滤器，覆盖 ICMP、DNS 的五十三端口、DNS over TLS 的八百五十三端口，以及 SMB。

83
00:06:34,706 --> 00:06:37,386
在线身份不装这套阻断。

84
00:06:37,386 --> 00:06:44,946
在线身份存在的意义，是给那些需要联网但又要在独立身份下运行的场景留一个位置。

85
00:06:44,946 --> 00:06:46,941
再补一句更细的。

86
00:06:46,941 --> 00:06:55,824
用户配置根默认会把 .ssh 这类目录挖空，但这份排除列表只有在你登录了专用账户之后才会生效。

87
00:06:55,824 --> 00:06:59,165
因为它是挂在那个账户的 ACL 上的。

88
00:06:59,165 --> 00:07:08,023
所以「挖空 .ssh 」这件事，在令牌档是根本不存在的机制，不是没配好，是那一层压根没有这个挂载点。

89
00:07:08,180 --> 00:07:13,863
第三道关卡就是这个 WFP 过滤器，这里有几件必须提前知道的事。

90
00:07:13,813 --> 00:07:18,777
第一，过滤器用稳定的 GUID 标识，并且带着持久标志。

91
00:07:18,777 --> 00:07:21,517
意思是重启之后它们还在。

92
00:07:21,517 --> 00:07:27,719
这句话反过来读更重要，卸载程序不会自动拆掉它们，进程崩溃也不会。

93
00:07:27,719 --> 00:07:33,632
第二，仓库里检索不到删除用户或者拆除 WFP 过滤器的函数。

94
00:07:33,632 --> 00:07:38,933
也就是说，一旦装上，清理工作要靠人工或者别的方式完成。

95
00:07:38,933 --> 00:07:41,529
第三，失败处理不对称。

96
00:07:41,529 --> 00:07:46,445
WFP 失败被写成非致命，安装过程会继续往下走。

97
00:07:46,445 --> 00:07:50,315
但防火墙失败会让整个安装步骤失败。

98
00:07:50,315 --> 00:07:53,128
还有一件演示里很直观的事情。

99
00:07:53,128 --> 00:08:04,630
把读取 SSH 私钥这个动作留在原地，把档位从 Elevated 降到 RestrictedToken，你会发现第二关和第三关直接消失了，那条读取会一路穿过去。

100
00:08:04,630 --> 00:08:11,156
因为在令牌档你根本没有那两个专用账户，第三关连存在的条件都不满足。

101
00:08:11,156 --> 00:08:17,515
这解释了为什么课堂上一个反复强调的判断是，WFP 是挂在身份上的。

102
00:08:17,515 --> 00:08:21,625
没有那个身份，整套网络策略就没有挂载点。

103
00:08:21,625 --> 00:08:25,255
把这三点放在一起看，结论很清楚。

104
00:08:25,255 --> 00:08:29,498
这套方案的摩擦不在运行时，在安装和卸载。

105
00:08:29,498 --> 00:08:36,517
选它之前要先确认自己的环境允许建本地账户，企业策略可能直接禁止。

106
00:08:36,670 --> 00:08:41,223
现在回答那个被问得最多的问题，为什么不上 AppContainer。

107
00:08:41,173 --> 00:08:47,423
原因是它的可读范围默认很窄，任意路径读取必须去改宿主的 DACL。

108
00:08:47,423 --> 00:08:54,154
而 Codex 需要的是工作区可读、平台根可读，再按拒绝列表挖空特定目录。

109
00:08:54,154 --> 00:08:58,577
受限令牌加 ACL 明显更贴合这份权限模型。

110
00:08:58,577 --> 00:09:02,231
在业务代码里检索 AppContainer，零命中。

111
00:09:02,231 --> 00:09:13,348
同样地，对照那个 harness 把这条判断写成了明文，AppContainer 令牌没有环境读权限，任意路径读取必须预先授权，对不上它的读模型。

112
00:09:13,348 --> 00:09:16,161
那 Elevated 为什么也不做默认。

113
00:09:16,161 --> 00:09:22,904
因为要把读和网络都管住，就得建账户、弹 UAC、改防火墙、装 WFP。

114
00:09:22,904 --> 00:09:28,036
每一条都是安装摩擦，而且企业策略可能禁止本地建用户。

115
00:09:28,036 --> 00:09:31,509
这里有个产品层面的判断值得单独说。

116
00:09:31,509 --> 00:09:41,017
把默认设成关，等于是把建账户、弹 UAC、装过滤器这一整套产品摩擦，推迟到用户第一次真正启用的时候。

117
00:09:41,017 --> 00:09:48,817
好处是装上就能跑，代价是大多数用户从来没走到那一档，也就从来没拿到对应的保护。

118
00:09:48,817 --> 00:09:52,219
所以枚举自己把默认值写成了关。

119
00:09:52,219 --> 00:09:59,106
配置里没写 Windows 沙箱字段，两个历史遗留开关也没有的时候，就落到 Disabled。

120
00:09:59,106 --> 00:10:02,098
历史遗留开关已经被标成移除。

121
00:10:02,098 --> 00:10:06,173
源码里那个默认属性标注就钉在 Disabled 上。

122
00:10:06,173 --> 00:10:08,192
关掉之后会怎样。

123
00:10:08,192 --> 00:10:14,478
平台沙箱确实没了，Windows 上取到的是空值，初始选择把它变成无沙箱类型。

124
00:10:14,478 --> 00:10:17,207
命令仍然可能被策略层拦住。

125
00:10:17,207 --> 00:10:19,430
这一条被测试锁死了。

126
00:10:19,430 --> 00:10:25,596
没有后端的时候，只读档案加上 Never 审批，未匹配的命令必须是 Forbidden。

127
00:10:25,596 --> 00:10:29,959
也就是说，兜底的位置在策略层，不在沙箱层。

128
00:10:29,959 --> 00:10:32,014
最后一句很重要。

129
00:10:32,014 --> 00:10:37,447
跨平台能力不对等的时候，对外的说法必须按实际启用的那一层写。

130
00:10:37,447 --> 00:10:41,894
Mac 默认就有 Seatbelt，Windows 默认可能是 Disabled。

131
00:10:41,894 --> 00:10:46,642
用户听到有沙箱，会默认三台机器是同一道墙。

132
00:10:46,770 --> 00:10:50,085
最后横向对比同一道题的三种答法。

133
00:10:50,035 --> 00:10:55,972
第一种，同一个 harness 用了完全相同的那组 CreateRestrictedToken 标志。

134
00:10:55,972 --> 00:11:02,932
它的 README 写得很诚实，读、网络、进程可见性都不受限，强制程度标成 partial。

135
00:11:02,932 --> 00:11:06,213
它不建专用账户，也不装 WFP。

136
00:11:06,213 --> 00:11:13,641
这三种答法放到一起看，你会发现它们的技术栈高度重叠，差别全在产品判断上。

137
00:11:13,641 --> 00:11:19,770
同一组标志，有人停在写限制，有人一路做到专用账户和内核过滤。

138
00:11:19,770 --> 00:11:25,900
选择的依据不是能不能做，而是愿意让用户在安装时付多少代价。

139
00:11:25,900 --> 00:11:27,631
第二种更干脆。

140
00:11:27,631 --> 00:11:32,222
有个产品在本地原生 Windows 上直接写明没有沙箱。

141
00:11:32,222 --> 00:11:41,778
企业策略要求沙箱并且禁止无沙箱命令的时候，PowerShell 直接拒绝执行，文案就是 sandboxing is not available on native Windows。

142
00:11:41,778 --> 00:11:43,965
它不假装有一层隔离。

143
00:11:43,965 --> 00:11:53,941
另一个产品的沙箱包把平台写成 linux 加 landlock 和 macos 加 seatbelt，其余标成 unknown，代码里也检索不到对应的 Windows 实现。

144
00:11:53,941 --> 00:12:00,407
第三种就是 Codex，把身份、ACL、WFP 和专用账户全都做进去。

145
00:12:00,407 --> 00:12:05,323
三者的差别不在技术能力，而在愿意付多少安装摩擦。

146
00:12:05,323 --> 00:12:18,629
答案分三档，第一档在 Windows 上不做文件系统沙箱，第二档做了写限制、装得上，读和网络留给别的机制，第三档全做进去，代价是摩擦和卸载残留，所以默认关着。

147
00:12:18,790 --> 00:12:22,069
最后收一下，六条能带走的东西。

148
00:12:22,019 --> 00:12:24,795
第零条其实比第一条更基础。

149
00:12:24,795 --> 00:12:30,709
先确认这套沙箱在你这台机器上到底开没开，再讨论它能挡什么。

150
00:12:30,709 --> 00:12:34,783
默认关着的机制，讨论它的强度没有意义。

151
00:12:34,783 --> 00:12:37,524
第一，先问手上有什么原语。

152
00:12:37,524 --> 00:12:41,081
没有进程级隔离，就换按身份锁资源。

153
00:12:41,081 --> 00:12:46,454
主体能做什么和客体能被谁碰，是两套原语，别硬照抄。

154
00:12:46,454 --> 00:12:50,024
第二，把每个机制的管辖范围写清楚。

155
00:12:50,024 --> 00:12:53,353
那三个标志管写，不管读和网络。

156
00:12:53,353 --> 00:12:57,031
知道它不管什么，比知道它管什么更重要。

157
00:12:57,031 --> 00:13:00,528
第三，隔离深度要和安装预算挂钩。

158
00:13:00,528 --> 00:13:04,783
只防模型误改仓库，当前用户受限令牌就够。

159
00:13:04,783 --> 00:13:11,129
要防读出密钥再从本机往外传，必须再做读限制和按身份的网络过滤。

160
00:13:11,129 --> 00:13:16,851
第四，按身份做访问控制时，身份数量必须覆盖策略组合数。

161
00:13:16,851 --> 00:13:19,783
网络有两档，就要两个身份。

162
00:13:19,783 --> 00:13:26,346
这和 WFP 这个具体接口无关，和用两张门禁卡进两栋楼是同一件事。

163
00:13:26,346 --> 00:13:29,495
第五，默认关的东西要有兜底。

164
00:13:29,495 --> 00:13:36,190
沙箱不可用时，策略层要能把它判成 Forbidden 或者 Prompt，而不是什么都不做。

165
00:13:36,190 --> 00:13:40,577
第六，跨平台承诺按实际启用的那一层写。

166
00:13:40,577 --> 00:13:46,177
做不到对等就明说，别让模型和用户按不存在的墙做规划。

167
00:13:46,177 --> 00:13:48,305
举个最实在的例子。

168
00:13:48,305 --> 00:13:55,012
给 Windows 用户写文档的时候，如果你的默认档是 Disabled，就不要写「已启用沙箱保护」。

169
00:13:55,012 --> 00:14:03,557
写「默认未启用，需用 Elevated 档开启并确认策略允许本地账户」，虽然难看，但这是唯一诚实的写法。

170
00:14:03,557 --> 00:14:06,766
Windows 这一集的核心其实只有一句。

171
00:14:06,766 --> 00:14:13,449
没有 seatbelt 和 bubblewrap 的时候，隔离照样能做，但你必须想清楚用什么样的身份去换。

172
00:14:13,449 --> 00:14:15,552
这一集就到这里。

