refactor(util): split private helpers off DockerBottleBackend
test / run tests/run_tests.py (pull_request) Successful in 14s
test / run tests/run_tests.py (pull_request) Successful in 14s
New file claude_bottle/backend/util.py for cross-backend host-side
helpers:
host_skill_dir(name) — resolves $HOME/.claude/skills/<name>
docker/util.py gains:
docker_exec_root(container, argv) — `docker exec -u 0` wrapper used
by SSH provisioning
DockerBottleBackend drops the two methods that wrapped these
(`_host_skill_dir`, `_docker_exec_root`) — they had no instance state
and just lived on the class for organizational reasons. Call sites
now use the imported functions directly.
This commit is contained in:
@@ -24,6 +24,7 @@ from ...log import die, info
|
||||
from ...manifest import SshEntry
|
||||
from ...util import expand_tilde
|
||||
from .. import BottleBackend, BottleCleanupPlan, BottlePlan, BottleSpec
|
||||
from ..util import host_skill_dir
|
||||
from . import network as network_mod
|
||||
from . import util as docker_mod
|
||||
from .bottle import DockerBottle
|
||||
@@ -307,19 +308,13 @@ class DockerBottleBackend(BottleBackend):
|
||||
user doesn't get a launch prompt for a plan that's already
|
||||
known to break."""
|
||||
for name in skills:
|
||||
path = self._host_skill_dir(name)
|
||||
path = host_skill_dir(name)
|
||||
if not os.path.isdir(path):
|
||||
die(
|
||||
f"skill '{name}' not found on host at {path}. "
|
||||
f"Create it under ~/.claude/skills/, then re-run."
|
||||
)
|
||||
|
||||
def _host_skill_dir(self, name: str) -> str:
|
||||
home = os.environ.get("HOME")
|
||||
if not home:
|
||||
die("HOME not set")
|
||||
return f"{home}/.claude/skills/{name}"
|
||||
|
||||
def provision_skills(self, plan: BottlePlan, target: str) -> None:
|
||||
"""Copy each of the agent's named skills from the host's
|
||||
~/.claude/skills/<name>/ into the container's equivalent path.
|
||||
@@ -345,7 +340,7 @@ class DockerBottleBackend(BottleBackend):
|
||||
)
|
||||
|
||||
for n in agent.skills:
|
||||
src = self._host_skill_dir(n)
|
||||
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}"
|
||||
@@ -423,14 +418,14 @@ class DockerBottleBackend(BottleBackend):
|
||||
keys_dir = "/root/.claude-bottle-keys"
|
||||
|
||||
# ~/.ssh for node (700, owned by node).
|
||||
self._docker_exec_root(container, ["mkdir", "-p", container_ssh])
|
||||
self._docker_exec_root(container, ["chown", "node:node", container_ssh])
|
||||
self._docker_exec_root(container, ["chmod", "700", container_ssh])
|
||||
docker_mod.docker_exec_root(container, ["mkdir", "-p", container_ssh])
|
||||
docker_mod.docker_exec_root(container, ["chown", "node:node", container_ssh])
|
||||
docker_mod.docker_exec_root(container, ["chmod", "700", container_ssh])
|
||||
|
||||
# /root/.claude-bottle-keys for root (700, root-owned).
|
||||
self._docker_exec_root(container, ["mkdir", "-p", keys_dir])
|
||||
self._docker_exec_root(container, ["chown", "root:root", keys_dir])
|
||||
self._docker_exec_root(container, ["chmod", "700", keys_dir])
|
||||
docker_mod.docker_exec_root(container, ["mkdir", "-p", keys_dir])
|
||||
docker_mod.docker_exec_root(container, ["chown", "root:root", keys_dir])
|
||||
docker_mod.docker_exec_root(container, ["chmod", "700", keys_dir])
|
||||
|
||||
config_file = plan.stage_dir / "ssh_config"
|
||||
known_hosts_file = plan.stage_dir / "ssh_known_hosts"
|
||||
@@ -459,8 +454,8 @@ class DockerBottleBackend(BottleBackend):
|
||||
stdout=subprocess.DEVNULL,
|
||||
check=True,
|
||||
)
|
||||
self._docker_exec_root(container, ["chown", "root:root", container_key_path])
|
||||
self._docker_exec_root(container, ["chmod", "600", container_key_path])
|
||||
docker_mod.docker_exec_root(container, ["chown", "root:root", container_key_path])
|
||||
docker_mod.docker_exec_root(container, ["chmod", "600", container_key_path])
|
||||
|
||||
container_key_paths.append(container_key_path)
|
||||
|
||||
@@ -533,8 +528,8 @@ class DockerBottleBackend(BottleBackend):
|
||||
stdout=subprocess.DEVNULL,
|
||||
check=True,
|
||||
)
|
||||
self._docker_exec_root(container, ["chown", "node:node", f"{container_ssh}/config"])
|
||||
self._docker_exec_root(container, ["chmod", "600", f"{container_ssh}/config"])
|
||||
docker_mod.docker_exec_root(container, ["chown", "node:node", f"{container_ssh}/config"])
|
||||
docker_mod.docker_exec_root(container, ["chmod", "600", f"{container_ssh}/config"])
|
||||
|
||||
if known_hosts_file.stat().st_size > 0:
|
||||
info(f"writing {container_ssh}/known_hosts")
|
||||
@@ -543,15 +538,8 @@ class DockerBottleBackend(BottleBackend):
|
||||
stdout=subprocess.DEVNULL,
|
||||
check=True,
|
||||
)
|
||||
self._docker_exec_root(container, ["chown", "node:node", f"{container_ssh}/known_hosts"])
|
||||
self._docker_exec_root(container, ["chmod", "600", f"{container_ssh}/known_hosts"])
|
||||
|
||||
def _docker_exec_root(self, container: str, argv: list[str]) -> None:
|
||||
subprocess.run(
|
||||
["docker", "exec", "-u", "0", container, *argv],
|
||||
stdout=subprocess.DEVNULL,
|
||||
check=True,
|
||||
)
|
||||
docker_mod.docker_exec_root(container, ["chown", "node:node", f"{container_ssh}/known_hosts"])
|
||||
docker_mod.docker_exec_root(container, ["chmod", "600", f"{container_ssh}/known_hosts"])
|
||||
|
||||
def provision_git(self, plan: BottlePlan, target: str) -> None:
|
||||
"""If --cwd was set and the host cwd has a .git directory, copy
|
||||
|
||||
@@ -48,6 +48,16 @@ def container_exists(name: str) -> bool:
|
||||
return bool(result.stdout.strip())
|
||||
|
||||
|
||||
def docker_exec_root(container: str, argv: list[str]) -> None:
|
||||
"""Run `docker exec -u 0` in the named container, check=True. Used
|
||||
by SSH provisioning to chown/chmod files that need root."""
|
||||
subprocess.run(
|
||||
["docker", "exec", "-u", "0", container, *argv],
|
||||
stdout=subprocess.DEVNULL,
|
||||
check=True,
|
||||
)
|
||||
|
||||
|
||||
_SLUG_RE = re.compile(r"[^a-z0-9]+")
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
"""Cross-backend utility helpers — host-side primitives shared by
|
||||
every backend implementation. Backend-specific helpers live one level
|
||||
deeper (e.g. claude_bottle/backend/docker/util.py)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from ..log import die
|
||||
|
||||
|
||||
def host_skill_dir(name: str) -> str:
|
||||
"""Return the host-side path for a named skill:
|
||||
`$HOME/.claude/skills/<name>`. Dies if HOME is unset."""
|
||||
home = os.environ.get("HOME")
|
||||
if not home:
|
||||
die("HOME not set")
|
||||
return f"{home}/.claude/skills/{name}"
|
||||
Reference in New Issue
Block a user