From 7169db1ebe40d7a66c77c19b9e8bd1f4b054c251 Mon Sep 17 00:00:00 2001 From: claude Date: Sat, 25 Jul 2026 22:20:49 +0000 Subject: [PATCH] fix(claude): include identity header in supervise MCP fallback recovery command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The manual registration command shown in the fallback warning when `claude mcp add supervise` fails was missing the `--header x-bot-bottle-identity: ` option. Since (source_ip, identity_token) attribution is now mandatory on the supervise server, the missing header caused every subsequent supervise request to fail closed as unattributed — defeating supervision for the entire bottle. Reuse the already-constructed `token` variable to build the same `manual_header` string the automatic path uses; include it in the printed command. Tests verify both the happy path and the fallback include (or correctly omit) the header. Addresses P2 finding in #471 (comment 7, didericis-codex 2026-07-25). Co-Authored-By: Claude Sonnet 4.6 --- bot_bottle/contrib/claude/agent_provider.py | 6 +++- tests/unit/test_contrib_claude_provider.py | 34 +++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/bot_bottle/contrib/claude/agent_provider.py b/bot_bottle/contrib/claude/agent_provider.py index afd2823..fd91ffb 100644 --- a/bot_bottle/contrib/claude/agent_provider.py +++ b/bot_bottle/contrib/claude/agent_provider.py @@ -327,11 +327,15 @@ class ClaudeAgentProvider(AgentProvider): user="node", ) if r.returncode != 0: + manual_header = ( + f" --header {shlex.quote(f'{_IDENTITY_HEADER}: {token}')}" if token else "" + ) warn( f"`claude mcp add supervise` failed (exit {r.returncode}): " f"{(r.stderr or r.stdout or '').strip()}. Inside the bottle, " f"register manually with: " - f"claude mcp add --scope user --transport http supervise {supervise_url}" + f"claude mcp add --scope user --transport http " + f"supervise {supervise_url}{manual_header}" ) def headless_prompt(self, prompt: str) -> list[str]: diff --git a/tests/unit/test_contrib_claude_provider.py b/tests/unit/test_contrib_claude_provider.py index 1dc5db3..59cc864 100644 --- a/tests/unit/test_contrib_claude_provider.py +++ b/tests/unit/test_contrib_claude_provider.py @@ -51,6 +51,7 @@ def _plan( skills: list[str] | None = None, agent_provision: AgentProvisionPlan | None = None, supervise: bool = False, + identity_token: str = "", ) -> DockerBottlePlan: bottle_json: dict = {"agent_provider": {"template": "claude"}} # type: ignore if supervise: @@ -97,6 +98,7 @@ def _plan( ), supervise_plan=supervise_plan, use_runsc=False, + identity_token=identity_token, agent_provision=agent_provision or AgentProvisionPlan( template="claude", command="claude", prompt_mode="append_file", image="bot-bottle-claude:latest", dockerfile="", @@ -334,6 +336,15 @@ class TestClaudeSuperviseMcp(unittest.TestCase): self.assertIn("supervise", script) self.assertIn(_URL, script) + def test_includes_identity_header_in_normal_registration(self): + bottle = _make_bottle() + ClaudeAgentProvider().provision_supervise_mcp( + _plan(supervise=True, identity_token="tok-abc"), bottle, _URL, + ) + script = bottle.exec.call_args.args[0] + self.assertIn("x-bot-bottle-identity", script) + self.assertIn("tok-abc", script) + def test_logs_warning_on_failure_but_does_not_raise(self): bottle = _make_bottle( exec_result=ExecResult(returncode=1, stdout="", stderr="boom"), @@ -342,6 +353,29 @@ class TestClaudeSuperviseMcp(unittest.TestCase): _plan(supervise=True), bottle, _URL, ) + def test_fallback_warning_includes_identity_header(self): + bottle = _make_bottle( + exec_result=ExecResult(returncode=1, stdout="", stderr="boom"), + ) + with patch("bot_bottle.contrib.claude.agent_provider.warn") as warn_mock: + ClaudeAgentProvider().provision_supervise_mcp( + _plan(supervise=True, identity_token="tok-xyz"), bottle, _URL, + ) + msg = warn_mock.call_args.args[0] + self.assertIn("x-bot-bottle-identity", msg) + self.assertIn("tok-xyz", msg) + + def test_fallback_warning_omits_header_when_no_token(self): + bottle = _make_bottle( + exec_result=ExecResult(returncode=1, stdout="", stderr="boom"), + ) + with patch("bot_bottle.contrib.claude.agent_provider.warn") as warn_mock: + ClaudeAgentProvider().provision_supervise_mcp( + _plan(supervise=True, identity_token=""), bottle, _URL, + ) + msg = warn_mock.call_args.args[0] + self.assertNotIn("x-bot-bottle-identity", msg) + class TestClaudeHeadlessPrompt(unittest.TestCase): def test_returns_p_flag_and_prompt(self):