专题篇章 · 拆开 OpenAI Codex

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

出站要过几道门。真 token 不进子进程。模型看见的是假值和一句 403。

本页解决的问题

先给结论

「网络与凭据代理:模型看不见的那把钥匙」要解决的关键问题是什么?

出站要过几道门。真 token 不进子进程。模型看见的是假值和一句 403。

判断标准

跟着交接处走,不要只看 Demo。 系统是否可靠,往往取决于模型、工具、状态、权限和人的交接处。把每次交接都当成可以观察、测试和恢复的地方。

下一步

为一个自动化步骤写清输入、负责人、审批和恢复动作。

常见误区

一次运行成功了,却说不清发生了什么,也无法安全重放。

课程目标读完能说清三件事。域名怎么被判:先 deny,再本地,最后 allowlist。真 token 在 spawn 前换成同形状假值,出站时代理按 host 换回去。不肯走代理的客户端,会被操作系统沙箱按进同一条漏斗。
先玩一遍 · 一个带凭据的请求过代理
换域名、换方法,看请求在哪一关停下,假值在哪一步换成真值
域名
方法
模式
先看假值怎么换成真值。再换成 POST,或让 Go 绕过代理。
客户端
这次请求
GET https://api.github.com/repos/you/app/issues
Authorization
Bearer github_pat_dmy8f3a2
关卡 1 · 沙箱待命
只放行代理端口。直连 loopback 在这一关停。
关卡 2 · 域名待命
先 deny,再本地或私有,最后 allowlist。
关卡 3 · 方法待命
Limited 只放 GET、HEAD、OPTIONS。
关卡 4 · 凭据待命
请求头带着假值,才换成经纪里的真 token。
子进程环境GH_TOKEN = github_pat_dmy8f3a2
经纪内存真 token 锁在这里
模型上下文看不见真值
点播放,看这个请求依次走过四道关,假值在哪一步换成真值。
逻辑轨迹 · 动画每一步对应源码里的哪一段
  1. 拉起命令前,把真 token 换成同形状假值credential_broker.rs L92
  2. 沙箱只放行代理端口,直连 loopback 到此为止seatbelt.rs L325
  3. 解析 host,deny 先判且恒胜runtime.rs L553
  4. 本地字面量没有精确允许,按私有地址拦runtime.rs L578
  5. 域名解析到非公网,即便在名单里也拦runtime.rs L582
  6. allowlist 为空或未命中,回 NotAllowedruntime.rs L598
  7. 只有 NotAllowed 才问决策器network_policy.rs L349
  8. Limited 下非 GET、HEAD、OPTIONS 就拦config.rs L311
  9. HTTPS 要看见内层方法,MITM 缺失就拦http_proxy.rs L312
  10. 请求头带着假值,才注入对应的真凭据mitm.rs L301
点播放,看这个请求依次走过四道关,假值在哪一步换成真值。
域名先判deny 恒胜。本地字面量要精确允许。解析到内网的名字,写进名单也过不了。
真钥匙不出经纪子进程环境和模型上下文始终是假值。真 token 只在出站那一步被换上去。
沙箱是漏斗口Go 绕过代理时,策略函数不会跑。操作系统只放行代理端口,直连在第一关就停。
教学示意:名单、假值形状与解析结果为课程化设定。逻辑轨迹右侧行号对应 openai/codex 仓库 commit 4f39251a01。
思路一 · 域名怎么被判
它解决什么问题

你让模型去 GitHub 提一个 issue。它写出 curl,环境里有真 token。下一秒域名换成 evil.example,或者打到 169.254.169.254。只拦「不在名单里的域名」,deny 条目和内网地址会漏。空名单若默认放行,忘写配置等于全开。

思路是什么

判定顺序写死。先 deny,再本地或私有,最后 allowlist。空名单和没命中,都拦。决策器只能翻 allowlist 漏掉的案,翻不了 denylist,也翻不了本地网段。

出处:codex-rs/network-proxy/src/runtime.rs 第 549 至 552 行;codex-rs/network-proxy/src/network_policy.rs 第 346 至 378 行;codex-rs/network-proxy/README.md 第 161 至 162 行

*.example.com 不含 apex,**.example.com 才含。解析到私有 IP 的域名,精确写进名单也拦。本地字面量要精确写上 localhost127.0.0.1,通配 * 不算。

出处:codex-rs/network-proxy/src/policy.rs 第 321 至 331 行;codex-rs/network-proxy/src/runtime.rs 第 1028 至 1043 行

解析 host 失败也拦 1 deny 命中即停 403 denylist 2 本地或私有 字面量要精确 403 allowlist 3 allowlist 空名单也拦
教学化结构图:三步顺序固定。Denied 和 NotAllowedLocal 不会去问决策器。

