Network and Credential Proxy: The Key the Model Never Sees
Outbound traffic has to clear several doors. The real token never enters the child process. What the model sees is a fake value and a 403.
- Before spawning the command, swap the real token for a same-shape fakecredential_broker.rs L92
- The sandbox only allows the proxy port; a direct loopback ends hereseatbelt.rs L325
- Resolve the host; deny is judged first and always winsruntime.rs L553
- A local literal without an exact allow is blocked as a private addressruntime.rs L578
- A name that resolves off the public net is blocked even if it is on the listruntime.rs L582
- An empty or miss on the allowlist returns NotAllowedruntime.rs L598
- Only NotAllowed asks the decidernetwork_policy.rs L349
- Under Limited, anything but GET, HEAD, OPTIONS is blockedconfig.rs L311
- HTTPS must see the inner method; missing MITM blocks ithttp_proxy.rs L312
- Only if the header carries the fake is the matching real credential injectedmitm.rs L301
You ask the model to file a GitHub issue. It writes curl; the env has a real token. A second later the host becomes evil.example, or it hits 169.254.169.254. Block only “hosts not on the list” and deny entries plus intranet addresses leak. If an empty list defaults to allow, forgetting the config is opening everything.
The judge order is written in stone. Deny first, then local or private, then the allowlist. An empty list and a miss both block. The decider can only retry a case the allowlist dropped — not the denylist, not a local segment.
Source:codex-rs/network-proxy/src/runtime.rs lines 549–552;codex-rs/network-proxy/src/network_policy.rs lines 346–378;codex-rs/network-proxy/README.md lines 161–162
*.example.com does not include the apex; **.example.com does. A name that resolves to a private IP is blocked even if it is written exactly on the list. A local literal must be written exactly as localhost or 127.0.0.1; a * glob does not count.
Source:codex-rs/network-proxy/src/policy.rs lines 321–331;codex-rs/network-proxy/src/runtime.rs lines 1028–1043
On a block the command process gets a 403; the header is x-proxy-error, the body is one human sentence. Outwardly not_allowed and not_allowed_local are both blocked-by-allowlist; only the body splits “not on the list” from “the sandbox blocked local”. That 403 becomes exec output fed back to the model; the sample loop does not stop.
Source:codex-rs/network-proxy/src/responses.rs lines 52–83
If the same pattern is both allow and deny, the effective value is the larger. Enum order is None < Allow < Deny.
Source:codex-rs/network-proxy/src/config.rs lines 19–27
Default deny, deny always wins, the intranet needs an explicit door — that’s the generic SSRF-defense shape. Rewrite it in another language and the same questions still apply: what if the list is empty, who wins a conflict, does a name that resolves to the intranet count as allow?
The token rides the command into the rollout. The next turn the model can still see it; the turn after that it may hit a log. Once a credential enters model context, every later redaction is a patch.
Before spawn, the credential broker swaps GH_TOKEN and OPENAI_API_KEY for fakes of the same length and prefix. If the model printenvs, it sees the fake. A fake in context is useless upstream.
Source:codex-rs/network-proxy/src/credential_broker.rs lines 92–119
On the way out, MITM filters by host. The request header must carry the fake before it is swapped for the real value. A hook’s strip/inject runs after the broker and can peel off the Authorization just injected. A user-edited env value is not restored. The mark is untrusted input. The broker only covers GitHub and OpenAI.
Source:codex-rs/network-proxy/src/mitm.rs lines 297–302
If the outbound policy is wrong, the model can still change its mind from a 403. A credential in context is expensive to revoke. So the real value is taken before spawn, and the put-back happens in the proxy process’s memory. The fake keeps its shape so a client that checks format can still start.
Go’s net/http skips HTTP_PROXY for loopback. You think the traffic entered the proxy; it actually hit 127.0.0.1 directly. Locally there is an admin port, a docker socket. The policy file has a list; the request never reached that layer.
The sandbox only allows the proxy port. Restricted Seatbelt on macOS writes only localhost:{port}. Linux takes ProxyOnly. When allow_local_binding is false, NO_PROXY is written as an empty string, and a loopback literal must also take the proxy. Turning local binding on is admitting loopback no longer goes through the allowlist.
Source:codex-rs/sandboxing/src/seatbelt.rs lines 309–336;codex-rs/network-proxy/src/proxy.rs lines 450–460
An application-layer proxy cannot stop a client that won’t take it. The next layer must be the OS or a firewall, with one mouth left. The three doors stacked together make a managed network. Any one door alone cannot cover a face another door dropped.
Grok: the valve is at the tool door; loopback is allowed by default
Grok’s web_fetch does a domain list and a post-resolve IP check inside the tool. An empty list blocks every URL — same direction as Codex’s allowlist-first. A path can also be written as a prefix, narrowed to one stretch of docs.
The SSRF check blocks RFC1918, link-local, CGNAT. Loopback is explicitly allowed; the comment says local development. curl in bash does not take this list. Codex puts the valve on every child-process exit, so it has to be tied to the sandbox.
web_fetch/domain.rs lines 110–144;web_fetch/ssrf.rs lines 17–19
DSH: config stores only the variable name; spawn wipes the env by key
DSH makes the secret a reference. The settings file only carries the env-var name; the provider resolves on every operation. The config surface never sees the value.
The child has one more wipe. Keys named like KEY or TOKEN, and every DSH_*, are dropped. That stops a real value appearing in the config file. If the model can still printenv, unless spawn used this wipe, the env may still hold a real token.
packages/credentials/credentials/src/index.ts lines 1–7; packages/subprocess/subprocess/src/index.ts lines 60–66 · DSH · Credentials resolved each time
Does a star allow localhost?
allow_local_binding = false, and the allowlist is only *. Call the judge once on 127.0.0.1 — what do you expect? Call it again on the public IP 8.8.8.8 — what then?
Hint: a local literal refuses a glob. Allowlist compile explicitly allows a global *; denylist compile refuses it.