PRD 0004: Split out provisioners #7

Merged
didericis merged 5 commits from split-out-provisioners into main 2026-05-11 19:47:27 -04:00
3 changed files with 53 additions and 28 deletions
Showing only changes of commit 1b17b36988 - Show all commits
+2 -28
View File
@@ -35,6 +35,7 @@ from .pipelock import (
pipelock_proxy_host_port, pipelock_proxy_host_port,
pipelock_proxy_url, pipelock_proxy_url,
) )
from .provision import prompt as _prompt
# Where the repo root lives, for `docker build` context. Computed once. # 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}") info(f"name conflict; retrying as {container}")
def provision_prompt(self, plan: BottlePlan, target: str) -> str | None: 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) assert isinstance(plan, DockerBottlePlan)
container = target return _prompt.provision_prompt(plan, 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
def validate_skills(self, skills: list[str]) -> None: def validate_skills(self, skills: list[str]) -> None:
"""Fail loudly if any named skill is missing from the host's """Fail loudly if any named skill is missing from the host's
@@ -0,0 +1,8 @@
"""Per-provisioner modules for the Docker backend.
Each module exports one top-level function:
provision_<thing>(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."""
@@ -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