feat(egress-proxy): cutover from cred-proxy (PRD 0017 chunk 2) #29

Merged
didericis merged 3 commits from egress-proxy-cutover into main 2026-05-25 15:04:27 -04:00
Owner

Summary

Chunk 2 of PRD 0017 (docs/prds/0017-egress-proxy-via-mitmproxy.md). Hard cutover: cred-proxy is gone, egress-proxy is the agent's HTTP_PROXY. PR #28 (chunk 1) shipped the artifact alongside cred-proxy; this PR makes the switch.

Net –3,242 LOC. 355 unit + 24 integration tests pass (was 427 pre-cutover; cred-proxy-specific tests removed).

Includes one follow-up commit on top of the cutover: universal HTTPS git-push block in the addon (is_git_push_request in egress_proxy_addon_core, called from the mitmproxy request hook before route matching). 403s any /git-receive-pack or info/refs?service=git-receive-pack — defense-in-depth so git-gate (PRD 0008) remains the only sanctioned outbound path for git writes, gitleaks-scanned by its pre-receive. Replicates cred-proxy's is_git_push_request behavior.

What changes for users

  • bottle.cred_proxy → hard error with a migration recipe pointing at PRD 0017 and showing each route field's new home (path+upstreamhost, flat auth_scheme+token_ref → nested auth: { scheme, token_ref }, path_allowlist is new, role dropped).
  • Bottles that declare any egress_proxy.routes[] now run mitmproxy as their HTTP_PROXY; egress-proxy enforces path_allowlist, injects auth, blocks HTTPS git-push, and forwards through pipelock (which keeps the egress allowlist + DLP body scan on the upstream leg). Bottles with no egress_proxy routes are unchanged — they still talk straight to pipelock.

Topology

[Agent] --HTTP_PROXY=egress-proxy-->  [egress-proxy (mitmproxy)]
            MITM with egress-proxy CA
            path_allowlist enforcement
            Authorization header injection
            HTTPS git-push 403 (universal)
         --HTTPS_PROXY=pipelock--> [pipelock]
            MITM with pipelock CA
            hostname allowlist + DLP body scan
         --egress--> Internet

Two per-bottle CAs:

  • egress-proxy CA — minted by egress_proxy_tls_init (reuses pipelock's tls init subcommand; concatenates cert+key into mitmproxy's PEM format). Installed in the agent's trust store via provision_ca (selects egress-proxy CA over pipelock CA when egress_proxy routes are declared).
  • pipelock CA — unchanged. Egress-proxy trusts it on the outbound leg via --set ssl_verify_upstream_trusted_ca=$EGRESS_PROXY_UPSTREAM_CA (Dockerfile entrypoint conditionally appends the flag based on the env var, so standalone runs without a mounted pipelock CA still boot).

