fix(claude): include identity header in supervise MCP fallback recovery command
tracker-policy-pr / check-pr (pull_request) Successful in 10s
test / integration-docker (pull_request) Successful in 40s
test / unit (pull_request) Successful in 44s
lint / lint (push) Successful in 51s
test / integration-firecracker (pull_request) Successful in 3m51s
test / coverage (pull_request) Successful in 22s
test / publish-infra (pull_request) Has been skipped
tracker-policy-pr / check-pr (pull_request) Successful in 10s
test / integration-docker (pull_request) Successful in 40s
test / unit (pull_request) Successful in 44s
lint / lint (push) Successful in 51s
test / integration-firecracker (pull_request) Successful in 3m51s
test / coverage (pull_request) Successful in 22s
test / publish-infra (pull_request) Has been skipped
The manual registration command shown in the fallback warning when `claude mcp add supervise` fails was missing the `--header x-bot-bottle-identity: <token>` 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 <noreply@anthropic.com>
This commit is contained in:
@@ -327,11 +327,15 @@ class ClaudeAgentProvider(AgentProvider):
|
|||||||
user="node",
|
user="node",
|
||||||
)
|
)
|
||||||
if r.returncode != 0:
|
if r.returncode != 0:
|
||||||
|
manual_header = (
|
||||||
|
f" --header {shlex.quote(f'{_IDENTITY_HEADER}: {token}')}" if token else ""
|
||||||
|
)
|
||||||
warn(
|
warn(
|
||||||
f"`claude mcp add supervise` failed (exit {r.returncode}): "
|
f"`claude mcp add supervise` failed (exit {r.returncode}): "
|
||||||
f"{(r.stderr or r.stdout or '').strip()}. Inside the bottle, "
|
f"{(r.stderr or r.stdout or '').strip()}. Inside the bottle, "
|
||||||
f"register manually with: "
|
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]:
|
def headless_prompt(self, prompt: str) -> list[str]:
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ def _plan(
|
|||||||
skills: list[str] | None = None,
|
skills: list[str] | None = None,
|
||||||
agent_provision: AgentProvisionPlan | None = None,
|
agent_provision: AgentProvisionPlan | None = None,
|
||||||
supervise: bool = False,
|
supervise: bool = False,
|
||||||
|
identity_token: str = "",
|
||||||
) -> DockerBottlePlan:
|
) -> DockerBottlePlan:
|
||||||
bottle_json: dict = {"agent_provider": {"template": "claude"}} # type: ignore
|
bottle_json: dict = {"agent_provider": {"template": "claude"}} # type: ignore
|
||||||
if supervise:
|
if supervise:
|
||||||
@@ -97,6 +98,7 @@ def _plan(
|
|||||||
),
|
),
|
||||||
supervise_plan=supervise_plan,
|
supervise_plan=supervise_plan,
|
||||||
use_runsc=False,
|
use_runsc=False,
|
||||||
|
identity_token=identity_token,
|
||||||
agent_provision=agent_provision or AgentProvisionPlan(
|
agent_provision=agent_provision or AgentProvisionPlan(
|
||||||
template="claude", command="claude", prompt_mode="append_file",
|
template="claude", command="claude", prompt_mode="append_file",
|
||||||
image="bot-bottle-claude:latest", dockerfile="",
|
image="bot-bottle-claude:latest", dockerfile="",
|
||||||
@@ -334,6 +336,15 @@ class TestClaudeSuperviseMcp(unittest.TestCase):
|
|||||||
self.assertIn("supervise", script)
|
self.assertIn("supervise", script)
|
||||||
self.assertIn(_URL, 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):
|
def test_logs_warning_on_failure_but_does_not_raise(self):
|
||||||
bottle = _make_bottle(
|
bottle = _make_bottle(
|
||||||
exec_result=ExecResult(returncode=1, stdout="", stderr="boom"),
|
exec_result=ExecResult(returncode=1, stdout="", stderr="boom"),
|
||||||
@@ -342,6 +353,29 @@ class TestClaudeSuperviseMcp(unittest.TestCase):
|
|||||||
_plan(supervise=True), bottle, _URL,
|
_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):
|
class TestClaudeHeadlessPrompt(unittest.TestCase):
|
||||||
def test_returns_p_flag_and_prompt(self):
|
def test_returns_p_flag_and_prompt(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user