diff --git a/bot_bottle/agent_provider.py b/bot_bottle/agent_provider.py index b0e538e..e438ff2 100644 --- a/bot_bottle/agent_provider.py +++ b/bot_bottle/agent_provider.py @@ -34,7 +34,6 @@ class AgentProviderRuntime: image: str dockerfile: str auth_role: str - placeholder_env: str prompt_mode: PromptMode bypass_args: tuple[str, ...] resume_args: tuple[str, ...] @@ -100,7 +99,6 @@ _RUNTIMES = { image="bot-bottle-claude:latest", dockerfile=str(_REPO_ROOT / "Dockerfile.claude"), auth_role="claude_code_oauth", - placeholder_env="CLAUDE_CODE_OAUTH_TOKEN", prompt_mode="append_file", bypass_args=("--dangerously-skip-permissions",), resume_args=("--continue",), @@ -112,7 +110,6 @@ _RUNTIMES = { image="bot-bottle-codex:latest", dockerfile=str(_REPO_ROOT / "Dockerfile.codex"), auth_role="", - placeholder_env="", prompt_mode="read_prompt_file", bypass_args=("--dangerously-bypass-approvals-and-sandbox",), resume_args=("resume", "--last"), @@ -125,6 +122,13 @@ def runtime_for(template: str) -> AgentProviderRuntime: return _RUNTIMES[template] +def placeholder_env_for(template: str) -> str: + """Return the provider auth placeholder env var name, or empty string.""" + if template == PROVIDER_CLAUDE: + return "CLAUDE_CODE_OAUTH_TOKEN" + return "" + + def agent_provision_plan( *, template: str, @@ -133,10 +137,13 @@ def agent_provision_plan( guest_home: str = "/home/node", guest_env: dict[str, str] | None = None, forward_host_credentials: bool = False, - has_provider_auth: bool = False, + manifest_egress_routes: tuple[EgressRoute, ...] = (), host_env: dict[str, str] | None = None, ) -> AgentProvisionPlan: runtime = runtime_for(template) + has_provider_auth = bool(runtime.auth_role) and any( + runtime.auth_role in r.roles for r in manifest_egress_routes + ) resolved_guest_env = dict(guest_env or {}) env_vars: dict[str, str] = {} dirs: list[AgentProvisionDir] = [] @@ -193,6 +200,7 @@ def agent_provision_plan( "guest, but Codex did not accept it" ))) if template == PROVIDER_CLAUDE and has_provider_auth: + env_vars["CLAUDE_CODE_OAUTH_TOKEN"] = "egress-placeholder" env_vars["CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC"] = "1" env_vars["DISABLE_ERROR_REPORTING"] = "1" diff --git a/bot_bottle/backend/docker/prepare.py b/bot_bottle/backend/docker/prepare.py index 201f699..9a41825 100644 --- a/bot_bottle/backend/docker/prepare.py +++ b/bot_bottle/backend/docker/prepare.py @@ -168,19 +168,6 @@ 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) - # 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 - and provider_runtime.auth_role in r.roles - for r in egress_manifest_routes(bottle) - ) - if has_provider_auth and provider_runtime.placeholder_env: - forwarded_env[provider_runtime.placeholder_env] = "egress-placeholder" _write_env_file(resolved, env_file) prompt_file.write_text(agent.prompt) @@ -191,7 +178,7 @@ def resolve_plan( state_dir=agent_dir, guest_home=os.environ.get("BOT_BOTTLE_CONTAINER_HOME", "/home/node"), forward_host_credentials=provider.forward_host_credentials, - has_provider_auth=has_provider_auth, + manifest_egress_routes=egress_manifest_routes(bottle), host_env=dict(os.environ), ) guest_env = dict(agent_provision.guest_env) diff --git a/bot_bottle/backend/print_util.py b/bot_bottle/backend/print_util.py index 5e277c8..3fe931a 100644 --- a/bot_bottle/backend/print_util.py +++ b/bot_bottle/backend/print_util.py @@ -9,7 +9,7 @@ from __future__ import annotations from typing import Sequence -from ..agent_provider import runtime_for +from ..agent_provider import placeholder_env_for from ..log import info @@ -41,5 +41,5 @@ def visible_agent_env_names( think a real key is entering the agent, so hide only the active provider-owned placeholder. """ - hidden = {runtime_for(agent_provider_template).placeholder_env} + hidden = {placeholder_env_for(agent_provider_template)} return sorted({name for name in env_names if name and name not in hidden}) diff --git a/bot_bottle/backend/smolmachines/prepare.py b/bot_bottle/backend/smolmachines/prepare.py index 447a927..7f1777b 100644 --- a/bot_bottle/backend/smolmachines/prepare.py +++ b/bot_bottle/backend/smolmachines/prepare.py @@ -100,20 +100,6 @@ def resolve_plan( git_gate_dir.mkdir(parents=True, exist_ok=True) git_gate_plan = GitGate().prepare(bottle, slug, git_gate_dir) - # 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 - and provider_runtime.auth_role in r.roles - for r in egress_manifest_routes(bottle) - ) - if has_provider_auth and provider_runtime.placeholder_env: - guest_env[provider_runtime.placeholder_env] = "egress-placeholder" - # Prompt file is always written (mode 0o600) so the in-VM # path always exists. Content is the agent's `prompt` # field (markdown body) — empty for agents with no prompt. @@ -148,7 +134,7 @@ def resolve_plan( guest_home=os.environ.get("BOT_BOTTLE_GUEST_HOME", "/home/node"), guest_env=guest_env, forward_host_credentials=provider.forward_host_credentials, - has_provider_auth=has_provider_auth, + manifest_egress_routes=egress_manifest_routes(bottle), host_env=dict(os.environ), ) merged_guest_env = dict(agent_provision.guest_env) diff --git a/tests/unit/test_agent_provider.py b/tests/unit/test_agent_provider.py index e8905f2..eb64232 100644 --- a/tests/unit/test_agent_provider.py +++ b/tests/unit/test_agent_provider.py @@ -13,7 +13,7 @@ from bot_bottle.agent_provider import ( agent_provision_plan, runtime_for, ) -from bot_bottle.egress import CODEX_HOST_CREDENTIAL_TOKEN_REF +from bot_bottle.egress import CODEX_HOST_CREDENTIAL_TOKEN_REF, EgressRoute def _jwt(exp: int) -> str: @@ -24,15 +24,13 @@ def _jwt(exp: int) -> str: class TestAgentProviderRuntime(unittest.TestCase): - def test_claude_keeps_oauth_placeholder(self): + def test_claude_has_auth_role(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): + def test_codex_has_no_auth_role(self): runtime = runtime_for("codex") self.assertEqual("", runtime.auth_role) - self.assertEqual("", runtime.placeholder_env) def test_codex_plan_declares_home_state(self): with tempfile.TemporaryDirectory(prefix="bb-provider.") as tmp: @@ -81,18 +79,19 @@ class TestAgentProviderRuntime(unittest.TestCase): self.assertEqual(1, len(plan.verify)) self.assertIn("CODEX_HOME=/run/codex-home", plan.verify[0].argv) - def test_claude_with_provider_auth_disables_nonessential_traffic(self): + def test_claude_with_provider_auth_sets_placeholder_and_disables_nonessential_traffic(self): with tempfile.TemporaryDirectory(prefix="bb-provider.") as tmp: plan = agent_provision_plan( template="claude", dockerfile="/tmp/Dockerfile.claude", state_dir=Path(tmp), - has_provider_auth=True, + manifest_egress_routes=(EgressRoute( + host="api.anthropic.com", + roles=("claude_code_oauth",), + ),), ) - self.assertEqual( - "1", - plan.env_vars["CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC"], - ) + self.assertEqual("egress-placeholder", plan.env_vars["CLAUDE_CODE_OAUTH_TOKEN"]) + self.assertEqual("1", plan.env_vars["CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC"]) self.assertEqual("1", plan.env_vars["DISABLE_ERROR_REPORTING"]) def test_codex_forward_host_credentials_populates_egress_routes(self):