From 36d3e7f73944378c7c0aa30f21e07b93fbce16d8 Mon Sep 17 00:00:00 2001 From: didericis Date: Mon, 11 May 2026 19:41:32 -0400 Subject: [PATCH] refactor(docker): move provision_skills into provision/skills.py --- claude_bottle/backend/docker/backend.py | 45 +------------- .../backend/docker/provision/skills.py | 62 +++++++++++++++++++ 2 files changed, 64 insertions(+), 43 deletions(-) create mode 100644 claude_bottle/backend/docker/provision/skills.py diff --git a/claude_bottle/backend/docker/backend.py b/claude_bottle/backend/docker/backend.py index b848337..d0a588d 100644 --- a/claude_bottle/backend/docker/backend.py +++ b/claude_bottle/backend/docker/backend.py @@ -36,6 +36,7 @@ from .pipelock import ( pipelock_proxy_url, ) from .provision import prompt as _prompt +from .provision import skills as _skills # Where the repo root lives, for `docker build` context. Computed once. @@ -300,50 +301,8 @@ class DockerBottleBackend(BottleBackend): ) def provision_skills(self, plan: BottlePlan, target: str) -> None: - """Copy each of the agent's named skills from the host's - ~/.claude/skills// into the container's equivalent path. - For each skill: ensure parent dir, wipe any prior copy, then - `docker cp /. :/` so the contents are - copied into a freshly-created destination dir. No-op when the - agent has no skills.""" assert isinstance(plan, DockerBottlePlan) - agent = plan.spec.manifest.agents[plan.spec.agent_name] - if not agent.skills: - return - - container = target - container_home = os.environ.get("CLAUDE_BOTTLE_CONTAINER_HOME", "/home/node") - skills_dir = os.environ.get( - "CLAUDE_BOTTLE_CONTAINER_SKILLS_DIR", f"{container_home}/.claude/skills" - ) - - subprocess.run( - ["docker", "exec", container, "mkdir", "-p", skills_dir], - stdout=subprocess.DEVNULL, - check=True, - ) - - for n in agent.skills: - src = host_skill_dir(n) - if not os.path.isdir(src): - die(f"skill '{n}' disappeared from host between validation and copy at {src}.") - dst = f"{skills_dir}/{n}" - info(f"copying skill {n} into {container}:{dst}") - subprocess.run( - ["docker", "exec", container, "rm", "-rf", dst], - stdout=subprocess.DEVNULL, - check=True, - ) - subprocess.run( - ["docker", "exec", container, "mkdir", "-p", dst], - stdout=subprocess.DEVNULL, - check=True, - ) - subprocess.run( - ["docker", "cp", f"{src}/.", f"{container}:{dst}/"], - stdout=subprocess.DEVNULL, - check=True, - ) + _skills.provision_skills(plan, target) def validate_ssh_entries(self, entries: Sequence[SshEntry]) -> None: """Each entry's IdentityFile must exist on the host (after diff --git a/claude_bottle/backend/docker/provision/skills.py b/claude_bottle/backend/docker/provision/skills.py new file mode 100644 index 0000000..410b76e --- /dev/null +++ b/claude_bottle/backend/docker/provision/skills.py @@ -0,0 +1,62 @@ +"""Copy host-side skill directories into a running Docker bottle. + +Skills are validated on the host before launch by +`DockerBottleBackend.validate_skills`; this module assumes that +validation has already run. A skill disappearing between validation +and copy still dies loudly rather than silently producing a partial +container.""" + +from __future__ import annotations + +import os +import subprocess + +from ....log import die, info +from ...util import host_skill_dir +from ..bottle_plan import DockerBottlePlan + + +def provision_skills(plan: DockerBottlePlan, target: str) -> None: + """Copy each of the agent's named skills from the host's + ~/.claude/skills// into the container's equivalent path. + For each skill: ensure parent dir, wipe any prior copy, then + `docker cp /. :/` so the contents are + copied into a freshly-created destination dir. No-op when the + agent has no skills.""" + agent = plan.spec.manifest.agents[plan.spec.agent_name] + if not agent.skills: + return + + container = target + container_home = os.environ.get("CLAUDE_BOTTLE_CONTAINER_HOME", "/home/node") + skills_dir = os.environ.get( + "CLAUDE_BOTTLE_CONTAINER_SKILLS_DIR", f"{container_home}/.claude/skills" + ) + + subprocess.run( + ["docker", "exec", container, "mkdir", "-p", skills_dir], + stdout=subprocess.DEVNULL, + check=True, + ) + + for n in agent.skills: + src = host_skill_dir(n) + if not os.path.isdir(src): + die(f"skill '{n}' disappeared from host between validation and copy at {src}.") + dst = f"{skills_dir}/{n}" + info(f"copying skill {n} into {container}:{dst}") + subprocess.run( + ["docker", "exec", container, "rm", "-rf", dst], + stdout=subprocess.DEVNULL, + check=True, + ) + subprocess.run( + ["docker", "exec", container, "mkdir", "-p", dst], + stdout=subprocess.DEVNULL, + check=True, + ) + subprocess.run( + ["docker", "cp", f"{src}/.", f"{container}:{dst}/"], + stdout=subprocess.DEVNULL, + check=True, + )