From 68e5097534c75c99e0c5b67b5aac54464cb9fc15 Mon Sep 17 00:00:00 2001 From: didericis-codex Date: Mon, 1 Jun 2026 16:38:34 -0400 Subject: [PATCH] fix(codex): make host-credential bottles actually authenticate Debugging a live codex smolmachines bottle surfaced three independent failures past the sign-in screen; fix each so forward_host_credentials works end to end: - codex_auth: dummy access/id tokens now inherit the *real* host token's exp instead of now+1h. Codex (0.135) refreshes when its local token's JWT exp lapses; with a placeholder refresh_token that refresh fails and drops to the sign-in screen. Aligning exp tracks the real token's life. - prepare: set CODEX_CA_CERTIFICATE to the agent CA bundle for codex bottles. Codex is rustls and ignores the system store / NODE_EXTRA_CA_ CERTS; it reads CODEX_CA_CERTIFICATE (fallback SSL_CERT_FILE) for custom roots across HTTPS + wss, so it must be pointed at the egress MITM CA or injection can't work without tls_passthrough. - pipelock: auto tls_passthrough the Codex API hosts when forward_host_credentials is on. Egress injects the bearer before pipelock, whose header DLP then flags the JWT ("request header contains secret") and the retry storm trips its 429. passthrough host-gates the CONNECT but skips decrypt+rescan of egress-owned auth. The auto-added routes aren't in bottle.egress.routes, so the hosts are added explicitly. Co-Authored-By: Claude Opus 4.8 --- README.md | 11 ++-- bot_bottle/backend/smolmachines/bottle.py | 39 +++++------ bot_bottle/backend/smolmachines/prepare.py | 18 ++++++ .../smolmachines/provision/provider_auth.py | 19 ++++++ bot_bottle/codex_auth.py | 64 +++++++++++++------ bot_bottle/pipelock.py | 19 +++++- .../0029-codex-host-credentials-egress.md | 4 +- tests/unit/test_codex_auth.py | 29 ++++++++- tests/unit/test_pipelock_allowlist.py | 19 ++++++ tests/unit/test_smolmachines_bottle.py | 9 ++- tests/unit/test_smolmachines_provision.py | 24 +++++++ 11 files changed, 197 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index 0d2a034..333f043 100644 --- a/README.md +++ b/README.md @@ -373,11 +373,12 @@ launcher reads `tokens.access_token` from the host's `~/.codex/auth.json`, verifies it is fresh user/device auth, and passes it to the sidecar's `EGRESS_TOKEN_N` env slot. The agent container gets a dummy `~/.codex/auth.json` that preserves the host auth-mode shape -but replaces credential values with placeholders, so Codex chooses the -user/device auth path without receiving real access tokens, refresh -tokens, or `OPENAI_API_KEY`. The effective egress table automatically -adds or upgrades `api.openai.com` and `chatgpt.com` to authenticated -routes when `forward_host_credentials` is true. +but replaces credential values with placeholders. It keeps the selected +ChatGPT account id so Codex sends requests for the same account while +egress owns the real bearer token. The agent never receives real access +tokens, refresh tokens, or `OPENAI_API_KEY`. The effective egress table +automatically adds or upgrades `api.openai.com` and `chatgpt.com` to +authenticated routes when `forward_host_credentials` is true. The built-in Codex template uses `Dockerfile.codex`; set `agent_provider.dockerfile` to build the agent from a custom Dockerfile diff --git a/bot_bottle/backend/smolmachines/bottle.py b/bot_bottle/backend/smolmachines/bottle.py index 5cebc4c..2553cb2 100644 --- a/bot_bottle/backend/smolmachines/bottle.py +++ b/bot_bottle/backend/smolmachines/bottle.py @@ -45,19 +45,11 @@ _HOME_FOR = { } -def _env_flags_for(user: str) -> list[str]: +def _env_assignments_for(user: str, env: Mapping[str, str]) -> list[str]: home = _HOME_FOR.get(user, f"/home/{user}") - return ["-e", f"HOME={home}", "-e", f"USER={user}"] - - -def _guest_env_flags(env: Mapping[str, str]) -> list[str]: - """Render `{K: V}` into a flat `-e K=V` argv slice for - `smolvm machine exec`. `smolvm machine create -e` set env - on PID 1 but it doesn't propagate to fresh exec process - trees, so we have to re-pass them every call.""" - out: list[str] = [] + out = [f"HOME={home}", f"USER={user}"] for k, v in env.items(): - out += ["-e", f"{k}={v}"] + out.append(f"{k}={v}") return out @@ -98,9 +90,8 @@ class SmolmachinesBottle(Bottle): flags = ["smolvm", "machine", "exec", "--name", self.name] if tty: flags += ["-i", "-t"] - flags += _env_flags_for("node") - flags += _guest_env_flags(self._guest_env) - agent_tail = [self.agent_command] + agent_tail = ["env", *_env_assignments_for("node", self._guest_env), + self.agent_command] provider_prompt_args = prompt_args( self._agent_prompt_mode, self._prompt_path, argv=argv, ) @@ -148,16 +139,16 @@ class SmolmachinesBottle(Bottle): on both backends. Pass `user="root"` for tests that need root. - `runuser -u -- /bin/sh -c