Files
bot-bottle/claude_bottle/backend/docker/sidecar_bundle.py
T
didericis a1180adec1
test / unit (pull_request) Successful in 21s
test / integration (pull_request) Successful in 1m12s
feat(compose): emit bundle shape behind feature flag (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 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>
2026-05-27 00:43:08 -04:00

53 lines
2.0 KiB
Python

"""Sidecar bundle constants + helpers for the Docker backend
(PRD 0024 chunk 2).
The bundle image (built by Dockerfile.sidecars, see PRD 0024
chunk 1) collapses pipelock + egress + git-gate + supervise into
one container per bottle. Whether the renderer emits the bundle
shape (one `sidecars` service) or the legacy four-sidecar shape
is controlled by `CLAUDE_BOTTLE_SIDECAR_BUNDLE`; chunk 2 ships
both shapes side by side behind the flag so existing operators
keep working unchanged while the bundle path soaks.
This module is intentionally tiny — just the constants + the
flag + the container-name helper. The compose-renderer branch
that consumes it lives in `compose.py`.
"""
from __future__ import annotations
import os
# Bundle image. Defaults to a built-locally tag (built from the
# repo's Dockerfile.sidecars via compose `build:`). Operators
# pinning to a published digest can override via env, matching
# the existing `CLAUDE_BOTTLE_PIPELOCK_IMAGE` shape.
SIDECAR_BUNDLE_IMAGE = os.environ.get(
"CLAUDE_BOTTLE_SIDECAR_IMAGE",
"claude-bottle-sidecars:latest",
)
SIDECAR_BUNDLE_DOCKERFILE = "Dockerfile.sidecars"
def sidecar_bundle_container_name(slug: str) -> str:
"""`claude-bottle-sidecars-<slug>`. Same prefix scheme as the
per-sidecar containers it replaces, so the dashboard's
discovery-by-prefix logic keeps working."""
return f"claude-bottle-sidecars-{slug}"
def sidecar_bundle_enabled(env: dict[str, str] | None = None) -> bool:
"""Feature-flag check. The flag is opt-in for chunk 2:
unset / "" / "0" / "false" → legacy four-sidecar shape;
anything else → bundle shape. Chunks 4-5 flip the default and
then delete the flag.
`env` defaults to `os.environ` at call time so tests can
monkey-patch the environment without re-importing the module."""
if env is None:
env = dict(os.environ)
raw = env.get("CLAUDE_BOTTLE_SIDECAR_BUNDLE", "").strip().lower()
return raw not in ("", "0", "false", "no", "off")