The agent's HTTP_PROXY env points at pipelock, so an
ANTHROPIC_BASE_URL like http://cred-proxy:9099/anthropic doesn't
short-circuit through Docker's embedded DNS — it gets forwarded
through pipelock, which then checks its api_allowlist for the
hostname `cred-proxy` and 403's because the name isn't there. The
agent surfaces the failure as "API Error: 403 blocked: domain not
in allowlist: cred-proxy" on Claude's first call.
Fix: pipelock_effective_allowlist auto-adds CRED_PROXY_HOSTNAME
when bottle.cred_proxy.routes is non-empty (i.e., when the
sidecar will actually be running and reachable).
Move CRED_PROXY_HOSTNAME from backend/docker/cred_proxy.py to the
backend-agnostic claude_bottle/cred_proxy.py so pipelock can
reference it without a layering violation; the docker concrete
imports it from the same place.
Three leftovers from the manifest refactor:
1. provision/cred_proxy.py:223 referenced u.kind == 'gitea' for the
tea login count — kind was removed from the runtime class, so any
bottle with a tea-login route raised AttributeError at provision
time. Switch to `'tea-login' in r.roles`.
2. The runtime class CredProxyUpstream is renamed to CredProxyRoute
(its data is a route on the proxy, not an "upstream"; the field
route.upstream is the upstream URL). Module's own naming now
aligns with manifest.CredProxyRoute and routes.json.
3. cred_proxy_upstreams_for_bottle -> cred_proxy_routes_for_bottle;
CredProxyPlan.upstreams -> CredProxyPlan.routes; local
`upstreams` collections become `routes`. Callers in
backend.py, launch.py, prepare.py, bottle_plan.py,
provision/cred_proxy.py, and tests updated.
Also strips lingering `bottle.tokens` references from docstrings
(pipelock.py, cred_proxy.py prepare(), manifest._parse_https_host,
test_pipelock_allowlist.py module doc) and removes dead helpers
from the integration test (the _bottle helper used a tokens field
that no longer parses).
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.
Three coupled fixes that close a documented bypass of git-gate's
gitleaks pre-receive hook:
1. cred-proxy refuses git smart-HTTP push at runtime. Any path
ending in /git-receive-pack or /info/refs?service=git-receive-pack
returns 403 with a pointer at the bottle.git SSH path. Fetch
(upload-pack) is still allowed — the bypass we're closing is
push, where gitleaks is the load-bearing scanner. Hard guarantee.
2. The provisioner suppresses the cred-proxy `~/.gitconfig` insteadOf
rewrite for any host already declared in bottle.git. git-gate is
the canonical git path there; we don't write a competing rule
that would let `git clone https://<host>/...` succeed in ways
that confuse on push. Defense in depth — (1) is the hard guarantee.
3. cred-proxy routes its outbound HTTPS through pipelock. The
sidecar's environ now sets HTTPS_PROXY=<pipelock-url>, and the
image's entrypoint runs `update-ca-certificates` over the
per-bottle pipelock CA (docker cp'd into
/usr/local/share/ca-certificates/pipelock.crt before start) so
the proxy's HTTPS client trusts pipelock's bumped certs.
Consequence: pipelock's allowlist + body scanner now sit in the
cred-proxy egress path the same way they sit in front of direct
agent traffic. The cred-proxy upstream hosts (api.github.com,
github.com, gitea hosts, registry.npmjs.org) come OFF
pipelock's passthrough_domains. Only api.anthropic.com remains
on passthrough (LLM body content legitimately trips DLP).
PRD 0010 updated to reflect all three. Tests adjusted: the
"cred-proxy hosts go on passthrough" assertion in
test_pipelock_allowlist flips to "they don't", a new
TestIsGitPushRequest exercises the smart-HTTP refusal predicate,
and the gitconfig renderer tests cover the per-host suppression
matrix.
bottle.tokens declarations contribute their upstream hosts to both
pipelock's allowlist (so cred-proxy can reach them) and
passthrough_domains (so pipelock doesn't MITM the connection —
cred-proxy validates real upstream certs with the system CA bundle).
Mapping: anthropic -> api.anthropic.com (already on defaults);
github -> api.github.com + github.com; gitea -> the entry's host;
npm -> registry.npmjs.org.
- Delete tests/unit/test_ssh_gate.py and the fixture_with_ssh helpers.
- test_pipelock_yaml: drop the ssh-leak guard (structurally
impossible now); the remaining tests switch to fixture_minimal.
- test_pipelock_allowlist: rewrite the union/dedup test to
exercise an egress.allowlist that duplicates a baked default
(the property the ssh-leak assertion was hitching onto).
- test_manifest_git: shadow-route assertion becomes a legacy-ssh-
dies-with-hint assertion, since bottle.ssh is now parse-fail.
- test_orphan_cleanup: drop the SSHGate.stop idempotency check;
pipelock equivalent stays.
- test_dry_run_plan: drop assertions on the removed ssh_hosts /
ssh_gate keys.
52 unit tests pass.
PRD 0007: SSH traffic now flows through the per-agent ssh-gate
sidecar, so pipelock should know nothing about bottle.ssh.
Removed:
- pipelock_bottle_ssh_hostnames, _trusted_domains, _ip_cidrs.
- The trusted_domains / ssrf blocks built from ssh entries.
- pipelock_proxy_host_port — its last caller (the ssh provisioner)
is gone.
- is_ipv4_literal — only used to classify ssh hostnames into
trusted_domains vs ssrf.ip_allowlist, both of which are gone.
api_allowlist now derives solely from baked-in defaults +
bottle.egress.allowlist. Tests updated to pin the new shape and
assert ssh hostnames do NOT leak into pipelock's config.
The four lower-level helpers (pipelock_bottle_allowlist,
pipelock_bottle_ssh_hostnames, pipelock_bottle_ssh_ip_cidrs,
pipelock_bottle_ssh_trusted_domains) are one-line filters; testing
each in isolation duplicates coverage that pipelock_effective_allowlist
already provides end-to-end. The /32 CIDR suffix is the only behavior
beyond filtering, so it keeps a tiny dedicated test.
Drops the misplaced test_rejects_non_string_entry — that's manifest
validation, not allowlist resolution. Belongs in a manifest-validation
test file (which doesn't exist yet); leaving for a separate PR rather
than adding a one-branch sample here.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the hand-maintained INTEGRATION_NAMES classifier (and the
bespoke run_tests.py around it) with a directory-driven split:
tests/unit/ unit tests, always run
tests/integration/ Docker-dependent, skip cleanly without Docker
tests/canaries/ upstream-regression checks, opt-in via
CLAUDE_BOTTLE_RUN_CANARIES=1
The pinned-pipelock-image check moves to the canary suite — it tests
upstream packaging, not our code, so it shouldn't gate every dev push.
A scheduled canaries.yml workflow runs it weekly.
The manifest-runtime tests collapse the four assertRaises cases for
distinct 'runtime' values into one subTest loop and drop the
error-message-wording assertions; the contract is "any value is
rejected", not "the error literally contains 'auto-detect'".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>