被拦时回给命令进程 403,头是 x-proxy-error,正文是一句人话。not_allowednot_allowed_local 对外都叫 blocked-by-allowlist,正文才能分开「不在名单」和「沙箱拦了本地」。这条 403 变成 exec 输出喂回模型,采样循环不会停。

出处:codex-rs/network-proxy/src/responses.rs 第 52 至 83 行

同一 pattern 既 allow 又 deny,有效值取大的。枚举序是 None < Allow < Deny

出处:codex-rs/network-proxy/src/config.rs 第 19 至 27 行

为什么长期成立

默认拒绝、deny 恒胜、内网要显式开门,这是 SSRF 防护的通用形状。换语言重写,该问的还是同一组问题:没写名单时怎么办,冲突时谁赢,解析到内网的名字算不算放行。

思路二 · 真钥匙只在代理里出现
它解决什么问题

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

思路是什么

拉起命令前,凭据经纪把 GH_TOKENOPENAI_API_KEY 换成同长度、同前缀的假值。模型若 printenv,看见的是假的。假值进上下文,对上游无用。

出处:codex-rs/network-proxy/src/credential_broker.rs 第 92 至 119 行

出站时 MITM 按 host 过滤。请求头里必须带着假值,才换成真值。hook 的剥头、注头发生在经纪之后,可以剥掉刚注入的 Authorization。用户改过的环境值不还原。标记当不可信输入。经纪只覆盖 GitHub 和 OpenAI。

出处:codex-rs/network-proxy/src/mitm.rs 第 297 至 302 行

模型 看不见真 token 子进程环境 同形状假值 代理 MITM 按 host 换真值 上游 真 token 真值只在代理进程内存里出现,不进 rollout,也不进模型下一轮上下文
教学化时序:假值走全程可见面,真值只在出站那一跳出现。
子进程口袋里是假票根,柜台后面锁着真票。
为什么长期成立

出网策略写错,模型还能凭 403 改主意。凭据进上下文,撤销成本高。所以真值在 spawn 前拿走,回填发生在代理进程内存里。假值保持形状,是为了让校验格式的客户端还能启动。

思路三 · 沙箱把流量按进代理
它解决什么问题

Go 的 net/http 对 loopback 绕过 HTTP_PROXY。你以为流量进了代理,其实它直连 127.0.0.1。本地有管理口、docker socket。策略文件写了名单,请求根本没走到那一层。

思路是什么

沙箱只放行代理端口。macOS 受限 Seatbelt 只写 localhost:{port}。Linux 走 ProxyOnlyallow_local_binding 为假时,NO_PROXY 写成空串,loopback 字面量也必须走代理。打开本地绑定等于承认 loopback 不再经过 allowlist。

出处:codex-rs/sandboxing/src/seatbelt.rs 第 309 至 336 行;codex-rs/network-proxy/src/proxy.rs 第 450 至 460 行

为什么长期成立

应用层代理挡不住不肯走代理的客户端。下一层必须是操作系统或防火墙,只留一个口。三道门叠在一起才构成托管网络。单独一道都挡不住另一道漏掉的面。

横向对比 · 阀门装在不同的层

Grok:阀门在工具入口,loopback 默认放行

Grok 的 web_fetch 在工具内部做域名名单和解析后 IP 检查。空名单拦所有 URL,和 Codex 的 allowlist-first 同方向。path 还可以写成前缀,收窄到某一段文档。

SSRF 检查拦 RFC1918、link-local、CGNAT。loopback 明确放行,注释写 local development。bash 里的 curl 不走这张名单。Codex 把阀门放在所有子进程出口,所以要和沙箱绑在一起。

两侧均已核对源码 · 2026-08-22 · 出处:web_fetch/domain.rs 第 110 至 144 行;web_fetch/ssrf.rs 第 17 至 19 行

DSH:配置只存变量名,spawn 时按键名擦环境

DSH 把密钥做成引用。设置文件只带环境变量名,provider 在每次操作时 resolve。配置表面从不看见值。

子进程另有一层擦除。名字像 KEYTOKEN 的键和所有 DSH_* 会被丢掉。它挡得住配置文件里出现真值。模型若还能 printenv,除非 spawn 用了这层擦除,环境里仍可能有真 token。

两侧均已核对源码 · 2026-08-22 · 出处:packages/credentials/credentials/src/index.ts 第 1 至 7 行;packages/subprocess/subprocess/src/index.ts 第 60 至 66 行 · DSH · 凭据每次现取
课堂练习
01

星号放行 localhost 吗

allow_local_binding = false,allowlist 只有 *。对 127.0.0.1 调一次判定,期望是什么。对公网 IP 8.8.8.8 再调一次,期望又是什么。

提示:本地字面量拒绝通配。allowlist 编译显式允许全局 *,denylist 编译拒绝它。

Takeaway:出站默认拒绝,deny 恒胜,内网要精确开门。真密钥不要进模型可见的环境,spawn 前换成假值,出站再换。代理挡不住绕过它的客户端,下一层必须是只放行代理端口的沙箱。

