feat(manifest): add agent_provider.auth_token for Claude OAuth via egress

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
This commit is contained in:
2026-06-02 01:24:18 +00:00
committed by didericis
parent 952dcd7eec
commit de9bd7eb83
11 changed files with 136 additions and 113 deletions
+10 -14
View File
@@ -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:
+42 -31
View File
@@ -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([{
+4 -4
View File
@@ -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"}),
),
)