af65c10361b49e679b8f661e7fefc86d20b4c01c
24 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
727f30d422 |
refactor(docker): drop legacy per-sidecar container_name functions
Same line of cleanup as the supervise rename: the per-sidecar container names (`claude-bottle-pipelock-<slug>`, `claude-bottle-egress-<slug>`, `claude-bottle-git-gate-<slug>`) were docker-network aliases pointing at the bundle, kept so legacy URLs would keep resolving. Replaces them with short hostnames (`pipelock`, `egress`, `git-gate`) matching the existing `EGRESS_HOSTNAME` pattern, and inlines the bundle-loopback URL (`http://127.0.0.1:8888`) for the in-bundle egress→pipelock hop — matching what smolmachines already does. Drops the three `*_container_name` functions, `pipelock_proxy_url`, and `git_gate_host`. Their callers move to the new constants: - `PIPELOCK_HOSTNAME = "pipelock"` (claude_bottle/pipelock.py) - `GIT_GATE_HOSTNAME = "git-gate"` (claude_bottle/git_gate.py) - `BUNDLE_LOCAL_PIPELOCK_URL` (backend/docker/pipelock.py) The agent's HTTP_PROXY now reads `http://pipelock:8888` (vs the old `http://claude-bottle-pipelock-<slug>:8888`); the gitconfig insteadOf rewrites become `git://git-gate/<repo>.git`. The prepare- time orphan probe is collapsed onto the bundle container name (`claude-bottle-sidecars-<slug>`) instead of the four legacy per-sidecar names that no backend creates anymore. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
539234f29e |
refactor(sidecars): drop vestigial start/stop methods (PRD 0024 chunk 3)
Compose-up has owned per-container lifecycle since PRD 0018 ch3;
the .start() / .stop() methods on DockerPipelockProxy /
DockerEgress / DockerGitGate / DockerSupervise (and their
abstractmethod declarations in the four base ABCs) were already
documented as vestigial. With the bundle path in flight
(PRD 0024 ch2), they are truly dead — collapse to nothing.
Changes:
- Removed start/stop methods from the four DockerSidecar
classes. Plan dataclasses, image/path constants,
container-name helpers, and the .prepare() methods all stay
(the renderer + apply path still need them).
- Removed the matching @abstractmethod declarations in the
base ABCs so concrete subclasses don't have to stub them.
- launch.launch() and prepare.resolve_plan() no longer take
proxy/git_gate/egress/supervise instance parameters. backend.py
loses the four instance attributes it threaded through.
prepare.resolve_plan() instantiates the four classes itself
to call their .prepare() methods.
- Deleted four integration tests that only exercised the
removed lifecycle: test_pipelock_sidecar_smoke,
test_supervise_sidecar, test_git_gate_sidecar,
test_git_gate_mirror.
- Dropped the .stop-idempotency case in test_orphan_cleanup;
the network-cleanup cases stay (those test real production
code).
- Marked test_pipelock_apply @skip pending chunk 4 — its
bringup helper used .start; chunk 4 rewrites it with direct
`docker run`.
Dockerfile deletion deferred to chunk 5 (when the bundle flag
default flips) — the legacy compose path still needs
Dockerfile.{egress,git-gate,supervise} until then.
Net: 708 lines removed, 80 added.
533 unit tests + 27 integration tests passing (5 skipped: the
chunk-4-pending case + existing GITEA_ACTIONS guards).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
f1c5816d1f |
refactor(compose): drop pre-create networks + pipelock CIDR allowlist
PRD 0018 chunk 4 spike: empirically verified that pipelock's SSRF guard checks proxied-request destinations (e.g. api.anthropic.com → public IP) and not source IPs of incoming connections. The bottle's own internal CIDR was being added to ssrf.ip_allowlist defensively, but that defense isn't load-bearing — direct pipelock probe (`curl --proxy http://pipelock https://api.anthropic.com/`) returns 404 from upstream rather than blocking on SSRF. So: - Networks become compose-managed (`internal: true` on the internal network; the egress one is a normal user-defined bridge). Compose creates + removes them via up/down. - launch.py drops the `docker network create` + `network_inspect_cidr` + pipelock yaml re-render dance. - The pre-create/external scaffolding from chunk 3 goes with it. End-to-end `./cli.py start` still works; cleanup leaves no orphans. If real-world use surfaces an SSRF block we hadn't predicted, the allowlist can come back via subnet-pinning rather than pre-create. |
||
|
|
cefdc8c6e9 |
feat(launch): switch start to docker compose project per bottle
PRD 0018 chunk 3. Each instance is now one `docker compose` project:
- launch.py renders the compose spec via chunk-1's
bottle_plan_to_compose, writes it to state/<slug>/docker-compose.yml,
`docker compose up -d`s, and (on teardown) dumps
`docker compose logs --no-color --timestamps` to
state/<slug>/compose.log before `docker compose down`.
- Networks are pre-created (`docker network create --internal` +
user-defined bridge) so pipelock yaml can know the internal CIDR
before compose-up. Compose references them with `external: true`;
the launch step's ExitStack still owns network removal.
- Agent still runs `sleep infinity`; claude reaches it via
`docker exec -it` exactly like before (per the PRD's resolved
TTY question).
- metadata.json grows a `compose_project` field so dashboard /
cleanup tooling can derive compose invocations without
re-deriving the slug.
Security follow-ups from chunk-2 review:
(b) CA private keys: pipelock + egress ca-key.pem land at 0o600
explicitly. The mitmproxy cert+key concat stays 0o644 because
the egress container's uid-1000 user reads it through the
bind mount; parent dir at 0o700 still restricts host-side
reach.
(c) Apply atomicity: egress_apply + pipelock_apply switch from
`docker cp` to host-side write-temp-then-rename on the
bind-mount source. POSIX rename is atomic on the same
filesystem, so a sidecar SIGHUP racing the apply can't see
a half-written routes.yaml / pipelock.yaml.
Per-sidecar Docker{Sidecar}.start/stop methods stay in place — the
integration test suite drives them directly to validate each image
in isolation, which is still useful. launch.py no longer calls
them; a follow-up chunk can prune if the integration tests move to
the compose lifecycle.
git-gate entrypoint's chmod 600 on the keyfile + known_hosts now
tolerates EROFS (`|| true`) — the host SSH key is already 0600
(SSH refuses to load otherwise), so the inside-container chmod
was already a no-op in the docker-cp path and now just needs to
not error on the read-only bind mount.
422 unit tests pass; supervise integration test passes; end-to-end
`./cli.py start implementer` brings up the project, attaches,
captures full merged logs on teardown, and reaps all containers +
networks.
|
||
|
|
cd82a48399 |
refactor(state): write prepare-time scratch files under state/<slug>/
PRD 0018 chunk 2. Each sidecar's prepare-time output (pipelock yaml + CAs, egress routes.yaml + CAs, git-gate entrypoint + hooks, supervise current-config, agent env + prompt) now lands in ~/.claude-bottle/state/<slug>/<service>/ instead of an ephemeral mktemp dir. The state subdirs become the stable bind-mount sources that chunk 3's docker compose project will reference. The SDK launch path is unchanged — `docker cp` still copies from the plan-held paths into containers, just from new locations. start.py's session-end cleanup is now in `finally`, which also reaps state dirs left behind by dry-run / preflight-N / prepare-exception paths (previously only the post-launch path settled state). |
||
|
|
1e5b0dcfca |
refactor: rename egress-proxy → egress everywhere
The manifest key is `egress:` now; finish the rename so the rest of the codebase matches. Files (Dockerfile.egress, claude_bottle/egress.py etc.), classes (Egress, EgressConfig, EgressRoute, EgressPlan, DockerEgress), constants (EGRESS_HOSTNAME, EGRESS_ROUTES, ...), container name prefix (claude-bottle-egress-*), docker network alias (egress), the introspection host (_egress.local), the MCP tool IDs (egress-block, list-egress-routes), and the preflight label all drop the `-proxy` suffix. |
||
|
|
14c8a51c16 |
refactor(manifest): rename egress_proxy key to egress
Now that `bottle.egress` (the old allowlist/dlp_action block) is
gone, the longer `egress_proxy:` disambiguator isn't needed. The
manifest field reads more naturally as just `egress:` with the
same nested `routes: [...]` shape.
Renamed:
- Manifest YAML key: `egress_proxy:` → `egress:`
- Bottle dataclass attr: `bottle.egress_proxy` → `bottle.egress`
- `_BOTTLE_KEYS` entry, schema docstring, and all
user-facing error message labels (`egress.routes[N]`,
`egress has unknown key …`, etc.).
Kept (these refer to the egress-proxy SIDECAR, not the manifest
field):
- File names: `egress_proxy.py`, `egress_proxy_apply.py`,
`egress_proxy_addon.py`, `egress_proxy_addon_core.py`.
- Class names: `EgressProxyConfig`, `EgressProxyRoute`,
`EgressProxyPlan`, `EgressProxy`, `DockerEgressProxy`.
- Helper names: `egress_proxy_manifest_routes`,
`egress_proxy_routes_for_bottle`,
`egress_proxy_token_env_map`, etc.
- Constants: `EGRESS_PROXY_HOSTNAME`, `EGRESS_PROXY_ROLES`,
`EGRESS_PROXY_AUTH_SCHEMES`, `EGRESS_PROXY_FORWARD_PROXY`,
`EGRESS_PROXY_INTROSPECT_URL`, `EGRESS_PROXY_PORT`, etc.
- Container name prefix `claude-bottle-egress-proxy-*`, the
`egress-proxy` docker network alias, the
`egress-proxy-block` + `list-egress-proxy-routes` MCP tool
IDs, the `egress-proxy` audit-log component label.
Local bottle migrated (`~/.claude-bottle/bottles/dev.md` already
updated). The legacy `egress_proxy` key isn't surfaced anywhere
anymore; the generic unknown-key validator catches typos with a
"did you mean: egress, env, git, supervise" hint.
409 unit + integration tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
fad76d3364 |
fix(supervise): stage current-config routes file as routes.yaml
The supervise sidecar mounted a snapshot named routes.json into the agent at /etc/claude-bottle/current-config/routes.json, but the egress-proxy-block tool description (and the live proxy file the apply step writes) say routes.yaml. The agent couldn't find the file at the documented path, composed proposals against stale or empty current state, and reported "routes wasn't updated on disk" because it was looking at the wrong filename. Rename the staged file to routes.yaml so the tool description, the staged snapshot, and the live proxy file all agree on the name. Content stays JSON-in-a-yaml-extension (per PRD 0017 chunk 1's decision: every JSON document is valid YAML, stdlib parsers handle it on both ends). Note: the staged file is still a one-shot snapshot taken at bottle prep time. It does NOT auto-update when the operator approves an egress-proxy-block. Agents that want to verify their proposal took effect should retry the request that triggered the block — a successful upstream response is the real signal. Fixing the snapshot-staleness UX is a separate follow-up. Tests migrated from routes.json → routes.yaml. 364 pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
c4cf2453e2 |
fix(launch): also set lowercase {http,https,no}_proxy on the agent
CVE-2016-5388 ("httpoxy") mitigation: libcurl ignores uppercase
HTTP_PROXY for http:// URLs to prevent untrusted CGI HTTP_*
headers from hijacking the proxy. Only lowercase http_proxy is
honored for HTTP. Without the lowercase var, plain-HTTP requests
from the agent skip egress-proxy entirely — they go direct,
which is "network unreachable" on the agent's --internal bridge,
not the egress-proxy 403 we expect.
Confirmed against a live bottle: `curl http://1.1.1.1/` reported
"Immediate connect fail for 1.1.1.1: Network is unreachable"
instead of the addon's "host not in allowlist" 403. With both
cases set the agent's curl honors the proxy and our allowlist
enforcement kicks in.
Also set lowercase HTTPS_PROXY + NO_PROXY for symmetry. Some
tools check one case only; sending both means we don't have to
audit which convention each tool uses.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
70f773ac61 |
feat(egress-proxy): cutover from cred-proxy (PRD 0017 chunk 2)
Hard cutover. cred-proxy is deleted; egress-proxy is now the agent's
HTTP_PROXY (when routes are declared) with pipelock on its outbound
leg. Two per-bottle CAs are minted: egress-proxy's (agent trust
store) and pipelock's (egress-proxy's outbound trust store).
Manifest:
- `bottle.cred_proxy` → hard error with a migration recipe.
- `bottle.egress_proxy` is the new shape (PRD 0017 chunk 1).
- CredProxy* types + role validators removed.
Wiring:
- launch.py: `egress_proxy_tls_init` mints the egress-proxy CA
(cert+key concat for mitmproxy + cert-only for agent trust);
`DockerEgressProxy.start` docker-cps both CAs in, sets
`HTTPS_PROXY=pipelock` + `EGRESS_PROXY_UPSTREAM_CA` so mitmdump
trusts pipelock's MITM. Agent's HTTP_PROXY points at
egress-proxy when routes exist, else falls back to pipelock
(no-routes bottles unchanged).
- prepare.py / backend.py: `cred_proxy` arg → `egress_proxy`;
sidecar-orphan probe + plan field + dashboard view all
renamed.
- provision_ca: selects the egress-proxy CA when present, else
pipelock's (filename renamed to claude-bottle-mitm-ca.crt).
- bottle.provision: cred-proxy dotfile rewrites (~/.npmrc,
~/.gitconfig insteadOf, tea config) are gone — HTTP_PROXY
catches everything respecting it.
Pipelock helpers:
- `pipelock_token_hosts` → `pipelock_route_hosts` (now reading
egress_proxy.routes).
- cred-proxy hostname auto-allow → egress-proxy hostname
auto-allow.
- Anthropic seed-phrase workaround now triggers when an
egress_proxy route targets api.anthropic.com (was based on the
cred-proxy `anthropic-base-url` role).
Dockerfile.egress-proxy:
- Entrypoint conditionally passes
`--set ssl_verify_upstream_trusted_ca=$EGRESS_PROXY_UPSTREAM_CA`
(via the `${VAR:+...}` shell expansion) so standalone runs without
a mounted pipelock CA still boot.
- mkdirs `/home/mitmproxy/.mitmproxy` ahead of `docker cp`.
Deleted: claude_bottle/{cred_proxy,cred_proxy_server}.py,
backend/docker/{cred_proxy,provision/cred_proxy}.py,
Dockerfile.cred-proxy, plus the corresponding unit + integration
tests. backend/docker/cred_proxy_apply.py stays as a stub for
chunk 3 to rewrite (its container-name + routes-path constants
are inlined so it survives without the deleted module).
Test changes:
- test_pipelock_allowlist rewritten against egress-proxy routes
+ the new `pipelock_route_hosts`.
- test_manifest_md_load + test_pipelock_yaml + test_yaml_subset
fixtures migrated to the `egress_proxy: { routes: [...] }`
shape.
- test_supervise_sidecar's round-trip test switched from
`dashboard.approve` to `dashboard.reject`: the approval-apply
path on cred-proxy-block proposals hits a deleted sidecar in
chunk 2's transitional state. Chunk 3 restores the approval
test once the remediation flow is retargeted at egress-proxy.
376 tests pass (was 427; net delta is removed cred-proxy tests).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
307400f08a |
fix(supervise): bypass pipelock for agent → supervise MCP traffic
`/mcp` showed the supervise server as ✔ connected (initialize is fast), but any actual tool call failed because the supervise MCP design is long-poll — the sidecar holds the HTTP request open until the operator approves in the dashboard (potentially minutes) and only then returns the response. Pipelock is a forward proxy with idle timeouts; it cut the long- polled HTTPS-style request well before the operator could act, and claude-code reported the tool as ✘ failed. Fix: add `supervise` to the agent's NO_PROXY when bottle.supervise is true. The supervise sidecar is on the bottle's internal network with the `supervise` network-alias, so the agent can dial it directly via docker DNS — no proxy, no idle timeout. Body-scanning supervise traffic isn't critical because the operator reviews every proposal in the TUI before approving. The earlier pipelock allowlist auto-add for `supervise` stays as belt-and- braces (handles any proxy-respecting client other than claude-code that might dial supervise). Existing bottles need a restart to pick up the new NO_PROXY value (env can't be changed on a running container). The dashboard's pipelock-edit workaround from PR #25 unblocks short-running tool calls in the meantime but won't survive the pipelock idle timeout on a long-polled call. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
02811e0417 |
feat(bottle): per-bottle Dockerfile state + image build hook (PRD 0016)
Phase 1 of PRD 0016. Lays the per-bottle state plumbing that capability-block remediation will write into: - claude_bottle/backend/docker/bottle_state.py: bottle_state_dir, per_bottle_dockerfile (read), write_per_bottle_dockerfile, per_bottle_image_tag (unique per slug), transcript_snapshot_dir. Stores under ~/.claude-bottle/state/<slug>/. - prepare.py: when a per-bottle Dockerfile exists, use per_bottle_image_tag(slug) as the base image and pass the per-bottle Dockerfile path through DockerBottlePlan.dockerfile_path. --cwd still layers a derived image on top. - launch.py: passes plan.dockerfile_path to build_image so the per-bottle Dockerfile is what docker build reads. - DockerBottlePlan gains dockerfile_path field; print() surfaces it in the preflight summary so the operator can see at-a-glance that this bottle is running on a rebuilt image. Phase 2 will write to write_per_bottle_dockerfile (capability-block approval); Phase 3 wires it into the dashboard. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
4b2dbcdefd |
feat(supervise): Docker lifecycle + bottle integration (PRD 0013)
Phase 3 of PRD 0013. Wires the supervise sidecar into bottle launch: - Manifest: bottle.supervise (bool, default False). Opt-in for v1 so existing bottles are unchanged. - supervise.py: adds SupervisePlan + abstract Supervise(ABC) with a prepare template that stages the per-bottle queue dir on the host and the current-config dir under stage_dir (routes.json + allowlist + Dockerfile). Stdlib-only so it still runs as the in-container shared helper. - backend/docker/supervise.py: DockerSupervise concrete start/stop. No egress network (the sidecar doesn't make outbound calls); just the bottle's internal network with network-alias "supervise" and a bind-mount of the host queue dir at /run/supervise/queue. - Prepare wires supervise.prepare into the DockerBottlePlan, derives routes_content from cred_proxy_plan, allowlist_content from pipelock_effective_allowlist, and dockerfile_content from the repo's Dockerfile. supervise sidecar added to the orphan probe. - Launch starts the supervise sidecar after pipelock + cred-proxy but before the agent (so DNS resolution for `supervise` is up on the agent's first tool call). - Agent container gets a read-only bind-mount of the current-config dir at /etc/claude-bottle/current-config when supervise is enabled. - bottle_plan print + to_dict surface the supervise state. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
51b20340a9 |
fix(pipelock): allow agent->sidecar traffic via SSRF exception
The agent's HTTP_PROXY points at pipelock, so a request to http://cred-proxy:9099/... arrives at pipelock; pipelock resolves the host, sees an RFC1918 address (the bottle's internal Docker network sits in 172.x), and 403's "SSRF blocked: cred-proxy resolves to internal IP 172.20.0.4". Bypassing pipelock entirely would also remove its body scanner from the agent->cred-proxy leg — we want to keep that DLP coverage. Pipelock has `ssrf.ip_allowlist` for exactly this: CIDRs that override the built-in internal-IP block while api_allowlist + body scanning + tls_interception keep firing. Wiring: - `pipelock_build_config` accepts `ssrf_ip_allowlist`; when non-empty, emits an `ssrf: { ip_allowlist: [...] }` block. - `pipelock_render_yaml` renders that block. - `PipelockProxyPlan` gains `internal_network_cidr`. - New `network_inspect_cidr(name)` helper reads the Docker-assigned subnet via `docker network inspect`. - launch.py: after `network_create_internal`, inspect the CIDR, re-render the yaml with `ssrf_ip_allowlist=(cidr,)`, overwrite the file in place; `DockerPipelockProxy.start` then docker-cp's the updated content. Prepare's initial render stays unchanged (CIDR isn't known yet at prepare time). The exception scope is the bottle's own internal network only — agent ↔ pipelock / git-gate / cred-proxy. Body scanning still applies to the bytes flowing through pipelock; pipelock just no longer treats those internal IPs as exfil targets. |
||
|
|
2990c3c903 |
refactor(cred_proxy): rename Upstream -> Route, fix tea-login AttributeError
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). |
||
|
|
27b2d78b11 |
fix(cred_proxy): close git-push bypass + route through pipelock (PRD 0010)
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. |
||
|
|
8334f51268 |
feat(cred_proxy): wire DockerCredProxy through backend (PRD 0010)
- 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. |
||
|
|
3d66ad2a86 |
feat(ssh-gate)!: remove ssh-gate sidecar and provisioner (PRD 0009)
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. |
||
|
|
f787edb861 |
feat(git-gate): wire DockerGitGate through prepare/launch/plan
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 |
||
|
|
2533f8a00b |
feat(ssh-gate): wire gate into DockerBottlePlan, prepare, launch
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. |
||
|
|
86a9b499bc |
feat(provision): install pipelock CA into the agent + add curl
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> |
||
|
|
3755e66abe |
feat(pipelock): enable tls_interception with per-bottle ephemeral CA
First step of PRD 0006. Pipelock now does the CONNECT bumping that PR #8's mitmproxy chain was supposed to provide — natively, in the same single sidecar PRD 0001 wired up. - claude_bottle/pipelock.py: pipelock_build_config grows optional ca_cert_path / ca_key_path kwargs. When both are passed the rendered YAML carries a `tls_interception: { enabled: true, ca_cert, ca_key }` block. PipelockProxy gains class-level CA_CERT_IN_CONTAINER / CA_KEY_IN_CONTAINER constants that subclasses set to wherever they place the CA inside the sidecar. PipelockProxyPlan gains ca_cert_host_path / ca_key_host_path fields (default empty Path() — sentinel for "not yet populated", filled by launch via dataclasses.replace). - claude_bottle/backend/docker/pipelock.py: new pipelock_tls_init(stage_dir) helper runs `pipelock tls init` in a one-shot container against a host-mounted scratch dir. DockerPipelockProxy sets its class constants to /etc/pipelock-ca.pem and /etc/pipelock-ca-key.pem; .start docker-cp's the cert + key into those paths between `docker create` and `docker start`. Pipelock runs as root in its distroless image, so no chown is needed (verified). - claude_bottle/backend/docker/launch.py: calls pipelock_tls_init between network creation and proxy.start. Prepare stays side-effect-free on docker; the one-shot ca-init container only runs on a real launch, not on `start --dry-run`. - tests/unit/test_pipelock_yaml.py: new assertions that pipelock_build_config emits the tls_interception block only when both paths are supplied (and rejects a half-set pair), plus a test that the docker proxy's prepare plumbs the in-container paths through to the rendered YAML. The end-to-end "bumping actually fires" assertion lands in chunk 4 (HTTPS integration tests). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
5da2b47f72 |
refactor(docker): move force_remove_container into the docker util module
The helper is a thin subprocess wrapper over `container_exists` + `docker rm -f`, so it belongs alongside the other docker primitives in util.py rather than as a private in launch.py. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
1546acad00 |
refactor(docker): split backend.py into prepare / launch / cleanup
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> |