refactor(pipelock): take stage_dir, derive yaml_path internally
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:
@@ -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."""
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user