fix(smolmachines): mount git-gate files at boot via virtiofs staging dirs

Two issues caused krun_start_enter -22 (VM boot crash):

1. git-gate entrypoint missing at boot: sidecar_init.py starts all
   daemons (including git-gate) immediately as PID 1. The entrypoint
   was copied in via machine_cp which only runs after machine_start
   returns — too late. virtiofs also can't mount files at root (/).

   Fix: bake a static /git-gate-entrypoint.sh wrapper into the
   Dockerfile.sidecars image that delegates to /etc/git-gate/entrypoint.sh.
   For smolmachines, stage per-bottle scripts with correct in-VM names
   under the git-gate state dir and virtiofs-mount to /etc/git-gate/ at
   VM creation time. docker/macOS backends are unchanged (their file
   bind-mount/docker cp still overrides the static wrapper).

2. Egress confdir mounted read-only: egress_entrypoint.sh writes
   combined-trust.pem to confdir under set -e; mitmproxy also needs
   to write its per-host cert cache there. Both fail fatally on a
   read-only virtiofs mount.

   Fix: change the egress confdir virtiofs mount to writable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-07-09 17:05:44 -04:00
parent 2b53c36608
commit 62d2e86e7e
3 changed files with 102 additions and 31 deletions
+15 -3
View File
@@ -14,9 +14,10 @@
# /app/supervise_server.py + .py supervise MCP server # /app/supervise_server.py + .py supervise MCP server
# /app/sidecar_init.py PID 1 supervisor # /app/sidecar_init.py PID 1 supervisor
# /etc/egress/routes.yaml bind-mounted at run time # /etc/egress/routes.yaml bind-mounted at run time
# /etc/git-gate/pre-receive docker-cp'd at start time # /etc/git-gate/entrypoint.sh per-bottle (docker-cp or virtiofs mount)
# /git-gate-entrypoint.sh docker-cp'd at start time # /etc/git-gate/pre-receive per-bottle (docker-cp or virtiofs mount)
# /git-gate/creds/* docker-cp'd at start time # /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 # /git/* bare repos, populated at runtime
# /run/supervise/bot-bottle.db bind-mounted at run time # /run/supervise/bot-bottle.db bind-mounted at run time
# /home/mitmproxy/.mitmproxy/ mitmproxy CA dir # /home/mitmproxy/.mitmproxy/ mitmproxy CA dir
@@ -90,6 +91,17 @@ RUN mkdir -p \
/run/supervise \ /run/supervise \
/home/mitmproxy/.mitmproxy /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 # Documentation only — the compose renderer publishes whichever
# subset the bottle uses. # subset the bottle uses.
EXPOSE 8888 9099 9418 9420 9100 EXPOSE 8888 9099 9418 9420 9100
+76 -26
View File
@@ -14,6 +14,7 @@ from __future__ import annotations
import dataclasses import dataclasses
import os import os
import shutil
from contextlib import ExitStack, contextmanager from contextlib import ExitStack, contextmanager
from pathlib import Path from pathlib import Path
from typing import Callable, Generator from typing import Callable, Generator
@@ -32,12 +33,7 @@ from ..docker.egress import (
EGRESS_PORT as _EGRESS_PORT, EGRESS_PORT as _EGRESS_PORT,
egress_tls_init, egress_tls_init,
) )
from ..docker.git_gate import ( from ..docker.git_gate import GIT_GATE_CREDS_DIR_IN_CONTAINER
GIT_GATE_ACCESS_HOOK_IN_CONTAINER,
GIT_GATE_CREDS_DIR_IN_CONTAINER,
GIT_GATE_ENTRYPOINT_IN_CONTAINER,
GIT_GATE_HOOK_IN_CONTAINER,
)
from ...git_gate import ( from ...git_gate import (
provision_git_gate_dynamic_keys, provision_git_gate_dynamic_keys,
revoke_git_gate_provisioned_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) _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 # Per-host cache for `smolvm pack create` outputs. Keyed by the
# docker image ID so a Dockerfile change automatically invalidates # docker image ID so a Dockerfile change automatically invalidates
# the cache. `pack create` is idempotent on the smolvm side but # 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}") 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( def _bundle_launch_spec(
plan: SmolmachinesBottlePlan, network: str, proxy_host: str, plan: SmolmachinesBottlePlan, network: str, proxy_host: str,
) -> _bundle.BundleLaunchSpec: ) -> _bundle.BundleLaunchSpec:
@@ -378,33 +423,32 @@ def _bundle_launch_spec(
# --- egress ----------------------------------------------- # --- egress -----------------------------------------------
ep = plan.egress_plan 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: if ep.routes:
volumes.append((str(ep.routes_path.parent), str(Path(EGRESS_ROUTES_IN_CONTAINER).parent), True)) volumes.append((str(ep.routes_path.parent), str(Path(EGRESS_ROUTES_IN_CONTAINER).parent), True))
env.extend(egress_sidecar_env_entries(ep)) env.extend(egress_sidecar_env_entries(ep))
# --- git-gate --------------------------------------------- # --- 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 gp = plan.git_gate_plan
if gp.upstreams: if gp.upstreams:
daemons += ["git-gate", "git-http"] daemons += ["git-gate", "git-http"]
volumes += [ scripts_dir, creds_dir = _stage_git_gate_for_smolvm(plan)
(str(gp.entrypoint_script), GIT_GATE_ENTRYPOINT_IN_CONTAINER, True), volumes.append((str(scripts_dir), _GIT_GATE_SCRIPTS_DIR_IN_VM, True))
(str(gp.hook_script), GIT_GATE_HOOK_IN_CONTAINER, True), volumes.append((str(creds_dir), GIT_GATE_CREDS_DIR_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,
))
# --- supervise -------------------------------------------- # --- supervise --------------------------------------------
sp = plan.supervise_plan sp = plan.supervise_plan
@@ -415,7 +459,13 @@ def _bundle_launch_spec(
f"SUPERVISE_DB_PATH={DB_PATH_IN_CONTAINER}", f"SUPERVISE_DB_PATH={DB_PATH_IN_CONTAINER}",
f"SUPERVISE_PORT={SUPERVISE_PORT}", 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 — # Container ports the agent reaches from the smolvm guest —
# published on `proxy_host` so the TSI allowlist and the docker # published on `proxy_host` so the TSI allowlist and the docker
+11 -2
View File
@@ -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( self.assertEqual(
"egress,git-gate,git-http", "egress,git-gate,git-http",
@@ -430,7 +434,9 @@ class TestBundleLaunchSpec(unittest.TestCase):
spec = _bundle_launch_spec(plan, "net", "127.0.0.16") spec = _bundle_launch_spec(plan, "net", "127.0.0.16")
self.assertIn("supervise", spec.daemons_csv) self.assertIn("supervise", spec.daemons_csv)
self.assertIn(f"SUPERVISE_DB_PATH={DB_PATH_IN_CONTAINER}", spec.environment) 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): def test_canary_env_visible_to_smolvm_guest(self):
plan = _plan(canary=True) plan = _plan(canary=True)
@@ -556,6 +562,9 @@ class TestLaunchResourceWiring(unittest.TestCase):
"bot_bottle.backend.smolmachines.launch._bundle.start_bundle_vm", "bot_bottle.backend.smolmachines.launch._bundle.start_bundle_vm",
return_value=raw_launch, return_value=raw_launch,
) as start_vm, patch( ) 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", "bot_bottle.backend.smolmachines.launch._forward.start_forwarder",
return_value=handle, return_value=handle,
) as start_forwarder: ) as start_forwarder: