PRD 0001: Per-agent egress proxy via pipelock #1
Reference in New Issue
Block a user
Delete Branch "prd-0001-per-agent-egress-proxy-via-pipelock"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Tracking PR for PRD 0001. Implementation complete on this branch.
Summary
Adds a per-agent pipelock sidecar on a Docker
--internalnetwork so each agent container's only egress route is through pipelock's HTTP forward proxy (hostname allowlist + 48-pattern DLP + subdomain-entropy DNS-exfil detection). Newlib/network.shandlib/pipelock.shmodules;cli.shwires the lifecycle; the bottle schema gains anegress.allowlistarray; pipelock image is pinned by digest.Follow-ups (deferred to a later PR)
lib/pipelock.sh:81from a plain assignment to${CLAUDE_BOTTLE_PIPELOCK_DEFAULT_ALLOWLIST:-...}so users can override without editing the file.pipelock_bottle_allowlistbefore they reach the YAML emitter (regex against^[A-Za-z0-9.*_-]+$) to prevent malformed manifest entries from breaking the YAML.Notes
enterprise/subtree remains an open question carried over from the PRD; the features used here are all in the Apache-2.0 docs.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 CodeAdds tests/ with a tiny bash assert harness, manifest fixtures, and a runner. No framework dependency — each test file is self-contained and exits 0 on pass / 1 on fail; tests/run_tests.sh aggregates. Unit tests (no docker): - pipelock_naming: container_name, proxy_url, proxy_host_port shape - pipelock_classify: _pipelock_is_ipv4_literal classifier coverage - pipelock_allowlist: bottle_allowlist + ssh hostnames/ip_cidrs/ trusted_domains + effective_allowlist union/dedup/sort, plus rejection of non-string entries - pipelock_yaml: emitter shape (mode/enforce/api_allowlist/forward_proxy/ dlp), conditional ssrf+trusted_domains blocks, secret hygiene (manifest env values must not appear in YAML), file mode 600 Integration tests (require docker, skip cleanly otherwise): - pipelock_image: pinned digest's ENTRYPOINT is /pipelock and CMD contains 'run' and the binary --version succeeds — would catch a future image bump that changes the launcher's argv contract - pipelock_sidecar_smoke: docker create + cp YAML to /etc/pipelock.yaml + start, then probe /health — the regression test for the bug where the YAML was written to /etc/pipelock/ (parent dir absent in the distroless image) - dry_run_plan: cli.sh start --dry-run shows the egress line, counts the bottle's entry into the effective allowlist, prints the dry-run banner, and creates zero docker resources - orphan_cleanup: the cleanup primitives the start-flow trap depends on (network_remove, pipelock_stop) are idempotent against missing/never-existed resources, so the trap is safe even if pipelock_start dies before everything is wired up Assisted-by: Claude Code