refactor(cred_proxy): flat routes, role-driven provisioning (PRD 0010)
Replace bottle.tokens (with Kind enum and hardcoded per-kind
route/auth tables) with bottle.cred_proxy.routes — each route
declares its own path, upstream, auth_scheme, token_ref, and
optional role[]. The manifest is now the source of truth for the
proxy's runtime route table; adding an upstream is a manifest edit,
not a code change.
Agent-side rewrites move from per-kind dispatch to per-role tags
on routes:
anthropic-base-url -> set ANTHROPIC_BASE_URL=<proxy><path>
npm-registry -> write ~/.npmrc registry=
git-insteadof -> write ~/.gitconfig [url] insteadOf, keyed
off route.upstream (suppressed when
bottle.git brokers the same host)
tea-login -> add a ~/.config/tea/config.yml login
Roles are a list (string accepted as sugar). A gitea route
typically carries ["git-insteadof", "tea-login"]. Singleton roles
(anthropic-base-url, npm-registry) appear on at most one route.
token_env slots are assigned per distinct TokenRef in declaration
order — two routes sharing a token_ref (e.g. github API + git
endpoints) share a slot.
Drops: TOKEN_KINDS, _KIND_ROUTES, _KIND_AUTH_SCHEME, _TOKEN_DEFAULT_HOST,
cred_proxy_route_path_for_gitea, the kind field on CredProxyUpstream,
and the kind-based hardcoding in pipelock_token_hosts (now derives
from route.UpstreamHost).
Legacy bottle.tokens manifests now die with a hint pointing at
bottle.cred_proxy.routes + this PRD. Tests rewritten end-to-end.
Docs + example.json + the dev ~/claude-bottle.json updated to match.
This commit is contained in:
@@ -89,21 +89,32 @@ 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)
|
||||
has_anthropic_token = any(t.Kind == "anthropic" for t in bottle.tokens)
|
||||
if spec.forward_oauth_token and not has_anthropic_token:
|
||||
# Find the (at most one) cred-proxy route claiming the
|
||||
# anthropic-base-url role. Manifest validation enforces the
|
||||
# singleton constraint.
|
||||
anthropic_route = next(
|
||||
(u for u in cred_proxy_plan.upstreams if "anthropic-base-url" in u.roles),
|
||||
None,
|
||||
)
|
||||
if spec.forward_oauth_token and anthropic_route is None:
|
||||
# Pre-PRD 0010 behavior: agent reads CLAUDE_CODE_OAUTH_TOKEN
|
||||
# directly. Still the path when bottle.tokens has no anthropic
|
||||
# entry; the cred-proxy sidecar holds the token otherwise.
|
||||
# directly. Still the path when no cred_proxy.routes entry
|
||||
# is tagged anthropic-base-url; otherwise the sidecar holds
|
||||
# the token.
|
||||
forwarded_env["CLAUDE_CODE_OAUTH_TOKEN"] = os.environ["CLAUDE_BOTTLE_OAUTH_TOKEN"]
|
||||
if has_anthropic_token:
|
||||
if anthropic_route is not None:
|
||||
# Point claude-code at the cred-proxy. The sidecar holds the
|
||||
# OAuth token; the agent's environ does not.
|
||||
forwarded_env["ANTHROPIC_BASE_URL"] = f"{cred_proxy_url()}/anthropic"
|
||||
# OAuth token; the agent's environ does not. Strip the
|
||||
# trailing slash so claude-code's path-join produces e.g.
|
||||
# http://cred-proxy:9099/anthropic/v1/messages.
|
||||
forwarded_env["ANTHROPIC_BASE_URL"] = (
|
||||
f"{cred_proxy_url()}{anthropic_route.path}".rstrip("/")
|
||||
)
|
||||
# claude-code refuses to start without *some* credential in
|
||||
# its env. The proxy strips inbound Authorization on every
|
||||
# request and injects the real one — so a non-secret
|
||||
# placeholder is sufficient and the SC1 test still holds
|
||||
# (the placeholder is not a `bottle.tokens[].TokenRef`
|
||||
# (the placeholder is not a `cred_proxy.routes[].TokenRef`
|
||||
# value). The agent cannot exfiltrate this string because
|
||||
# it carries no meaning to api.anthropic.com.
|
||||
forwarded_env["CLAUDE_CODE_OAUTH_TOKEN"] = "cred-proxy-placeholder"
|
||||
|
||||
Reference in New Issue
Block a user