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
+7 -14
View File
@@ -1,11 +1,11 @@
"""DockerEgress — the Docker-specific lifecycle for the """Docker-side egress helpers: port pin, in-container CA paths,
per-bottle egress sidecar (PRD 0017). Inherits the platform- container naming, and the host-side mitmproxy CA mint. The
agnostic prepare step (route lift + routes.yaml render + token-env prepare-time routes-yaml rendering itself lives on the
map derivation) from `Egress`. platform-neutral `Egress` ABC — backends instantiate it directly.
Chunks 1+2 of the PRD: the lifecycle is implemented and wired into The per-container `.start()` / `.stop()` lifecycle was removed in
launch.py — cred-proxy is gone. Chunk 3 retargets the cred-proxy- PRD 0024 chunk 3; the sidecar bundle (PRD 0024) runs egress
block remediation flow (PRD 0014).""" under its python init supervisor."""
from __future__ import annotations from __future__ import annotations
@@ -13,7 +13,6 @@ import os
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from ...egress import Egress
from ...log import die from ...log import die
@@ -130,9 +129,3 @@ def egress_tls_init(stage_dir: Path) -> tuple[Path, Path]:
mitm.write_bytes(cert_path.read_bytes() + key_path.read_bytes()) mitm.write_bytes(cert_path.read_bytes() + key_path.read_bytes())
mitm.chmod(0o644) mitm.chmod(0o644)
return (mitm, cert_path) return (mitm, cert_path)
class DockerEgress(Egress):
"""Docker-flavored Egress: inherits `.prepare()` from the base.
Container lifecycle is owned by compose; per-container
`.start()` / `.stop()` were removed in PRD 0024 chunk 3."""
+6 -13
View File
@@ -1,13 +1,12 @@
"""DockerGitGate — Docker-flavored git-gate config (PRD 0008). """Docker-side git-gate helpers: in-container paths the renderer's
Inherits the platform-agnostic prepare step (upstream lift + bind-mounts target, port pin, and container naming. The
entrypoint/hook render) from `GitGate`. The git-gate daemon runs prepare-time entrypoint/hook render lives on the platform-neutral
inside the sidecar bundle (PRD 0024); this module just holds the `GitGate` ABC — backends instantiate it directly. The git-gate
in-container paths the renderer's bind-mounts target.""" daemon's container lifecycle is owned by the sidecar bundle
(PRD 0024)."""
from __future__ import annotations from __future__ import annotations
from ...git_gate import GitGate
GIT_GATE_ENTRYPOINT_IN_CONTAINER = "/git-gate-entrypoint.sh" GIT_GATE_ENTRYPOINT_IN_CONTAINER = "/git-gate-entrypoint.sh"
GIT_GATE_HOOK_IN_CONTAINER = "/etc/git-gate/pre-receive" GIT_GATE_HOOK_IN_CONTAINER = "/etc/git-gate/pre-receive"
@@ -31,9 +30,3 @@ def git_gate_host(slug: str) -> str:
the bundle's network alias to the bundle container, where the the bundle's network alias to the bundle container, where the
git-gate daemon listens on GIT_GATE_PORT.""" git-gate daemon listens on GIT_GATE_PORT."""
return git_gate_container_name(slug) return git_gate_container_name(slug)
class DockerGitGate(GitGate):
"""Docker-flavored GitGate: inherits `.prepare()` from the base.
The git-gate daemon's container lifecycle is owned by the
sidecar bundle (PRD 0024)."""
+13 -25
View File
@@ -1,14 +1,12 @@
"""DockerPipelockProxy — the Docker-specific implementation of the """Docker-side pipelock helpers: image pin, container naming, and
sidecar's `.prepare()` step + in-container CA path constants. the one-shot `pipelock tls init` host-side CA mint. The
Inherits the platform-agnostic YAML-config generation from prepare-time YAML rendering itself lives on the platform-neutral
PipelockProxy. `PipelockProxy` ABC — backends instantiate it directly.
The per-container `.start()` / `.stop()` lifecycle was deleted in The per-container `.start()` / `.stop()` lifecycle was deleted in
PRD 0024 chunk 3 compose-up owns the container lifecycle (PRD PRD 0024 chunk 3; compose-up owns the container lifecycle (PRD
0018) and the bundle path (PRD 0024) collapses pipelock + egress 0018) and the bundle path (PRD 0024) collapses pipelock + egress
+ git-gate + supervise into one container. What remains here is + git-gate + supervise into one container."""
the prepare-time YAML rendering + the CA path constants the
compose renderer reads."""
from __future__ import annotations from __future__ import annotations
@@ -17,7 +15,13 @@ import subprocess
from pathlib import Path from pathlib import Path
from ...log import die from ...log import die
from ...pipelock import PipelockProxy # Re-exported for the compose renderer + smolmachines launch step
# (they used to import these from this module before they moved to
# the platform-neutral pipelock module).
from ...pipelock import ( # noqa: F401
PIPELOCK_CA_CERT_IN_CONTAINER,
PIPELOCK_CA_KEY_IN_CONTAINER,
)
# Pipelock image, pinned by digest. The digest is the multi-arch image # Pipelock image, pinned by digest. The digest is the multi-arch image
@@ -30,12 +34,6 @@ PIPELOCK_IMAGE = os.environ.get(
# Listening port for pipelock's forward proxy. # Listening port for pipelock's forward proxy.
PIPELOCK_PORT = os.environ.get("CLAUDE_BOTTLE_PIPELOCK_PORT", "8888") PIPELOCK_PORT = os.environ.get("CLAUDE_BOTTLE_PIPELOCK_PORT", "8888")
# In-container paths where the per-bottle CA cert + key land via
# the compose renderer's bind-mounts. Pipelock's rendered YAML
# references these paths under `tls_interception`.
PIPELOCK_CA_CERT_IN_CONTAINER = "/etc/pipelock-ca.pem"
PIPELOCK_CA_KEY_IN_CONTAINER = "/etc/pipelock-ca-key.pem"
def pipelock_container_name(slug: str) -> str: def pipelock_container_name(slug: str) -> str:
return f"claude-bottle-pipelock-{slug}" return f"claude-bottle-pipelock-{slug}"
@@ -82,13 +80,3 @@ def pipelock_tls_init(stage_dir: Path) -> tuple[Path, Path]:
key.chmod(0o600) key.chmod(0o600)
cert.chmod(0o644) cert.chmod(0o644)
return (cert, key) return (cert, key)
class DockerPipelockProxy(PipelockProxy):
"""Docker-flavored PipelockProxy: inherits `.prepare()` from the
base, exposes the in-container CA paths the renderer reads.
Container lifecycle is owned by compose."""
CA_CERT_IN_CONTAINER = PIPELOCK_CA_CERT_IN_CONTAINER
CA_KEY_IN_CONTAINER = PIPELOCK_CA_KEY_IN_CONTAINER
+12 -8
View File
@@ -14,13 +14,17 @@ import os
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from ...egress import Egress
from ...env import ResolvedEnv, resolve_env from ...env import ResolvedEnv, resolve_env
from ...git_gate import GitGate
from ...log import die from ...log import die
from ...pipelock import PipelockProxy
from ...supervise import Supervise
from .. import BottleSpec from .. import BottleSpec
from . import util as docker_mod from . import util as docker_mod
from .bottle_plan import DockerBottlePlan from .bottle_plan import DockerBottlePlan
from .egress import DockerEgress, egress_container_name from .egress import egress_container_name
from .git_gate import DockerGitGate, git_gate_container_name from .git_gate import git_gate_container_name
from .bottle_state import ( from .bottle_state import (
BottleMetadata, BottleMetadata,
agent_state_dir, agent_state_dir,
@@ -35,8 +39,8 @@ from .bottle_state import (
supervise_state_dir, supervise_state_dir,
write_metadata, write_metadata,
) )
from .pipelock import DockerPipelockProxy, pipelock_container_name from .pipelock import pipelock_container_name
from .supervise import DockerSupervise, supervise_container_name from .supervise import supervise_container_name
def resolve_plan( def resolve_plan(
@@ -49,10 +53,10 @@ def resolve_plan(
validation already ran in the base class.""" validation already ran in the base class."""
docker_mod.require_docker() docker_mod.require_docker()
proxy = DockerPipelockProxy() proxy = PipelockProxy()
git_gate = DockerGitGate() git_gate = GitGate()
egress = DockerEgress() egress = Egress()
supervise = DockerSupervise() supervise = Supervise()
manifest = spec.manifest manifest = spec.manifest
agent = manifest.agents[spec.agent_name] agent = manifest.agents[spec.agent_name]
+7 -13
View File
@@ -1,13 +1,13 @@
"""DockerSupervise — Docker-flavored supervise config (PRD 0013). """Docker-side supervise helpers: container naming for the legacy
Inherits the platform-agnostic prepare step (queue dir + per-sidecar topology (kept so the bundle's docker-network alias
current-config staging) from `Supervise`. The supervise daemon resolves the old name to the bundle IP). The prepare-time
runs inside the sidecar bundle (PRD 0024); this module just holds queue-dir + current-config staging lives on the platform-neutral
the container-name helper the renderer's network alias targets.""" `Supervise` ABC — backends instantiate it directly. The
supervise daemon's container lifecycle is owned by the sidecar
bundle (PRD 0024)."""
from __future__ import annotations from __future__ import annotations
from ...supervise import Supervise
def supervise_container_name(slug: str) -> str: def supervise_container_name(slug: str) -> str:
"""The legacy per-sidecar container name. Kept as a function so """The legacy per-sidecar container name. Kept as a function so
@@ -15,9 +15,3 @@ def supervise_container_name(slug: str) -> str:
bundle — any code still referring to bundle — any code still referring to
`claude-bottle-supervise-<slug>` resolves to the bundle's IP.""" `claude-bottle-supervise-<slug>` resolves to the bundle's IP."""
return f"claude-bottle-supervise-{slug}" return f"claude-bottle-supervise-{slug}"
class DockerSupervise(Supervise):
"""Docker-flavored Supervise: inherits `.prepare()` from the base.
The supervise daemon's container lifecycle is owned by the
sidecar bundle (PRD 0024)."""
+5 -5
View File
@@ -25,6 +25,10 @@ from contextlib import ExitStack, contextmanager
from typing import Callable, Generator from typing import Callable, Generator
from ...egress import EGRESS_ROUTES_IN_CONTAINER, egress_resolve_token_values from ...egress import EGRESS_ROUTES_IN_CONTAINER, egress_resolve_token_values
from ...pipelock import (
PIPELOCK_CA_CERT_IN_CONTAINER,
PIPELOCK_CA_KEY_IN_CONTAINER,
)
from ...supervise import QUEUE_DIR_IN_CONTAINER, SUPERVISE_PORT from ...supervise import QUEUE_DIR_IN_CONTAINER, SUPERVISE_PORT
from ...util import expand_tilde from ...util import expand_tilde
from ..docker.egress import ( from ..docker.egress import (
@@ -38,11 +42,7 @@ from ..docker.git_gate import (
GIT_GATE_ENTRYPOINT_IN_CONTAINER, GIT_GATE_ENTRYPOINT_IN_CONTAINER,
GIT_GATE_HOOK_IN_CONTAINER, GIT_GATE_HOOK_IN_CONTAINER,
) )
from ..docker.pipelock import ( from ..docker.pipelock import pipelock_tls_init
PIPELOCK_CA_CERT_IN_CONTAINER,
PIPELOCK_CA_KEY_IN_CONTAINER,
pipelock_tls_init,
)
from . import sidecar_bundle as _bundle from . import sidecar_bundle as _bundle
from . import smolvm as _smolvm from . import smolvm as _smolvm
from .bottle import SmolmachinesBottle from .bottle import SmolmachinesBottle
+14 -16
View File
@@ -21,10 +21,10 @@ from ...backend.docker.bottle_state import (
supervise_state_dir, supervise_state_dir,
write_metadata, write_metadata,
) )
from ...backend.docker.egress import DockerEgress from ...egress import Egress
from ...backend.docker.git_gate import DockerGitGate from ...git_gate import GitGate
from ...backend.docker.pipelock import DockerPipelockProxy from ...pipelock import PipelockProxy
from ...backend.docker.supervise import DockerSupervise from ...supervise import Supervise
from . import smolvm as _smolvm from . import smolvm as _smolvm
from .bottle_plan import SmolmachinesBottlePlan from .bottle_plan import SmolmachinesBottlePlan
from .util import smolmachines_bundle_subnet, smolmachines_preflight from .util import smolmachines_bundle_subnet, smolmachines_preflight
@@ -94,31 +94,29 @@ def resolve_plan(
f"http://{bundle_ip}:{_BUNDLE_SUPERVISE_PORT}" f"http://{bundle_ip}:{_BUNDLE_SUPERVISE_PORT}"
) )
# Inner Plans for the four bundle daemons. Use the docker # Inner Plans for the four bundle daemons. The ABCs are
# backend's concrete subclasses — the `.prepare()` method # platform-neutral — `.prepare()` writes config files + returns
# they inherit is platform-neutral (writes config files + # a Plan dataclass with no backend-specific assumptions. State
# returns a Plan dataclass); the docker-specific subclasses # dirs are still keyed by slug under the docker backend's
# exist only to satisfy ABC instantiation. Future: factor # bottle_state layout (shared on-host convention; not a docker
# the prepare logic out of the docker subpackage so # dependency).
# smolmachines doesn't have to reach across the backend
# boundary.
pipelock_dir = pipelock_state_dir(slug) pipelock_dir = pipelock_state_dir(slug)
pipelock_dir.mkdir(parents=True, exist_ok=True) pipelock_dir.mkdir(parents=True, exist_ok=True)
proxy_plan = DockerPipelockProxy().prepare(bottle, slug, pipelock_dir) proxy_plan = PipelockProxy().prepare(bottle, slug, pipelock_dir)
git_gate_dir = git_gate_state_dir(slug) git_gate_dir = git_gate_state_dir(slug)
git_gate_dir.mkdir(parents=True, exist_ok=True) git_gate_dir.mkdir(parents=True, exist_ok=True)
git_gate_plan = DockerGitGate().prepare(bottle, slug, git_gate_dir) git_gate_plan = GitGate().prepare(bottle, slug, git_gate_dir)
egress_dir = egress_state_dir(slug) egress_dir = egress_state_dir(slug)
egress_dir.mkdir(parents=True, exist_ok=True) egress_dir.mkdir(parents=True, exist_ok=True)
egress_plan = DockerEgress().prepare(bottle, slug, egress_dir) egress_plan = Egress().prepare(bottle, slug, egress_dir)
supervise_plan = None supervise_plan = None
if bottle.supervise: if bottle.supervise:
supervise_dir = supervise_state_dir(slug) supervise_dir = supervise_state_dir(slug)
supervise_dir.mkdir(parents=True, exist_ok=True) supervise_dir.mkdir(parents=True, exist_ok=True)
supervise_plan = DockerSupervise().prepare(slug, supervise_dir) supervise_plan = Supervise().prepare(slug, supervise_dir)
# Prompt file is always written (mode 0o600) so the in-VM # Prompt file is always written (mode 0o600) so the in-VM
# path always exists. Content is the agent's `prompt` # path always exists. Content is the agent's `prompt`
+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 __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import cast 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 -------------------------------------------------- # --- Allowlist resolution --------------------------------------------------
@@ -301,25 +309,22 @@ class PipelockProxyPlan:
ca_key_host_path: Path = Path() ca_key_host_path: Path = Path()
class PipelockProxy(ABC): class PipelockProxy:
"""The pipelock egress proxy. Encapsulates the YAML-config """The pipelock egress proxy. Encapsulates the YAML-config
generation; the sidecar's start/stop lifecycle is backend-specific generation; the container lifecycle is owned by whatever
and lives on concrete subclasses. wraps the daemon (compose-managed pipelock container on docker,
sidecar-bundle PID 1 on smolmachines).
The class-level constants `CA_CERT_IN_CONTAINER` / Backends instantiate the class directly — there are no
`CA_KEY_IN_CONTAINER` are the in-container paths the YAML config platform-specific subclasses; the in-container CA paths are
references — they correspond to wherever the backend's `.start` universal module-level constants
places the CA cert and key inside the sidecar. Subclasses (`PIPELOCK_CA_CERT_IN_CONTAINER` / `PIPELOCK_CA_KEY_IN_CONTAINER`)."""
override the constants."""
CA_CERT_IN_CONTAINER: str = ""
CA_KEY_IN_CONTAINER: str = ""
def prepare( def prepare(
self, bottle: Bottle, slug: str, stage_dir: Path self, bottle: Bottle, slug: str, stage_dir: Path
) -> PipelockProxyPlan: ) -> PipelockProxyPlan:
"""Write the pipelock yaml config (mode 600) under `stage_dir` """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. subprocess.
`slug` is the agent-derived identifier (lowercased, `slug` is the agent-derived identifier (lowercased,
@@ -327,18 +332,18 @@ class PipelockProxy(ABC):
resource name — the agent container, the pipelock container resource name — the agent container, the pipelock container
(`claude-bottle-pipelock-<slug>`), the internal/egress (`claude-bottle-pipelock-<slug>`), the internal/egress
networks. It's stored on the returned plan so the backend's 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 The CA paths the YAML references are the module-level
from the concrete subclass's class-level constants. The in-container constants. The host-side counterparts are
host-side counterparts are generated by the launch step generated by the launch step (not here, so prepare stays
(not here, so prepare stays side-effect-free on docker) and side-effect-free on docker) and added to the plan via
added to the plan via `dataclasses.replace` before `.start`.""" `dataclasses.replace` before the daemon starts."""
yaml_path = stage_dir / "pipelock.yaml" yaml_path = stage_dir / "pipelock.yaml"
cfg = pipelock_build_config( cfg = pipelock_build_config(
bottle, bottle,
ca_cert_path=self.CA_CERT_IN_CONTAINER, ca_cert_path=PIPELOCK_CA_CERT_IN_CONTAINER,
ca_key_path=self.CA_KEY_IN_CONTAINER, ca_key_path=PIPELOCK_CA_KEY_IN_CONTAINER,
) )
yaml_path.write_text(pipelock_render_yaml(cfg)) yaml_path.write_text(pipelock_render_yaml(cfg))
yaml_path.chmod(0o600) yaml_path.chmod(0o600)
+2 -2
View File
@@ -35,9 +35,9 @@ from claude_bottle.backend.docker.network import (
from claude_bottle.backend.docker.pipelock import ( from claude_bottle.backend.docker.pipelock import (
PIPELOCK_CA_CERT_IN_CONTAINER, PIPELOCK_CA_CERT_IN_CONTAINER,
PIPELOCK_CA_KEY_IN_CONTAINER, PIPELOCK_CA_KEY_IN_CONTAINER,
DockerPipelockProxy,
pipelock_tls_init, pipelock_tls_init,
) )
from claude_bottle.pipelock import PipelockProxy
from claude_bottle.backend.docker.pipelock_apply import ( from claude_bottle.backend.docker.pipelock_apply import (
PipelockApplyError, PipelockApplyError,
apply_allowlist_change, apply_allowlist_change,
@@ -99,7 +99,7 @@ class TestPipelockApply(unittest.TestCase):
the updated config.""" the updated config."""
state_dir = pipelock_state_dir(self.slug) state_dir = pipelock_state_dir(self.slug)
state_dir.mkdir(parents=True, exist_ok=True) state_dir.mkdir(parents=True, exist_ok=True)
prep = DockerPipelockProxy().prepare( prep = PipelockProxy().prepare(
fixture_minimal().bottles["dev"], self.slug, state_dir, fixture_minimal().bottles["dev"], self.slug, state_dir,
) )
self.internal_net = network_create_internal(self.slug) self.internal_net = network_create_internal(self.slug)
+7 -7
View File
@@ -12,10 +12,10 @@ import unittest
from pathlib import Path from pathlib import Path
from typing import Any, cast from typing import Any, cast
from claude_bottle.backend.docker.pipelock import DockerPipelockProxy
from claude_bottle.manifest import Manifest from claude_bottle.manifest import Manifest
from claude_bottle.pipelock import ( from claude_bottle.pipelock import (
DEFAULT_TLS_PASSTHROUGH, DEFAULT_TLS_PASSTHROUGH,
PipelockProxy,
pipelock_build_config, pipelock_build_config,
pipelock_render_yaml, pipelock_render_yaml,
) )
@@ -54,7 +54,7 @@ class TestBuildConfig(unittest.TestCase):
self.assertNotIn("tls_interception", cfg) self.assertNotIn("tls_interception", cfg)
def test_tls_interception_block_emitted_when_paths_supplied(self): 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 # constants; this directly pins the dict shape. passthrough_domains
# is baked in so LLM provider endpoints (api.anthropic.com) skip # is baked in so LLM provider endpoints (api.anthropic.com) skip
# MITM — pipelock's docs explicitly recommend this for LLM hosts, # MITM — pipelock's docs explicitly recommend this for LLM hosts,
@@ -152,7 +152,7 @@ class TestRenderAndWrite(unittest.TestCase):
self.assertNotIn("ssrf:", text) self.assertNotIn("ssrf:", text)
def test_prepare_writes_file_at_mode_600(self): def test_prepare_writes_file_at_mode_600(self):
plan = DockerPipelockProxy().prepare( plan = PipelockProxy().prepare(
fixture_minimal().bottles["dev"], "demo", self.out_dir fixture_minimal().bottles["dev"], "demo", self.out_dir
) )
self.assertEqual(0o600, os.stat(plan.yaml_path).st_mode & 0o777) 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"}}, "agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
}) })
plan = DockerPipelockProxy().prepare( plan = PipelockProxy().prepare(
manifest.bottles["dev"], "demo", self.out_dir manifest.bottles["dev"], "demo", self.out_dir
) )
content = plan.yaml_path.read_text() content = plan.yaml_path.read_text()
@@ -179,13 +179,13 @@ class TestRenderAndWrite(unittest.TestCase):
self.assertNotIn("prompt-message", content) self.assertNotIn("prompt-message", content)
def test_render_emits_tls_interception_via_prepare(self): def test_render_emits_tls_interception_via_prepare(self):
"""`DockerPipelockProxy.prepare` plumbs its in-container CA """`PipelockProxy.prepare` plumbs the module-level in-container
constants through to the YAML. The block should land in the CA constants through to the YAML. The block should land in the
rendered output with `enabled: true`, the configured paths, rendered output with `enabled: true`, the configured paths,
and the baked LLM-provider passthrough list. The actual and the baked LLM-provider passthrough list. The actual
host-side CA generation happens in launch (not prepare), so host-side CA generation happens in launch (not prepare), so
this test exercises only the YAML rendering.""" this test exercises only the YAML rendering."""
plan = DockerPipelockProxy().prepare( plan = PipelockProxy().prepare(
fixture_minimal().bottles["dev"], "demo", self.out_dir fixture_minimal().bottles["dev"], "demo", self.out_dir
) )
content = plan.yaml_path.read_text() content = plan.yaml_path.read_text()