feat(egress-proxy): drive claude-code OAuth placeholder off a role marker
test / unit (pull_request) Successful in 18s
test / integration (pull_request) Successful in 1m3s

The chunk 2 detection keyed on `token_ref == "CLAUDE_CODE_OAUTH_TOKEN"`,
which broke any bottle whose host env var has a different name (e.g.
`CLAUDE_BOTTLE_OAUTH_TOKEN`). The token_ref is the user's choice —
the placeholder-env trigger shouldn't be locked to one specific
string.

Restoring a minimal `role` marker on `EgressProxyRoute`:

  - `EGRESS_PROXY_ROLES = frozenset({"claude_code_oauth"})` — one
    marker for now; the field is back so we can grow it.
  - `EGRESS_PROXY_SINGLETON_ROLES` — claude_code_oauth is a
    singleton (only one route per bottle can carry it).
  - `Role: tuple[str, ...]` field on `EgressProxyRoute` (manifest +
    runtime), parsed as string or list-of-strings; unknown roles
    are rejected so typos can't become silent no-ops.

`prepare.py:has_anthropic_auth` now checks for `"claude_code_oauth"
in r.roles` instead of matching a literal token_ref string. Bottles
can name their host OAuth env var anything; the role marker is what
flips on `CLAUDE_CODE_OAUTH_TOKEN=<placeholder>` and the
telemetry-off env vars on the agent.

Test coverage: 7 new manifest tests (omitted / string / list /
unknown role rejected / non-string rejected / list-item non-string
rejected / singleton enforced).

364 tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 15:28:11 -04:00
parent 9cd583fbbb
commit f04fbb68a9
4 changed files with 146 additions and 11 deletions
+51
View File
@@ -128,6 +128,57 @@ class TestAuth(unittest.TestCase):
}])
class TestRole(unittest.TestCase):
def test_omitted_means_no_roles(self):
b = _bottle([{"host": "x.example"}])
self.assertEqual((), b.egress_proxy.routes[0].Role)
def test_string_normalizes_to_tuple(self):
b = _bottle([{
"host": "api.anthropic.com",
"role": "claude_code_oauth",
"auth": {"scheme": "Bearer", "token_ref": "T"},
}])
self.assertEqual(("claude_code_oauth",),
b.egress_proxy.routes[0].Role)
def test_list_supported(self):
b = _bottle([{
"host": "api.anthropic.com",
"role": ["claude_code_oauth"],
"auth": {"scheme": "Bearer", "token_ref": "T"},
}])
self.assertEqual(("claude_code_oauth",),
b.egress_proxy.routes[0].Role)
def test_unknown_role_rejected(self):
# The role enum is locked down — typos shouldn't silently
# become no-op markers.
with self.assertRaises(Die):
_bottle([{"host": "x.example", "role": "totally-made-up"}])
def test_non_string_role_rejected(self):
with self.assertRaises(Die):
_bottle([{"host": "x.example", "role": 42}])
def test_list_with_non_string_item_rejected(self):
with self.assertRaises(Die):
_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(Die):
_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"}},
])
class TestRouteValidation(unittest.TestCase):
def test_duplicate_hosts_rejected(self):
# Routes match by exact host; duplicates leave the choice