Stop injecting Codex API-key placeholder #108

Merged
didericis merged 2 commits from codex/codex-device-auth-runtime into main 2026-05-29 02:48:16 -04:00
9 changed files with 85 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")
+10 -4
View File
@@ -188,6 +188,13 @@ 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: placeholder marker reserved for follow-up Codex
# credential-injection work. It is still accepted so
# existing manifests and future egress-held auth flows
# have a stable role name, but it no longer triggers an
# OPENAI_API_KEY placeholder. Codex bottles should prefer
# device/ChatGPT login state today.
#
# 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 +203,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",
+4 -1
View File
@@ -86,7 +86,10 @@ 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 as a placeholder marker for follow-up Codex
credential-injection work. 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",