fix(pipelock): auto-allow supervise hostname like cred-proxy
test / unit (pull_request) Successful in 18s
test / integration (pull_request) Successful in 1m35s

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 <bottle>` 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 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 07:27:30 -04:00
parent 0e2fc97aa8
commit d2e047fa66
2 changed files with 27 additions and 7 deletions
+12 -7
View File
@@ -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())
+15
View File
@@ -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):