feat(smolmachines): provision_ca + provision_git + provision_supervise (PRD 0023 chunk 4d)
test / unit (pull_request) Successful in 26s
test / integration (pull_request) Successful in 43s
test / unit (push) Successful in 26s
test / integration (push) Successful in 42s

End-to-end provisioning parity with the docker backend. After this
chunk a smolmachines bottle has a working trust store, git-gate
gitconfig, and supervise MCP registration — same shape as docker,
dispatched via `smolvm machine cp` / `smolvm machine exec` instead
of `docker cp` / `docker exec`.

Adds three new provision modules:
- ca.py:        select egress vs pipelock CA (same logic as
                docker), machine cp + update-ca-certificates,
                log sha256 fingerprint.
- git.py:       copy host .git when --cwd was passed; render
                ~/.gitconfig with insteadOf URLs. URL prefix is
                `git://<bundle_ip>:9418/...` (no DNS in the
                TSI-allowlisted guest) vs docker's
                `git://git-gate/...`.
- supervise.py: `claude mcp add` via machine_exec; URL is
                `http://<bundle_ip>:9100/`. Failure is logged but
                non-fatal (matches docker).

Shared render: `render_git_gate_gitconfig` moves out of
backend/docker/provision/git.py into the platform-neutral
claude_bottle/git_gate.py (renamed to git_gate_render_gitconfig
for consistency with the existing git_gate_render_* helpers),
parameterized on a `gate_host` argument so both backends use the
same logic with different addresses.

Path/user fixups for the post-chunk-4c agent image (real
claude-bottle image, USER node, $HOME=/home/node):
- prompt.py default path moves from /root/... to
  /home/node/.claude-bottle-prompt.txt; chown + chmod after
  machine cp.
- skills.py default skills dir moves from /root/.claude/skills to
  /home/node/.claude/skills; chown -R per skill.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit was merged in pull request #72.
This commit is contained in:
2026-05-27 14:15:58 -04:00
parent 1fa17d1822
commit ac8c7ba696
10 changed files with 661 additions and 77 deletions
+2 -23
View File
@@ -19,9 +19,8 @@ import os
import subprocess
from pathlib import Path
from ....git_gate import GIT_GATE_HOSTNAME
from ....git_gate import GIT_GATE_HOSTNAME, git_gate_render_gitconfig
from ....log import info
from ....manifest import GitEntry
from .. import util as docker_mod
from ..bottle_plan import DockerBottlePlan
@@ -56,26 +55,6 @@ def _provision_cwd_git(plan: DockerBottlePlan, target: str) -> None:
)
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."""
@@ -86,7 +65,7 @@ def _provision_git_gate_config(plan: DockerBottlePlan, target: str) -> None:
container_home = os.environ.get("CLAUDE_BOTTLE_CONTAINER_HOME", "/home/node")
container_gitconfig = f"{container_home}/.gitconfig"
content = render_git_gate_gitconfig(bottle.git)
content = git_gate_render_gitconfig(bottle.git, GIT_GATE_HOSTNAME)
config_file = plan.stage_dir / "agent_gitconfig"
config_file.write_text(content)
config_file.chmod(0o600)