Third step of PRD 0005. The preflight now surfaces the TLS-
intercept layer so the operator sees it before agreeing to launch.
- Text output: one new line under the egress summary —
"tls intercept : mitmproxy (per-bottle ephemeral CA, generated
at launch)".
- JSON output (--format=json contract): new
egress.mitm: { enabled: true, ca_fingerprint: null } block.
Fingerprint is always null at dry-run because the CA only
exists after the sidecar starts; real launches print it as a
stderr log line from provision_ca.
- Pin the new shape in the dry-run integration test.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
ResolvedEnv.forwarded now carries name->value pairs instead of names
whose values had been side-loaded into os.environ. The Docker backend
collects the dict (plus the renamed OAuth token) and passes it via
subprocess.run(env=...) so docker run -e NAME forwards by-name from
the child's environment, not the parent's.
Values are excluded from the dataclass repr (forwarded on ResolvedEnv,
forwarded_env on DockerBottlePlan) so accidental logging cannot leak
secret or interpolated values.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
DockerBottlePlan.print and .to_dict each pulled the same agent /
bottle / env_names / ssh_hosts / prompt-first-line out of the spec
before formatting. Extract a private _view() helper that returns a
small frozen _PlanView dataclass with those derived fields; both
methods consume it. Removes the duplicated derivation and the risk
that one renderer drifts from the other (the OAuth-name append in
particular existed twice).
Previously prepare wrote two on-disk artifacts that launch consumed:
agent.env (NAME=VALUE) and docker-args (paired -e\nNAME\n lines), with
launch parsing the second back into argv. Docker requires the literals
file on disk for --env-file, but the args-file round-trip was a pure
serialize/deserialize trip with hand-rolled line pairing logic.
Drop docker-args entirely. Pass forwarded names as a structured
tuple[str, ...] field on DockerBottlePlan; launch iterates it directly
to extend docker_args. _write_env_files becomes _write_env_file (only
the literals file remains).
BottlePlan gains a to_dict method (abstract on the base, implemented
on DockerBottlePlan) returning a JSON-serializable view of the resolved
plan. `cli.py start --dry-run --format=json` prints it to stdout and
exits zero. --format=json without --dry-run is rejected — emitting JSON
during a real launch would race the y/N prompt.
The dry-run integration test now parses the JSON and asserts on
structured fields (agent, bottle, runtime, hosts sorted+deduped, etc.)
instead of regex-matching the human-readable preflight stdout. That
kills the magic-"8 hosts allowed" coupling — adding a new baked
default doesn't break the test.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ProxyPlan -> PipelockProxyPlan, with two additional fields populated
in launch: internal_network, egress_network (default ""). prepare
fills yaml_path + slug; launch uses dataclasses.replace to populate
the networks before calling start.
pipelock_start -> PipelockProxy.start(plan). Reads yaml_path, slug,
internal_network, egress_network off the plan. Returns the resolved
container name.
pipelock_stop -> PipelockProxy.stop(proxy_target). Takes the resolved
container name directly (the value that start returned); no longer
needs to know about slugs or naming conventions.
Backend launch passes the running container name (state["pipelock"])
to stop. Test for stop's idempotency uses pipelock_container_name to
construct the proxy_target.
Add a frozen ProxyPlan dataclass to pipelock.py (currently one field:
yaml_path; kept as a class so future proxy-level state has a home).
- prepare_proxy(spec, stage_dir) now returns pipelock.ProxyPlan
instead of a raw Path.
- DockerBottlePlan replaces pipelock_yaml_path + pipelock_yaml_filename
with a single proxy: ProxyPlan field.
- launch reads plan.proxy.yaml_path.parent / .name when calling
pipelock_start. Eventually pipelock_start should just take a Path
but that's a separate change.
Across the package:
- claude_bottle/platform/ -> claude_bottle/backend/
- platform/docker/platform.py -> backend/docker/backend.py
- class BottlePlatform -> BottleBackend
- class DockerBottlePlatform -> DockerBottleBackend
- get_bottle_platform() -> get_bottle_backend()
- env var CLAUDE_BOTTLE_PLATFORM -> CLAUDE_BOTTLE_BACKEND
- dict _PLATFORMS -> _BACKENDS
"Backend" is shorter and more established as the term for a
pluggable strategy-pattern implementation. "Platform" was vague
(could mean OS, hardware, cloud) and mildly redundant — Docker is
itself a platform.
The previous PRD section claiming "the Backend protocol was
rejected" referred to a low-level run/exec/cp/network_connect
protocol; the name was never the reason. The PRD is updated to
describe that rejected design by shape rather than by name.
The bottle/agent concepts and the manifest schema are unchanged.