diff --git a/Dockerfile.sidecars b/Dockerfile.sidecars index 89b33b3..c287391 100644 --- a/Dockerfile.sidecars +++ b/Dockerfile.sidecars @@ -14,9 +14,10 @@ # /app/supervise_server.py + .py supervise MCP server # /app/sidecar_init.py PID 1 supervisor # /etc/egress/routes.yaml bind-mounted at run time -# /etc/git-gate/pre-receive docker-cp'd at start time -# /git-gate-entrypoint.sh docker-cp'd at start time -# /git-gate/creds/* docker-cp'd at start time +# /etc/git-gate/entrypoint.sh per-bottle (docker-cp or virtiofs mount) +# /etc/git-gate/pre-receive per-bottle (docker-cp or virtiofs mount) +# /git-gate-entrypoint.sh static wrapper → /etc/git-gate/entrypoint.sh +# /git-gate/creds/* per-bottle (docker-cp or virtiofs mount) # /git/* bare repos, populated at runtime # /run/supervise/bot-bottle.db bind-mounted at run time # /home/mitmproxy/.mitmproxy/ mitmproxy CA dir @@ -90,6 +91,17 @@ RUN mkdir -p \ /run/supervise \ /home/mitmproxy/.mitmproxy +# Static wrapper for the git-gate entrypoint. The per-bottle +# entrypoint script is either: +# - docker-cp'd to /git-gate-entrypoint.sh (docker/macOS backends), +# which overwrites this wrapper; or +# - virtiofs-mounted at /etc/git-gate/entrypoint.sh (smolmachines), +# where this wrapper delegates to it at runtime. +# Either way sidecar_init.py calls `/bin/sh /git-gate-entrypoint.sh`. +RUN printf '#!/bin/sh\nexec /etc/git-gate/entrypoint.sh "$@"\n' \ + > /git-gate-entrypoint.sh \ + && chmod 755 /git-gate-entrypoint.sh + # Documentation only — the compose renderer publishes whichever # subset the bottle uses. EXPOSE 8888 9099 9418 9420 9100 diff --git a/bot_bottle/backend/smolmachines/launch.py b/bot_bottle/backend/smolmachines/launch.py index 1cb1f4a..c40d685 100644 --- a/bot_bottle/backend/smolmachines/launch.py +++ b/bot_bottle/backend/smolmachines/launch.py @@ -14,6 +14,7 @@ from __future__ import annotations import dataclasses import os +import shutil from contextlib import ExitStack, contextmanager from pathlib import Path from typing import Callable, Generator @@ -32,12 +33,7 @@ from ..docker.egress import ( EGRESS_PORT as _EGRESS_PORT, egress_tls_init, ) -from ..docker.git_gate import ( - GIT_GATE_ACCESS_HOOK_IN_CONTAINER, - GIT_GATE_CREDS_DIR_IN_CONTAINER, - GIT_GATE_ENTRYPOINT_IN_CONTAINER, - GIT_GATE_HOOK_IN_CONTAINER, -) +from ..docker.git_gate import GIT_GATE_CREDS_DIR_IN_CONTAINER from ...git_gate import ( provision_git_gate_dynamic_keys, revoke_git_gate_provisioned_keys, @@ -61,6 +57,14 @@ from .local_registry import crane_push_tarball, ephemeral_registry _REPO_DIR = str(Path(__file__).resolve().parent.parent.parent.parent) +# In-VM directory containing git-gate scripts staged for virtiofs mount. +# The Dockerfile.sidecars static wrapper at /git-gate-entrypoint.sh +# delegates to this path, so the smolmachines backend can supply the +# per-bottle entrypoint via a directory virtiofs mount instead of a +# post-boot machine_cp (which runs too late for PID 1 daemons). +_GIT_GATE_SCRIPTS_DIR_IN_VM = "/etc/git-gate" + + # Per-host cache for `smolvm pack create` outputs. Keyed by the # docker image ID so a Dockerfile change automatically invalidates # the cache. `pack create` is idempotent on the smolvm side but @@ -358,6 +362,47 @@ def _port_for_label(label: str) -> int: raise ValueError(f"unknown sidecar forward label: {label}") +def _stage_git_gate_for_smolvm( + plan: SmolmachinesBottlePlan, +) -> tuple[Path, Path]: + """Stage git-gate scripts and credentials as virtiofs-mountable dirs. + + virtiofs requires directory sources; the per-bottle git-gate scripts + have host-side names that differ from their in-VM paths (e.g. + `git_gate_entrypoint.sh` → `entrypoint.sh` in /etc/git-gate/), so + they cannot be expressed as parent-dir mounts without renaming. + + Copies them into per-bottle staging dirs under the git-gate state + dir. Returns `(scripts_dir, creds_dir)`: + - scripts_dir is mounted to /etc/git-gate/ in the sidecar VM; + the image's static /git-gate-entrypoint.sh wrapper delegates there. + - creds_dir is mounted to /git-gate/creds/ in the sidecar VM.""" + gp = plan.git_gate_plan + state_dir = git_gate_state_dir(plan.slug) + + scripts_dir = state_dir / "smolvm-scripts" + scripts_dir.mkdir(parents=True, exist_ok=True) + shutil.copy2(str(gp.entrypoint_script), str(scripts_dir / "entrypoint.sh")) + shutil.copy2(str(gp.hook_script), str(scripts_dir / "pre-receive")) + shutil.copy2(str(gp.access_hook_script), str(scripts_dir / "access-hook")) + for name in ("entrypoint.sh", "pre-receive", "access-hook"): + (scripts_dir / name).chmod(0o755) + + creds_dir = state_dir / "smolvm-creds" + creds_dir.mkdir(parents=True, exist_ok=True) + for u in gp.upstreams: + keypath = Path(expand_tilde(u.identity_file)) + dest_key = creds_dir / f"{u.name}-key" + shutil.copy2(str(keypath), str(dest_key)) + dest_key.chmod(0o600) + if u.known_hosts_file: + dest_kh = creds_dir / f"{u.name}-known_hosts" + shutil.copy2(str(u.known_hosts_file), str(dest_kh)) + dest_kh.chmod(0o600) + + return scripts_dir, creds_dir + + def _bundle_launch_spec( plan: SmolmachinesBottlePlan, network: str, proxy_host: str, ) -> _bundle.BundleLaunchSpec: @@ -378,33 +423,32 @@ def _bundle_launch_spec( # --- egress ----------------------------------------------- ep = plan.egress_plan - volumes.append((str(ep.mitmproxy_ca_host_path), EGRESS_CA_IN_CONTAINER, True)) + # virtiofs (smolvm) requires directory mounts — mount the CA's + # parent dir so mitmproxy-ca.pem lands at the right in-VM path. + # Writable so egress_entrypoint.sh can write combined-trust.pem + # and mitmproxy can create its per-host cert cache. + volumes.append(( + str(ep.mitmproxy_ca_host_path.parent), + str(Path(EGRESS_CA_IN_CONTAINER).parent), + False, + )) if ep.routes: volumes.append((str(ep.routes_path.parent), str(Path(EGRESS_ROUTES_IN_CONTAINER).parent), True)) env.extend(egress_sidecar_env_entries(ep)) # --- git-gate --------------------------------------------- + # virtiofs requires directory mounts; git-gate scripts have + # host-side names that differ from their in-VM paths. Stage + # them with correct names under the git-gate state dir and + # mount those staging dirs. The image's static wrapper at + # /git-gate-entrypoint.sh delegates to the staged entrypoint, + # so the per-bottle script is available at VM boot time. gp = plan.git_gate_plan if gp.upstreams: daemons += ["git-gate", "git-http"] - volumes += [ - (str(gp.entrypoint_script), GIT_GATE_ENTRYPOINT_IN_CONTAINER, True), - (str(gp.hook_script), GIT_GATE_HOOK_IN_CONTAINER, True), - (str(gp.access_hook_script), GIT_GATE_ACCESS_HOOK_IN_CONTAINER, True), - ] - for u in gp.upstreams: - keypath = expand_tilde(u.identity_file) - volumes.append(( - keypath, - f"{GIT_GATE_CREDS_DIR_IN_CONTAINER}/{u.name}-key", - True, - )) - if u.known_hosts_file: - volumes.append(( - str(u.known_hosts_file), - f"{GIT_GATE_CREDS_DIR_IN_CONTAINER}/{u.name}-known_hosts", - True, - )) + scripts_dir, creds_dir = _stage_git_gate_for_smolvm(plan) + volumes.append((str(scripts_dir), _GIT_GATE_SCRIPTS_DIR_IN_VM, True)) + volumes.append((str(creds_dir), GIT_GATE_CREDS_DIR_IN_CONTAINER, True)) # --- supervise -------------------------------------------- sp = plan.supervise_plan @@ -415,7 +459,13 @@ def _bundle_launch_spec( f"SUPERVISE_DB_PATH={DB_PATH_IN_CONTAINER}", f"SUPERVISE_PORT={SUPERVISE_PORT}", ] - volumes.append((str(sp.db_path), DB_PATH_IN_CONTAINER, False)) + # virtiofs requires directory mount — mount the DB's parent + # dir so bot-bottle.db lands at the right in-VM path. + volumes.append(( + str(sp.db_path.parent), + str(Path(DB_PATH_IN_CONTAINER).parent), + False, + )) # Container ports the agent reaches from the smolvm guest — # published on `proxy_host` so the TSI allowlist and the docker diff --git a/tests/unit/test_smolmachines_provision.py b/tests/unit/test_smolmachines_provision.py index f5c2aa8..959fc87 100644 --- a/tests/unit/test_smolmachines_provision.py +++ b/tests/unit/test_smolmachines_provision.py @@ -404,7 +404,11 @@ class TestBundleLaunchSpec(unittest.TestCase): ), ) - spec = _bundle_launch_spec(plan, "net", "127.0.0.16") + with patch( + "bot_bottle.backend.smolmachines.launch._stage_git_gate_for_smolvm", + return_value=(Path("/tmp/smolvm-scripts"), Path("/tmp/smolvm-creds")), + ): + spec = _bundle_launch_spec(plan, "net", "127.0.0.16") self.assertEqual( "egress,git-gate,git-http", @@ -430,7 +434,9 @@ class TestBundleLaunchSpec(unittest.TestCase): spec = _bundle_launch_spec(plan, "net", "127.0.0.16") self.assertIn("supervise", spec.daemons_csv) self.assertIn(f"SUPERVISE_DB_PATH={DB_PATH_IN_CONTAINER}", spec.environment) - self.assertIn(("/tmp/bot-bottle.db", DB_PATH_IN_CONTAINER, False), spec.volumes) + # virtiofs requires directory mounts; the DB's parent dir is + # mounted so bot-bottle.db lands at the right in-VM path. + self.assertIn(("/tmp", str(Path(DB_PATH_IN_CONTAINER).parent), False), spec.volumes) def test_canary_env_visible_to_smolvm_guest(self): plan = _plan(canary=True) @@ -556,6 +562,9 @@ class TestLaunchResourceWiring(unittest.TestCase): "bot_bottle.backend.smolmachines.launch._bundle.start_bundle_vm", return_value=raw_launch, ) as start_vm, patch( + "bot_bottle.backend.smolmachines.launch._stage_git_gate_for_smolvm", + return_value=(Path("/tmp/smolvm-scripts"), Path("/tmp/smolvm-creds")), + ), patch( "bot_bottle.backend.smolmachines.launch._forward.start_forwarder", return_value=handle, ) as start_forwarder: