From d2e047fa664a5187d5d6645992e39c7be392f6d2 Mon Sep 17 00:00:00 2001 From: didericis Date: Mon, 25 May 2026 07:27:30 -0400 Subject: [PATCH] fix(pipelock): auto-allow `supervise` hostname like `cred-proxy` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When PR #19 added the supervise sidecar (PRD 0013), I forgot to mirror the cred-proxy auto-allow in pipelock_effective_allowlist. The agent's HTTP_PROXY points at pipelock, so a request for http://supervise:9100/ (the MCP endpoint claude-code dials) arrives at pipelock as hostname `supervise` — and pipelock 403s it because the host isn't in api_allowlist. End-user symptom: even after `claude mcp add` registers the supervise server, `/mcp` shows it as ✘ failed and the supervise sidecar's docker logs are silent (request never gets through). Mirror what cred-proxy already does: when bottle.supervise is True, add SUPERVISE_HOSTNAME to the rendered pipelock allowlist. New tests cover both the auto-add and the no-add-when-disabled invariants. Existing bottles: the dashboard `pipelock edit ` verb (or backend.docker.pipelock_apply.apply_allowlist_change) can apply this fix to a running bottle without a relaunch. Co-Authored-By: Claude Opus 4.7 --- claude_bottle/pipelock.py | 19 ++++++++++++------- tests/unit/test_pipelock_allowlist.py | 15 +++++++++++++++ 2 files changed, 27 insertions(+), 7 deletions(-) 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):