refactor(pipelock): take stage_dir, derive yaml_path internally
test / unit (pull_request) Successful in 11s
test / integration (pull_request) Failing after 12s

PipelockProxy.prepare now accepts (bottle, slug, stage_dir) and derives
the yaml_path itself, so callers don't need to know the filename.
DockerBottleBackend.prepare_proxy becomes a one-line wrapper whose only
caller already has bottle and slug in scope, so it's inlined and
deleted.
This commit is contained in:
2026-05-11 16:50:22 -04:00
parent 479adc625a
commit f943e14891
4 changed files with 12 additions and 22 deletions
+1 -11
View File
@@ -104,7 +104,7 @@ class DockerBottleBackend(BottleBackend):
prompt_file.write_text("") prompt_file.write_text("")
prompt_file.chmod(0o600) prompt_file.chmod(0o600)
proxy_plan = self.prepare_proxy(spec, stage_dir) proxy_plan = self._proxy.prepare(bottle, slug, stage_dir)
resolved = resolve_env(manifest, spec.agent_name) resolved = resolve_env(manifest, spec.agent_name)
self._write_env_files(resolved, env_file, args_file) self._write_env_files(resolved, env_file, args_file)
prompt_file.write_text(agent.prompt) prompt_file.write_text(agent.prompt)
@@ -151,16 +151,6 @@ class DockerBottleBackend(BottleBackend):
args_lines = [f"-e\n{name}" for name in resolved.forwarded] args_lines = [f"-e\n{name}" for name in resolved.forwarded]
args_file.write_text("\n".join(args_lines) + ("\n" if args_lines else "")) args_file.write_text("\n".join(args_lines) + ("\n" if args_lines else ""))
def prepare_proxy(self, spec: BottleSpec, stage_dir: Path) -> pipelock.PipelockProxyPlan:
"""Decide where the pipelock yaml lives in `stage_dir`, delegate
to PipelockProxy to write it, and return the resolved
PipelockProxyPlan for the launch step to consume. Stage-only:
no Docker resources created yet."""
yaml_path = stage_dir / "pipelock.yaml"
bottle = spec.manifest.bottle_for(spec.agent_name)
slug = docker_mod.slugify(spec.agent_name)
return self._proxy.prepare(bottle, slug, yaml_path)
@contextmanager @contextmanager
def launch(self, plan: BottlePlan) -> Iterator[DockerBottle]: def launch(self, plan: BottlePlan) -> Iterator[DockerBottle]:
"""Build, launch, and provision a Docker bottle. Teardown on exit.""" """Build, launch, and provision a Docker bottle. Teardown on exit."""
+3 -2
View File
@@ -177,9 +177,9 @@ class PipelockProxy(ABC):
and lives on concrete subclasses.""" and lives on concrete subclasses."""
def prepare( def prepare(
self, bottle: Bottle, slug: str, yaml_path: Path self, bottle: Bottle, slug: str, stage_dir: Path
) -> PipelockProxyPlan: ) -> PipelockProxyPlan:
"""Write the pipelock yaml config (mode 600) to `yaml_path` """Write the pipelock yaml config (mode 600) under `stage_dir`
and return the plan for `.start`. and return the plan for `.start`.
`slug` is the agent-derived identifier (lowercased, `slug` is the agent-derived identifier (lowercased,
@@ -188,6 +188,7 @@ class PipelockProxy(ABC):
(`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.""" start step can derive the sidecar's container name."""
yaml_path = stage_dir / "pipelock.yaml"
self._build_pipelock_yaml(bottle, yaml_path) self._build_pipelock_yaml(bottle, yaml_path)
return PipelockProxyPlan(yaml_path=yaml_path, slug=slug) return PipelockProxyPlan(yaml_path=yaml_path, slug=slug)
@@ -68,8 +68,7 @@ class TestPipelockSidecarSmoke(unittest.TestCase):
def test_prepare_and_start_yield_healthy_sidecar(self): def test_prepare_and_start_yield_healthy_sidecar(self):
proxy = DockerPipelockProxy() proxy = DockerPipelockProxy()
yaml_path = self.work_dir / "pipelock.yaml" prep = proxy.prepare(fixture_minimal().bottles["dev"], self.slug, self.work_dir)
prep = proxy.prepare(fixture_minimal().bottles["dev"], self.slug, yaml_path)
self.internal_net = network_create_internal(self.slug) self.internal_net = network_create_internal(self.slug)
self.egress_net = network_create_egress(self.slug) self.egress_net = network_create_egress(self.slug)
+7 -7
View File
@@ -66,11 +66,10 @@ class TestRenderAndWrite(unittest.TestCase):
self.assertIn(required, text) self.assertIn(required, text)
def test_prepare_writes_file_at_mode_600(self): def test_prepare_writes_file_at_mode_600(self):
yaml_path = self.out_dir / "min.yaml" plan = DockerPipelockProxy().prepare(
DockerPipelockProxy().prepare( fixture_minimal().bottles["dev"], "demo", self.out_dir
fixture_minimal().bottles["dev"], "demo", yaml_path
) )
self.assertEqual(0o600, os.stat(yaml_path).st_mode & 0o777) self.assertEqual(0o600, os.stat(plan.yaml_path).st_mode & 0o777)
def test_prepare_does_not_leak_env_names_or_values(self): def test_prepare_does_not_leak_env_names_or_values(self):
manifest = Manifest.from_json_obj({ manifest = Manifest.from_json_obj({
@@ -85,9 +84,10 @@ class TestRenderAndWrite(unittest.TestCase):
}, },
"agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}}, "agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
}) })
yaml_path = self.out_dir / "secret.yaml" plan = DockerPipelockProxy().prepare(
DockerPipelockProxy().prepare(manifest.bottles["dev"], "demo", yaml_path) manifest.bottles["dev"], "demo", self.out_dir
content = yaml_path.read_text() )
content = plan.yaml_path.read_text()
self.assertNotIn("literal-value-should-not-appear", content) self.assertNotIn("literal-value-should-not-appear", content)
self.assertNotIn("MY_SECRET", content) self.assertNotIn("MY_SECRET", content)
self.assertNotIn("prompt-message", content) self.assertNotIn("prompt-message", content)