来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》)
本内容改编自小山学堂《学 AI 产品,从入门到精通》,为二次演绎配音版
本集属于技术侧(S2)模块 T3「解剖 OpenAI Codex:把安全观写进类型系统」,取材自 2 节课:
两节课的共同主线:
把模型不擅长的精确性拿走,放到运行时去做,同时给模型留一份它能看懂的回执。
| 能力 | 具体表现 |
|---|---|
| 复述判定顺序 | 先 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 短横/弯引号归一→ 失败即报 |
| 区分原子性边界 | 单文件算完再写;跨文件无事务,已写文件留在磁盘 |
只拦「不在 allowlist 里的域名」会导致:
| 漏洞 | 后果 |
|---|---|
| 忽略 deny 条目 | denylist 被绕过 |
| 忽略内网地址 | 打到 169.254.169.254(云元数据)等 |
| 空名单默认放行 | 忘写配置 = 全开 |
1. deny → 恒胜,直接拦
2. 本地 / 私有 → 拦
3. allowlist → 为空或未命中 → 拦(NotAllowed)
只有NotAllowed才去问决策器。Denied和NotAllowedLocal不会去问。
| 坑 | 规则 |
|---|---|
| apex 匹配 | *.example.com 不含 apex;**.example.com 才含 |
| 解析结果 | 域名解析到非公网 IP,即便在名单里也拦 |
| 本地字面量 | 必须精确写 localhost 或 127.0.0.1,通配 * 不算 |
可以给模型留「申请放行」的余地,但要保证真正的红线永远翻不动。
让决策器能推翻「不在名单」,但永远翻不了 denylist 和本地网段——留余地和守红线可以同时做到。
换语言重写,该问的还是同一组问题:
| 组成 | 内容 |
|---|---|
| 状态码 | 403 |
| 头 | x-proxy-error |
| 正文 | 一句人话 |
关键设计:not_allowed与not_allowed_local对外暴露同一个名字,但正文写得不一样。
更关键的是后果:这条 403 作为 exec 输出喂回模型,采样循环不停。这是一个可恢复的错误——模型看得到原因,可以换域名或请用户授权。
成本对比:出网策略写错,模型还能凭回执改主意;凭据一旦进了模型上下文,后面每一层脱敏都是补救。两者差着数量级。
token 跟着命令进 rollout → 下一轮模型还能看见 → 再下一轮可能进日志。凭据一旦进入模型上下文,后面每一层处理都是补救。
| 步骤 | 时机 | 内容 |
|---|---|---|
| ① 换成假值 | 拉起命令前(spawn 前) | GH_TOKEN、OPENAI_API_KEY → 同长度、同前缀的假值 |
| ② 换回真值 | 出站时 | MITM 按 host 过滤;请求头必须带着假值才注入真 token |
子进程口袋里是假票根,柜台后面锁着真票。
为了让那些启动时会校验格式的客户端还能正常起来。 形状不对,客户端在第一步就拒绝启动,整条链路都走不到出站。
做安全决策时先问一句:这个东西错了能不能撤回?
边界须知:经纪只覆盖 GitHub 与 OpenAI;用户改过的环境值不还原,这些标记一律当不可信输入。
Go 的net/http对 loopback 绕过HTTP_PROXY。 你以为流量进了代理,其实它直连127.0.0.1——本机可能开着管理口、docker socket。这时策略文件写再多也没用,请求根本没走到那一层。
| 平台 | 做法 |
|---|---|
| macOS | 受限 Seatbelt 只写 localhost:{port} |
| Linux | ProxyOnly |
allow_local_binding = false | NO_PROXY 写成空串,loopback 字面量也必须走代理 |
打开本地绑定 = 承认 loopback 不再经过 allowlist。
核心句:应用层代理挡不住不肯走代理的客户端;下一层必须是只留一个口的操作系统防线。三道门叠在一起才构成托管网络。
| 方案 | 阀门位置 | 覆盖面 |
|---|---|---|
Grok web_fetch | 工具入口 | 空名单拦所有 URL(方向同 Codex);明确放行 loopback(注释:local development);bash 里的 curl 不走这张名单 |
| Codex | 所有子进程出口 | 必须与沙箱绑在一起 |
SSRF 检查拦 RFC1918 / link-local / CGNAT,但放行 loopback —— 差别全在阀门层级。
模型改 greet() 里的 pass → return 123,吐出 @@ -47,3 +47,3 @@:
greet 已在第 49 行失败方式更糟:patch(1) 按行号找,找不到就 fuzz;fuzz 可能把变更贴到邻近的另一个函数——测试还是绿的,只是改错了函数。
//! change_context: ("@@" | "@@ " /(.+)/) LF
//! change_line: ("+" | "-" | " ") /(.+)/ LF
//! eof_line: "*** End of File" LF
@@ = 从当前位置继续搜seek_sequencesimilar::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 头怎么拼。
| 档 | 规则 |
|---|---|
| 1 | 整行精确相等 |
| 2 | 去掉行尾空白再比 |
| 3 | 两边都 trim() |
| 4 | 常见 Unicode 短横 / 弯引号收成 ASCII |
四级都失败 → 返回空 → 报 Failed to find context 或 Failed to find expected lines。没有「按附近几行再试」这个循环。
容错要分清两类差异:
| 类型 | 例子 | 处理 |
|---|---|---|
| 无意义的字节差 | 行尾空格、弯引号 | 可以归一 |
| 位置偏差 | 行号偏了两行 | 应该报错让模型重写(这是贴错地方) |
归一化边界:中文全角引号不在表内——模型写全角左引号、文件里是半角,四级都会失败。
(早期一次事故专门为奇怪的 Unicode 字符加了第四级。)
| 方案 | 模糊匹配范围 |
|---|---|
Claude Code FileEditTool | 只覆盖引号(精确 → 弯引号收直引号);无行尾空白档、无短横归一化 |
| Codex | 四级,含行尾空白与 Unicode 短横 |
想少一次工具往返,抄 Codex 的四级;想让模型收到更具体的错误信息,抄 Claude 那几条带 errorCode 的拒绝文案。
compute_replacements 把每个 chunk 先收成替换列表,某一个对不上就立刻返回错误,还没走到 write_file —— 这个文件保持原样。
附加约束:更新一个文件时,chunks 必须按在文件中出现的先后排列。
apply_hunks_to_files 按 hunk 顺序写盘,失败时带着已提交的 AppliedPatchDelta 返回,循环里没有回滚。
测试 015 钉死:先成功新增created.txt,再更新一个不存在的文件 —— 磁盘上created.txt还在。
「没有部分成功」这个保证只对单个文件成立,不要说成全局保证。
要不要跨文件事务是产品选择,不是格式本身的承诺。
| 范围 | 能否原子 | 原因 |
|---|---|---|
| 单文件 | ✅ | 可在内存里算完全部替换,写一次 |
| 多文件 | ❌ | 已落盘,回滚要再写一遍,还要处理 Move 这种源/目标都动过的半成功 |
| 方案 | 先读约束 | 多处命中 |
|---|---|---|
| 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 出现两次 → 取第一处,不报歧义 |
网络与凭据
NO_PROXY 是否已置空补丁格式
openai/codex,commit 4f39251a01,核对日期 2026-08-22;代码块保留源码原文。web_fetch/domain.rs、ssrf.rs;DSH credentials/、subprocess/、editIntent;Claude FileEditTool。出站默认拒绝,deny 恒胜,内网要精确开门。真密钥不要进模型可见的环境,spawn 前换成假值,出站再换。代理挡不住绕过它的客户端,下一层必须是只放行代理端口的沙箱。
给模型的 diff 不要行号,定位交给运行时搜上下文。空白和标点可以逐级放宽,位置不猜。单文件对不上就不写盘;跨多个文件时,已经写下的文件会留在磁盘上。
来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》)
本内容改编自小山学堂《学 AI 产品,从入门到精通》,为二次演绎配音版