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.
````
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`)
+2 -2
View File
@@ -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"),
+10 -10
View File
@@ -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,
+7 -7
View File
@@ -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})
+10 -10
View File
@@ -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")
+10 -4
View File
@@ -188,6 +188,13 @@ EGRESS_AUTH_SCHEMES = ("Bearer", "token")
# logic — declare the role on whichever route
# injects the OAuth header.
#
# codex_auth: placeholder marker reserved for follow-up Codex
didericis-codex marked this conversation as resolved Outdated
Outdated
Review

call this a placeholder marker rather than a legacy marker, and cite the reason for keeping it for follow up updates for better credential injection behavior

call this a placeholder marker rather than a legacy marker, and cite the reason for keeping it for follow up updates for better credential injection behavior
# 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
# enforces path_allowlist + injects auth on its own, but nothing
# special happens on the agent side.
@@ -196,10 +203,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",
+4 -1
View File
@@ -86,7 +86,10 @@ 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 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.
## 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):
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",