学 AI 产品 · 专业 AI 产品经理播客第 3 章 · T3 解剖 OpenAI Codex:把安全观写进类型系统 · EP 09
第 3 章 · EP 09

沙箱管理器:把权限档案编译成一行命令 · macOS:把安全策略拼成一个字符串

时长 17:50音色 云健 · 男声

同步字幕

章节导航(点击跳转)

0:00开场 · 同一条命令,套上三层壳1:46
1:46一 · 要不要,和有没有,是两道题2:28
4:15二 · 一份档案,两个出口2:33
6:48三 · 开口用并集,批准用求交1:55
8:44四 · 路径走配料表,不进菜单1:47
10:32五 · 先关大门,再按片段开门1:48
12:20六 · 挖空要挡住节点、子孙和搬家2:14
14:35七 · 网络段,和失败倒向哪一边2:00
16:35收尾 · 今天可以带走的五条1:14
解读全文

ep20 · 沙箱管理器:把权限档案编译成一行命令 · macOS:把安全策略拼成一个字符串

  • 模块:T3 解剖 OpenAI Codex:把安全观写进类型系统
  • 集页:https://xueai-podcast.pages.dev/t/codex09/
  • 来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》)
  • 说明:本页为音频的文字稿与延展解读,音频为二次演绎配音版,内容以源课程素材为准。

本集解决什么工程问题

同一份权限档案,为什么在三台机器上跑出三种命令形态?

配置里写的是同一份档案(工作区可写,网络收紧),模型发出同一条 git status:

平台实际形态
macOS套上 /usr/bin/sandbox-exec,策略走 -p,路径走 -D,用户命令在 -- 后
Linux整份档案序列化进 --permission-profile,交给 helper
Windows开关关着 → SandboxType::None,原 argv 出门

因为从档案到命令之间隔着一次编译,而这次编译要先回答两个问题:这台机器要不要上沙箱?这台机器有没有可用后端?

排障最容易在这里走歪:日志里档案还在,模型上下文里的 environment_context 也还在,看起来一切正常——只是平台层没把档案编译成包装命令。你以为沙箱失效了,实际上它从来没被装上过。


能力地图

能力判据对应源码位置
两问拆分拿到一次沙箱未生效,能分别说出"要不要"和"有没有"的答案sandboxing/src/manager.rs L293–L329
出口识别能说清同一份档案的两个出口各自服务谁、边界在哪manager.rs L365–L459、context/environment_context.rs L96–L116
运算选择能判断一个权限合并该用并集还是求交policy_transforms.rs L90–L214
模板隔离能解释为什么路径必须走 -D 而不是拼进 SBPL 正文sandboxing/src/seatbelt.rs L1022–L1028
挖空完整性能背出挖空一条路径要挡住的三种绕法seatbelt.rs L548–L555、L504–L509
失败倒向能对比三个项目在"缺后端"时各自倒向哪一边manager.rs L62–L76;DSH / Grok 对比章节

上半场 · 沙箱管理器

一 · 要不要,和有没有,是两道题

函数一:should_sandbox → 布尔(manager.rs L310–L329、policy_transforms.rs L541–L561)

三态结果附加条件
Forbid恒假—
Require恒真—
Auto看档案形状有托管网络要求 → 必须上;网络收紧且非"调用方自管文件系统" → 上;网络放开且文件系统不受限 → 跳过

函数二:get_platform_sandbox → 可选后端(manager.rs L62–L76)

  • macOS → MacosSeatbelt(没有总开关)
  • Linux → seccomp helper
  • Windows → 多一个开关,关着就是空
空的语义是"这台主机没有可派发的实现",不是"不需要"。

select_initial 先问前者,再把后者的空收成 SandboxType::None。档案说需要,主机给不出,类型仍是 None。

为什么不报错:要不要是策略问题,有没有是能力问题,失败后果不同。策略要但能力给不出,是可接受的降级——决策权交给后面的策略层,而不是让沙箱层直接掐死命令。

二 · 一份档案,两个出口

PermissionProfile 三变体(protocol/src/models.rs L411–L425)写的是谁负责建外层沙箱:

变体含义
ManagedCodex 自己拼包装命令
Disabled不要外层
External文件系统由调用方负责,网络仍可能归 Codex 管

出口一:包装命令(manager.rs L365–L459)

  • None → 原样传出 argv,连本机 cwd 都不准备(未沙箱的请求可能带着外机路径)
  • macOS → 只信任 /usr/bin/sandbox-exec,策略 -p、路径 -D、用户命令 -- 后;PATH 上放同名二进制没用
  • Linux → 整份档案序列化进 --permission-profile,helper 缺失直接报错,不按无沙箱降级
  • Windows → 第一拍只校验,argv 不动;包装器就是当前 exe,套壳推迟到 transform_for_direct_spawn