Code-level

  • claude_bottle/{cred_proxy,cred_proxy_server}.py, backend/docker/{cred_proxy,provision/cred_proxy}.py, Dockerfile.cred-proxy — gone.
  • backend/docker/cred_proxy_apply.py — kept as a stub for chunk 3 to rewrite; the two constants it pulled from the deleted module are inlined.
  • manifest.pyCredProxyRoute/CredProxyConfig/role validators removed; cred_proxy key hard-fails; egress_proxy stays (added in chunk 1).
  • launch.pyegress_proxy_tls_init runs alongside pipelock_tls_init; egress-proxy sidecar wiring replaces cred-proxy. Agent's HTTP_PROXY is _agent_proxy_url(plan) (egress-proxy when routes exist, else pipelock).
  • prepare.pyegress_proxy: DockerEgressProxy parameter; sidecar-orphan probe + plan field + dashboard view renamed. The cred-proxy anthropic-base-url role/dance is replaced by a simple check: when any egress_proxy route uses token_ref="CLAUDE_CODE_OAUTH_TOKEN", set the placeholder + telemetry-off envs.
  • pipelock.pypipelock_token_hostspipelock_route_hosts; the cred-proxy hostname auto-allow is replaced by an egress-proxy auto-allow; the seed-phrase-detection workaround now triggers on host == api.anthropic.com routes.
  • egress_proxy_addon_core.pyis_git_push_request added; addon hook 403s git-receive-pack regardless of route.
  • bottle.provision — drops the cred-proxy dotfile-rewrite step entirely. The agent-side ~/.npmrc / tea-config / git-insteadof rewrites had no clear function in the egress-proxy world (HTTP_PROXY catches everything respecting it, and the only role host values matched the tools' built-in defaults). If a future bottle needs a non-default npm registry or tea login, we'll ship a more direct mechanism then.

Validated locally

  • python3 -m unittest discover -s tests -t . → 379 pass (1 skipped, environment-dependent).
  • docker build -f Dockerfile.egress-proxy . succeeds.
  • Manual import smoke: python3 -c "import claude_bottle.cli.dashboard, claude_bottle.backend.docker.backend, claude_bottle.cli.start" clean.

What's left for chunk 3

  • Retarget the cred-proxy-block MCP tool at egress-proxy (rename or keep the ID; rewrite cred_proxy_apply.py to docker-exec into egress-proxy + SIGHUP it on apply).
  • Restore the round-trip approval test in test_supervise_sidecar.py (this PR temporarily flipped it to a reject path because the approval path hits a deleted sidecar).
  • Annotate PRDs 0010 + 0014 as superseded.
## Summary Chunk 2 of PRD 0017 ([docs/prds/0017-egress-proxy-via-mitmproxy.md](../src/branch/main/docs/prds/0017-egress-proxy-via-mitmproxy.md)). Hard cutover: cred-proxy is gone, egress-proxy is the agent's HTTP_PROXY. PR #28 (chunk 1) shipped the artifact alongside cred-proxy; this PR makes the switch. **Net –3,242 LOC.** 355 unit + 24 integration tests pass (was 427 pre-cutover; cred-proxy-specific tests removed). Includes one follow-up commit on top of the cutover: universal HTTPS git-push block in the addon (`is_git_push_request` in `egress_proxy_addon_core`, called from the mitmproxy request hook before route matching). 403s any `/git-receive-pack` or `info/refs?service=git-receive-pack` — defense-in-depth so git-gate (PRD 0008) remains the only sanctioned outbound path for git writes, gitleaks-scanned by its pre-receive. Replicates cred-proxy's `is_git_push_request` behavior. ## What changes for users - `bottle.cred_proxy` → hard error with a migration recipe pointing at PRD 0017 and showing each route field's new home (`path`+`upstream` → `host`, flat `auth_scheme`+`token_ref` → nested `auth: { scheme, token_ref }`, `path_allowlist` is new, `role` dropped). - Bottles that declare any `egress_proxy.routes[]` now run mitmproxy as their HTTP_PROXY; egress-proxy enforces `path_allowlist`, injects auth, blocks HTTPS git-push, and forwards through pipelock (which keeps the egress allowlist + DLP body scan on the upstream leg). Bottles with no egress_proxy routes are unchanged — they still talk straight to pipelock. ## Topology ``` [Agent] --HTTP_PROXY=egress-proxy--> [egress-proxy (mitmproxy)] MITM with egress-proxy CA path_allowlist enforcement Authorization header injection HTTPS git-push 403 (universal) --HTTPS_PROXY=pipelock--> [pipelock] MITM with pipelock CA hostname allowlist + DLP body scan --egress--> Internet ``` Two per-bottle CAs: - **egress-proxy CA** — minted by `egress_proxy_tls_init` (reuses pipelock's `tls init` subcommand; concatenates cert+key into mitmproxy's PEM format). Installed in the agent's trust store via `provision_ca` (selects egress-proxy CA over pipelock CA when egress_proxy routes are declared). - **pipelock CA** — unchanged. Egress-proxy trusts it on the outbound leg via `--set ssl_verify_upstream_trusted_ca=$EGRESS_PROXY_UPSTREAM_CA` (Dockerfile entrypoint conditionally appends the flag based on the env var, so standalone runs without a mounted pipelock CA still boot). ## Code-level - `claude_bottle/{cred_proxy,cred_proxy_server}.py`, `backend/docker/{cred_proxy,provision/cred_proxy}.py`, `Dockerfile.cred-proxy` — gone. - `backend/docker/cred_proxy_apply.py` — kept as a stub for chunk 3 to rewrite; the two constants it pulled from the deleted module are inlined. - `manifest.py` — `CredProxyRoute`/`CredProxyConfig`/role validators removed; `cred_proxy` key hard-fails; egress_proxy stays (added in chunk 1). - `launch.py` — `egress_proxy_tls_init` runs alongside `pipelock_tls_init`; egress-proxy sidecar wiring replaces cred-proxy. Agent's HTTP_PROXY is `_agent_proxy_url(plan)` (egress-proxy when routes exist, else pipelock). - `prepare.py` — `egress_proxy: DockerEgressProxy` parameter; sidecar-orphan probe + plan field + dashboard view renamed. The cred-proxy `anthropic-base-url` role/dance is replaced by a simple check: when any egress_proxy route uses `token_ref="CLAUDE_CODE_OAUTH_TOKEN"`, set the placeholder + telemetry-off envs. - `pipelock.py` — `pipelock_token_hosts` → `pipelock_route_hosts`; the cred-proxy hostname auto-allow is replaced by an egress-proxy auto-allow; the seed-phrase-detection workaround now triggers on `host == api.anthropic.com` routes. - `egress_proxy_addon_core.py` — `is_git_push_request` added; addon hook 403s git-receive-pack regardless of route. - `bottle.provision` — drops the cred-proxy dotfile-rewrite step entirely. The agent-side ~/.npmrc / tea-config / git-insteadof rewrites had no clear function in the egress-proxy world (HTTP_PROXY catches everything respecting it, and the only `role` host values matched the tools' built-in defaults). If a future bottle needs a non-default npm registry or tea login, we'll ship a more direct mechanism then. ## Validated locally - `python3 -m unittest discover -s tests -t .` → 379 pass (1 skipped, environment-dependent). - `docker build -f Dockerfile.egress-proxy .` succeeds. - Manual import smoke: `python3 -c "import claude_bottle.cli.dashboard, claude_bottle.backend.docker.backend, claude_bottle.cli.start"` clean. ## What's left for chunk 3 - Retarget the `cred-proxy-block` MCP tool at egress-proxy (rename or keep the ID; rewrite `cred_proxy_apply.py` to docker-exec into egress-proxy + SIGHUP it on apply). - Restore the round-trip approval test in `test_supervise_sidecar.py` (this PR temporarily flipped it to a reject path because the approval path hits a deleted sidecar). - Annotate PRDs 0010 + 0014 as superseded.
didericis added 1 commit 2026-05-25 14:31:14 -04:00
feat(egress-proxy): cutover from cred-proxy (PRD 0017 chunk 2)
test / unit (pull_request) Successful in 17s
test / integration (pull_request) Successful in 1m3s
70f773ac61
Hard cutover. cred-proxy is deleted; egress-proxy is now the agent's
HTTP_PROXY (when routes are declared) with pipelock on its outbound
leg. Two per-bottle CAs are minted: egress-proxy's (agent trust
store) and pipelock's (egress-proxy's outbound trust store).

