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.
|
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`)
|
||||||
|
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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})
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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):
|
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",
|
||||||
|
|||||||
Reference in New Issue
Block a user