出口二:模型可见的 environment_context(environment_context.rs L96–L116)

Disabled 渲染成 type="disabled" + unrestricted 文件系统。

模型可见文本挡不住越权,只减少无效尝试。 真正的墙在子进程入口。
平台给不出实现时,渲染给模型的说明不会改口——它按档案描述意图,不按主机能力撒谎。

三 · 开口用并集,批准用求交

运算规则回答的问题
并集(合并)任一侧开网 → 结果开网;文件系统条目首尾相接去重这条命令本次开多大的口
求交(批准)网络需两侧都开才留下;文件系统只保留落在请求范围内的已批条目人批下来的有没有宽过请求

为什么批准必须求交:用并集的话,一次误批就能把请求里根本没有的路径写进会话授权。开口是这次的事,批准会沉淀成后续每一轮的默认——后果量级完全不同。

空集的含义:会话不记账,基座档案继续生效 → 这次额外开口没留下任何加宽。不要解释成"改成最严",也不要解释成"整条命令被禁止";命令能不能跑要看后面的策略和沙箱。


下半场 · macOS 上的 SBPL 拼装

四 · 路径走配料表,不进菜单

SBPL 里括号、引号、分号都是语法。工程放在 ~/work/app (copy) 这类目录里,路径一旦插进策略正文,用户目录名就变成语法 → sandbox-exec 解析失败,模型看到的是包装器错误,不是沙箱拒绝(最难查的一类失败)。

解法:动态读写规则只往正文写 (param "WRITABLE_ROOT_0") 这类键名;真实路径写成 -DKEY=value,成为独立 argv。排除子路径同样走参数表。正文和 -D 必须同时出现——测试把这件事锁成合同。

通用原则:安全策略如果最终是一段文本,用户可控的字符串不要进这段文本。模板只含占位符,真实路径/域名/端口走另一份参数表。与 Seatbelt 语法无关。

五 · 先关大门,再按片段开门

四份 .sbpl 用 include_str! 编进二进制当静态基线(seatbelt.rs L21–L27),第一句业务规则是 (deny default)——没写明的事一律拒绝。子进程继承这份底稿,sandbox-exec 拉起的 bash 再 fork 出来的子孙仍在同一套规则里。

动态段按本次的可读可写根现拼,拼接顺序固定(seatbelt.rs L985–L1012):基线 → 读 → 写 → 网络。全盘可读才加 preferences;受限读才加平台默认路径;祖先的 file-write-unlink 放最后,避免前面更宽的 allow 把 rename 要用的 unlink 重新打开。

关键不是顺序本身,是顺序背后的合同:更窄的 deny 必须能压住前面更宽的 allow。

六 · 挖空要挡住节点、子孙和搬家

排除子路径时 require-not 要写两条,一起放进 require-all(seatbelt.rs L548–L555):

规则管什么少写的后果
literal目录自己mkdir .codex 会成功——目录还不存在,管内容的规则没有适用对象
subpath目录下面的内容目录里的内容不受保护
deny file-write-unlink(可写根上)改名搬走rename 能把只读子树挪出挖空区,下次按原路径授权时实际写的是链接对面

三条一起生成,用测试锁住形状。 少一条就漏一种绕法,而这种漏法只有在有人刻意尝试时才暴露。

其他硬约束:

  • 可写根默认保护三个顶层名字:.git、.agents、.codex(protocol/src/permissions.rs L24–L33)
  • 可写根中间若有用户可控的符号链接 → 拼装直接报错,不跟着链接走
  • 跨平台错误文案不同:macOS 常见 Operation not permitted,能被现有关键词表接住,不要假设另外两个平台也是这句话(sandboxing/src/denial.rs L50–L58)

七 · 网络段

要走代理却没有可用端口 → 返回受限网络段 + 网络基线,不给 (allow network-outbound) 这种整网放行。网络基线还在,只是没有 blanket outbound——宁可让网络不好用,也不为让它能用而放开整网。


横向对比

