diff --git a/claude_bottle/backend/docker/backend.py b/claude_bottle/backend/docker/backend.py index 3f97db3..b848337 100644 --- a/claude_bottle/backend/docker/backend.py +++ b/claude_bottle/backend/docker/backend.py @@ -35,6 +35,7 @@ from .pipelock import ( pipelock_proxy_host_port, pipelock_proxy_url, ) +from .provision import prompt as _prompt # Where the repo root lives, for `docker build` context. Computed once. @@ -282,35 +283,8 @@ class DockerBottleBackend(BottleBackend): info(f"name conflict; retrying as {container}") def provision_prompt(self, plan: BottlePlan, target: str) -> str | None: - """Copy the prompt file into the container, fix ownership/mode. - Returns the in-container path if the agent has a non-empty - prompt (drives --append-system-prompt-file), else None. The - file is copied either way so the path always exists.""" assert isinstance(plan, DockerBottlePlan) - container = target - container_home = os.environ.get("CLAUDE_BOTTLE_CONTAINER_HOME", "/home/node") - in_container_prompt_path = f"{container_home}/.claude-bottle-prompt.txt" - - subprocess.run( - ["docker", "cp", str(plan.prompt_file), f"{container}:{in_container_prompt_path}"], - stdout=subprocess.DEVNULL, - check=True, - ) - # `docker cp` preserves host UID; re-own/mode as root so node - # can read its own mode-600 prompt regardless of host UID. - subprocess.run( - ["docker", "exec", "-u", "0", container, "chown", "node:node", in_container_prompt_path], - stdout=subprocess.DEVNULL, - check=True, - ) - subprocess.run( - ["docker", "exec", "-u", "0", container, "chmod", "600", in_container_prompt_path], - stdout=subprocess.DEVNULL, - check=True, - ) - - agent = plan.spec.manifest.agents[plan.spec.agent_name] - return in_container_prompt_path if agent.prompt else None + return _prompt.provision_prompt(plan, target) def validate_skills(self, skills: list[str]) -> None: """Fail loudly if any named skill is missing from the host's diff --git a/claude_bottle/backend/docker/provision/__init__.py b/claude_bottle/backend/docker/provision/__init__.py new file mode 100644 index 0000000..2f66425 --- /dev/null +++ b/claude_bottle/backend/docker/provision/__init__.py @@ -0,0 +1,8 @@ +"""Per-provisioner modules for the Docker backend. + +Each module exports one top-level function: + provision_(plan: DockerBottlePlan, target: str) -> ... + +`DockerBottleBackend.provision_*` methods delegate to these. The +abstract `BottleBackend.provision_*` surface is unchanged; this +subpackage exists only to keep `backend.py` from being a god-file.""" diff --git a/claude_bottle/backend/docker/provision/prompt.py b/claude_bottle/backend/docker/provision/prompt.py new file mode 100644 index 0000000..29df62c --- /dev/null +++ b/claude_bottle/backend/docker/provision/prompt.py @@ -0,0 +1,43 @@ +"""Copy the agent prompt into a running Docker bottle. + +The prompt file is always copied (so the in-container path always +exists) but `--append-system-prompt-file` only fires when the agent +actually has a prompt — the return value signals which case.""" + +from __future__ import annotations + +import os +import subprocess + +from ..bottle_plan import DockerBottlePlan + + +def provision_prompt(plan: DockerBottlePlan, target: str) -> str | None: + """Copy the prompt file into the container, fix ownership/mode. + Returns the in-container path if the agent has a non-empty + prompt (drives --append-system-prompt-file), else None. The + file is copied either way so the path always exists.""" + container = target + container_home = os.environ.get("CLAUDE_BOTTLE_CONTAINER_HOME", "/home/node") + in_container_prompt_path = f"{container_home}/.claude-bottle-prompt.txt" + + subprocess.run( + ["docker", "cp", str(plan.prompt_file), f"{container}:{in_container_prompt_path}"], + stdout=subprocess.DEVNULL, + check=True, + ) + # `docker cp` preserves host UID; re-own/mode as root so node + # can read its own mode-600 prompt regardless of host UID. + subprocess.run( + ["docker", "exec", "-u", "0", container, "chown", "node:node", in_container_prompt_path], + stdout=subprocess.DEVNULL, + check=True, + ) + subprocess.run( + ["docker", "exec", "-u", "0", container, "chmod", "600", in_container_prompt_path], + stdout=subprocess.DEVNULL, + check=True, + ) + + agent = plan.spec.manifest.agents[plan.spec.agent_name] + return in_container_prompt_path if agent.prompt else None