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
+26 -21
View File
@@ -17,7 +17,6 @@ Image pin: ghcr.io/luckypipewrench/pipelock@sha256:<digest> for tag 2.3.0.
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
from pathlib import Path
from typing import cast
@@ -47,6 +46,15 @@ DEFAULT_TLS_PASSTHROUGH: tuple[str, ...] = (
)
# In-container paths the rendered pipelock YAML references under
# `tls_interception`. The pipelock binary expects the per-bottle CA
# cert + key at these exact paths inside its container — independent
# of how the daemon is wrapped (own container, sidecar bundle, etc.),
# which is why they live in the platform-neutral module.
PIPELOCK_CA_CERT_IN_CONTAINER = "/etc/pipelock-ca.pem"
PIPELOCK_CA_KEY_IN_CONTAINER = "/etc/pipelock-ca-key.pem"
# --- Allowlist resolution --------------------------------------------------
@@ -301,25 +309,22 @@ class PipelockProxyPlan:
ca_key_host_path: Path = Path()
class PipelockProxy(ABC):
class PipelockProxy:
"""The pipelock egress proxy. Encapsulates the YAML-config
generation; the sidecar's start/stop lifecycle is backend-specific
and lives on concrete subclasses.
generation; the container lifecycle is owned by whatever
wraps the daemon (compose-managed pipelock container on docker,
sidecar-bundle PID 1 on smolmachines).
The class-level constants `CA_CERT_IN_CONTAINER` /
`CA_KEY_IN_CONTAINER` are the in-container paths the YAML config
references — they correspond to wherever the backend's `.start`
places the CA cert and key inside the sidecar. Subclasses
override the constants."""
CA_CERT_IN_CONTAINER: str = ""
CA_KEY_IN_CONTAINER: str = ""
Backends instantiate the class directly — there are no
platform-specific subclasses; the in-container CA paths are
universal module-level constants
(`PIPELOCK_CA_CERT_IN_CONTAINER` / `PIPELOCK_CA_KEY_IN_CONTAINER`)."""
def prepare(
self, bottle: Bottle, slug: str, stage_dir: Path
) -> PipelockProxyPlan:
"""Write the pipelock yaml config (mode 600) under `stage_dir`
and return the plan for `.start`. Pure host-side, no docker
and return the plan for launch. Pure host-side, no docker
subprocess.
`slug` is the agent-derived identifier (lowercased,
@@ -327,18 +332,18 @@ class PipelockProxy(ABC):
resource name — the agent container, the pipelock container
(`claude-bottle-pipelock-<slug>`), the internal/egress
networks. It's stored on the returned plan so the backend's
start step can derive the sidecar's container name.
launch step can derive the sidecar's container name.
The CA paths the YAML references are the in-container paths
from the concrete subclass's class-level constants. The
host-side counterparts are generated by the launch step
(not here, so prepare stays side-effect-free on docker) and
added to the plan via `dataclasses.replace` before `.start`."""
The CA paths the YAML references are the module-level
in-container constants. The host-side counterparts are
generated by the launch step (not here, so prepare stays
side-effect-free on docker) and added to the plan via
`dataclasses.replace` before the daemon starts."""
yaml_path = stage_dir / "pipelock.yaml"
cfg = pipelock_build_config(
bottle,
ca_cert_path=self.CA_CERT_IN_CONTAINER,
ca_key_path=self.CA_KEY_IN_CONTAINER,
ca_cert_path=PIPELOCK_CA_CERT_IN_CONTAINER,
ca_key_path=PIPELOCK_CA_KEY_IN_CONTAINER,
)
yaml_path.write_text(pipelock_render_yaml(cfg))
yaml_path.chmod(0o600)