refactor(sidecars): instantiate sidecar ABCs directly from any backend
test / unit (pull_request) Successful in 21s
test / integration (pull_request) Successful in 40s

The four sidecar prepare-time helpers (PipelockProxy, Egress, GitGate,
Supervise) had docker-flavored subclasses that existed only as
instantiation shims for ABCs that already had no abstract methods.
PipelockProxy.prepare() reached for class-level CA path constants
that were only defined on the docker subclass — so smolmachines had
to import DockerPipelockProxy to render pipelock yaml, reaching
across the backend boundary for what's actually a platform-neutral
operation.

This moves the universal in-container CA paths
(PIPELOCK_CA_CERT_IN_CONTAINER / PIPELOCK_CA_KEY_IN_CONTAINER) to
claude_bottle/pipelock.py, drops the class-attr indirection on the
ABC, and deletes the four empty docker subclasses. Both backends
now instantiate the ABCs directly; the docker-side modules keep
the docker-flavored helpers (image pin, container naming, host CA
mint) and re-export the moved pipelock constants for compat.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 05:42:20 -04:00
parent 1dfc359141
commit 73dc0d4a40
10 changed files with 99 additions and 124 deletions
+2 -2
View File
@@ -35,9 +35,9 @@ from claude_bottle.backend.docker.network import (
from claude_bottle.backend.docker.pipelock import (
PIPELOCK_CA_CERT_IN_CONTAINER,
PIPELOCK_CA_KEY_IN_CONTAINER,
DockerPipelockProxy,
pipelock_tls_init,
)
from claude_bottle.pipelock import PipelockProxy
from claude_bottle.backend.docker.pipelock_apply import (
PipelockApplyError,
apply_allowlist_change,
@@ -99,7 +99,7 @@ class TestPipelockApply(unittest.TestCase):
the updated config."""
state_dir = pipelock_state_dir(self.slug)
state_dir.mkdir(parents=True, exist_ok=True)
prep = DockerPipelockProxy().prepare(
prep = PipelockProxy().prepare(
fixture_minimal().bottles["dev"], self.slug, state_dir,
)
self.internal_net = network_create_internal(self.slug)
+7 -7
View File
@@ -12,10 +12,10 @@ import unittest
from pathlib import Path
from typing import Any, cast
from claude_bottle.backend.docker.pipelock import DockerPipelockProxy
from claude_bottle.manifest import Manifest
from claude_bottle.pipelock import (
DEFAULT_TLS_PASSTHROUGH,
PipelockProxy,
pipelock_build_config,
pipelock_render_yaml,
)
@@ -54,7 +54,7 @@ class TestBuildConfig(unittest.TestCase):
self.assertNotIn("tls_interception", cfg)
def test_tls_interception_block_emitted_when_paths_supplied(self):
# PRD 0006: paths flow in via DockerPipelockProxy's in-container
# PRD 0006: paths flow in via the platform-neutral in-container
# constants; this directly pins the dict shape. passthrough_domains
# is baked in so LLM provider endpoints (api.anthropic.com) skip
# MITM — pipelock's docs explicitly recommend this for LLM hosts,
@@ -152,7 +152,7 @@ class TestRenderAndWrite(unittest.TestCase):
self.assertNotIn("ssrf:", text)
def test_prepare_writes_file_at_mode_600(self):
plan = DockerPipelockProxy().prepare(
plan = PipelockProxy().prepare(
fixture_minimal().bottles["dev"], "demo", self.out_dir
)
self.assertEqual(0o600, os.stat(plan.yaml_path).st_mode & 0o777)
@@ -170,7 +170,7 @@ class TestRenderAndWrite(unittest.TestCase):
},
"agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
})
plan = DockerPipelockProxy().prepare(
plan = PipelockProxy().prepare(
manifest.bottles["dev"], "demo", self.out_dir
)
content = plan.yaml_path.read_text()
@@ -179,13 +179,13 @@ class TestRenderAndWrite(unittest.TestCase):
self.assertNotIn("prompt-message", content)
def test_render_emits_tls_interception_via_prepare(self):
"""`DockerPipelockProxy.prepare` plumbs its in-container CA
constants through to the YAML. The block should land in the
"""`PipelockProxy.prepare` plumbs the module-level in-container
CA constants through to the YAML. The block should land in the
rendered output with `enabled: true`, the configured paths,
and the baked LLM-provider passthrough list. The actual
host-side CA generation happens in launch (not prepare), so
this test exercises only the YAML rendering."""
plan = DockerPipelockProxy().prepare(
plan = PipelockProxy().prepare(
fixture_minimal().bottles["dev"], "demo", self.out_dir
)
content = plan.yaml_path.read_text()