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

网络与凭据代理:模型看不见的那把钥匙 等 2 节

时长 15:30音色 云健 · 男声

同步字幕

章节导航(点击跳转)

0:00开场 · 模型看不见的钥匙,和没有行号的补丁1:50
1:50域名怎么判 · 顺序写死,deny 恒胜1:42
3:33拒绝要说人话 · 那条会喂回模型的 4031:03
4:36真钥匙只在代理里出现2:15
6:52沙箱是漏斗口 · 挡住不肯走代理的客户端1:37
8:29apply-patch · 给模型的那份不要行号1:57
10:27四级匹配 · 空白可以放宽,位置不猜1:35
12:02算完再写盘 · 跨文件没有事务1:53
13:56可带走的设计原则 · 六条1:33
解读全文

codex14 · 网络与凭据代理:模型看不见的那把钥匙 · apply-patch,给模型设计一种 diff

来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》)
本内容改编自小山学堂《学 AI 产品,从入门到精通》,为二次演绎配音版

本集定位

本集属于技术侧(S2)模块 T3「解剖 OpenAI Codex:把安全观写进类型系统」,取材自 2 节课:

  1. 网络与凭据代理:模型看不见的那把钥匙
  2. apply-patch,给模型设计一种 diff

两节课的共同主线:

把模型不擅长的精确性拿走,放到运行时去做,同时给模型留一份它能看懂的回执。
  • 网络侧:域名判定顺序写死(运行时精确);被拦时回一句人话 403(模型可读的回执)
  • 凭据侧:真 token 在 spawn 前换走(运行时持有);模型只见同形状假值
  • 补丁侧:给模型的格式不含行号(模型不必做算术);定位交给运行时字符串搜索;失配明确报错

一、能力地图

能力具体表现
复述判定顺序先 deny → 再本地/私有 → 最后 allowlist;空名单与未命中都拦
理解决策器边界只能翻 allowlist 漏掉的案,翻不了 denylist,也翻不了本地网段
处理通配陷阱*.example.com 不含 apex,**.example.com 才含;本地字面量需精确写 localhost / 127.0.0.1,通配 * 不算
处理解析陷阱解析到私有 IP 的域名,精确写进名单也会被拦
冲突优先级同一 pattern 既 allow 又 deny,取枚举序更大的(None < Allow < Deny)
解释 403 设计错误码给机器,正文区分「不在名单」与「沙箱拦了本地」;可喂回模型,采样不停
复述凭据两步spawn 前换同形状假值 → 出站按 host 换回真值
解释为什么是假值形状让校验格式的客户端还能启动
解释沙箱必要性Go net/http 对 loopback 绕过 HTTP_PROXY;应用层代理挡不住不肯走代理的客户端
说明给模型的 diff没有行号,头一行只有上下文锚点;@@ 单独出现表示从当前位置继续搜
复述四级匹配整行精确 → 去行尾空白 → 两边 trim → Unicode 短横/弯引号归一→ 失败即报
区分原子性边界单文件算完再写;跨文件无事务,已写文件留在磁盘

二、思路一 · 域名怎么被判

2.1 错误做法与它的三个漏洞

只拦「不在 allowlist 里的域名」会导致:

漏洞后果
忽略 deny 条目denylist 被绕过
忽略内网地址打到 169.254.169.254(云元数据)等
空名单默认放行忘写配置 = 全开

2.2 判定顺序(写死)


1. deny        → 恒胜,直接拦
2. 本地 / 私有 → 拦
3. allowlist   → 为空或未命中 → 拦(NotAllowed)
只有 NotAllowed 才去问决策器。 Denied 和 NotAllowedLocal 不会去问。

2.3 通配与解析的两个坑

坑规则
apex 匹配*.example.com 不含 apex;**.example.com 才含
解析结果域名解析到非公网 IP,即便在名单里也拦
本地字面量必须精确写 localhost 或 127.0.0.1,通配 * 不算

提示一 · 决策器的红线

可以给模型留「申请放行」的余地,但要保证真正的红线永远翻不动。

让决策器能推翻「不在名单」,但永远翻不了 denylist 和本地网段——留余地和守红线可以同时做到。

2.4 这条 SSRF 防护形状的通用问法

换语言重写,该问的还是同一组问题:

  1. 没写名单时怎么办?(默认拒绝)
  2. 冲突时谁赢?(deny 恒胜)
  3. 解析到内网的名字算不算放行?(不算,且要显式开门)

三、拒绝要说人话 · 那条会喂回模型的 403

组成内容
状态码403
头x-proxy-error
正文一句人话
关键设计:not_allowed 与 not_allowed_local 对外暴露同一个名字,但正文写得不一样。
  • 错误码 → 给机器看
  • 正文 → 区分「这个域名不在名单里」和「沙箱把本地地址拦了」

