"""mitmproxy TLS-interception sidecar for the per-bottle egress topology (PRD 0005). Sits in front of pipelock on the bottle's egress path so pipelock's body / header / URL DLP scanners see plaintext for HTTPS targets. The sidecar runs in mitmproxy's `regular` mode and loads the vendored addon at `addon.py`; the addon forwards each decrypted request to pipelock as a plain HTTP forward-proxy call and gates the mitmproxy flow on pipelock's verdict. This module is platform-agnostic: it owns the abstract proxy lifecycle (prepare / start / stop / extract_ca_cert). The Docker-specific lifecycle lives in `claude_bottle/backend/docker/mitmproxy.py`. """ from __future__ import annotations from abc import ABC, abstractmethod from dataclasses import dataclass from pathlib import Path @dataclass(frozen=True) class MitmproxyProxyPlan: """Output of MitmproxyProxy.prepare; consumed by .start when the sidecar needs to be brought up. `addon_src` is the host-side path to the vendored addon.py, resolved at prepare time. `slug` is the per-agent identifier used as the suffix in every per-bottle resource name. The network fields default to empty and are populated by the backend's launch step (via dataclasses.replace) once those networks have actually been created — same pattern as PipelockProxyPlan.""" addon_src: Path slug: str internal_network: str = "" egress_network: str = "" class MitmproxyProxy(ABC): """The mitmproxy TLS-interception sidecar. The proxy-config + addon bundling are platform-agnostic; the sidecar's start/stop lifecycle and the CA extraction step are backend-specific and live on concrete subclasses.""" def prepare(self, slug: str) -> MitmproxyProxyPlan: """Locate the vendored addon source and return the start plan. The addon is checked into the project and identical across bottles; per-bottle wiring (pipelock URL) is injected via env vars at start time, not via a generated config.""" addon_src = Path(__file__).resolve().parent / "addon.py" if not addon_src.is_file(): raise FileNotFoundError( f"mitmproxy addon not found at {addon_src}; the " f"package was installed incompletely" ) return MitmproxyProxyPlan(addon_src=addon_src, slug=slug) @abstractmethod def start(self, plan: MitmproxyProxyPlan, *, pipelock_url: str) -> str: """Bring up the mitmproxy sidecar according to `plan`. `pipelock_url` is injected into the sidecar's env (as CLAUDE_BOTTLE_PIPELOCK_URL) so the addon knows where to scan. Returns the proxy_target string identifying the running sidecar — the same value to pass to `.stop` and `.extract_ca_cert`.""" @abstractmethod def stop(self, proxy_target: str) -> None: """Tear down the sidecar identified by `proxy_target`. Idempotent: a missing target is success.""" @abstractmethod def extract_ca_cert(self, proxy_target: str, dest_path: Path) -> None: """Copy the public CA cert from the running sidecar to `dest_path` on the host. Polls the sidecar for the cert file to appear (mitmproxy generates the CA on first launch). The private key never leaves the sidecar."""