feat(compose): bundle shape behind feature flag (PRD 0024 chunk 2) #56

Merged
didericis merged 1 commits from prd-0024-chunk-2-renderer-collapse into main 2026-05-27 00:46:51 -04:00
Owner

Summary

PRD 0024 chunk 2: the docker backend's compose renderer now emits a single sidecars service in place of the four per-sidecar services when CLAUDE_BOTTLE_SIDECAR_BUNDLE is set. Default (unset / 0 / false) keeps the legacy five-service shape — chunks 4-5 flip the default and delete the flag.

The bundle service consumes the bundle image from PR #55. It joins both networks with aliases for every legacy shortname + per-slug long-form so the agent's existing HTTPS_PROXY URL keeps resolving with no agent-side change. Daemon subset narrows via CLAUDE_BOTTLE_SIDECAR_DAEMONS=<csv> for bottles that don't use git-gate or supervise. Volumes are the union of the four prior services' bind-mounts at the same in-container paths.

HTTPS_PROXY scoping

One subtle correctness issue: today egress's compose service env carries HTTPS_PROXY so its outbound goes through pipelock. Propagating that to the bundle would route git-gate's git fetches through pipelock too — wrong (pipelock doesn't proxy SSH, and pipelock would TLS-MITM github.com which isn't on the allowlist). Moved the export into egress_entrypoint.sh so only mitmdump's subprocess sees it.

Tests

  • 72 compose tests passing (44 existing legacy-shape tests unchanged + 20 new TestSidecarBundleShape cases asserting services / aliases / env / volumes / depends_on under the flag + 8 new TestSidecarBundleFlag cases locking down the env-var parser).
  • Full unit suite: 533 passing.
  • Verified end-to-end render with CLAUDE_BOTTLE_SIDECAR_BUNDLE=1 against a full-matrix fixture — the resulting compose dict is clean.

Sample (flag on, full matrix)

name: claude-bottle-demo-abc12
services:
  sidecars:
    image: claude-bottle-sidecars:latest
    build: { dockerfile: Dockerfile.sidecars }
    container_name: claude-bottle-sidecars-demo-abc12
    networks:
      internal:
        aliases:
          - claude-bottle-pipelock-demo-abc12
          - egress
          - claude-bottle-egress-demo-abc12
          - claude-bottle-git-gate-demo-abc12
          - supervise
          - claude-bottle-supervise-demo-abc12
      egress: null
    environment:
      - CLAUDE_BOTTLE_SIDECAR_DAEMONS=egress,pipelock,git-gate,supervise
      - EGRESS_UPSTREAM_PROXY=http://claude-bottle-pipelock-demo-abc12:8888
      - EGRESS_UPSTREAM_CA=/home/mitmproxy/.mitmproxy/pipelock-ca.pem
      - EGRESS_TOKEN_0
      - SUPERVISE_BOTTLE_SLUG=demo-abc12
      - SUPERVISE_QUEUE_DIR=/run/supervise/queue
      - SUPERVISE_PORT=9100
    volumes: [pipelock yaml + ca, egress routes + ca, git-gate hooks + creds, supervise queue]
  agent:
    ...
    depends_on: [sidecars]

Out of scope (chunks 3-5)

  • Backend Python trim: backend/docker/{pipelock,egress,git_gate,supervise}.py still carry their per-container start/stop helpers; chunk 3 strips them and consolidates container-name lookups onto a single bundle helper.
  • Deletion of Dockerfile.{egress,git-gate,supervise}. Chunk 3.
  • Bundle-aware orphan cleanup + integration test sweep. Chunk 4.
  • Flag default flip + flag removal. Chunk 5.