Manifest:
  - `bottle.cred_proxy` → hard error with a migration recipe.
  - `bottle.egress_proxy` is the new shape (PRD 0017 chunk 1).
  - CredProxy* types + role validators removed.

Wiring:
  - launch.py: `egress_proxy_tls_init` mints the egress-proxy CA
    (cert+key concat for mitmproxy + cert-only for agent trust);
    `DockerEgressProxy.start` docker-cps both CAs in, sets
    `HTTPS_PROXY=pipelock` + `EGRESS_PROXY_UPSTREAM_CA` so mitmdump
    trusts pipelock's MITM. Agent's HTTP_PROXY points at
    egress-proxy when routes exist, else falls back to pipelock
    (no-routes bottles unchanged).
  - prepare.py / backend.py: `cred_proxy` arg → `egress_proxy`;
    sidecar-orphan probe + plan field + dashboard view all
    renamed.
  - provision_ca: selects the egress-proxy CA when present, else
    pipelock's (filename renamed to claude-bottle-mitm-ca.crt).
  - bottle.provision: cred-proxy dotfile rewrites (~/.npmrc,
    ~/.gitconfig insteadOf, tea config) are gone — HTTP_PROXY
    catches everything respecting it.

Pipelock helpers:
  - `pipelock_token_hosts` → `pipelock_route_hosts` (now reading
    egress_proxy.routes).
  - cred-proxy hostname auto-allow → egress-proxy hostname
    auto-allow.
  - Anthropic seed-phrase workaround now triggers when an
    egress_proxy route targets api.anthropic.com (was based on the
    cred-proxy `anthropic-base-url` role).

Dockerfile.egress-proxy:
  - Entrypoint conditionally passes
    `--set ssl_verify_upstream_trusted_ca=$EGRESS_PROXY_UPSTREAM_CA`
    (via the `${VAR:+...}` shell expansion) so standalone runs without
    a mounted pipelock CA still boot.
  - mkdirs `/home/mitmproxy/.mitmproxy` ahead of `docker cp`.

Deleted: claude_bottle/{cred_proxy,cred_proxy_server}.py,
backend/docker/{cred_proxy,provision/cred_proxy}.py,
Dockerfile.cred-proxy, plus the corresponding unit + integration
tests. backend/docker/cred_proxy_apply.py stays as a stub for
chunk 3 to rewrite (its container-name + routes-path constants
are inlined so it survives without the deleted module).

