fix(codex): forward host credentials to api route

This commit is contained in:
2026-05-29 03:34:11 -04:00
committed by didericis
parent 711cb9c194
commit 62dd7b2aa5
4 changed files with 89 additions and 40 deletions
+4 -3
View File
@@ -353,7 +353,8 @@ For a Codex-backed base bottle, set `agent_provider.template: codex`.
The Codex template expects ChatGPT/device login state instead of an The Codex template expects ChatGPT/device login state instead of an
`OPENAI_API_KEY` env var; no API-key placeholder is forwarded into the `OPENAI_API_KEY` env var; no API-key placeholder is forwarded into the
agent. To let bot-bottle read the host's current Codex ChatGPT access agent. To let bot-bottle read the host's current Codex ChatGPT access
token and inject it from egress only, opt in explicitly: token and inject it from egress only for Codex's API calls, opt in
explicitly:
```yaml ```yaml
agent_provider: agent_provider:
@@ -373,8 +374,8 @@ launcher reads only `tokens.access_token` from the host's
to the sidecar's `EGRESS_TOKEN_N` env slot. The agent container does to the sidecar's `EGRESS_TOKEN_N` env slot. The agent container does
not receive `auth.json`, refresh tokens, access-token env vars, or not receive `auth.json`, refresh tokens, access-token env vars, or
`OPENAI_API_KEY`. The effective egress table automatically adds or `OPENAI_API_KEY`. The effective egress table automatically adds or
upgrades `chatgpt.com` to an authenticated route when upgrades `api.openai.com` and `chatgpt.com` to authenticated routes
`forward_host_credentials` is true. when `forward_host_credentials` is true.
The built-in Codex template uses `Dockerfile.codex`; set The built-in Codex template uses `Dockerfile.codex`; set
`agent_provider.dockerfile` to build the agent from a custom Dockerfile `agent_provider.dockerfile` to build the agent from a custom Dockerfile
+33 -13
View File
@@ -31,7 +31,7 @@ from pathlib import Path
from .log import die from .log import die
from .manifest import Bottle from .manifest import Bottle
CODEX_CHATGPT_HOST = "chatgpt.com" CODEX_HOST_CREDENTIAL_HOSTS = ("api.openai.com", "chatgpt.com")
CODEX_HOST_CREDENTIAL_TOKEN_REF = "BOT_BOTTLE_CODEX_HOST_ACCESS_TOKEN" CODEX_HOST_CREDENTIAL_TOKEN_REF = "BOT_BOTTLE_CODEX_HOST_ACCESS_TOKEN"
@@ -181,7 +181,7 @@ def egress_routes_for_bottle(
`bottle.egress.routes` as an authenticated route or bare-pass entry `bottle.egress.routes` as an authenticated route or bare-pass entry
(`- host: <name>`). Codex host-credential forwarding is the (`- host: <name>`). Codex host-credential forwarding is the
provider-owned exception: when explicitly enabled, it adds or provider-owned exception: when explicitly enabled, it adds or
upgrades `chatgpt.com` to an egress-owned authenticated route. The upgrades the Codex API hosts to egress-owned authenticated routes. The
legacy `bottle.egress.allowlist` folding is gone — egress is the legacy `bottle.egress.allowlist` folding is gone — egress is the
single allowlist surface.""" single allowlist surface."""
routes = list(egress_manifest_routes(bottle)) routes = list(egress_manifest_routes(bottle))
@@ -191,36 +191,56 @@ def egress_routes_for_bottle(
if bottle.agent_provider.template != "codex": if bottle.agent_provider.template != "codex":
return tuple(routes) return tuple(routes)
for host in CODEX_HOST_CREDENTIAL_HOSTS:
routes = _ensure_codex_host_credential_route(routes, host)
return tuple(routes)
def _next_token_env(routes: list[EgressRoute]) -> str:
return f"EGRESS_TOKEN_{len({r.token_env for r in routes if r.token_env})}"
def _codex_host_credential_token_env(routes: list[EgressRoute]) -> str:
for route in routes:
if route.token_ref == CODEX_HOST_CREDENTIAL_TOKEN_REF:
return route.token_env
return _next_token_env(routes)
def _ensure_codex_host_credential_route(
routes: list[EgressRoute], host: str,
) -> list[EgressRoute]:
for idx, route in enumerate(routes): for idx, route in enumerate(routes):
if route.host.lower() != CODEX_CHATGPT_HOST: if route.host.lower() != host:
continue continue
if route.auth_scheme or route.token_ref: if route.auth_scheme or route.token_ref:
if (
route.auth_scheme == "Bearer"
and route.token_ref == CODEX_HOST_CREDENTIAL_TOKEN_REF
):
return routes
die( die(
"codex host credential forwarding conflicts with an " "codex host credential forwarding conflicts with an "
"authenticated egress route for chatgpt.com. Remove that " f"authenticated egress route for {host}. Remove that "
"route auth block or disable agent_provider.forward_host_credentials." "route auth block or disable agent_provider.forward_host_credentials."
) )
routes[idx] = EgressRoute( routes[idx] = EgressRoute(
host=route.host, host=route.host,
path_allowlist=route.path_allowlist, path_allowlist=route.path_allowlist,
auth_scheme="Bearer", auth_scheme="Bearer",
token_env=_next_token_env(routes), token_env=_codex_host_credential_token_env(routes),
token_ref=CODEX_HOST_CREDENTIAL_TOKEN_REF, token_ref=CODEX_HOST_CREDENTIAL_TOKEN_REF,
roles=route.roles, roles=route.roles,
) )
return tuple(routes) return routes
routes.append(EgressRoute( routes.append(EgressRoute(
host=CODEX_CHATGPT_HOST, host=host,
auth_scheme="Bearer", auth_scheme="Bearer",
token_env=_next_token_env(routes), token_env=_codex_host_credential_token_env(routes),
token_ref=CODEX_HOST_CREDENTIAL_TOKEN_REF, token_ref=CODEX_HOST_CREDENTIAL_TOKEN_REF,
)) ))
return tuple(routes) return routes
def _next_token_env(routes: list[EgressRoute]) -> str:
return f"EGRESS_TOKEN_{len({r.token_env for r in routes if r.token_env})}"
def egress_token_env_map( def egress_token_env_map(
+28 -21
View File
@@ -13,12 +13,13 @@ explicit `agent_provider.forward_host_credentials` manifest flag.
## Problem ## Problem
Codex bottles can reach `chatgpt.com` after the host is added to egress, Codex bottles can reach OpenAI hosts after they are added to egress, but
but requests to `chatgpt.com/backend-api/codex/...` still fail with requests to Codex's ChatGPT-backed API endpoints still fail with HTTP
HTTP 403. The egress proxy strips agent-originated `Authorization` 403 when the egress route is unauthenticated. The egress proxy strips
headers and only re-injects auth for routes that declare an egress-owned agent-originated `Authorization` headers and only re-injects auth for
token. A bare `chatgpt.com` route therefore forwards Codex requests routes that declare an egress-owned token. Bare `api.openai.com` or
without the ChatGPT bearer token. `chatgpt.com` routes therefore forward Codex requests without the
ChatGPT bearer token.
Copying `~/.codex/auth.json` into the agent would solve auth but would Copying `~/.codex/auth.json` into the agent would solve auth but would
also put access and refresh material inside the agent sandbox. That cuts also put access and refresh material inside the agent sandbox. That cuts
@@ -27,8 +28,8 @@ should live in the sidecar boundary when possible, not in the agent.
## Goals / Success Criteria ## Goals / Success Criteria
- A Codex bottle with host ChatGPT auth can call - A Codex bottle with host ChatGPT auth can call Codex's
`chatgpt.com/backend-api/codex/...` through egress. `api.openai.com` and `chatgpt.com` endpoints through egress.
- Host credential forwarding happens only when the bottle declares - Host credential forwarding happens only when the bottle declares
`agent_provider.forward_host_credentials: true`. `agent_provider.forward_host_credentials: true`.
- The agent container does not receive `OPENAI_API_KEY`, - The agent container does not receive `OPENAI_API_KEY`,
@@ -48,9 +49,9 @@ should live in the sidecar boundary when possible, not in the agent.
- Copying host `~/.codex/auth.json` into the agent. - Copying host `~/.codex/auth.json` into the agent.
- Allowing arbitrary host credential forwarding. This PRD covers Codex - Allowing arbitrary host credential forwarding. This PRD covers Codex
ChatGPT/device-login credentials only. ChatGPT/device-login credentials only.
- Hot-applying a new authenticated `chatgpt.com` route to an existing - Hot-applying new authenticated Codex routes to an existing running
running sidecar. The current hot-apply path cannot safely populate new sidecar. The current hot-apply path cannot safely populate new token
token env slots in an already-running container. env slots in an already-running container.
## Scope ## Scope
@@ -64,8 +65,9 @@ should live in the sidecar boundary when possible, not in the agent.
- Extract only `tokens.access_token`. - Extract only `tokens.access_token`.
- Validate that `auth_mode` is `chatgpt` and the access token is present, - Validate that `auth_mode` is `chatgpt` and the access token is present,
JWT-shaped, and not expired. JWT-shaped, and not expired.
- Add or upgrade a `chatgpt.com` egress route to inject that access token - Add or upgrade `api.openai.com` and `chatgpt.com` egress routes to
via an `EGRESS_TOKEN_N` sidecar env slot. inject that access token via a shared `EGRESS_TOKEN_N` sidecar env
slot.
- Pass the extracted token only into the sidecar compose/run - Pass the extracted token only into the sidecar compose/run
environment, alongside other egress token values. environment, alongside other egress token values.
@@ -109,16 +111,21 @@ operator at `codex login --device-auth`, without printing token values.
### Egress route ### Egress route
When forwarding host Codex credentials, the effective egress route table When forwarding host Codex credentials, the effective egress route table
should contain an authenticated `chatgpt.com` route. If the bottle should contain authenticated `api.openai.com` and `chatgpt.com` routes.
already declares `chatgpt.com` as a bare-pass route, upgrade it in the If the bottle already declares either host as a bare-pass route, upgrade
effective route table rather than requiring a duplicate manifest entry. it in the effective route table rather than requiring a duplicate
If the bottle already declares an authenticated `chatgpt.com` route, manifest entry. If the bottle already declares an authenticated route for
fail rather than guessing whether to override operator-provided auth. either host, fail rather than guessing whether to override
operator-provided auth, unless that route already uses the synthetic
Codex host credential token reference.
The rendered route should look like any other egress-owned auth route: The rendered route should look like any other egress-owned auth route:
```yaml ```yaml
routes: routes:
- host: "api.openai.com"
auth_scheme: "Bearer"
token_env: "EGRESS_TOKEN_N"
- host: "chatgpt.com" - host: "chatgpt.com"
auth_scheme: "Bearer" auth_scheme: "Bearer"
token_env: "EGRESS_TOKEN_N" token_env: "EGRESS_TOKEN_N"
@@ -135,7 +142,7 @@ flowchart LR
H["Host ~/.codex/auth.json"] --> L["bot-bottle launch"] H["Host ~/.codex/auth.json"] --> L["bot-bottle launch"]
L -->|access token only| S["egress sidecar env"] L -->|access token only| S["egress sidecar env"]
A["Codex agent"] -->|HTTPS via proxy, auth stripped| E["egress"] A["Codex agent"] -->|HTTPS via proxy, auth stripped| E["egress"]
E -->|Bearer injected from env| C["chatgpt.com"] E -->|Bearer injected from env| C["api.openai.com / chatgpt.com"]
``` ```
## Implementation chunks ## Implementation chunks
@@ -146,8 +153,8 @@ flowchart LR
unit tests. unit tests.
3. **Host Codex auth reader.** Add a small stdlib-only helper for 3. **Host Codex auth reader.** Add a small stdlib-only helper for
parsing and validating host Codex auth without printing values. parsing and validating host Codex auth without printing values.
4. **Effective egress route.** Add/upgrade the `chatgpt.com` route when 4. **Effective egress route.** Add/upgrade the Codex API routes when the
the flag is enabled, and add tests for bare route upgrade, flag is enabled, and add tests for bare route upgrade,
missing-route insertion, and authenticated-route conflict. missing-route insertion, and authenticated-route conflict.
5. **Launch wiring.** Pass the host access token into the egress sidecar 5. **Launch wiring.** Pass the host access token into the egress sidecar
env for Docker and smolmachines without exposing it to the agent. env for Docker and smolmachines without exposing it to the agent.
+24 -3
View File
@@ -123,13 +123,16 @@ class TestRoutesForBottleUsesManifestOnly(unittest.TestCase):
effective = [r.host for r in egress_routes_for_bottle(b)] effective = [r.host for r in egress_routes_for_bottle(b)]
self.assertEqual(["x.example"], effective) self.assertEqual(["x.example"], effective)
def test_codex_forward_host_credentials_adds_chatgpt_route(self): def test_codex_forward_host_credentials_adds_codex_routes(self):
b = _codex_bottle(forward_host_credentials=True, routes=[]) b = _codex_bottle(forward_host_credentials=True, routes=[])
routes = egress_routes_for_bottle(b) routes = egress_routes_for_bottle(b)
self.assertEqual(["chatgpt.com"], [r.host for r in routes]) self.assertEqual(["api.openai.com", "chatgpt.com"], [r.host for r in routes])
self.assertEqual("Bearer", routes[0].auth_scheme) self.assertEqual("Bearer", routes[0].auth_scheme)
self.assertEqual("EGRESS_TOKEN_0", routes[0].token_env) self.assertEqual("EGRESS_TOKEN_0", routes[0].token_env)
self.assertEqual(CODEX_HOST_CREDENTIAL_TOKEN_REF, routes[0].token_ref) self.assertEqual(CODEX_HOST_CREDENTIAL_TOKEN_REF, routes[0].token_ref)
self.assertEqual("Bearer", routes[1].auth_scheme)
self.assertEqual("EGRESS_TOKEN_0", routes[1].token_env)
self.assertEqual(CODEX_HOST_CREDENTIAL_TOKEN_REF, routes[1].token_ref)
def test_codex_forward_host_credentials_upgrades_bare_chatgpt_route(self): def test_codex_forward_host_credentials_upgrades_bare_chatgpt_route(self):
b = _codex_bottle( b = _codex_bottle(
@@ -137,12 +140,30 @@ class TestRoutesForBottleUsesManifestOnly(unittest.TestCase):
routes=[{"host": "chatgpt.com", "path_allowlist": ["/backend-api/"]}], routes=[{"host": "chatgpt.com", "path_allowlist": ["/backend-api/"]}],
) )
routes = egress_routes_for_bottle(b) routes = egress_routes_for_bottle(b)
self.assertEqual(1, len(routes)) self.assertEqual(2, len(routes))
self.assertEqual("chatgpt.com", routes[0].host) self.assertEqual("chatgpt.com", routes[0].host)
self.assertEqual("Bearer", routes[0].auth_scheme) self.assertEqual("Bearer", routes[0].auth_scheme)
self.assertEqual("EGRESS_TOKEN_0", routes[0].token_env) self.assertEqual("EGRESS_TOKEN_0", routes[0].token_env)
self.assertEqual(CODEX_HOST_CREDENTIAL_TOKEN_REF, routes[0].token_ref) self.assertEqual(CODEX_HOST_CREDENTIAL_TOKEN_REF, routes[0].token_ref)
self.assertEqual(("/backend-api/",), routes[0].path_allowlist) self.assertEqual(("/backend-api/",), routes[0].path_allowlist)
self.assertEqual("api.openai.com", routes[1].host)
self.assertEqual("EGRESS_TOKEN_0", routes[1].token_env)
def test_codex_forward_host_credentials_accepts_explicit_synthetic_route(self):
b = _codex_bottle(
forward_host_credentials=True,
routes=[{
"host": "api.openai.com",
"auth": {
"scheme": "Bearer",
"token_ref": CODEX_HOST_CREDENTIAL_TOKEN_REF,
},
}],
)
routes = egress_routes_for_bottle(b)
self.assertEqual(["api.openai.com", "chatgpt.com"], [r.host for r in routes])
self.assertEqual("EGRESS_TOKEN_0", routes[0].token_env)
self.assertEqual("EGRESS_TOKEN_0", routes[1].token_env)
def test_codex_forward_host_credentials_conflicts_with_authed_route(self): def test_codex_forward_host_credentials_conflicts_with_authed_route(self):
b = _codex_bottle( b = _codex_bottle(