The agent container is on an --internal Docker network with no default
route — only the pipelock sidecar is reachable. HTTPS_PROXY routes
HTTP through pipelock, but raw TCP (e.g. SSH on port 30009) had no
egress path, so `git fetch` against any bottle.ssh entry failed with
"Network is unreachable".
Fix: tunnel SSH through pipelock's HTTP CONNECT proxy.
- lib/ssh.sh injects `ProxyCommand socat - PROXY:<pipelock>:%h:%p,proxyport=<n>`
into each Host block in the in-container ~/.ssh/config. socat is
already in the image (apt-installed for the ssh-agent forwarder).
- lib/pipelock.sh auto-adds each bottle.ssh[].Hostname to the effective
allowlist so pipelock permits the CONNECT.
- cli.sh threads the pipelock host:port into ssh_setup.
Note: works for SSH hosts pipelock's SSRF layer doesn't block. CGNAT
(100.64.0.0/10) and other non-RFC1918 ranges should pass; if a future
host gets blocked, expose pipelock's trusted_domains as a follow-up.
Assisted-by: Claude Code
Previously cleanup_all was defined AFTER network_create_internal /
network_create_egress / pipelock_start ran, so a failure during
pipelock_start (or in network_create_egress added by the prior commit)
would land in the cleanup_stage trap that knows nothing about networks.
The internal and egress networks would survive the failed launch and
accumulate as orphans on the host.
Move the cleanup_all definition + `trap … EXIT INT TERM` install ahead
of the resource creation, and gate the CONTAINER branch on
`-n "${CONTAINER:-}"` since CONTAINER is set earlier in the function
but the trap now runs in the early-failure window. pipelock_stop and
network_remove are already idempotent against missing resources.
Smoke test: with `CLAUDE_BOTTLE_PIPELOCK_IMAGE` pinned to a nonexistent
digest, `./cli.sh start implementer` now creates both networks, fails
at pipelock_start, and exits with both networks removed —
`docker network ls | grep claude-bottle` returns nothing.
Assisted-by: Claude Code
Docker's legacy `bridge` network has no embedded DNS resolver — only
user-defined bridges do — so attaching the pipelock sidecar to `bridge`
made it unable to resolve `api.anthropic.com` and dead-ended Claude Code
traffic. Add `network_create_egress`, refactored around a shared
`_network_create_with_prefix` helper, and wire it through `pipelock_start`
and `cli.sh` so the sidecar straddles the agent's --internal network and
a per-agent user-defined egress bridge instead. The agent container
itself still attaches to the internal network only.
Assisted-by: Claude Code
PRD 0001 cli.sh integration:
- Source the new lib/network.sh and lib/pipelock.sh.
- During plan resolution: generate the per-bottle pipelock YAML into
the existing mktemp stage dir (mode 600, hostnames only) and
resolve a one-line "<N> hosts allowed (...)" summary.
- Add the egress summary as a sub-bullet under the bottle in the y/N
preflight, alongside the existing ssh hosts line.
- After the y/N gate (and after build_image): create the per-agent
--internal Docker network with a slug-derived name, then start the
pipelock sidecar attached to it.
- docker run argv: agent attaches to the internal network with
HTTPS_PROXY / HTTP_PROXY pointing at the sidecar by service name on
that network. NO_PROXY only covers loopback. The internal network
has no default gateway, so any path that ignores the proxy env
hits no-route-to-host rather than leaking.
- Exit trap: tear down the agent container, then the sidecar (so the
network is empty), then remove the network, then run the existing
stage cleanup. Order matters — docker refuses to remove a network
with attached containers.
- --dry-run continues to exit before any docker network/run/cp/exec
call; the YAML write into the mktemp dir is the only new
side-effect inside the dry-run path.
Verified against a temp fixture: defaults-only bottle shows
"7 hosts allowed", a bottle with two extra entries shows
"9 hosts allowed (api.anthropic.com, api.openai.com, claude.ai,
+6 more)", and dry-run exits before any docker calls.
Refs: docs/prds/0001-per-agent-egress-proxy-via-pipelock.md
Assisted-by: Claude Code