refactor(sidecars): bundle is the only shape (PRD 0024 chunk 5)
test / unit (pull_request) Successful in 21s
test / integration (pull_request) Successful in 43s

The CLAUDE_BOTTLE_SIDECAR_BUNDLE feature flag is gone. Every
bottle ships with the agent + bundle pair — no opt-in, no legacy
four-sidecar fallback.

Changes:

- Renderer (compose.py): bottle_plan_to_compose unconditionally
  emits {agent, sidecars}. Deleted _pipelock_service,
  _git_gate_service, _egress_service, _supervise_service helpers.
  _agent_service.depends_on collapses to ["sidecars"].

- sidecar_bundle.py: deleted sidecar_bundle_enabled (the flag
  parser). SIDECAR_BUNDLE_IMAGE + container-name helper stay.

- pipelock_apply.py: docker cp + docker restart now target
  sidecar_bundle_container_name(slug). Bundle restart bounces
  all four daemons together (per-daemon reload is the eventual
  feature, not v1).

- Per-sidecar modules trimmed:
  - egress.py: dropped EGRESS_IMAGE, EGRESS_DOCKERFILE,
    build_egress_image, egress_url. Kept EGRESS_PORT, CA paths,
    egress_container_name (still used by the renderer's network
    aliases).
  - git_gate.py: dropped GIT_GATE_IMAGE, GIT_GATE_DOCKERFILE,
    build_git_gate_image. Kept git_gate_host + GIT_GATE_PORT.
  - supervise.py: dropped SUPERVISE_IMAGE, SUPERVISE_DOCKERFILE,
    build_supervise_image, supervise_url.

- Deleted Dockerfile.{egress,git-gate,supervise}. The bundle's
  Dockerfile.sidecars is the only sidecar image now.

- test_compose.py: deleted TestPipelockAlwaysPresent,
  TestConditionalGitGate, TestConditionalEgress,
  TestConditionalSupervise, TestFullMatrix (legacy-shape only),
  TestSidecarBundleFlag (flag is gone). TestSidecarBundleShape
  drops its patch.dict wrapper. TestAgentAlwaysPresent's
  depends_on cases collapse to one.

- test_pipelock_apply.py: bringup container name uses
  sidecar_bundle_container_name(slug) to match the production
  target.

- README.md Architecture section rewritten to describe the
  agent + bundle pair.

Net: -626 lines.

Test status: 498 unit + 27 integration + 1 skipped (chunk-4
pending — superseded by this chunk's rewrite). Locally verified
end-to-end bottle launch produces exactly 2 containers
(claude-bottle-<slug> + claude-bottle-sidecars-<slug>).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 01:37:21 -04:00
parent 9348d4b343
commit 62f6f8db34
12 changed files with 117 additions and 743 deletions
+6 -27
View File
@@ -1,18 +1,11 @@
"""Sidecar bundle constants + helpers for the Docker backend
(PRD 0024 chunk 2).
(PRD 0024).
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`.
"""
The bundle image (built by Dockerfile.sidecars, PRD 0024 chunk 1)
runs pipelock + egress + git-gate + supervise as one container
per bottle under a small Python init supervisor. As of chunk 5
the bundle is the only shape — the legacy four-sidecar topology
and its `CLAUDE_BOTTLE_SIDECAR_BUNDLE` feature flag are gone."""
from __future__ import annotations
@@ -36,17 +29,3 @@ def sidecar_bundle_container_name(slug: str) -> str:
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")