Files
bot-bottle/tests/unit/test_pipelock_allowlist.py
T
didericis 6456904763
test / unit (pull_request) Successful in 17s
test / integration (pull_request) Successful in 1m4s
refactor(manifest): drop bottle.egress field, egress_proxy is the only allowlist
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>
2026-05-25 21:12:56 -04:00

115 lines
4.5 KiB
Python

"""Unit: pipelock_effective_allowlist — pipelock's allowlist
mirrors `egress_proxy_routes_for_bottle` (which folds in
DEFAULT_ALLOWLIST). Git upstreams declared in `bottle.git` don't
contribute; they flow through the per-agent git-gate (PRD 0008)."""
import unittest
from claude_bottle.manifest import Manifest
from claude_bottle.pipelock import (
pipelock_effective_allowlist,
pipelock_effective_tls_passthrough,
)
def _bottle(spec):
return Manifest.from_json_obj({
"bottles": {"dev": spec},
"agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
}).bottles["dev"]
def _routes(routes):
return {"egress_proxy": {"routes": routes}}
class TestEffectiveAllowlist(unittest.TestCase):
def test_default_allowlist_present_without_any_manifest_routes(self):
# No egress_proxy routes declared → pipelock allowlist is
# just the baked DEFAULT_ALLOWLIST (folded in by
# egress_proxy_routes_for_bottle).
eff = pipelock_effective_allowlist(_bottle({}))
self.assertIn("api.anthropic.com", eff)
self.assertIn("sentry.io", eff)
def test_sorted_and_deduped(self):
# Manifest route for a default host collapses to one entry.
eff = pipelock_effective_allowlist(_bottle(_routes([
{"host": "api.anthropic.com",
"auth": {"scheme": "Bearer", "token_ref": "T"}},
])))
self.assertEqual(len(eff), len(set(eff)))
self.assertEqual(eff, sorted(eff))
class TestAllowlistWithRoutes(unittest.TestCase):
def test_manifest_route_hosts_present(self):
eff = pipelock_effective_allowlist(_bottle(_routes([
{"host": "registry.npmjs.org",
"auth": {"scheme": "Bearer", "token_ref": "N"}},
{"host": "api.github.com",
"auth": {"scheme": "Bearer", "token_ref": "G"}},
])))
self.assertIn("registry.npmjs.org", eff)
self.assertIn("api.github.com", eff)
def test_baked_defaults_still_present_alongside_manifest_routes(self):
eff = pipelock_effective_allowlist(_bottle(_routes([
{"host": "x.example",
"auth": {"scheme": "Bearer", "token_ref": "T"}},
])))
for default in ("api.anthropic.com", "sentry.io"):
self.assertIn(default, eff)
self.assertIn("x.example", eff)
def test_egress_proxy_hostname_NOT_in_pipelock_allowlist(self):
# The agent never dials egress-proxy via the proxy mechanism
# — it IS the proxy. Pipelock receives upstream hostnames
# from egress-proxy's CONNECT requests, not the
# `egress-proxy` hostname itself.
eff = pipelock_effective_allowlist(_bottle(_routes([
{"host": "x.example",
"auth": {"scheme": "Bearer", "token_ref": "T"}},
])))
self.assertNotIn("egress-proxy", eff)
def test_supervise_hostname_auto_added_when_supervise_enabled(self):
eff = pipelock_effective_allowlist(_bottle({"supervise": True}))
self.assertIn("supervise", eff)
def test_supervise_hostname_NOT_added_when_disabled(self):
eff = pipelock_effective_allowlist(_bottle({}))
self.assertNotIn("supervise", eff)
eff_explicit = pipelock_effective_allowlist(_bottle({"supervise": False}))
self.assertNotIn("supervise", eff_explicit)
def test_path_allowlist_does_not_affect_pipelock_allowlist(self):
# path_allowlist is enforced by egress-proxy, not pipelock.
# Pipelock only sees the upstream hostname; the path filter
# has already passed (or 403'd) at egress-proxy.
eff = pipelock_effective_allowlist(_bottle(_routes([
{"host": "github.com", "path_allowlist": ["/x/", "/y/"]},
])))
self.assertIn("github.com", eff)
for entry in eff:
self.assertFalse(entry.startswith("/"))
class TestTlsPassthrough(unittest.TestCase):
def test_default_includes_api_anthropic(self):
passthrough = pipelock_effective_tls_passthrough(_bottle({}))
self.assertEqual(["api.anthropic.com"], passthrough)
def test_route_hosts_NOT_added_to_passthrough(self):
passthrough = pipelock_effective_tls_passthrough(_bottle(_routes([
{"host": "api.github.com",
"auth": {"scheme": "Bearer", "token_ref": "G"}},
{"host": "registry.npmjs.org",
"auth": {"scheme": "Bearer", "token_ref": "N"}},
])))
self.assertEqual(["api.anthropic.com"], passthrough)
if __name__ == "__main__":
unittest.main()