From cea832b21dec4433a5a3db37cc7c6410a66e1e35 Mon Sep 17 00:00:00 2001 From: didericis-codex Date: Fri, 29 May 2026 02:39:37 -0400 Subject: [PATCH] fix(codex): stop injecting api key placeholder --- README.md | 22 ++++++++++++++++----- bot_bottle/agent_provider.py | 4 ++-- bot_bottle/backend/docker/prepare.py | 20 +++++++++---------- bot_bottle/backend/print_util.py | 14 ++++++------- bot_bottle/backend/smolmachines/prepare.py | 20 +++++++++---------- bot_bottle/manifest.py | 12 +++++++---- docs/prds/0026-agent-provider-templates.md | 4 +++- tests/unit/test_agent_provider.py | 23 ++++++++++++++++++++++ tests/unit/test_print_util.py | 4 ++-- 9 files changed, 82 insertions(+), 41 deletions(-) create mode 100644 tests/unit/test_agent_provider.py diff --git a/README.md b/README.md index 528f533..48a8334 100644 --- a/README.md +++ b/README.md @@ -349,11 +349,23 @@ The `gitea-dev` bottle. Backs my work on personal projects: provider auth through egress and gitea.dideric.is over SSH. ```` -For a Codex-backed base bottle, set `agent_provider.template: codex` -and use the `codex_auth` egress role for the OpenAI API route. The -built-in Codex template uses `Dockerfile.codex`; set -`agent_provider.dockerfile` to build the agent from a custom -Dockerfile while keeping the bot-bottle sidecars in place. +For a Codex-backed base bottle, set `agent_provider.template: codex`. +The Codex template expects ChatGPT/device login state instead of an +`OPENAI_API_KEY` env var; no API-key placeholder is forwarded into the +agent. To let headless device-code login request a user code, add an +unauthenticated egress route for the device-auth endpoint: + +```yaml +egress: + routes: + - host: auth.openai.com + path_allowlist: + - /api/accounts/deviceauth/ +``` + +The built-in Codex template uses `Dockerfile.codex`; set +`agent_provider.dockerfile` to build the agent from a custom Dockerfile +while keeping the bot-bottle sidecars in place. ### Example agent (`~/.bot-bottle/agents/gitea-helper.md`) diff --git a/bot_bottle/agent_provider.py b/bot_bottle/agent_provider.py index b557c3e..b8b9ec8 100644 --- a/bot_bottle/agent_provider.py +++ b/bot_bottle/agent_provider.py @@ -53,8 +53,8 @@ _RUNTIMES = { command="codex", image="bot-bottle-codex:latest", dockerfile=str(_REPO_ROOT / "Dockerfile.codex"), - auth_role="codex_auth", - placeholder_env="OPENAI_API_KEY", + auth_role="", + placeholder_env="", prompt_mode="read_prompt_file", bypass_args=("--dangerously-bypass-approvals-and-sandbox",), resume_args=("resume", "--last"), diff --git a/bot_bottle/backend/docker/prepare.py b/bot_bottle/backend/docker/prepare.py index d885d9c..c2c5943 100644 --- a/bot_bottle/backend/docker/prepare.py +++ b/bot_bottle/backend/docker/prepare.py @@ -201,18 +201,18 @@ def resolve_plan( # never lands on argv or in env_file) goes into one dict. Nothing # mutates the host os.environ. forwarded_env: dict[str, str] = dict(resolved.forwarded) - # When the bottle declares an egress route with the - # `claude_code_oauth` role marker, claude-code's outbound - # Authorization gets stripped + re-injected by egress. The - # agent's environ still needs *something* claude-code recognises - # as a credential or it refuses to start; ship a non-secret - # placeholder. The placeholder isn't any real token value, so - # leaking it would tell an attacker only that egress is in - # front. Manifest validation enforces singleton on this role. + # Some provider CLIs refuse to start without *some* credential + # env var even when egress will strip + re-inject the real + # Authorization header. For those providers, auth_role names the + # route marker that enables a non-secret placeholder env. Codex is + # intentionally absent here: it should use its device/ChatGPT login + # state, and an OPENAI_API_KEY placeholder would force API-key auth. has_provider_auth = any( - provider_runtime.auth_role in r.roles for r in egress_plan.routes + provider_runtime.auth_role + and provider_runtime.auth_role in r.roles + for r in egress_plan.routes ) - if has_provider_auth: + if has_provider_auth and provider_runtime.placeholder_env: forwarded_env[provider_runtime.placeholder_env] = "egress-placeholder" if provider.template == "claude" and has_provider_auth: # Belt-and-braces: turn off telemetry endpoints (statsig, diff --git a/bot_bottle/backend/print_util.py b/bot_bottle/backend/print_util.py index 9615882..5e277c8 100644 --- a/bot_bottle/backend/print_util.py +++ b/bot_bottle/backend/print_util.py @@ -34,12 +34,12 @@ def visible_agent_env_names( ) -> list[str]: """Env names worth showing in launch summaries. - Provider auth placeholders (`OPENAI_API_KEY`, - `CLAUDE_CODE_OAUTH_TOKEN`) are implementation details: they are - non-secret dummy values that satisfy the provider CLI while egress - injects the real upstream Authorization header. Showing them in - preflight makes the operator think a real key is entering the - agent, so hide only that provider-owned placeholder. + Provider auth placeholders (currently `CLAUDE_CODE_OAUTH_TOKEN`) + are implementation details: they are non-secret dummy values that + satisfy the provider CLI while egress injects the real upstream + Authorization header. Showing them in preflight makes the operator + think a real key is entering the agent, so hide only the active + provider-owned placeholder. """ hidden = {runtime_for(agent_provider_template).placeholder_env} - return sorted({name for name in env_names if name not in hidden}) + return sorted({name for name in env_names if name and name not in hidden}) diff --git a/bot_bottle/backend/smolmachines/prepare.py b/bot_bottle/backend/smolmachines/prepare.py index 18bdfc6..91a5d88 100644 --- a/bot_bottle/backend/smolmachines/prepare.py +++ b/bot_bottle/backend/smolmachines/prepare.py @@ -112,18 +112,18 @@ def resolve_plan( egress_dir.mkdir(parents=True, exist_ok=True) egress_plan = Egress().prepare(bottle, slug, egress_dir) - # Claude-code refuses to start without *something* it - # recognises as a credential. When the bottle has an egress - # route carrying the `claude_code_oauth` role marker, egress - # strips + re-injects the real Authorization header on the - # outbound leg using a token held in egress's own environ — so - # the agent gets a non-secret placeholder here (matches the - # docker backend's forwarded_env logic in - # bot_bottle/backend/docker/prepare.py). + # Some provider CLIs refuse to start without *some* credential + # env var even when egress will strip + re-inject the real + # Authorization header. For those providers, auth_role names the + # route marker that enables a non-secret placeholder env. Codex is + # intentionally absent here: it should use its device/ChatGPT login + # state, and an OPENAI_API_KEY placeholder would force API-key auth. has_provider_auth = any( - provider_runtime.auth_role in r.roles for r in egress_plan.routes + provider_runtime.auth_role + and provider_runtime.auth_role in r.roles + for r in egress_plan.routes ) - if has_provider_auth: + if has_provider_auth and provider_runtime.placeholder_env: guest_env[provider_runtime.placeholder_env] = "egress-placeholder" if provider.template == "claude" and has_provider_auth: guest_env.setdefault("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC", "1") diff --git a/bot_bottle/manifest.py b/bot_bottle/manifest.py index 529ba50..7c9dc7d 100644 --- a/bot_bottle/manifest.py +++ b/bot_bottle/manifest.py @@ -188,6 +188,11 @@ EGRESS_AUTH_SCHEMES = ("Bearer", "token") # logic — declare the role on whichever route # injects the OAuth header. # +# codex_auth: legacy marker for Codex API-key-style egress routes. +# It is still accepted for older bottle manifests, but +# no longer triggers an OPENAI_API_KEY placeholder. Codex +# bottles should prefer device/ChatGPT login state. +# # Routes without a `role` are pure proxy entries: egress # enforces path_allowlist + injects auth on its own, but nothing # special happens on the agent side. @@ -196,10 +201,9 @@ EGRESS_ROLES = frozenset({ "codex_auth", }) -# Singleton roles may appear on at most one route per bottle. -# claude_code_oauth drives a single placeholder env var; two routes -# claiming it would leave "which one is the canonical OAuth route?" -# ambiguous for any future role-aware logic. +# Singleton roles may appear on at most one route per bottle. Some +# roles drive a single provider auth path; two routes claiming one +# marker would leave "which one is canonical?" ambiguous. EGRESS_SINGLETON_ROLES = frozenset({ "claude_code_oauth", "codex_auth", diff --git a/docs/prds/0026-agent-provider-templates.md b/docs/prds/0026-agent-provider-templates.md index 289c2d3..60ea02a 100644 --- a/docs/prds/0026-agent-provider-templates.md +++ b/docs/prds/0026-agent-provider-templates.md @@ -86,7 +86,9 @@ agent_provider: ## Open questions -- The initial Codex auth role is `codex_auth`; it provides a non-secret `OPENAI_API_KEY` placeholder to the agent while egress holds the real token. +- `codex_auth` is retained only as a legacy accepted route marker. The + Codex template should not inject an `OPENAI_API_KEY` placeholder; + Codex bottles use device/ChatGPT login state instead. - Existing state-folder transcript capture is Claude-specific and should remain gated to Claude until the follow-up state/transcript refactor. ## References diff --git a/tests/unit/test_agent_provider.py b/tests/unit/test_agent_provider.py new file mode 100644 index 0000000..f69e326 --- /dev/null +++ b/tests/unit/test_agent_provider.py @@ -0,0 +1,23 @@ +"""Unit: provider runtime defaults.""" + +from __future__ import annotations + +import unittest + +from bot_bottle.agent_provider import runtime_for + + +class TestAgentProviderRuntime(unittest.TestCase): + def test_claude_keeps_oauth_placeholder(self): + runtime = runtime_for("claude") + self.assertEqual("claude_code_oauth", runtime.auth_role) + self.assertEqual("CLAUDE_CODE_OAUTH_TOKEN", runtime.placeholder_env) + + def test_codex_does_not_inject_openai_api_key_placeholder(self): + runtime = runtime_for("codex") + self.assertEqual("", runtime.auth_role) + self.assertEqual("", runtime.placeholder_env) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit/test_print_util.py b/tests/unit/test_print_util.py index b3e376f..d4bea06 100644 --- a/tests/unit/test_print_util.py +++ b/tests/unit/test_print_util.py @@ -8,9 +8,9 @@ from bot_bottle.backend.print_util import visible_agent_env_names class TestVisibleAgentEnvNames(unittest.TestCase): - def test_hides_codex_auth_placeholder(self): + def test_codex_shows_openai_api_key_if_user_declares_it(self): self.assertEqual( - ["CUSTOM"], + ["CUSTOM", "OPENAI_API_KEY"], visible_agent_env_names( ["OPENAI_API_KEY", "CUSTOM"], agent_provider_template="codex",