Test changes:
  - test_pipelock_allowlist rewritten against egress-proxy routes
    + the new `pipelock_route_hosts`.
  - test_manifest_md_load + test_pipelock_yaml + test_yaml_subset
    fixtures migrated to the `egress_proxy: { routes: [...] }`
    shape.
  - test_supervise_sidecar's round-trip test switched from
    `dashboard.approve` to `dashboard.reject`: the approval-apply
    path on cred-proxy-block proposals hits a deleted sidecar in
    chunk 2's transitional state. Chunk 3 restores the approval
    test once the remediation flow is retargeted at egress-proxy.

376 tests pass (was 427; net delta is removed cred-proxy tests).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
didericis added 1 commit 2026-05-25 14:48:19 -04:00
feat(egress-proxy): block HTTPS git push + restore role provisioner
test / unit (pull_request) Successful in 17s
test / integration (pull_request) Successful in 1m1s
fa06a3a0ab
Two related fixes on top of PR #29's chunk-2 cutover:

1. Universal HTTPS git-push block in the egress-proxy addon
   (`is_git_push_request` in egress_proxy_addon_core, called from the
   mitmproxy request hook before route matching). 403s any
   `/git-receive-pack` or `info/refs?service=git-receive-pack` —
   defense in depth so git-gate (PRD 0008) remains the only outbound
   path for writes, gitleaks-scanned by its pre-receive. Replicates
   cred-proxy's `is_git_push_request` behavior.

2. Restored agent-side role provisioner. Brings back `Role` on
   EgressProxyRoute (manifest + runtime) with three roles —
   `anthropic-base-url`, `npm-registry`, `tea-login`. Singleton
   constraint on the first two carries over from cred-proxy.
   `git-insteadof` is intentionally absent (option 1 above handles
   the push-bypass concern, and the canonical-URL rewrite has no
   function when egress-proxy is on HTTPS_PROXY).

   The provisioner (`backend/docker/provision/egress_proxy.py`):
     - `~/.npmrc` registry= the canonical upstream URL.
     - `~/.config/tea/config.yml` logins[] entry per tea-login route.
     - `ANTHROPIC_BASE_URL` env set in prepare.py based on the
       anthropic-base-url role (was a token_ref="CLAUDE_CODE_OAUTH_TOKEN"
       check in this PR's earlier draft — the role marker is cleaner
       and matches the cred-proxy precedent the user wants kept).

   All three dotfile values point at canonical upstream URLs; the
   agent's HTTPS_PROXY=egress-proxy routes them through the proxy
   automatically.

Tests: 11 new role-validation tests, 11 new provisioner-render tests,
the chunk-1 manifest fixture exercise role=anthropic-base-url. 400
tests pass (was 376).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
didericis added 1 commit 2026-05-25 15:02:22 -04:00
revert(egress-proxy): drop Role + agent provisioner (keep git-push block)
test / unit (pull_request) Successful in 17s
test / integration (pull_request) Successful in 1m3s
4abea282e0
Partial revert of fa06a3a. The role + agent-side provisioner felt
overengineered: anthropic-base-url + npm-registry's only realistic
host values match the tool defaults, so the role tags drove no-op
dotfile writes most of the time. If non-default npm registry / tea
config is needed in a future bottle, we can ship it through a more
direct mechanism then.

What stays from fa06a3a:
  - Universal HTTPS git-push block in the egress-proxy addon
    (`is_git_push_request` in egress_proxy_addon_core, called from
    the request hook before route matching; 403s git-receive-pack
    regardless of route). This is the security backstop so git-gate
    remains the only outbound write path; PR #29 keeps it.

What gets reverted:
  - `Role` field on EgressProxyRoute (manifest + runtime).
  - `EGRESS_PROXY_ROLES` + `EGRESS_PROXY_SINGLETON_ROLES` constants
    and singleton-role validation.
  - `backend/docker/provision/egress_proxy.py` (npmrc + tea config).
  - `provision_egress_proxy` slot in `BottleBackend.provision`.
  - `prepare.py`'s role-based ANTHROPIC_BASE_URL detection (back to
    the token_ref="CLAUDE_CODE_OAUTH_TOKEN" auto-detect).
  - Manifest + provisioner tests for the above.

355 unit + 24 integration tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
didericis merged commit a135415dfe into main 2026-05-25 15:04:27 -04:00
Sign in to join this conversation.