fix(codex): stop injecting api key placeholder
This commit is contained in:
@@ -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`)
|
||||
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user