更关键的是后果:这条 403 作为 exec 输出喂回模型,采样循环不停。这是一个可恢复的错误——模型看得到原因,可以换域名或请用户授权。

成本对比:出网策略写错,模型还能凭回执改主意;凭据一旦进了模型上下文,后面每一层脱敏都是补救。两者差着数量级。

四、思路二 · 真钥匙只在代理里出现

4.1 问题

token 跟着命令进 rollout → 下一轮模型还能看见 → 再下一轮可能进日志。凭据一旦进入模型上下文,后面每一层处理都是补救。

4.2 两步走

步骤时机内容
① 换成假值拉起命令前(spawn 前)GH_TOKEN、OPENAI_API_KEY → 同长度、同前缀的假值
② 换回真值出站时MITM 按 host 过滤;请求头必须带着假值才注入真 token
子进程口袋里是假票根,柜台后面锁着真票。

4.3 为什么是「同形状」假值

为了让那些启动时会校验格式的客户端还能正常起来。 形状不对,客户端在第一步就拒绝启动,整条链路都走不到出站。

提示二 · 用「撤回成本」做决策

做安全决策时先问一句:这个东西错了能不能撤回?
  • 能在一条回执里撤回的 → 可以让模型参与决策(如域名不在名单)
  • 一旦进了上下文就撤不回的 → 必须在源头拦住(如凭据)

边界须知:经纪只覆盖 GitHub 与 OpenAI;用户改过的环境值不还原,这些标记一律当不可信输入。


五、思路三 · 沙箱把流量按进代理

5.1 应用层代理的致命弱点

Go 的 net/http 对 loopback 绕过 HTTP_PROXY。 你以为流量进了代理,其实它直连 127.0.0.1——本机可能开着管理口、docker socket。这时策略文件写再多也没用,请求根本没走到那一层。

5.2 下一层必须是 OS

平台做法
macOS受限 Seatbelt 只写 localhost:{port}
LinuxProxyOnly
allow_local_binding = falseNO_PROXY 写成空串,loopback 字面量也必须走代理
打开本地绑定 = 承认 loopback 不再经过 allowlist。

核心句:应用层代理挡不住不肯走代理的客户端;下一层必须是只留一个口的操作系统防线。三道门叠在一起才构成托管网络。

5.3 对比:阀门装在哪一层

方案阀门位置覆盖面
Grok web_fetch工具入口空名单拦所有 URL(方向同 Codex);明确放行 loopback(注释:local development);bash 里的 curl 不走这张名单
Codex所有子进程出口必须与沙箱绑在一起

SSRF 检查拦 RFC1918 / link-local / CGNAT,但放行 loopback —— 差别全在阀门层级。


六、思路一(补丁)· 给模型的那份不要行号

6.1 标准 unified diff 怎么翻车

模型改 greet() 里的 pass → return 123,吐出 @@ -47,3 +47,3 @@:

  • 文件刚被别处插了两行,greet 已在第 49 行
  • 模型在带行号的摘录里数,写补丁时还要自己加算起始行与跨度
  • 四个数字一起错,是常态,不是意外

失败方式更糟:patch(1) 按行号找,找不到就 fuzz;fuzz 可能把变更贴到邻近的另一个函数——测试还是绿的,只是改错了函数。

6.2 Codex 的答案:填写与阅读彻底拆开


//! change_context: ("@@" | "@@ " /(.+)/) LF
//! change_line:    ("+" | "-" | " ") /(.+)/ LF
//! eof_line:       "*** End of File" LF
  • 模型填的那份没有行号,头一行只写锚点文本
  • 单独 @@ = 从当前位置继续搜
  • 定位交给运行时的 seek_sequence
  • 人看变更时,界面另外用 similar::TextDiff 生成标准 unified diff
坐标靠加算,内容靠识别。 把「找到哪一段」从填写时的算术,改成应用时的字符串搜索——这个分工不依赖 Rust,也不依赖 unified diff 这个具体格式。

说明书把这套语言定义为 *stripped-down, file-oriented diff format designed to be easy to parse and safe to apply*。Add / Delete / Move 是三种标记,解析器按标记分发——模型不用记 ---、+++、/dev/null、rename 头怎么拼。


七、思路二(补丁)· 空白可以放宽,位置不猜

7.1 四级匹配(从紧到松)

档规则
1整行精确相等
2去掉行尾空白再比
3两边都 trim()
4常见 Unicode 短横 / 弯引号收成 ASCII

四级都失败 → 返回空 → 报 Failed to find context 或 Failed to find expected lines。没有「按附近几行再试」这个循环。

提示三 · 分清两类差异

