"""Install the per-bottle MITM CA into the smolmachines guest's trust store (PRD 0023 chunk 4d). Mirrors `backend.docker.provision.ca`: select the right CA (egress when the bottle has routes, else pipelock), `smolvm machine cp` it to Debian's `/usr/local/share/ca-certificates/` path, `update-ca-certificates` to rebuild the trust bundle, and log the fingerprint once. The selected cert depends on the agent's HTTP_PROXY target — same logic as the docker backend, since the agent dials the same daemons through the same bundle. `smolvm machine exec` runs commands as root in the VM (no `-u` flag exists; the VM init is root), so we don't need the explicit `-u 0` the docker backend uses on its `docker exec` calls.""" from __future__ import annotations import hashlib import ssl from pathlib import Path from ....log import die, info from ...docker.provision.ca import AGENT_CA_BUNDLE, AGENT_CA_PATH from .. import smolvm as _smolvm from ..bottle_plan import SmolmachinesBottlePlan def _select_ca_cert(plan: SmolmachinesBottlePlan) -> tuple[Path, str]: """Pick the CA cert (and a short label for the log line) that matches the proxy the agent's HTTP_PROXY points at. Egress-proxy wins when the bottle declares any routes; else pipelock. The launch step minted both CAs (pipelock always; egress when routes are declared) and stored their host paths back into the inner Plans via `dataclasses.replace`. If those paths are empty here something has gone wrong in launch's bringup.""" if plan.egress_plan.routes: cert = plan.egress_plan.mitmproxy_ca_cert_only_host_path if cert == Path() or not cert.is_file(): die( f"egress CA cert missing at {cert or '(empty)'}; " f"launch must have called egress_tls_init and " f"re-bound the plan before provision" ) return cert, "egress" cert = plan.proxy_plan.ca_cert_host_path if not cert or not cert.is_file(): die( f"pipelock CA cert missing at {cert or '(empty)'}; " f"launch must have called pipelock_tls_init and re-bound " f"the plan before provision" ) return cert, "pipelock" def provision_ca(plan: SmolmachinesBottlePlan, target: str) -> None: """Copy the agent-facing CA cert into the guest, rebuild the trust bundle, emit a one-line fingerprint log. Called from `BottleBackend.provision` after the smolvm guest is up.""" cert_host_path, label = _select_ca_cert(plan) _smolvm.machine_cp(str(cert_host_path), f"{target}:{AGENT_CA_PATH}") # Mode 0644 — readable to non-root tools in the guest. # update-ca-certificates rebuilds the bundle at AGENT_CA_BUNDLE, # which is what curl / Python ssl / OpenSSL-based tools read by # default. The env trio (NODE_EXTRA_CA_CERTS / SSL_CERT_FILE / # REQUESTS_CA_BUNDLE) on the guest_env covers Node + Python # `requests` / libraries that don't load the system bundle. # # chown + chmod + update-ca-certificates run in one # `sh -c` so we only pay one machine_exec round trip; the # `&&` chaining surfaces the first failure as the return # code. r = _smolvm.machine_exec(target, [ "sh", "-c", f"chown root:root {AGENT_CA_PATH} && " f"chmod 644 {AGENT_CA_PATH} && " f"update-ca-certificates", ]) if r.returncode != 0 or "1 added" not in (r.stdout or ""): # update-ca-certificates not adding our cert is fatal — # claude-code's TLS handshake against the egress-MITM'd # api.anthropic.com would fail downstream. Bail early # with what we can see (output is captured by smolvm so # we can surface it). die( f"update-ca-certificates didn't add the agent CA " f"(exit {r.returncode}): " f"stdout={(r.stdout or '').strip()!r} " f"stderr={(r.stderr or '').strip()!r}" ) # Stdlib SHA-256 of the cert's DER bytes — the standard # fingerprint form. Never the private key. der = ssl.PEM_cert_to_DER_cert(cert_host_path.read_text()) fingerprint = hashlib.sha256(der).hexdigest() info(f"{label} ca fingerprint: sha256:{fingerprint[:32]}...") # Re-exported for the launch/provision_ca caller + tests. The path # constants come from the docker module because they're tied to # Debian's `update-ca-certificates` layout — same in both backends # since both guest images are Debian-family. __all__ = ["AGENT_CA_BUNDLE", "AGENT_CA_PATH", "provision_ca"]