「先玩一遍 · 一个带凭据的请求过代理」的能力藏在每次交接里

「你让模型去 GitHub 提一个 issue。它写出 curl ,环境里有真 token。下一秒域名换成 evil.example ,或者打到 169.254.169.254 。只拦「不在名单里的域名」,deny 条目和内网地址会漏。空名单若默认放行,忘写配置等于全开」说明,Agent 的表现不只由模型决定。模型、上下文、工具、状态、权限和人之间的每次交接,都会改变任务能否继续以及出了问题能否恢复。

先写清状态,再增加能力

从「判定顺序写死。先 deny,再本地或私有,最后 allowlist。空名单和没命中,都拦。决策器只能翻 allowlist 漏掉的案,翻不了 denylist,也翻不了本地网段」出发,可以把流程拆成起始状态、下一步动作、工具返回、状态更新和停止条件。这样调试时找的是第一处丢失信息或权限的位置,而不是笼统地说“模型变笨了”。

  • 拉起命令前,把真 token 换成同形状假值 credential_broker.rs L92
  • 沙箱只放行代理端口,直连 loopback 到此为止 seatbelt.rs L325
  • 解析 host,deny 先判且恒胜 runtime.rs L553

成功路径不能代表系统可靠

用「提示:本地字面量拒绝通配。allowlist 编译显式允许全局 * ,denylist 编译拒绝它」重放一次成功和一次失败,记录每一轮真正传入的上下文、工具结果和负责人;只要第二个人能复述这条链,系统才有可维护性。

从「先玩一遍 · 一个带凭据的请求过代理」走到「思路一 · 域名怎么被判」

「先玩一遍 · 一个带凭据的请求过代理」先把问题落在「换域名、换方法,看请求在哪一关停下,假值在哪一步换成真值 播放 单步 重置 域名 api.github.com evil.example localhost 解析到内网 方法 GET POST 模式 Limited Full 先看假值怎么换成真值。再换成 POST,或让 Go 绕过代理。 客户端 走代理 Go 绕过 loopback 这次请求 GET https://api.github.com/repos/you/…」上;到了「思路一 · 域名怎么被判」,讨论继续推进到「你让模型去 GitHub 提一个 issue。它写出 curl ,环境里有真 token。下一秒域名换成 evil.example ,或者打到 169.254.169.254 。只拦「不在名单里的域名」,deny 条目和内网地址会漏。空名单若默认放行,忘写配置等于全开」。两段连起来,重点就不只是记住一个结论,而是看清它成立所依赖的条件。

把这条判断带到下一个场景

分析 Agent 时,沿着状态、动作、工具结果和下一步的顺序走一遍;每次交接都要能说明信息从哪里来、由谁确认、失败时停在哪里。

  • 「先玩一遍 · 一个带凭据的请求过代理」:换域名、换方法,看请求在哪一关停下,假值在哪一步换成真值 播放 单步 重置 域名 api.github.com evil.example localhost 解析到内网 方法 GET POST 模式 Limited Full 先看假值怎么换成真值。再换成 POST,或让 Go 绕过代理。 客户端 走代理 Go 绕过 loopback 这次请求 GET https://api.github.com/repos/you/…
  • 「思路一 · 域名怎么被判」:你让模型去 GitHub 提一个 issue。它写出 curl ,环境里有真 token。下一秒域名换成 evil.example ,或者打到 169.254.169.254 。只拦「不在名单里的域名」,deny 条目和内网地址会漏。空名单若默认放行,忘写配置等于全开
  • 「最后的要点」:域名解析到非公网,即便在名单里也拦 runtime.rs L582

最后的「最后的要点」把讨论落到「域名解析到非公网,即便在名单里也拦 runtime.rs L582」。回看这条线索时,最值得保留的是:当输入、规模或风险改变,哪些判断需要重新做一遍。

标记为已学完 阅读进度会自动记录
← 上一篇下一篇 →

继续阅读

同一条线上的下一篇。

文章讨论

读到这里,留下一个判断。

把刚想明白的地方、还没想通的问题,留给下一位一起学习的人。

正在讨论 网络与凭据代理:模型看不见的那把钥匙 拆开 OpenAI Codex
3条讨论文章讨论 · 与共学社区同步
在共学社区查看
AM
Asha Morgan内容编辑
观点实践记录

我把这篇文章里的一个判断改写成了今天可以验证的小实验。比记住结论更有用的是,知道下一步要观察什么。

文章讨论7 有帮助
LH
Lin Harper独立开发者
观点观点

读完以后我先回头找它成立的条件,而不是直接把方法搬进项目。这个顺序让后面的取舍清楚很多。

文章讨论5 有帮助
KM
Kiki Moore产品运营
问题问题

如果把这个判断放到真实工作里,最先需要补的约束是什么?我想知道从阅读到第一次实践之间,哪一步最值得先做。

文章讨论4 有帮助