fix(supervise): provision MCP via claude mcp add #25

Merged
didericis merged 9 commits from supervise-mcp-add-via-cli into main 2026-05-25 08:31:17 -04:00
2 changed files with 67 additions and 2 deletions
Showing only changes of commit 307400f08a - Show all commits
+25 -2
View File
@@ -19,7 +19,7 @@ from typing import Callable, Generator
from ...log import die, info
from ...pipelock import pipelock_build_config, pipelock_render_yaml
from ...supervise import CURRENT_CONFIG_DIR_IN_AGENT
from ...supervise import CURRENT_CONFIG_DIR_IN_AGENT, SUPERVISE_HOSTNAME
from . import network as network_mod
from . import util as docker_mod
from .bottle import DockerBottle
@@ -185,6 +185,29 @@ def launch(
teardown()
def _agent_no_proxy(plan: DockerBottlePlan) -> str:
"""NO_PROXY value for the agent container. Standard loopback +
`supervise` when the supervise sidecar is enabled.
Supervise needs to bypass pipelock because the MCP tool-call
pattern is long-poll: claude-code opens an HTTPS-style request to
http://supervise:9100/, the sidecar holds it open until the
operator approves (potentially minutes), then returns the
response. Pipelock is a forward proxy with idle timeouts;
pipelock cuts the long-polled connection well before the operator
can act, and claude-code reports the tool as ✘ failed even
though /mcp shows ✔ connected.
The supervise sidecar is on the bottle's internal network with
the `supervise` network-alias, so the agent can dial it
directly via docker DNS. Body-scanning the supervise traffic
isn't critical — the operator reviews every proposal in the TUI."""
hosts = ["localhost", "127.0.0.1"]
if plan.supervise_plan is not None:
hosts.append(SUPERVISE_HOSTNAME)
return ",".join(hosts)
def _run_agent_container(plan: DockerBottlePlan, internal_network: str) -> str:
"""Build the `docker run` argv and execute it, handling name-
conflict races by incrementing the suffix (unless the name was
@@ -196,7 +219,7 @@ def _run_agent_container(plan: DockerBottlePlan, internal_network: str) -> str:
"--network", internal_network,
"-e", f"HTTPS_PROXY={proxy_url}",
"-e", f"HTTP_PROXY={proxy_url}",
"-e", "NO_PROXY=localhost,127.0.0.1",
"-e", f"NO_PROXY={_agent_no_proxy(plan)}",
# CA trust trio for the agent process. Docker propagates
# run-time env into `docker exec`, so `claude` sees these
# without per-exec threading. NODE_EXTRA_CA_CERTS points at
+42
View File
@@ -0,0 +1,42 @@
"""Unit: agent NO_PROXY value builder (PR #25 follow-up).
claude-code's HTTP MCP client must bypass pipelock for the supervise
sidecar — long-poll tool calls would hit pipelock's idle timeout
otherwise. This test pins the rule: localhost always; supervise iff
the supervise sidecar is in the plan."""
import unittest
from pathlib import Path
from claude_bottle.backend.docker.launch import _agent_no_proxy
class _FakePlan:
"""Just enough plan shape for the helper — no full DockerBottlePlan
construction needed."""
def __init__(self, supervise_plan):
self.supervise_plan = supervise_plan
class _SentinelSupervisePlan:
"""The helper only checks `supervise_plan is not None`; any object
is fine."""
class TestAgentNoProxy(unittest.TestCase):
def test_loopback_only_when_no_supervise(self):
self.assertEqual(
"localhost,127.0.0.1",
_agent_no_proxy(_FakePlan(supervise_plan=None)),
)
def test_supervise_appended_when_enabled(self):
self.assertEqual(
"localhost,127.0.0.1,supervise",
_agent_no_proxy(_FakePlan(supervise_plan=_SentinelSupervisePlan())),
)
if __name__ == "__main__":
unittest.main()