fix(codex): stop injecting api key placeholder
test / unit (pull_request) Successful in 27s
test / integration (pull_request) Successful in 41s

This commit is contained in:
2026-05-29 02:39:37 -04:00
parent 50baf63669
commit cea832b21d
9 changed files with 82 additions and 41 deletions
+17 -5
View File
@@ -349,11 +349,23 @@ The `gitea-dev` bottle. Backs my work on personal projects: provider
auth through egress and gitea.dideric.is over SSH. auth through egress and gitea.dideric.is over SSH.
```` ````
For a Codex-backed base bottle, set `agent_provider.template: codex` For a Codex-backed base bottle, set `agent_provider.template: codex`.
and use the `codex_auth` egress role for the OpenAI API route. The The Codex template expects ChatGPT/device login state instead of an
built-in Codex template uses `Dockerfile.codex`; set `OPENAI_API_KEY` env var; no API-key placeholder is forwarded into the
`agent_provider.dockerfile` to build the agent from a custom agent. To let headless device-code login request a user code, add an
Dockerfile while keeping the bot-bottle sidecars in place. 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`) ### Example agent (`~/.bot-bottle/agents/gitea-helper.md`)
+2 -2
View File
@@ -53,8 +53,8 @@ _RUNTIMES = {
command="codex", command="codex",
image="bot-bottle-codex:latest", image="bot-bottle-codex:latest",
dockerfile=str(_REPO_ROOT / "Dockerfile.codex"), dockerfile=str(_REPO_ROOT / "Dockerfile.codex"),
auth_role="codex_auth", auth_role="",
placeholder_env="OPENAI_API_KEY", placeholder_env="",
prompt_mode="read_prompt_file", prompt_mode="read_prompt_file",
bypass_args=("--dangerously-bypass-approvals-and-sandbox",), bypass_args=("--dangerously-bypass-approvals-and-sandbox",),
resume_args=("resume", "--last"), resume_args=("resume", "--last"),
+10 -10
View File
@@ -201,18 +201,18 @@ def resolve_plan(
# never lands on argv or in env_file) goes into one dict. Nothing # never lands on argv or in env_file) goes into one dict. Nothing
# mutates the host os.environ. # mutates the host os.environ.
forwarded_env: dict[str, str] = dict(resolved.forwarded) forwarded_env: dict[str, str] = dict(resolved.forwarded)
# When the bottle declares an egress route with the # Some provider CLIs refuse to start without *some* credential
# `claude_code_oauth` role marker, claude-code's outbound # env var even when egress will strip + re-inject the real
# Authorization gets stripped + re-injected by egress. The # Authorization header. For those providers, auth_role names the
# agent's environ still needs *something* claude-code recognises # route marker that enables a non-secret placeholder env. Codex is
# as a credential or it refuses to start; ship a non-secret # intentionally absent here: it should use its device/ChatGPT login
# placeholder. The placeholder isn't any real token value, so # state, and an OPENAI_API_KEY placeholder would force API-key auth.
# leaking it would tell an attacker only that egress is in
# front. Manifest validation enforces singleton on this role.
has_provider_auth = any( 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" forwarded_env[provider_runtime.placeholder_env] = "egress-placeholder"
if provider.template == "claude" and has_provider_auth: if provider.template == "claude" and has_provider_auth:
# Belt-and-braces: turn off telemetry endpoints (statsig, # Belt-and-braces: turn off telemetry endpoints (statsig,
+7 -7
View File
@@ -34,12 +34,12 @@ def visible_agent_env_names(
) -> list[str]: ) -> list[str]:
"""Env names worth showing in launch summaries. """Env names worth showing in launch summaries.
Provider auth placeholders (`OPENAI_API_KEY`, Provider auth placeholders (currently `CLAUDE_CODE_OAUTH_TOKEN`)
`CLAUDE_CODE_OAUTH_TOKEN`) are implementation details: they are are implementation details: they are non-secret dummy values that
non-secret dummy values that satisfy the provider CLI while egress satisfy the provider CLI while egress injects the real upstream
injects the real upstream Authorization header. Showing them in Authorization header. Showing them in preflight makes the operator
preflight makes the operator think a real key is entering the think a real key is entering the agent, so hide only the active
agent, so hide only that provider-owned placeholder. provider-owned placeholder.
""" """
hidden = {runtime_for(agent_provider_template).placeholder_env} 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})
+10 -10
View File
@@ -112,18 +112,18 @@ def resolve_plan(
egress_dir.mkdir(parents=True, exist_ok=True) egress_dir.mkdir(parents=True, exist_ok=True)
egress_plan = Egress().prepare(bottle, slug, egress_dir) egress_plan = Egress().prepare(bottle, slug, egress_dir)
# Claude-code refuses to start without *something* it # Some provider CLIs refuse to start without *some* credential
# recognises as a credential. When the bottle has an egress # env var even when egress will strip + re-inject the real
# route carrying the `claude_code_oauth` role marker, egress # Authorization header. For those providers, auth_role names the
# strips + re-injects the real Authorization header on the # route marker that enables a non-secret placeholder env. Codex is
# outbound leg using a token held in egress's own environ — so # intentionally absent here: it should use its device/ChatGPT login
# the agent gets a non-secret placeholder here (matches the # state, and an OPENAI_API_KEY placeholder would force API-key auth.
# docker backend's forwarded_env logic in
# bot_bottle/backend/docker/prepare.py).
has_provider_auth = any( 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" guest_env[provider_runtime.placeholder_env] = "egress-placeholder"
if provider.template == "claude" and has_provider_auth: if provider.template == "claude" and has_provider_auth:
guest_env.setdefault("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC", "1") guest_env.setdefault("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC", "1")
+8 -4
View File
@@ -188,6 +188,11 @@ EGRESS_AUTH_SCHEMES = ("Bearer", "token")
# logic — declare the role on whichever route # logic — declare the role on whichever route
# injects the OAuth header. # 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 # Routes without a `role` are pure proxy entries: egress
# enforces path_allowlist + injects auth on its own, but nothing # enforces path_allowlist + injects auth on its own, but nothing
# special happens on the agent side. # special happens on the agent side.
@@ -196,10 +201,9 @@ EGRESS_ROLES = frozenset({
"codex_auth", "codex_auth",
}) })
# Singleton roles may appear on at most one route per bottle. # Singleton roles may appear on at most one route per bottle. Some
# claude_code_oauth drives a single placeholder env var; two routes # roles drive a single provider auth path; two routes claiming one
# claiming it would leave "which one is the canonical OAuth route?" # marker would leave "which one is canonical?" ambiguous.
# ambiguous for any future role-aware logic.
EGRESS_SINGLETON_ROLES = frozenset({ EGRESS_SINGLETON_ROLES = frozenset({
"claude_code_oauth", "claude_code_oauth",
"codex_auth", "codex_auth",
+3 -1
View File
@@ -86,7 +86,9 @@ agent_provider:
## Open questions ## 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. - Existing state-folder transcript capture is Claude-specific and should remain gated to Claude until the follow-up state/transcript refactor.
## References ## References
+23
View File
@@ -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()
+2 -2
View File
@@ -8,9 +8,9 @@ from bot_bottle.backend.print_util import visible_agent_env_names
class TestVisibleAgentEnvNames(unittest.TestCase): 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( self.assertEqual(
["CUSTOM"], ["CUSTOM", "OPENAI_API_KEY"],
visible_agent_env_names( visible_agent_env_names(
["OPENAI_API_KEY", "CUSTOM"], ["OPENAI_API_KEY", "CUSTOM"],
agent_provider_template="codex", agent_provider_template="codex",