## Summary PRD 0024 chunk 2: the docker backend's compose renderer now emits a single `sidecars` service in place of the four per-sidecar services when `CLAUDE_BOTTLE_SIDECAR_BUNDLE` is set. Default (unset / `0` / `false`) keeps the legacy five-service shape — chunks 4-5 flip the default and delete the flag. The bundle service consumes the bundle image from PR #55. It joins both networks with aliases for every legacy shortname + per-slug long-form so the agent's existing `HTTPS_PROXY` URL keeps resolving with no agent-side change. Daemon subset narrows via `CLAUDE_BOTTLE_SIDECAR_DAEMONS=<csv>` for bottles that don't use git-gate or supervise. Volumes are the union of the four prior services' bind-mounts at the same in-container paths. ## HTTPS_PROXY scoping One subtle correctness issue: today `egress`'s compose service env carries `HTTPS_PROXY` so its outbound goes through pipelock. Propagating that to the bundle would route git-gate's git fetches through pipelock too — wrong (pipelock doesn't proxy SSH, and pipelock would TLS-MITM github.com which isn't on the allowlist). Moved the export into `egress_entrypoint.sh` so only mitmdump's subprocess sees it. ## Tests - **72 compose tests passing** (44 existing legacy-shape tests unchanged + 20 new `TestSidecarBundleShape` cases asserting services / aliases / env / volumes / depends_on under the flag + 8 new `TestSidecarBundleFlag` cases locking down the env-var parser). - Full unit suite: 533 passing. - Verified end-to-end render with `CLAUDE_BOTTLE_SIDECAR_BUNDLE=1` against a full-matrix fixture — the resulting compose dict is clean. ## Sample (flag on, full matrix) ``` name: claude-bottle-demo-abc12 services: sidecars: image: claude-bottle-sidecars:latest build: { dockerfile: Dockerfile.sidecars } container_name: claude-bottle-sidecars-demo-abc12 networks: internal: aliases: - claude-bottle-pipelock-demo-abc12 - egress - claude-bottle-egress-demo-abc12 - claude-bottle-git-gate-demo-abc12 - supervise - claude-bottle-supervise-demo-abc12 egress: null environment: - CLAUDE_BOTTLE_SIDECAR_DAEMONS=egress,pipelock,git-gate,supervise - EGRESS_UPSTREAM_PROXY=http://claude-bottle-pipelock-demo-abc12:8888 - EGRESS_UPSTREAM_CA=/home/mitmproxy/.mitmproxy/pipelock-ca.pem - EGRESS_TOKEN_0 - SUPERVISE_BOTTLE_SLUG=demo-abc12 - SUPERVISE_QUEUE_DIR=/run/supervise/queue - SUPERVISE_PORT=9100 volumes: [pipelock yaml + ca, egress routes + ca, git-gate hooks + creds, supervise queue] agent: ... depends_on: [sidecars] ``` ## Out of scope (chunks 3-5) - Backend Python trim: `backend/docker/{pipelock,egress,git_gate,supervise}.py` still carry their per-container start/stop helpers; chunk 3 strips them and consolidates container-name lookups onto a single bundle helper. - Deletion of `Dockerfile.{egress,git-gate,supervise}`. Chunk 3. - Bundle-aware orphan cleanup + integration test sweep. Chunk 4. - Flag default flip + flag removal. Chunk 5.
didericis added 1 commit 2026-05-27 00:43:33 -04:00
feat(compose): emit bundle shape behind feature flag (PRD 0024 chunk 2)
test / unit (pull_request) Successful in 21s
test / integration (pull_request) Successful in 1m12s
a1180adec1
The docker backend's compose renderer now emits a single
`sidecars` service in place of the four per-sidecar services
when CLAUDE_BOTTLE_SIDECAR_BUNDLE is truthy. Default (unset/0/
false) keeps the legacy five-service shape so existing operators
don't have to migrate atomically; chunks 4-5 flip the default
and delete the flag.

New module claude_bottle/backend/docker/sidecar_bundle.py owns
the bundle image constant (CLAUDE_BOTTLE_SIDECAR_IMAGE env var
override + claude-bottle-sidecars:latest default), the
Dockerfile reference, the container-name helper, and the
flag-parser.

The bundle service:
- joins both internal + egress networks with aliases for every
  legacy shortname + per-slug long form so the agent's
  HTTPS_PROXY URL (which dials `egress` or
  `claude-bottle-pipelock-<slug>`) keeps resolving with no
  agent-side change
- carries CLAUDE_BOTTLE_SIDECAR_DAEMONS=<csv> for the init
  supervisor to narrow which daemons to start
- carries the union of the four prior services' daemon-private
  env vars (EGRESS_UPSTREAM_PROXY, SUPERVISE_*, token env names)
- does NOT carry HTTPS_PROXY/HTTP_PROXY/NO_PROXY — those would
  route git-gate's git fetches through pipelock by mistake
- union'd bind-mounts at the same in-container paths as before

HTTPS_PROXY scoping moved into egress_entrypoint.sh so only
mitmdump's subprocess sees it. In the legacy four-sidecar shape
the env vars also lived in the egress service's compose env;
the shell script's export is additionally defensive.

Tests:
- All 44 existing TestCompose cases pass unchanged (flag off →
  legacy shape).
- 20 new TestSidecarBundleShape cases assert on the bundle's
  services / aliases / env / volumes / depends_on under the
  flag.
- 8 new TestSidecarBundleFlag cases lock down the env-var
  parser (unset / 0 / false / no / off → disabled; everything
  else → enabled).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
didericis merged commit c37344608b into main 2026-05-27 00:46:51 -04:00
Sign in to join this conversation.