diff --git a/bot_bottle/contrib/claude/agent_provider.py b/bot_bottle/contrib/claude/agent_provider.py index fd91ffbd..3d83a56f 100644 --- a/bot_bottle/contrib/claude/agent_provider.py +++ b/bot_bottle/contrib/claude/agent_provider.py @@ -35,6 +35,11 @@ if TYPE_CHECKING: _SUPERVISE_MCP_NAME = "supervise" # App-layer identity token header (mirrors egress_addon / git_http_backend). _IDENTITY_HEADER = "x-bot-bottle-identity" +# Placeholder stood in for the real identity token in the manual-recovery hint. +# The token is a per-bottle credential, so it must never be rendered into the +# host-side launch log (#476 review); the operator substitutes the value from +# inside the bottle (it rides in the agent's HTTPS_PROXY credentials). +_IDENTITY_TOKEN_PLACEHOLDER = "" def _skills_dir(guest_home: str) -> str: @@ -327,13 +332,18 @@ class ClaudeAgentProvider(AgentProvider): user="node", ) if r.returncode != 0: + # A placeholder — never the real token — keeps this per-bottle + # credential out of the host launch log (#476 review). The operator + # substitutes it from inside the bottle (it rides in the agent's + # HTTPS_PROXY credentials). manual_header = ( - f" --header {shlex.quote(f'{_IDENTITY_HEADER}: {token}')}" if token else "" + f" --header {shlex.quote(f'{_IDENTITY_HEADER}: {_IDENTITY_TOKEN_PLACEHOLDER}')}" + 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"register manually (substitute the bottle's identity token) with: " f"claude mcp add --scope user --transport http " f"supervise {supervise_url}{manual_header}" ) diff --git a/tests/unit/test_contrib_claude_provider.py b/tests/unit/test_contrib_claude_provider.py index 59cc8646..dbd617ed 100644 --- a/tests/unit/test_contrib_claude_provider.py +++ b/tests/unit/test_contrib_claude_provider.py @@ -353,7 +353,9 @@ class TestClaudeSuperviseMcp(unittest.TestCase): _plan(supervise=True), bottle, _URL, ) - def test_fallback_warning_includes_identity_header(self): + def test_fallback_warning_includes_header_but_not_raw_token(self): + # The recovery hint must show the header is required, but must NOT + # render the per-bottle identity token into the host log (#476 review). bottle = _make_bottle( exec_result=ExecResult(returncode=1, stdout="", stderr="boom"), ) @@ -363,7 +365,8 @@ class TestClaudeSuperviseMcp(unittest.TestCase): ) msg = warn_mock.call_args.args[0] self.assertIn("x-bot-bottle-identity", msg) - self.assertIn("tok-xyz", msg) + self.assertNotIn("tok-xyz", msg) + self.assertIn("", msg) def test_fallback_warning_omits_header_when_no_token(self): bottle = _make_bottle(