The previous provisioner wrote ~/.claude/settings.json with an
mcpServers entry — but claude-code doesn't read its mcpServers from
that path. Inside a bottle, /mcp showed "No MCP servers configured"
even though the sidecar was running.
Switch to the official `claude mcp add` command run via docker exec:
docker exec -u node <agent> \
claude mcp add --scope user --transport http supervise <url>
claude-code owns its config file format (~/.claude.json shape, key
names, scope semantics) and has changed it between versions. The
official command writes to the right place in the right shape for
whatever version is installed.
Failure is logged but not fatal — the bottle still works; you just
have to register the server manually with the command surfaced in
the warning. Worst case is a bad agent claude-code version, not a
bad bottle.
To fix an already-running bottle without restarting, the user can
run the same `docker exec` command directly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The supervise sidecar (PRD 0013) has been serving MCP at
http://supervise:9100/ since it landed, but the in-bottle Claude
Code had no `.mcp.json` or settings pointing there — so the agent
couldn't actually call cred-proxy-block / pipelock-block /
capability-block as tools. To exercise the flow you had to curl
the sidecar from a sibling container.
This closes that last mile.
- claude_bottle/backend/docker/provision/supervise.py (new):
provision_supervise(plan, target) writes
~/.claude/settings.json into the running agent container with an
mcpServers.supervise entry of type http pointing at the
per-bottle sidecar. No-op when bottle.supervise is False.
- BottleBackend.provision orchestrator gains provision_supervise as
the last step (after CA, prompt, skills, git, cred-proxy). Default
impl is a no-op so non-Docker backends aren't forced to implement it.
- DockerBottleBackend wires it through to the new module.
- Test covers the rendered settings shape so a future regression in
the MCP entry format would surface in unit-level CI.
To test the full flow end-to-end now:
./cli.py start <agent> --cwd # agent's claude sees supervise
# agent calls cred-proxy-block via MCP
./cli.py dashboard # approve
./cli.py resume <identity> # restart with new capabilities
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.
provision_cred_proxy(plan, target) drops:
- ~/.npmrc with registry= pointing at /npm/ on the proxy
- ~/.gitconfig insteadOf rules for github (https://github.com/) and
per-gitea hosts, appended after provision_git's git-gate rules
- ~/.config/tea/config.yml with a logins: entry per declared gitea
URL, pointing at /gitea/<host>/ on the proxy
Renderers are pure and unit-tested. The dispatcher reads
plan.cred_proxy_plan.upstreams, which the backend wiring (next
commit) populates on DockerBottlePlan.
ANTHROPIC_BASE_URL is deliberately *not* a dotfile — it goes into
the agent's docker run -e env so claude sees it from process start.
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.
The agent's ~/.gitconfig now uses insteadOf (not pushInsteadOf),
so every git operation against a declared upstream — push, fetch,
clone, pull, ls-remote — routes through the gate. Matches the
gate's now-bidirectional design: fetch is mirrored via the
access-hook, push is gated via gitleaks.
provision_git now does two things: copy the host cwd's .git (when
--cwd is set, existing behavior) and write ~/.gitconfig with
pushInsteadOf rules for each bottle.git entry. A 'git push <real
upstream URL>' from inside the agent transparently rewrites to
'git://<gate>/<name>.git' so the gate gets first crack at the
incoming refs.
pushInsteadOf (not insteadOf) keeps fetch on the original URL —
v1 of the git-gate is push-only scope per PRD 0008. The render
helper is exposed for testing without docker.
PRD 0007: stop tunneling ssh through pipelock. Each Host block in
the agent's ~/.ssh/config now points at the gate container + the
per-entry listen port; HostKeyAlias preserves host-key validation
against the real upstream name, and CheckHostIP=no skips the
resolved-IP path (which would otherwise hit the gate's IP).
known_hosts collapses to a single entry per upstream keyed on the
alias.
The pipelock_proxy_host_port import is gone from this module; the
function itself becomes dead code and gets removed alongside the
broader pipelock SSH carve-outs in the next commit.
Second step of PRD 0006. With pipelock now doing the bumping, the
agent's TLS library has to trust pipelock's per-bottle CA — or
every CONNECT to api.anthropic.com is a self-signed-cert error.
- BottleBackend.provision gains a non-abstract `provision_ca`
with a default no-op (so non-Docker backends aren't forced to
implement TLS interception) and orchestrates
ca → prompt → skills → ssh → git. CA install runs first so the
agent's trust store is rebuilt before anything else in the
agent makes a TLS call.
- New backend/docker/provision/ca.py: docker-cp's the CA cert
into the agent at /usr/local/share/ca-certificates/...,
`update-ca-certificates`, then emits a one-line stderr log
with the SHA-256 fingerprint (stdlib `ssl` + `hashlib`; no
subprocess for crypto). Module-level constants AGENT_CA_PATH
and AGENT_CA_BUNDLE are imported by launch.py so the env
trio set at docker run time matches the paths the provisioner
writes.
- launch.py: rebinds `plan` after `dataclasses.replace`s on the
pipelock proxy plan so provision_ca (which reads
`plan.proxy_plan.ca_cert_host_path`) sees the populated CA
paths. Three new -e flags on the agent's docker run for the
NODE_EXTRA_CA_CERTS / SSL_CERT_FILE / REQUESTS_CA_BUNDLE trio.
- Dockerfile: adds curl to the apt-get install line. curl
natively respects HTTPS_PROXY and sends CONNECT directly —
the agent doesn't need OS-level DNS for external hostnames
(pipelock resolves them on its side of the bumped tunnel).
This is the "simple HTTPS request" path the earlier turn
needed and Node's stdlib https.request couldn't provide.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Make BottleBackend.prepare a template method that runs a cross-backend
_validate step (agent exists, named skills present on host, SSH
IdentityFiles resolve) and then delegates to a subclass-implemented
_resolve_plan for backend-specific resolution.
A future backend that overrides _resolve_plan can no longer forget to
validate skills or SSH keys; the validation runs unconditionally via
prepare. Backends with additional preconditions can override _validate
and chain via super().
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>