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.
- DockerBottleBackend instantiates DockerCredProxy alongside pipelock
and git-gate; threads it through prepare and launch.
- DockerBottlePlan gains cred_proxy_plan; preflight rendering shows
the declared kinds + TokenRefs and to_dict emits a cred_proxy
array matching the routing table.
- prepare.py: when bottle.tokens has an anthropic entry, route the
agent at the proxy via ANTHROPIC_BASE_URL, drop the agent-side
CLAUDE_CODE_OAUTH_TOKEN forward (the token goes to the sidecar's
environ instead, set a non-secret placeholder so claude-code's
startup check passes), and default the telemetry-off env vars.
- launch.py: bring up the cred-proxy sidecar in ExitStack before the
agent container so DNS resolution for `cred-proxy` succeeds on the
agent's first call.
- backend/__init__.py: add provision_cred_proxy to the provision
template (runs after provision_git so it can append to ~/.gitconfig).
- bottle_plan _view: env_names is derived from the forwarded_env dict,
so the preflight reflects the PRD 0010 switch without ad-hoc
branching on spec.forward_oauth_token.
Delete claude_bottle/ssh_gate.py, the DockerSSHGate sidecar,
and the provision_ssh provisioner (~/.ssh/config + ssh-agent
wiring). Unwire the gate from the abstract BottleBackend
(provision orchestration drops the ssh step,
_validate_ssh_entries goes away) and from the Docker backend
(prepare/launch lose the `gate` kwarg, bottle_plan drops the
gate_plan field, dry-run JSON drops the ssh_hosts / ssh_gate
keys, y/N preflight drops the ssh-hosts block). cli/info now
prints declared git remotes instead of ssh hosts. pipelock's
docstring picks up the git-gate framing now that there's no
PRD-0007 boundary to call out.
BREAKING (dry-run JSON): the `ssh_hosts` and `ssh_gate` keys
are gone from `start --dry-run --format=json`. Consumers should
read `git_remotes` / `git_gate` instead.
DockerBottleBackend now instantiates a DockerGitGate alongside
DockerPipelockProxy and DockerSSHGate; the prepare step lifts
bottle.git into a GitGatePlan stored on DockerBottlePlan, and
launch starts/stops the sidecar in the same ExitStack as the
other two (only when bottle.git is non-empty).
bottle_plan.print now surfaces git remotes and per-upstream gate
forwards in the y/N preflight; to_dict adds git_remotes and
git_gate keys to the dry-run JSON payload for CLI consumers.
PRD: docs/prds/0008-git-gate.md
PRD 0007: thread the DockerSSHGate through the bottle lifecycle.
- DockerBottlePlan gains gate_plan: SSHGatePlan.
- prepare.resolve_plan accepts a gate and renders its entrypoint
script next to the pipelock yaml.
- launch.launch starts the gate sidecar after pipelock (so it's on
the same internal + egress networks) and registers its stop in
the ExitStack. Skipped when the bottle has no ssh entries.
- DockerBottleBackend instantiates DockerSSHGate alongside the
pipelock proxy.
- bottle_plan.print + to_dict surface the upstream table so
--dry-run shows the per-host listen-port mapping.
ssh_config provisioning still points at pipelock; that swap lands
in the next commit so this one stays a pure wiring change.
Move the resolution, bring-up, and orphan-cleanup logic out of
backend.py into three topic-named modules. DockerBottleBackend becomes
a thin façade that wires the per-instance pipelock proxy and the
provision orchestrator into the free functions.
backend.py drops from ~360 to ~70 lines and each topic now reads
end-to-end in one place. Mirrors the existing provision/ split.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>