727f30d422
Same line of cleanup as the supervise rename: the per-sidecar container names (`claude-bottle-pipelock-<slug>`, `claude-bottle-egress-<slug>`, `claude-bottle-git-gate-<slug>`) were docker-network aliases pointing at the bundle, kept so legacy URLs would keep resolving. Replaces them with short hostnames (`pipelock`, `egress`, `git-gate`) matching the existing `EGRESS_HOSTNAME` pattern, and inlines the bundle-loopback URL (`http://127.0.0.1:8888`) for the in-bundle egress→pipelock hop — matching what smolmachines already does. Drops the three `*_container_name` functions, `pipelock_proxy_url`, and `git_gate_host`. Their callers move to the new constants: - `PIPELOCK_HOSTNAME = "pipelock"` (claude_bottle/pipelock.py) - `GIT_GATE_HOSTNAME = "git-gate"` (claude_bottle/git_gate.py) - `BUNDLE_LOCAL_PIPELOCK_URL` (backend/docker/pipelock.py) The agent's HTTP_PROXY now reads `http://pipelock:8888` (vs the old `http://claude-bottle-pipelock-<slug>:8888`); the gitconfig insteadOf rewrites become `git://git-gate/<repo>.git`. The prepare- time orphan probe is collapsed onto the bundle container name (`claude-bottle-sidecars-<slug>`) instead of the four legacy per-sidecar names that no backend creates anymore. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
102 lines
3.8 KiB
Python
102 lines
3.8 KiB
Python
"""Git provisioning inside a running Docker bottle.
|
|
|
|
Two concerns, both about git in the agent:
|
|
|
|
1. If --cwd was passed AND the host cwd has a .git, copy that .git
|
|
into /home/node/workspace/.git so the agent operates on the
|
|
user's repo.
|
|
2. If the bottle declares `git` entries (PRD 0008), write a
|
|
~/.gitconfig with insteadOf rules so every git operation
|
|
against a declared upstream (push, fetch, clone, pull,
|
|
ls-remote) transparently hits the per-agent git-gate. The
|
|
gate mirrors the upstream in both directions, so URL
|
|
rewriting is symmetric.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
from ....git_gate import GIT_GATE_HOSTNAME
|
|
from ....log import info
|
|
from ....manifest import GitEntry
|
|
from .. import util as docker_mod
|
|
from ..bottle_plan import DockerBottlePlan
|
|
|
|
|
|
def provision_git(plan: DockerBottlePlan, target: str) -> None:
|
|
"""Set up git inside the bottle. Runs both subcases; each no-ops
|
|
when its condition isn't met."""
|
|
_provision_cwd_git(plan, target)
|
|
_provision_git_gate_config(plan, target)
|
|
|
|
|
|
def _provision_cwd_git(plan: DockerBottlePlan, target: str) -> None:
|
|
"""If --cwd was set and the host cwd has a .git directory, copy
|
|
it into /home/node/workspace/.git and fix ownership. No-op
|
|
otherwise."""
|
|
if not (plan.spec.copy_cwd and Path(plan.spec.user_cwd, ".git").is_dir()):
|
|
return
|
|
container = target
|
|
info(f"copying {plan.spec.user_cwd}/.git -> {container}:/home/node/workspace/.git")
|
|
subprocess.run(
|
|
["docker", "cp", f"{plan.spec.user_cwd}/.git", f"{container}:/home/node/workspace/.git"],
|
|
stdout=subprocess.DEVNULL,
|
|
check=True,
|
|
)
|
|
subprocess.run(
|
|
[
|
|
"docker", "exec", "-u", "0", container,
|
|
"chown", "-R", "node:node", "/home/node/workspace/.git",
|
|
],
|
|
stdout=subprocess.DEVNULL,
|
|
check=True,
|
|
)
|
|
|
|
|
|
def render_git_gate_gitconfig(entries: tuple[GitEntry, ...]) -> str:
|
|
"""Render the ~/.gitconfig content for git-gate `insteadOf`
|
|
rewrites. Pure host-side, no docker; exposed for tests.
|
|
|
|
Empty `entries` returns an empty string so callers can no-op
|
|
cleanly without conditional formatting at the call site."""
|
|
if not entries:
|
|
return ""
|
|
out = [
|
|
"# claude-bottle git-gate (PRD 0008): every git operation against\n",
|
|
"# a declared upstream routes through the gate, which mirrors\n",
|
|
"# the upstream bidirectionally (gitleaks-scanned push;\n",
|
|
"# fetch-from-upstream-before-every-upload-pack via access-hook).\n",
|
|
]
|
|
for entry in entries:
|
|
out.append(f'[url "git://{GIT_GATE_HOSTNAME}/{entry.Name}.git"]\n')
|
|
out.append(f"\tinsteadOf = {entry.Upstream}\n")
|
|
return "".join(out)
|
|
|
|
|
|
def _provision_git_gate_config(plan: DockerBottlePlan, target: str) -> None:
|
|
"""Write ~/.gitconfig in the bottle with the git-gate
|
|
insteadOf rules. No-op when the bottle has no `git` entries."""
|
|
bottle = plan.spec.manifest.bottle_for(plan.spec.agent_name)
|
|
if not bottle.git:
|
|
return
|
|
container = target
|
|
container_home = os.environ.get("CLAUDE_BOTTLE_CONTAINER_HOME", "/home/node")
|
|
container_gitconfig = f"{container_home}/.gitconfig"
|
|
|
|
content = render_git_gate_gitconfig(bottle.git)
|
|
config_file = plan.stage_dir / "agent_gitconfig"
|
|
config_file.write_text(content)
|
|
config_file.chmod(0o600)
|
|
|
|
info(f"writing {container_gitconfig} with {len(bottle.git)} insteadOf rule(s)")
|
|
subprocess.run(
|
|
["docker", "cp", str(config_file), f"{container}:{container_gitconfig}"],
|
|
stdout=subprocess.DEVNULL,
|
|
check=True,
|
|
)
|
|
docker_mod.docker_exec_root(container, ["chown", "node:node", container_gitconfig])
|
|
docker_mod.docker_exec_root(container, ["chmod", "644", container_gitconfig])
|