feat(mitmproxy): wire the sidecar into the bottle launch lifecycle
Second step of PRD 0005. The mitmproxy sidecar from the previous commit now actually runs alongside pipelock when a bottle launches. - BottleBackend gains a non-abstract provision_ca with a default no-op so non-Docker backends aren't forced to implement TLS interception. provision() orchestrates ca → prompt → skills → ssh → git; CA goes first so trust is set up before anything else runs inside the agent. - DockerBottlePlan gains `mitmproxy_plan: MitmproxyProxyPlan`. The prepare step builds it alongside the existing pipelock plan; no new manifest schema or host-side scratch files. - DockerBottleBackend grows self._mitm, threads it through prepare and launch. Mirror of the existing self._proxy pattern. - launch.py brings the mitmproxy sidecar up between pipelock and the agent container, passing pipelock's service-name URL via env. ExitStack callback handles teardown in reverse order. - The agent's HTTPS_PROXY / HTTP_PROXY now point at mitmproxy (not pipelock directly). Three new -e flags inject the CA trust trio (NODE_EXTRA_CA_CERTS / SSL_CERT_FILE / REQUESTS_CA_BUNDLE) at docker run time; Docker propagates those into docker exec so the claude process sees them without per-exec threading. - New provisioner backend/docker/provision/ca.py extracts the CA cert from the running mitmproxy sidecar, copies it into the agent at /usr/local/share/ca-certificates/claude-bottle-mitm.crt, runs update-ca-certificates, and emits a stderr line with the SHA-256 fingerprint (stdlib ssl + hashlib; no subprocess). Cleanup needs no change — `docker ps --filter name=^claude-bottle-` already catches the new claude-bottle-mitm-<slug> containers. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -22,8 +22,15 @@ from . import network as network_mod
|
||||
from . import util as docker_mod
|
||||
from .bottle import DockerBottle
|
||||
from .bottle_plan import DockerBottlePlan
|
||||
from .mitmproxy import DockerMitmproxyProxy, mitmproxy_proxy_url
|
||||
from .pipelock import DockerPipelockProxy, pipelock_proxy_url
|
||||
|
||||
# Path inside the agent container where the mitmproxy CA cert lives
|
||||
# after provision_ca runs. Exported as a module-level constant so
|
||||
# both the agent's docker-run env trio and the provisioner agree.
|
||||
AGENT_CA_PATH = "/usr/local/share/ca-certificates/claude-bottle-mitm.crt"
|
||||
AGENT_CA_BUNDLE = "/etc/ssl/certs/ca-certificates.crt"
|
||||
|
||||
|
||||
# Where the repo root lives, for `docker build` context. Computed once.
|
||||
_REPO_DIR = str(Path(__file__).resolve().parent.parent.parent.parent)
|
||||
@@ -34,6 +41,7 @@ def launch(
|
||||
plan: DockerBottlePlan,
|
||||
*,
|
||||
proxy: DockerPipelockProxy,
|
||||
mitm: DockerMitmproxyProxy,
|
||||
provision: Callable[[DockerBottlePlan, str], str | None],
|
||||
) -> Generator[DockerBottle, None, None]:
|
||||
"""Build, launch, and provision a Docker bottle. Teardown on exit.
|
||||
@@ -71,6 +79,17 @@ def launch(
|
||||
pipelock_name = proxy.start(proxy_plan)
|
||||
stack.callback(proxy.stop, pipelock_name)
|
||||
|
||||
# mitmproxy sits in front of pipelock on the agent's egress
|
||||
# path. mitmproxy's `addon.py` reaches pipelock via the
|
||||
# service-name URL we hand it here.
|
||||
mitm_plan = dataclasses.replace(
|
||||
plan.mitmproxy_plan,
|
||||
internal_network=internal_network,
|
||||
egress_network=egress_network,
|
||||
)
|
||||
mitm_name = mitm.start(mitm_plan, pipelock_url=pipelock_proxy_url(plan.slug))
|
||||
stack.callback(mitm.stop, mitm_name)
|
||||
|
||||
container = _run_agent_container(plan, internal_network)
|
||||
stack.callback(docker_mod.force_remove_container, container)
|
||||
|
||||
@@ -85,7 +104,10 @@ def _run_agent_container(plan: DockerBottlePlan, internal_network: str) -> str:
|
||||
"""Build the `docker run` argv and execute it, handling name-
|
||||
conflict races by incrementing the suffix (unless the name was
|
||||
user-pinned). Returns the resolved container name."""
|
||||
proxy_url = pipelock_proxy_url(plan.slug)
|
||||
# Agent traffic routes through mitmproxy, not pipelock directly.
|
||||
# mitmproxy decrypts and hands the plaintext to pipelock via its
|
||||
# addon; pipelock is unchanged from PRD 0001.
|
||||
proxy_url = mitmproxy_proxy_url(plan.slug)
|
||||
docker_args: list[str] = [
|
||||
"--rm", "-d",
|
||||
"--name", plan.container_name,
|
||||
@@ -93,6 +115,16 @@ def _run_agent_container(plan: DockerBottlePlan, internal_network: str) -> str:
|
||||
"-e", f"HTTPS_PROXY={proxy_url}",
|
||||
"-e", f"HTTP_PROXY={proxy_url}",
|
||||
"-e", "NO_PROXY=localhost,127.0.0.1",
|
||||
# CA trust trio for the agent process. Docker propagates
|
||||
# run-time env into `docker exec`, so `claude` sees these
|
||||
# without per-exec threading. NODE_EXTRA_CA_CERTS points at
|
||||
# the cert file (Node appends it to its bundled roots);
|
||||
# SSL_CERT_FILE / REQUESTS_CA_BUNDLE point at the system
|
||||
# bundle that `update-ca-certificates` rebuilds in
|
||||
# provision_ca.
|
||||
"-e", f"NODE_EXTRA_CA_CERTS={AGENT_CA_PATH}",
|
||||
"-e", f"SSL_CERT_FILE={AGENT_CA_BUNDLE}",
|
||||
"-e", f"REQUESTS_CA_BUNDLE={AGENT_CA_BUNDLE}",
|
||||
]
|
||||
if plan.use_runsc:
|
||||
docker_args.extend(["--runtime", "runsc"])
|
||||
|
||||
Reference in New Issue
Block a user