refactor(manifest): drop bottle.egress field, egress_proxy is the only allowlist
test / unit (pull_request) Successful in 17s
test / integration (pull_request) Successful in 1m4s

Goal: one allowlist surface (egress_proxy.routes), no second
free-form `egress:` knob. Anything that used to live there now
goes in `egress_proxy.routes` as a bare-pass entry
(`- host: <name>`).

Removed:
  - `BottleEgress` dataclass + DLP_ACTIONS constant + bottle.egress
    field on `Bottle`.
  - `pipelock_bottle_allowlist` helper.
  - `pipelock_allowlist_summary` helper (the compact preflight
    summary stopped using it after PR #31).
  - `allowlist_summary` field on `DockerBottlePlan`.
  - `bottle.egress.allowlist` folding in
    `egress_proxy_routes_for_bottle` — only DEFAULT_ALLOWLIST
    auto-folds now.
  - The two-branch logic in `pipelock_effective_allowlist`
    (egress-proxy-present vs not) — pipelock now just mirrors
    `egress_proxy_routes_for_bottle` unconditionally.

Hard-coded:
  - `request_body_scanning.action = "block"` in
    `pipelock_build_config` (was driven by
    `bottle.egress.dlp_action`). The previous default was already
    "block" — the knob to switch to "warn" was a foot-gun in a
    sandboxed agent context, so it's gone.

Tests:
  - `test_pipelock_allowlist.py` rewritten to assert the
    mirrored-from-egress-proxy semantics directly.
  - `test_manifest_md_load.py`, `test_pipelock_yaml.py`,
    `test_egress_proxy.py` fixtures migrated to put hosts in
    `egress_proxy.routes` instead of `egress.allowlist`.

Local bottle migrated too: `~/.claude-bottle/bottles/dev.md`
loses the `egress: { allowlist: [example.com] }` block, picks up
a bare-pass `- host: example.com` route.

409 unit + integration tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 21:12:56 -04:00
parent d79a976999
commit 6456904763
9 changed files with 56 additions and 209 deletions
+9 -8
View File
@@ -190,23 +190,24 @@ def egress_proxy_routes_for_bottle(
bottle: Bottle,
) -> tuple[EgressProxyRoute, ...]:
"""Effective egress-proxy routes: manifest routes followed by
bare-pass entries for DEFAULT_ALLOWLIST hosts and
`bottle.egress.allowlist` hosts. This is what gets rendered into
routes.yaml + what the addon enforces.
bare-pass entries for DEFAULT_ALLOWLIST hosts. This is what
gets rendered into routes.yaml + what the addon enforces.
Manifest routes win over defaults on host collision (manifest
routes carry more specific config — auth, path filter, role
markers). Hostname comparison is case-insensitive."""
markers). Hostname comparison is case-insensitive.
Operators that want to allow an arbitrary host that isn't in
DEFAULT_ALLOWLIST declare it directly in
`bottle.egress_proxy.routes` as a bare-pass entry
(`- host: <name>`). The legacy `bottle.egress.allowlist`
folding is gone — egress_proxy is the single allowlist surface."""
out: list[EgressProxyRoute] = list(egress_proxy_manifest_routes(bottle))
claimed: set[str] = {r.host.lower() for r in out}
for host in DEFAULT_ALLOWLIST:
if host.lower() not in claimed:
out.append(EgressProxyRoute(host=host))
claimed.add(host.lower())
for host in bottle.egress.allowlist:
if host and host.lower() not in claimed:
out.append(EgressProxyRoute(host=host))
claimed.add(host.lower())
return tuple(out)