From de9bd7eb83fe47ce01efa9ec8ef516d5242f6e76 Mon Sep 17 00:00:00 2001 From: claude Date: Tue, 2 Jun 2026 01:24:18 +0000 Subject: [PATCH] feat(manifest): add agent_provider.auth_token for Claude OAuth via egress MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operators can now declare: agent_provider: template: claude auth_token: BOT_BOTTLE_CLAUDE_OAUTH_TOKEN and the provisioner injects a provider-owned api.anthropic.com egress route (Bearer, tls_passthrough) rather than requiring a manually declared route with the former claude_code_oauth role. Changes: - Add auth_token field to AgentProvider; validate claude-only. - Remove claude_code_oauth from EGRESS_ROLES / PROVIDER_EGRESS_ROLES. Manifests that declare the role now fail at parse time with "unknown role" — the provisioner owns the route. - agent_provision_plan: replace manifest_egress_routes/has_provider_auth with auth_token; Claude branch injects the api.anthropic.com route, placeholder env, and nonessential-traffic flags when auth_token is set. - Add hidden_env_names: frozenset[str] to AgentProvisionPlan; Claude branch populates it with CLAUDE_CODE_OAUTH_TOKEN. - Remove auth_role from AgentProviderRuntime and placeholder_env_for(). - print_util.visible_agent_env_names: accept hidden_env_names from the plan instead of dispatching on agent_provider_template. - Both backends: drop manifest_egress_routes call, pass auth_token. - PRD 0029 rescoped to cover both Codex and Claude provider auth. Assisted-by: Claude Code --- bot_bottle/agent_provider.py | 32 ++++---- bot_bottle/backend/docker/bottle_plan.py | 2 +- bot_bottle/backend/docker/prepare.py | 4 +- bot_bottle/backend/print_util.py | 16 ++-- .../backend/smolmachines/bottle_plan.py | 2 +- bot_bottle/backend/smolmachines/prepare.py | 3 +- bot_bottle/manifest.py | 61 +++++++++------- .../0029-codex-host-credentials-egress.md | 24 ++++-- tests/unit/test_agent_provider.py | 24 +++--- tests/unit/test_manifest_egress.py | 73 +++++++++++-------- tests/unit/test_print_util.py | 8 +- 11 files changed, 136 insertions(+), 113 deletions(-) diff --git a/bot_bottle/agent_provider.py b/bot_bottle/agent_provider.py index e438ff2..32874b6 100644 --- a/bot_bottle/agent_provider.py +++ b/bot_bottle/agent_provider.py @@ -33,7 +33,6 @@ class AgentProviderRuntime: command: str image: str dockerfile: str - auth_role: str prompt_mode: PromptMode bypass_args: tuple[str, ...] resume_args: tuple[str, ...] @@ -73,6 +72,11 @@ class AgentProvisionPlan: pass to `Egress.prepare` and `PipelockProxy.prepare`. This keeps provider logic out of the egress and pipelock modules — they merge provider routes generically without knowing the provider type. + + `hidden_env_names` is the set of env var names the provider injected + as non-secret placeholders. `print_util.visible_agent_env_names` uses + this to suppress them from the preflight summary so operators don't + mistake them for real credentials. """ template: str @@ -87,6 +91,7 @@ class AgentProvisionPlan: pre_copy: tuple[AgentProvisionCommand, ...] = () verify: tuple[AgentProvisionCommand, ...] = () egress_routes: tuple[EgressRoute, ...] = () + hidden_env_names: frozenset[str] = field(default_factory=frozenset) _REPO_ROOT = Path(__file__).resolve().parent.parent @@ -98,7 +103,6 @@ _RUNTIMES = { command="claude", image="bot-bottle-claude:latest", dockerfile=str(_REPO_ROOT / "Dockerfile.claude"), - auth_role="claude_code_oauth", prompt_mode="append_file", bypass_args=("--dangerously-skip-permissions",), resume_args=("--continue",), @@ -109,7 +113,6 @@ _RUNTIMES = { command="codex", image="bot-bottle-codex:latest", dockerfile=str(_REPO_ROOT / "Dockerfile.codex"), - auth_role="", prompt_mode="read_prompt_file", bypass_args=("--dangerously-bypass-approvals-and-sandbox",), resume_args=("resume", "--last"), @@ -122,13 +125,6 @@ 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, @@ -136,14 +132,11 @@ def agent_provision_plan( state_dir: Path, guest_home: str = "/home/node", guest_env: dict[str, str] | None = None, + auth_token: str = "", forward_host_credentials: 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] = [] @@ -151,6 +144,7 @@ def agent_provision_plan( pre_copy: list[AgentProvisionCommand] = [] verify: list[AgentProvisionCommand] = [] egress_routes: list[EgressRoute] = [] + hidden_env_names: frozenset[str] = frozenset() if template == PROVIDER_CODEX: env_vars["CODEX_CA_CERTIFICATE"] = "/etc/ssl/certs/ca-certificates.crt" @@ -199,10 +193,17 @@ def agent_provision_plan( "codex host credentials: dummy auth was copied into the " "guest, but Codex did not accept it" ))) - if template == PROVIDER_CLAUDE and has_provider_auth: + if template == PROVIDER_CLAUDE and auth_token: + egress_routes.append(EgressRoute( + host="api.anthropic.com", + auth_scheme="Bearer", + token_ref=auth_token, + tls_passthrough=True, + )) env_vars["CLAUDE_CODE_OAUTH_TOKEN"] = "egress-placeholder" env_vars["CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC"] = "1" env_vars["DISABLE_ERROR_REPORTING"] = "1" + hidden_env_names = frozenset({"CLAUDE_CODE_OAUTH_TOKEN"}) return AgentProvisionPlan( template=template, @@ -217,6 +218,7 @@ def agent_provision_plan( pre_copy=tuple(pre_copy), verify=tuple(verify), egress_routes=tuple(egress_routes), + hidden_env_names=hidden_env_names, ) diff --git a/bot_bottle/backend/docker/bottle_plan.py b/bot_bottle/backend/docker/bottle_plan.py index b461e6c..e6240f1 100644 --- a/bot_bottle/backend/docker/bottle_plan.py +++ b/bot_bottle/backend/docker/bottle_plan.py @@ -89,7 +89,7 @@ class DockerBottlePlan(BottlePlan): | set(self.forwarded_env.keys()) | set(self.agent_provision.guest_env.keys()) ), - agent_provider_template=self.agent_provider_template, + hidden_env_names=self.agent_provision.hidden_env_names, ) print(file=sys.stderr) diff --git a/bot_bottle/backend/docker/prepare.py b/bot_bottle/backend/docker/prepare.py index 9a41825..3ce9c83 100644 --- a/bot_bottle/backend/docker/prepare.py +++ b/bot_bottle/backend/docker/prepare.py @@ -16,7 +16,7 @@ from dataclasses import replace from pathlib import Path from ...agent_provider import agent_provision_plan, runtime_for -from ...egress import Egress, egress_manifest_routes +from ...egress import Egress from ...env import ResolvedEnv, resolve_env from ...git_gate import GitGate from ...log import die @@ -178,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, - manifest_egress_routes=egress_manifest_routes(bottle), + auth_token=provider.auth_token, 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 3fe931a..4b4ec3d 100644 --- a/bot_bottle/backend/print_util.py +++ b/bot_bottle/backend/print_util.py @@ -9,7 +9,6 @@ from __future__ import annotations from typing import Sequence -from ..agent_provider import placeholder_env_for from ..log import info @@ -30,16 +29,13 @@ def print_multi(label: str, values: Sequence[str]) -> None: def visible_agent_env_names( - env_names: Sequence[str], *, agent_provider_template: str, + env_names: Sequence[str], *, hidden_env_names: frozenset[str], ) -> list[str]: """Env names worth showing in launch summaries. - 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. + Provider-injected placeholder env vars are implementation details: + they are non-secret dummy values that satisfy provider CLIs while + egress injects the real Authorization header. The plan's + `hidden_env_names` carries exactly which names to suppress. """ - hidden = {placeholder_env_for(agent_provider_template)} - return sorted({name for name in env_names if name and name not in hidden}) + return sorted({name for name in env_names if name and name not in hidden_env_names}) diff --git a/bot_bottle/backend/smolmachines/bottle_plan.py b/bot_bottle/backend/smolmachines/bottle_plan.py index d89205d..84ef5d4 100644 --- a/bot_bottle/backend/smolmachines/bottle_plan.py +++ b/bot_bottle/backend/smolmachines/bottle_plan.py @@ -125,7 +125,7 @@ class SmolmachinesBottlePlan(BottlePlan): set(bottle.env.keys()) | set(self.agent_provision.guest_env.keys()) ), - agent_provider_template=self.agent_provider_template, + hidden_env_names=self.agent_provision.hidden_env_names, ) upstreams = [ f"{g.Name} → {g.Upstream}" for g in bottle.git diff --git a/bot_bottle/backend/smolmachines/prepare.py b/bot_bottle/backend/smolmachines/prepare.py index 7f1777b..4746316 100644 --- a/bot_bottle/backend/smolmachines/prepare.py +++ b/bot_bottle/backend/smolmachines/prepare.py @@ -16,7 +16,6 @@ from dataclasses import replace from pathlib import Path from ...agent_provider import agent_provision_plan, runtime_for -from ...egress import egress_manifest_routes from ...backend import BottleSpec from ...backend.docker.bottle_state import ( BottleMetadata, @@ -134,7 +133,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, - manifest_egress_routes=egress_manifest_routes(bottle), + auth_token=provider.auth_token, host_env=dict(os.environ), ) merged_guest_env = dict(agent_provision.guest_env) diff --git a/bot_bottle/manifest.py b/bot_bottle/manifest.py index 9f69369..38d3386 100644 --- a/bot_bottle/manifest.py +++ b/bot_bottle/manifest.py @@ -179,40 +179,28 @@ EGRESS_AUTH_SCHEMES = ("Bearer", "token") # a specific named part in the bottle's auth flow"; the launch step # acts on the marker. # -# claude_code_oauth: this route auth-injects on the agent's -# claude-code OAuth flow. Triggers prepare.py -# to ship a placeholder CLAUDE_CODE_OAUTH_TOKEN -# to the agent (so claude-code starts) and -# disable nonessential-traffic / error-reporting -# env vars. Host doesn't matter to the placeholder -# logic — declare the role on whichever route -# 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. +# codex_auth: placeholder marker for Codex egress-held auth flows. +# Accepted on Codex routes for forward-compatibility; +# the provisioner does not act on it 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. +# +# Note: the former `claude_code_oauth` role has been removed. Claude +# OAuth is now provisioner-owned via `agent_provider.auth_token`; the +# provisioner injects the api.anthropic.com route automatically. EGRESS_ROLES = frozenset({ - "claude_code_oauth", "codex_auth", }) -# 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. +# Singleton roles may appear on at most one route per bottle. EGRESS_SINGLETON_ROLES = frozenset({ - "claude_code_oauth", "codex_auth", }) PROVIDER_EGRESS_ROLES = { - "claude": frozenset({"claude_code_oauth"}), + "claude": frozenset(), "codex": frozenset({"codex_auth"}), } @@ -224,20 +212,30 @@ class AgentProvider: `template` selects a built-in launch/runtime contract. `dockerfile` optionally points at a custom agent-image Dockerfile while leaving bot-bottle's sidecar infrastructure intact. + + `auth_token` names the host env var that holds the provider's OAuth + token (Claude only). The provisioner injects a provider-owned egress + route for api.anthropic.com that re-injects this token as the Bearer + header, and sets a placeholder CLAUDE_CODE_OAUTH_TOKEN in the agent + so the Claude Code CLI starts. + + `forward_host_credentials` forwards the host Codex auth token into + the egress sidecar (Codex only). """ template: str = "claude" dockerfile: str = "" + auth_token: str = "" forward_host_credentials: bool = False @classmethod def from_dict(cls, bottle_name: str, raw: object) -> "AgentProvider": d = _as_json_object(raw, f"bottle '{bottle_name}' agent_provider") for k in d: - if k not in {"template", "dockerfile", "forward_host_credentials"}: + if k not in {"template", "dockerfile", "auth_token", "forward_host_credentials"}: raise ManifestError( f"bottle '{bottle_name}' agent_provider has unknown key {k!r}; " - f"allowed: template, dockerfile, forward_host_credentials" + f"allowed: template, dockerfile, auth_token, forward_host_credentials" ) template = d.get("template", "claude") if not isinstance(template, str) or not template: @@ -256,6 +254,17 @@ class AgentProvider: f"bottle '{bottle_name}' agent_provider.dockerfile must be a " f"string (was {type(dockerfile).__name__})" ) + auth_token = d.get("auth_token", "") + if not isinstance(auth_token, str): + raise ManifestError( + f"bottle '{bottle_name}' agent_provider.auth_token must be a " + f"string (was {type(auth_token).__name__})" + ) + if auth_token and template != "claude": + raise ManifestError( + f"bottle '{bottle_name}' agent_provider.auth_token is only " + f"supported for template 'claude'" + ) forward_host_credentials = d.get("forward_host_credentials", False) if not isinstance(forward_host_credentials, bool): raise ManifestError( @@ -270,6 +279,7 @@ class AgentProvider: return cls( template=template, dockerfile=dockerfile, + auth_token=auth_token, forward_host_credentials=forward_host_credentials, ) @@ -428,10 +438,7 @@ class EgressRoute: manifest's `auth` block is omitted both fields are empty strings — no Authorization is written, no token forwarded. - `Role` is an optional tuple of named markers (see - EGRESS_ROLES). The launch step reads these and triggers - associated side effects (e.g. the `claude_code_oauth` marker - causes prepare.py to set a placeholder OAuth env on the agent). + `Role` is an optional tuple of named markers (see EGRESS_ROLES). Validation rules (enforced in `from_dict`): - `host` required, non-empty. diff --git a/docs/prds/0029-codex-host-credentials-egress.md b/docs/prds/0029-codex-host-credentials-egress.md index 7c8400d..f3f6a22 100644 --- a/docs/prds/0029-codex-host-credentials-egress.md +++ b/docs/prds/0029-codex-host-credentials-egress.md @@ -1,4 +1,4 @@ -# PRD 0029: Codex host credentials through egress +# PRD 0029: Provider auth credentials through egress - **Status:** Draft - **Author:** didericis-codex @@ -7,9 +7,12 @@ ## Summary -Allow Codex bottles to use a host-authorized ChatGPT/device-login -access token by forwarding it only into the egress sidecar, gated by an -explicit `agent_provider.forward_host_credentials` manifest flag. +Allow provider bottles to inject host credentials into the egress +sidecar without exposing them to the agent. Codex uses +`agent_provider.forward_host_credentials` for ChatGPT/device-login +access tokens. Claude uses `agent_provider.auth_token` to name the host +env var holding its OAuth token, which egress injects on +`api.anthropic.com` requests. ## Problem @@ -51,8 +54,8 @@ possible, not in the agent. current access token at launch; operators can restart after host Codex refreshes auth. - Copying host `~/.codex/auth.json` credentials into the agent. -- Allowing arbitrary host credential forwarding. This PRD covers Codex - ChatGPT/device-login credentials only. +- Allowing arbitrary host credential forwarding beyond the two providers + covered here (Codex ChatGPT/device-login and Claude OAuth). - Hot-applying new authenticated Codex routes to an existing running sidecar. The current hot-apply path cannot safely populate new token env slots in an already-running container. @@ -64,6 +67,15 @@ possible, not in the agent. - Add `agent_provider.forward_host_credentials` to the bottle manifest schema, defaulting to `false`. - Support the flag for `agent_provider.template: codex`. +- Add `agent_provider.auth_token` to the bottle manifest schema. +- Support the field for `agent_provider.template: claude`: the named + host env var is forwarded only into the egress sidecar as the Bearer + token for `api.anthropic.com`, and a placeholder + `CLAUDE_CODE_OAUTH_TOKEN` is set in the agent so the Claude Code CLI + starts without a real credential. +- Remove the `claude_code_oauth` egress route role, which previously + required operators to declare the OAuth route manually. The provisioner + now injects it from `auth_token`. - Read host Codex auth from `$CODEX_HOME/auth.json` when `CODEX_HOME` is set, otherwise from `~/.codex/auth.json`. - Extract only `tokens.access_token` for egress injection. diff --git a/tests/unit/test_agent_provider.py b/tests/unit/test_agent_provider.py index eb64232..5b333fb 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, EgressRoute +from bot_bottle.egress import CODEX_HOST_CREDENTIAL_TOKEN_REF def _jwt(exp: int) -> str: @@ -24,14 +24,6 @@ def _jwt(exp: int) -> str: class TestAgentProviderRuntime(unittest.TestCase): - def test_claude_has_auth_role(self): - runtime = runtime_for("claude") - self.assertEqual("claude_code_oauth", runtime.auth_role) - - def test_codex_has_no_auth_role(self): - runtime = runtime_for("codex") - self.assertEqual("", runtime.auth_role) - def test_codex_plan_declares_home_state(self): with tempfile.TemporaryDirectory(prefix="bb-provider.") as tmp: plan = agent_provision_plan( @@ -79,20 +71,24 @@ 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_sets_placeholder_and_disables_nonessential_traffic(self): + def test_claude_with_auth_token_injects_provider_route_and_placeholder(self): with tempfile.TemporaryDirectory(prefix="bb-provider.") as tmp: plan = agent_provision_plan( template="claude", dockerfile="/tmp/Dockerfile.claude", state_dir=Path(tmp), - manifest_egress_routes=(EgressRoute( - host="api.anthropic.com", - roles=("claude_code_oauth",), - ),), + auth_token="BOT_BOTTLE_CLAUDE_OAUTH_TOKEN", ) + self.assertEqual(1, len(plan.egress_routes)) + route = plan.egress_routes[0] + self.assertEqual("api.anthropic.com", route.host) + self.assertEqual("Bearer", route.auth_scheme) + self.assertEqual("BOT_BOTTLE_CLAUDE_OAUTH_TOKEN", route.token_ref) + self.assertTrue(route.tls_passthrough) 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"]) + self.assertEqual(frozenset({"CLAUDE_CODE_OAUTH_TOKEN"}), plan.hidden_env_names) def test_codex_forward_host_credentials_populates_egress_routes(self): with tempfile.TemporaryDirectory(prefix="bb-provider.") as tmp: diff --git a/tests/unit/test_manifest_egress.py b/tests/unit/test_manifest_egress.py index 4d82c6b..1a03d0b 100644 --- a/tests/unit/test_manifest_egress.py +++ b/tests/unit/test_manifest_egress.py @@ -85,6 +85,31 @@ class TestAgentProviderHostCredentials(unittest.TestCase): "forward_host_credentials": True, }) + def test_auth_token_defaults_empty(self): + b = _provider_config_bottle({"template": "claude"}) + self.assertEqual("", b.agent_provider.auth_token) + + def test_auth_token_allowed_for_claude(self): + b = _provider_config_bottle({ + "template": "claude", + "auth_token": "BOT_BOTTLE_CLAUDE_OAUTH_TOKEN", + }) + self.assertEqual("BOT_BOTTLE_CLAUDE_OAUTH_TOKEN", b.agent_provider.auth_token) + + def test_auth_token_must_be_string(self): + with self.assertRaises(ManifestError): + _provider_config_bottle({ + "template": "claude", + "auth_token": 42, + }) + + def test_auth_token_rejected_for_codex(self): + with self.assertRaises(ManifestError): + _provider_config_bottle({ + "template": "codex", + "auth_token": "SOME_TOKEN", + }) + class TestPathAllowlist(unittest.TestCase): def test_optional(self): @@ -179,22 +204,20 @@ class TestRole(unittest.TestCase): self.assertEqual((), b.egress.routes[0].Role) def test_string_normalizes_to_tuple(self): - b = _bottle([{ - "host": "api.anthropic.com", - "role": "claude_code_oauth", + b = _provider_bottle("codex", [{ + "host": "api.openai.com", + "role": "codex_auth", "auth": {"scheme": "Bearer", "token_ref": "T"}, }]) - self.assertEqual(("claude_code_oauth",), - b.egress.routes[0].Role) + self.assertEqual(("codex_auth",), b.egress.routes[0].Role) def test_list_supported(self): - b = _bottle([{ - "host": "api.anthropic.com", - "role": ["claude_code_oauth"], + b = _provider_bottle("codex", [{ + "host": "api.openai.com", + "role": ["codex_auth"], "auth": {"scheme": "Bearer", "token_ref": "T"}, }]) - self.assertEqual(("claude_code_oauth",), - b.egress.routes[0].Role) + self.assertEqual(("codex_auth",), b.egress.routes[0].Role) def test_unknown_role_rejected(self): # The role enum is locked down — typos shouldn't silently @@ -202,6 +225,14 @@ class TestRole(unittest.TestCase): with self.assertRaises(ManifestError): _bottle([{"host": "x.example", "role": "totally-made-up"}]) + def test_claude_code_oauth_role_rejected(self): + # claude_code_oauth was removed; provisioner injects the route + # automatically via agent_provider.auth_token. + with self.assertRaises(ManifestError): + _bottle([{"host": "api.anthropic.com", + "role": "claude_code_oauth", + "auth": {"scheme": "Bearer", "token_ref": "T"}}]) + def test_non_string_role_rejected(self): with self.assertRaises(ManifestError): _bottle([{"host": "x.example", "role": 42}]) @@ -209,19 +240,7 @@ class TestRole(unittest.TestCase): def test_list_with_non_string_item_rejected(self): with self.assertRaises(ManifestError): _bottle([{"host": "x.example", - "role": ["claude_code_oauth", 42]}]) - - def test_singleton_claude_code_oauth_enforced(self): - # Two routes both claiming the role would make "which one - # drives the placeholder env?" ambiguous. - with self.assertRaises(ManifestError): - _bottle([ - {"host": "api.anthropic.com", "role": "claude_code_oauth", - "auth": {"scheme": "Bearer", "token_ref": "T1"}}, - {"host": "api2.anthropic.example", - "role": "claude_code_oauth", - "auth": {"scheme": "Bearer", "token_ref": "T2"}}, - ]) + "role": ["codex_auth", 42]}]) def test_codex_auth_role_allowed_for_codex_provider(self): b = _provider_bottle("codex", [{ @@ -231,14 +250,6 @@ class TestRole(unittest.TestCase): }]) self.assertEqual(("codex_auth",), b.egress.routes[0].Role) - def test_claude_role_rejected_for_codex_provider(self): - with self.assertRaises(ManifestError): - _provider_bottle("codex", [{ - "host": "api.anthropic.com", - "role": "claude_code_oauth", - "auth": {"scheme": "Bearer", "token_ref": "T"}, - }]) - def test_codex_role_rejected_for_default_claude_provider(self): with self.assertRaises(ManifestError): _bottle([{ diff --git a/tests/unit/test_print_util.py b/tests/unit/test_print_util.py index d4bea06..a09a91c 100644 --- a/tests/unit/test_print_util.py +++ b/tests/unit/test_print_util.py @@ -8,21 +8,21 @@ from bot_bottle.backend.print_util import visible_agent_env_names class TestVisibleAgentEnvNames(unittest.TestCase): - def test_codex_shows_openai_api_key_if_user_declares_it(self): + def test_shows_all_when_no_hidden_names(self): self.assertEqual( ["CUSTOM", "OPENAI_API_KEY"], visible_agent_env_names( ["OPENAI_API_KEY", "CUSTOM"], - agent_provider_template="codex", + hidden_env_names=frozenset(), ), ) - def test_hides_only_active_provider_placeholder(self): + def test_hides_provider_placeholder(self): self.assertEqual( ["CUSTOM", "OPENAI_API_KEY"], visible_agent_env_names( ["CLAUDE_CODE_OAUTH_TOKEN", "OPENAI_API_KEY", "CUSTOM"], - agent_provider_template="claude", + hidden_env_names=frozenset({"CLAUDE_CODE_OAUTH_TOKEN"}), ), )