项目切在哪一层路径进不进文本缺后端 / 失败时
Codex每条命令的 argv(子进程入口)不进,走 -D 参数表Unix helper 缺失 → 报错不跑;Windows 开关关着 → 收成 None 交给策略层;Seatbelt 准备失败 → 返回错误,命令不裸跑
DeepSeek Harness子进程入口,confine 按主机选 runner进,经 sbplString 转义反斜杠和双引号后直接嵌进 -p,没有 -Dfail closed,不退回原 argv,文案写明 *refusing to run the command unconfined*;默认 (allow default) 再 (deny file-write*),只收回写权;本机 macOS 做不到"可写根里挖掉 .codex"这一粒度
Grok当前进程,apply() 再 install(),注释写明不可逆进文本,控制字符直接失败(避免"沙箱报 active、那条路径却没被挡住")平台不支持或 apply 失败 → 打警告、记 apply_failed、继续跑;代价是没法在同一进程里给两条命令两套边界
Claude Code每条命令,从 PATH 找 sandbox-exec(不写死 /usr/bin)进文本,用 JSON.stringify 包一层从 (allow file-read*) 往下加 deny,再在 deny 里 re-allow,后写规则优先;同样生成 deny file-write-unlink

差别本质:把不可逆的决策放在哪一层。放在进程入口,粒度粗但简单;放在每条命令,粒度细但复杂。

默认方向也不同:Codex 从 (deny default) 往上加 allow(付生成器复杂度,换用户路径不进语法);Claude 从 allow 往下加 deny(付"后写顺序必须对",换配置面接近 allowAllExcept);DSH 默认 (allow default) 再收回写权。


审查清单

沙箱管理器

  1. "要不要上沙箱"和"有没有后端"是不是两个独立函数?排障时能不能分别问出答案?
  2. 主机没有后端时,系统是报错、降级还是静默?这个选择有没有写在文档里?
  3. 渲染给模型的环境说明,会不会因为主机能力不足而"改口"?
  4. 临时加宽的合并用的是并集还是并集加求交两种?批准路径有没有可能被误批宽过请求?
  5. 求交为空时,系统会不会误解释为"改成最严"或"整条命令禁止"?

SBPL 拼装

  1. 策略正文里有没有出现用户可控的字符串?路径是不是全部走参数表?
  2. 有没有测试锁住"正文与参数表必须同时出现"这条合同?
  3. 排除子路径是不是 literal + subpath 两条都有?可写根上有没有 deny file-write-unlink?
  4. 遇到用户可控的符号链接,是报错还是跟着走?
  5. 网络段在没有可用端口时,会不会退化成整网放行?

约束说明

约束一:行号是一次快照。 全部对应 openai/codex 仓库 commit 4f39251a01,核对日期 2026-08-22。

约束二:macOS 没有总开关。 get_platform_sandbox 直接返回 MacosSeatbelt,那个布尔开关只对 Windows 有意义。别把 Windows 的开关语义套到 macOS 上。

约束三:None 不等于"不需要"。 它的语义是"这台主机没有可派发的实现"。把它当成"策略允许不开沙箱"来读,会得出错误的安全结论。

约束四:模型可见文本不是安全边界。 它只减少无效尝试。任何把"模型看到了限制描述"当成"限制已生效"的推理都是错的。

约束五:跨平台错误文案不同。 Operation not permitted 是 macOS 的常见文案,关键词表能接住;别假设 Linux / Windows 也是同一句。

约束六:Seatbelt 挡不住被写宽的 allow。 它挡得住策略里拒绝的操作,挡不住策略本身开得太宽,也挡不住模型在可写根里改业务代码。


实践提示

提示一 · 先拆两个函数,再谈实现

把"要不要"和"有没有"写成两个函数,一个返回布尔,一个返回可选的后端名。这条最便宜、收益最大——它直接决定了你以后能不能在三分钟内定位沙箱问题。

提示二 · 策略是文本时,值走参数表

模板只放占位符,真实路径、域名、端口走另一份参数表。成本接近于零,今天就能改。

提示三 · 区分临时加宽和长期批准

开口用并集,批准用求交。两者后果量级不同,运算规则就必须不同。空集表示"没留下加宽",别解释成最严或禁止。

提示四 · 给挖空写三条规则并锁测试

节点、子孙、搬家,三条一起生成。少一条就漏一种绕法,而这种漏法只有在有人刻意尝试时才暴露——所以必须用测试锁住形状,不能靠人工记住。


一句话 Takeaway

先把"需要隔离"和"这台机器能提供什么"写成两个函数:一个返回布尔,一个返回可选的后端名。一份权限档案同时喂给包装命令和模型——模型那一侧只负责减少无效尝试,不负责挡住越权,平台给不出实现时它也不会改口。macOS 上的沙箱是一段拼出来的 SBPL:路径走 -D,正文只留占位符;挖空要同时挡住节点、子孙和搬家。Seatbelt 挡得住策略里拒绝的操作,挡不住被写宽的 allow。


来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》)

本页文字稿与音频为同源二次演绎,音频由文本合成,措辞以本页为准。