Files
hermes-agent/tests/hermes_cli/test_mcp_probe_redaction.py
T
teknium1 23036e20a6 fix(ux): plain-language, actionable user-facing messages (core)
Squashed integration of the user-facing message audit for this surface set.
Full per-finding receipts: /tmp/ux-audit/lanes/*-receipt.md (campaign artifacts).
2026-09-15 04:12:13 -07:00

477 lines
17 KiB
Python

"""MCP test/dashboard must not fingerprint Authorization credentials (#97460)."""
import argparse
import itertools
import json
import pytest
import yaml
SYNTHETIC = "SYNTHETIC_MCP_BEARER_NOT_A_SECRET_123456"
SYNTHETIC_PREFIX = "SYNTHETIC_MCP_BEARER"
SYNTHETIC_SUFFIX = "SECRET_123456"
HEADER = f"Authorization: Bearer {SYNTHETIC}"
OPAQUE_API_KEY = "opaquecredential1234567890ABCDEF"
OPAQUE_PREFIX = OPAQUE_API_KEY[:6]
OPAQUE_SUFFIX = OPAQUE_API_KEY[-4:]
def _assert_fully_redacted(text: str) -> None:
assert SYNTHETIC not in text
assert SYNTHETIC_PREFIX not in text
assert SYNTHETIC_SUFFIX not in text
assert OPAQUE_API_KEY not in text
assert OPAQUE_PREFIX not in text
assert OPAQUE_SUFFIX not in text
def _make_args(**kwargs):
defaults = {
"name": "test-server",
"url": None,
"mcp_command": None,
"args": None,
"auth": None,
"preset": None,
"env": None,
"mcp_action": None,
"connect_timeout": None,
}
defaults.update(kwargs)
return argparse.Namespace(**defaults)
@pytest.fixture(autouse=True)
def _isolate_config(tmp_path, monkeypatch):
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
monkeypatch.setattr("hermes_cli.config.get_hermes_home", lambda: tmp_path)
config_path = tmp_path / "config.yaml"
env_path = tmp_path / ".env"
monkeypatch.setattr("hermes_cli.config.get_config_path", lambda: config_path)
monkeypatch.setattr("hermes_cli.config.get_env_path", lambda: env_path)
return tmp_path
def _seed_config(tmp_path, mcp_servers):
config_path = tmp_path / "config.yaml"
with open(config_path, "w", encoding="utf-8") as f:
yaml.safe_dump({"mcp_servers": mcp_servers, "_config_version": 9}, f)
class TestRedactMcpProbeText:
def test_authorization_header_is_fully_replaced(self):
from hermes_cli.mcp_config import redact_mcp_probe_text
out = redact_mcp_probe_text(f"401 {HEADER}")
_assert_fully_redacted(out)
assert "Authorization:" in out
assert "Bearer ***" in out
def test_bare_bearer_scheme_is_fully_replaced(self):
from hermes_cli.mcp_config import redact_mcp_probe_text
out = redact_mcp_probe_text(f"probe Bearer {SYNTHETIC} failed")
_assert_fully_redacted(out)
assert "Bearer ***" in out
def test_opaque_api_key_header_is_fully_replaced(self):
from hermes_cli.mcp_config import redact_mcp_probe_text
out = redact_mcp_probe_text(f"connect failed: X-Api-Key: {OPAQUE_API_KEY}")
_assert_fully_redacted(out)
assert "X-Api-Key: ***" in out
def test_digest_authorization_params_are_fully_replaced(self):
from hermes_cli.mcp_config import redact_mcp_probe_text
out = redact_mcp_probe_text(
f'Authorization: Digest username="u", response="{OPAQUE_API_KEY}"'
)
_assert_fully_redacted(out)
assert "Digest ***" in out
def test_digest_response_first_does_not_orphan_quoted_value(self):
from hermes_cli.mcp_config import redact_mcp_probe_text
out = redact_mcp_probe_text(
f'Authorization: Digest response="{OPAQUE_API_KEY}", username="u"'
)
_assert_fully_redacted(out)
assert "Digest ***" in out
assert "response=" not in out
def test_python_mapping_api_key_is_fully_replaced(self):
from hermes_cli.mcp_config import redact_mcp_probe_text
payload = {"X-Api-Key": OPAQUE_API_KEY}
out = redact_mcp_probe_text(f"headers={payload!r}")
_assert_fully_redacted(out)
assert "***" in out
def test_json_mapping_api_key_is_fully_replaced(self):
from hermes_cli.mcp_config import redact_mcp_probe_text
out = redact_mcp_probe_text(json.dumps({"X-Api-Key": OPAQUE_API_KEY}))
_assert_fully_redacted(out)
assert "***" in out
@pytest.mark.parametrize(
"params",
list(
itertools.permutations(
[
("username", "u"),
("response", OPAQUE_API_KEY),
("opaque", OPAQUE_API_KEY),
]
)
),
)
@pytest.mark.parametrize("quoted_values", [True, False])
def test_digest_parameter_order_and_quoting(self, params, quoted_values):
from hermes_cli.mcp_config import redact_mcp_probe_text
parts = []
for key, value in params:
parts.append(f'{key}="{value}"' if quoted_values else f"{key}={value}")
wire = "Authorization: Digest " + ", ".join(parts)
out = redact_mcp_probe_text(wire)
_assert_fully_redacted(out)
assert "Digest ***" in out
@pytest.mark.parametrize("key_quote,value_quote", [
("'", "'"),
('"', '"'),
("'", '"'),
('"', "'"),
])
def test_mapping_quotes_on_api_key(self, key_quote, value_quote):
from hermes_cli.mcp_config import redact_mcp_probe_text
text = (
"headers={"
f"{key_quote}X-Api-Key{key_quote}: "
f"{value_quote}{OPAQUE_API_KEY}{value_quote}"
"}"
)
out = redact_mcp_probe_text(text)
_assert_fully_redacted(out)
assert "***" in out
def test_mapping_digest_authorization_is_fully_replaced(self):
from hermes_cli.mcp_config import redact_mcp_probe_text
value = f'Digest response="{OPAQUE_API_KEY}", username="u"'
out = redact_mcp_probe_text(f"headers={{'Authorization': {value!r}}}")
_assert_fully_redacted(out)
assert "Digest ***" in out
def test_shared_secret_header_vocabulary_is_fully_replaced(self):
from agent.redact import _SECRET_HEADER_NAMES
from hermes_cli.mcp_config import redact_mcp_probe_text
inner = _SECRET_HEADER_NAMES
if inner.startswith("(?:") and inner.endswith(")"):
inner = inner[3:-1]
for name in inner.split("|"):
out = redact_mcp_probe_text({name: OPAQUE_API_KEY}.__repr__())
_assert_fully_redacted(out)
def test_header_display_masks_opaque_api_key(self):
from hermes_cli.mcp_config import redact_mcp_header_display
assert redact_mcp_header_display("X-Api-Key", OPAQUE_API_KEY) == "***"
assert redact_mcp_header_display(
"Authorization", f"Bearer ${{MCP_TEST_TOKEN}}; backup={SYNTHETIC}"
) == "***"
def test_header_display_keeps_pure_env_template(self):
from hermes_cli.mcp_config import redact_mcp_header_display
assert redact_mcp_header_display(
"Authorization", "Bearer ${MCP_TEST_TOKEN}"
) == "Bearer ${MCP_TEST_TOKEN}"
class TestProbeHelperRedactsBeforeRaise:
def test_probe_exception_leaving_helper_is_already_safe(self, monkeypatch):
import tools.mcp_tool_lifecycle as mcp_lifecycle
import tools.mcp_tool_loop as mcp_loop
from hermes_cli.mcp_config import _probe_single_server
monkeypatch.setattr(mcp_loop, "_ensure_mcp_loop", lambda: None)
monkeypatch.setattr(mcp_lifecycle, "_stop_mcp_loop_if_idle", lambda: None)
def boom(coro, timeout):
coro.close()
raise RuntimeError(f"connect failed: {HEADER}")
monkeypatch.setattr(mcp_loop, "_run_on_mcp_loop", boom)
with pytest.raises(Exception) as caught:
_probe_single_server("ink", {"url": "https://mcp.example/mcp"})
_assert_fully_redacted(str(caught.value))
assert "Bearer ***" in str(caught.value)
def test_probe_exception_redacts_opaque_api_key(self, monkeypatch):
import tools.mcp_tool_lifecycle as mcp_lifecycle
import tools.mcp_tool_loop as mcp_loop
from hermes_cli.mcp_config import _probe_single_server
monkeypatch.setattr(mcp_loop, "_ensure_mcp_loop", lambda: None)
monkeypatch.setattr(mcp_lifecycle, "_stop_mcp_loop_if_idle", lambda: None)
def boom(coro, timeout):
coro.close()
raise RuntimeError(f"connect failed: X-Api-Key: {OPAQUE_API_KEY}")
monkeypatch.setattr(mcp_loop, "_run_on_mcp_loop", boom)
with pytest.raises(Exception) as caught:
_probe_single_server("ink", {"url": "https://mcp.example/mcp"})
_assert_fully_redacted(str(caught.value))
assert "X-Api-Key: ***" in str(caught.value)
def test_probe_exception_redacts_digest_response_first(self, monkeypatch):
import tools.mcp_tool_lifecycle as mcp_lifecycle
import tools.mcp_tool_loop as mcp_loop
from hermes_cli.mcp_config import _probe_single_server
monkeypatch.setattr(mcp_loop, "_ensure_mcp_loop", lambda: None)
monkeypatch.setattr(mcp_lifecycle, "_stop_mcp_loop_if_idle", lambda: None)
def boom(coro, timeout):
coro.close()
raise RuntimeError(
f'connect failed: Authorization: Digest '
f'response="{OPAQUE_API_KEY}", username="u"'
)
monkeypatch.setattr(mcp_loop, "_run_on_mcp_loop", boom)
with pytest.raises(Exception) as caught:
_probe_single_server("ink", {"url": "https://mcp.example/mcp"})
_assert_fully_redacted(str(caught.value))
assert "Digest ***" in str(caught.value)
def test_probe_exception_redacts_python_mapping_api_key(self, monkeypatch):
import tools.mcp_tool_lifecycle as mcp_lifecycle
import tools.mcp_tool_loop as mcp_loop
from hermes_cli.mcp_config import _probe_single_server
monkeypatch.setattr(mcp_loop, "_ensure_mcp_loop", lambda: None)
monkeypatch.setattr(mcp_lifecycle, "_stop_mcp_loop_if_idle", lambda: None)
def boom(coro, timeout):
coro.close()
raise RuntimeError(f"headers={{'X-Api-Key': '{OPAQUE_API_KEY}'}}")
monkeypatch.setattr(mcp_loop, "_run_on_mcp_loop", boom)
with pytest.raises(Exception) as caught:
_probe_single_server("ink", {"url": "https://mcp.example/mcp"})
_assert_fully_redacted(str(caught.value))
assert "***" in str(caught.value)
class TestCmdMcpTestRedaction:
def test_success_display_keeps_env_template(self, tmp_path, capsys, monkeypatch):
monkeypatch.setenv("MCP_TEST_TOKEN", SYNTHETIC)
_seed_config(tmp_path, {
"ink": {
"url": "https://mcp.example/mcp",
"headers": {"Authorization": "Bearer ${MCP_TEST_TOKEN}"},
},
})
monkeypatch.setattr(
"hermes_cli.mcp_config._probe_single_server",
lambda *a, **k: [("ping", "Ping")],
)
from hermes_cli.mcp_config import cmd_mcp_test
cmd_mcp_test(argparse.Namespace(name="ink"))
out = capsys.readouterr().out
_assert_fully_redacted(out)
assert "Connected" in out
assert "${MCP_TEST_TOKEN}" in (tmp_path / "config.yaml").read_text(encoding="utf-8")
def test_success_display_masks_literal_header(self, tmp_path, capsys, monkeypatch):
_seed_config(tmp_path, {
"ink": {
"url": "https://mcp.example/mcp",
"headers": {"Authorization": f"Bearer {SYNTHETIC}"},
},
})
monkeypatch.setattr(
"hermes_cli.mcp_config._probe_single_server",
lambda *a, **k: [("ping", "Ping")],
)
from hermes_cli.mcp_config import cmd_mcp_test
cmd_mcp_test(argparse.Namespace(name="ink"))
out = capsys.readouterr().out
_assert_fully_redacted(out)
assert "Authorization:" in out
def test_success_display_masks_opaque_api_key_header(self, tmp_path, capsys, monkeypatch):
_seed_config(tmp_path, {
"ink": {
"url": "https://mcp.example/mcp",
"headers": {"X-Api-Key": OPAQUE_API_KEY},
},
})
monkeypatch.setattr(
"hermes_cli.mcp_config._probe_single_server",
lambda *a, **k: [("ping", "Ping")],
)
from hermes_cli.mcp_config import cmd_mcp_test
cmd_mcp_test(argparse.Namespace(name="ink"))
out = capsys.readouterr().out
_assert_fully_redacted(out)
assert "X-Api-Key:" in out
assert "***" in out
def test_success_display_masks_mixed_env_and_literal(self, tmp_path, capsys, monkeypatch):
_seed_config(tmp_path, {
"ink": {
"url": "https://mcp.example/mcp",
"headers": {
"Authorization": f"Bearer ${{MCP_TEST_TOKEN}}; backup={SYNTHETIC}",
},
},
})
monkeypatch.setattr(
"hermes_cli.mcp_config._probe_single_server",
lambda *a, **k: [("ping", "Ping")],
)
from hermes_cli.mcp_config import cmd_mcp_test
cmd_mcp_test(argparse.Namespace(name="ink"))
out = capsys.readouterr().out
_assert_fully_redacted(out)
assert "***" in out
def test_probe_exception_is_redacted(self, tmp_path, capsys, monkeypatch):
_seed_config(tmp_path, {
"ink": {"url": "https://mcp.example/mcp"},
})
def boom(*a, **k):
raise RuntimeError(f"connect failed: {HEADER}")
monkeypatch.setattr("hermes_cli.mcp_config._probe_single_server", boom)
from hermes_cli.mcp_config import cmd_mcp_test
cmd_mcp_test(argparse.Namespace(name="ink"))
out = capsys.readouterr().out
_assert_fully_redacted(out)
assert "Connection failed" in out
assert "***" in out # credential span masked, whichever redactor got it first
class TestDashboardMcpTestRedaction:
def test_probe_error_json_is_redacted(self, tmp_path, monkeypatch):
try:
from starlette.testclient import TestClient
except ImportError:
pytest.skip("fastapi/starlette not installed")
from hermes_cli.web_server import app, _SESSION_HEADER_NAME, _SESSION_TOKEN
import hermes_cli.mcp_config as mcp_config
_seed_config(tmp_path, {
"ink": {"url": "https://mcp.example/mcp"},
})
def boom(name, config, connect_timeout=30, details=None):
raise RuntimeError(f"connect failed: {HEADER}")
monkeypatch.setattr(mcp_config, "_probe_single_server", boom)
monkeypatch.setattr(mcp_config, "_get_mcp_servers", lambda: {
"ink": {"url": "https://mcp.example/mcp"},
})
client = TestClient(app)
client.headers[_SESSION_HEADER_NAME] = _SESSION_TOKEN
resp = client.post("/api/mcp/servers/ink/test")
assert resp.status_code == 200
body = resp.json()
assert body["ok"] is False
_assert_fully_redacted(body["error"])
assert "Bearer ***" in body["error"]
assert SYNTHETIC not in resp.text
def test_probe_error_json_redacts_digest_and_mapping(self, tmp_path, monkeypatch):
try:
from starlette.testclient import TestClient
except ImportError:
pytest.skip("fastapi/starlette not installed")
from hermes_cli.web_server import app, _SESSION_HEADER_NAME, _SESSION_TOKEN
import hermes_cli.mcp_config as mcp_config
_seed_config(tmp_path, {
"ink": {"url": "https://mcp.example/mcp"},
})
def boom(name, config, connect_timeout=30, details=None):
raise RuntimeError(
f"headers={{'X-Api-Key': '{OPAQUE_API_KEY}'}}; "
f'Authorization: Digest response="{OPAQUE_API_KEY}", username="u"'
)
monkeypatch.setattr(mcp_config, "_probe_single_server", boom)
monkeypatch.setattr(mcp_config, "_get_mcp_servers", lambda: {
"ink": {"url": "https://mcp.example/mcp"},
})
client = TestClient(app)
client.headers[_SESSION_HEADER_NAME] = _SESSION_TOKEN
resp = client.post("/api/mcp/servers/ink/test")
assert resp.status_code == 200
body = resp.json()
assert body["ok"] is False
_assert_fully_redacted(body["error"])
assert "***" in body["error"]
assert SYNTHETIC not in resp.text
assert OPAQUE_API_KEY not in resp.text
class TestSiblingProbeConsumersRedact:
def test_mcp_add_redacts_probe_exception(self, tmp_path, capsys, monkeypatch):
def boom(*a, **k):
raise RuntimeError(f"connect failed: {HEADER}")
monkeypatch.setattr("hermes_cli.mcp_config._probe_single_server", boom)
monkeypatch.setattr("hermes_cli.mcp_config._confirm", lambda *a, **k: False)
from hermes_cli.mcp_config import cmd_mcp_add
cmd_mcp_add(_make_args(name="ink", url="https://mcp.example/mcp"))
out = capsys.readouterr().out
_assert_fully_redacted(out)
assert "Failed to connect" in out
assert "***" in out # credential span masked, whichever redactor got it first
def test_mcp_login_redacts_probe_exception(self, tmp_path, capsys, monkeypatch):
_seed_config(tmp_path, {
"ink": {"url": "https://mcp.example/mcp", "auth": "oauth"},
})
def boom(*a, **k):
raise RuntimeError(f"connect failed: {HEADER}")
monkeypatch.setattr("hermes_cli.mcp_config._probe_single_server", boom)
monkeypatch.setattr(
"tools.mcp_oauth.humanize_oauth_registration_error",
lambda *a, **k: None,
)
from hermes_cli.mcp_config import cmd_mcp_login
cmd_mcp_login(_make_args(name="ink"))
out = capsys.readouterr().out
_assert_fully_redacted(out)
assert "Authentication failed" in out
assert "Bearer ***" in out