容错要分清两类差异:
类型例子处理
无意义的字节差行尾空格、弯引号可以归一
位置偏差行号偏了两行应该报错让模型重写(这是贴错地方)

归一化边界:中文全角引号不在表内——模型写全角左引号、文件里是半角,四级都会失败。

(早期一次事故专门为奇怪的 Unicode 字符加了第四级。)

7.2 对比

方案模糊匹配范围
Claude Code FileEditTool只覆盖引号(精确 → 弯引号收直引号);无行尾空白档、无短横归一化
Codex四级,含行尾空白与 Unicode 短横
想少一次工具往返,抄 Codex 的四级;想让模型收到更具体的错误信息,抄 Claude 那几条带 errorCode 的拒绝文案。

八、思路三(补丁)· 单文件算完再写,跨文件没有事务

8.1 单文件:有保证

compute_replacements 把每个 chunk 先收成替换列表,某一个对不上就立刻返回错误,还没走到 write_file —— 这个文件保持原样。

附加约束:更新一个文件时,chunks 必须按在文件中出现的先后排列。

8.2 跨文件:没有事务

apply_hunks_to_files 按 hunk 顺序写盘,失败时带着已提交的 AppliedPatchDelta 返回,循环里没有回滚。

测试 015 钉死:先成功新增 created.txt,再更新一个不存在的文件 —— 磁盘上 created.txt 还在。

提示四 · 说法要和实现对得上

「没有部分成功」这个保证只对单个文件成立,不要说成全局保证。

要不要跨文件事务是产品选择,不是格式本身的承诺。

范围能否原子原因
单文件✅可在内存里算完全部替换,写一次
多文件❌已落盘,回滚要再写一遍,还要处理 Move 这种源/目标都动过的半成功

8.3 对比:防错挂在哪

方案先读约束多处命中
DSH要求 session 观测过该文件,否则 FS_NOT_OBSERVED;观测记的是 dev:ino:size:mtimeNs:ctimeNs(不是内容 hash)old_string 多处命中 → FS_AMBIGUOUS_EDIT
Claude Code未读过 → errorCode 6(File has not been read yet)多处命中且 replace_all=false → errorCode 9,要求补上下文
Codex无先读约束,定位写在补丁里,运行时现搜old_lines 出现两次 → 取第一处,不报歧义

九、审查清单

网络与凭据

  • 判定顺序是否写死为 deny → 本地/私有 → allowlist
  • 空名单与未命中是否都拦
  • 决策器是否只能翻 allowlist 漏掉的案
  • 解析到私有 IP 的域名是否被拦(即使写了名单)
  • 本地字面量是否精确书写(通配不算)
  • 403 正文是否区分「不在名单」与「沙箱拦本地」
  • 403 能否喂回模型且采样不停
  • git 凭据是否在 spawn 前被替换成同形状假值
  • 出站是否按 host 注入且仅当请求头带假值
  • 是否知道经纪覆盖面上限(仅 GitHub / OpenAI)
  • 沙箱是否只放行代理端口;NO_PROXY 是否已置空

补丁格式

  • 给模型的格式不含行号
  • 失配是否明确报错(不做位置 fuzz)
  • 四级匹配的边界是否已如实告知(全角引号不覆盖)
  • 单文件是否算完再写
  • 跨文件失败时已知会产生部分结果,且有对应提示/回滚策略

十、约束说明

  1. 取材范围:仅使用本集 2 节课素材,不跨集取材,不引入源码外数据。
  2. 源码快照:仓库 openai/codex,commit 4f39251a01,核对日期 2026-08-22;代码块保留源码原文。
  3. 教学化内容已标注:名单、假值形状、解析结果、文件行块与行号为课程化设定;逻辑轨迹行号对应上述 commit。
  4. 横向对比各方均已核对源码(2026-08-22):Grok web_fetch/domain.rs、ssrf.rs;DSH credentials/、subprocess/、editIntent;Claude FileEditTool。
  5. 不作推断:本页不推断拦截成功率、性能开销或模型表现;机制存在不等于效果达标。
  6. 边界如实:不把单文件的原子保证说成全局事务;不夸大 Unicode 归一化的覆盖面。

十一、一句话 Takeaway

出站默认拒绝,deny 恒胜,内网要精确开门。真密钥不要进模型可见的环境,spawn 前换成假值,出站再换。代理挡不住绕过它的客户端,下一层必须是只放行代理端口的沙箱。
给模型的 diff 不要行号,定位交给运行时搜上下文。空白和标点可以逐级放宽,位置不猜。单文件对不上就不写盘;跨多个文件时,已经写下的文件会留在磁盘上。

来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》)
本内容改编自小山学堂《学 AI 产品,从入门到精通》,为二次演绎配音版