diff --git a/claude_bottle/pipelock.py b/claude_bottle/pipelock.py index db85926..7cbe4ad 100644 --- a/claude_bottle/pipelock.py +++ b/claude_bottle/pipelock.py @@ -18,6 +18,7 @@ from pathlib import Path from typing import cast from .cred_proxy import CRED_PROXY_HOSTNAME +from .supervise import SUPERVISE_HOSTNAME from .manifest import Bottle # Baked-in default allowlist for hosts Claude Code itself needs. @@ -76,16 +77,18 @@ def pipelock_token_hosts(bottle: Bottle) -> list[str]: def pipelock_effective_allowlist(bottle: Bottle) -> list[str]: """Deduplicated union of: baked-in defaults, bottle.egress.allowlist, the cred-proxy upstream hosts derived from bottle.cred_proxy.routes, - and the cred-proxy sidecar's own hostname when any cred_proxy route - is declared. Sorted for stability. Git upstreams declared in + the cred-proxy sidecar's own hostname when any cred_proxy route is + declared, and the supervise sidecar's hostname when bottle.supervise + is enabled. Sorted for stability. Git upstreams declared in `bottle.git` do NOT contribute here — git traffic flows through the per-agent git-gate sidecar (PRD 0008), not pipelock. - The cred-proxy hostname is auto-added because the agent's - HTTP_PROXY points at pipelock, so a manifest-driven URL like - `http://cred-proxy:9099/anthropic/...` arrives at pipelock as a - request for hostname `cred-proxy`. Without this auto-allow, - pipelock would 403 the request before it reached the sidecar.""" + The cred-proxy + supervise hostnames are auto-added because the + agent's HTTP_PROXY points at pipelock, so a manifest-driven URL + like `http://cred-proxy:9099/anthropic/...` or + `http://supervise:9100/` arrives at pipelock as a request for the + sidecar hostname. Without this auto-allow, pipelock would 403 the + request before it reached the sidecar.""" seen: dict[str, None] = {} for h in DEFAULT_ALLOWLIST: seen.setdefault(h, None) @@ -96,6 +99,8 @@ def pipelock_effective_allowlist(bottle: Bottle) -> list[str]: seen.setdefault(h, None) if bottle.cred_proxy.routes: seen.setdefault(CRED_PROXY_HOSTNAME, None) + if bottle.supervise: + seen.setdefault(SUPERVISE_HOSTNAME, None) return sorted(seen.keys()) diff --git a/tests/unit/test_pipelock_allowlist.py b/tests/unit/test_pipelock_allowlist.py index a10fae1..6c0a348 100644 --- a/tests/unit/test_pipelock_allowlist.py +++ b/tests/unit/test_pipelock_allowlist.py @@ -91,6 +91,21 @@ class TestAllowlistWithTokens(unittest.TestCase): eff = pipelock_effective_allowlist(_bottle({})) self.assertNotIn("cred-proxy", eff) + def test_supervise_hostname_auto_added_when_supervise_enabled(self): + # Same reasoning as cred-proxy: the agent's HTTP_PROXY points + # at pipelock, so http://supervise:9100/ (the MCP endpoint) + # arrives at pipelock as hostname `supervise`. Without this + # auto-allow, claude-code's MCP client gets a 403 and the + # supervise server shows up as "failed" in /mcp. + 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) + class TestTlsPassthrough(unittest.TestCase): def test_default_includes_api_anthropic(self):