Files
hermes-webui/tests/test_tool_card_preview_summary.py
nesquena-hermes 1aed605fb6 fix(#3267): harden collapsed tool-preview secret filter (Codex gate MUST-FIX)
Codex regression gate found the exact-name hidden-key set leaked secret-shaped
args (apiKey/access_token/clientSecret/Authorization/cookie/...) into the
always-visible collapsed tool-card header. Replace with a normalized
case-insensitive _toolArgPreviewKeyIsHidden() predicate matching secret-bearing
substrings + camelCase variants. Adds 22 parametrized regression tests pinning
the secret-key denial + a legit-key-still-shown guard. Co-authored-by preserved.
2026-05-31 19:21:15 +00:00

145 lines
5.0 KiB
Python

"""Regression coverage for quiet collapsed tool-card previews.
Collapsed tool rows are transcript metadata. They should summarize the action
(arguments/status) and keep verbose result JSON inside the expandable detail
body; otherwise long tool-heavy turns visually turn into raw debug logs.
"""
from __future__ import annotations
import json
import shutil
import subprocess
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).parent.parent.resolve()
UI_JS_PATH = REPO_ROOT / "static" / "ui.js"
NODE = shutil.which("node")
pytestmark = pytest.mark.skipif(NODE is None, reason="node not on PATH")
_DRIVER_SRC = r"""
const fs = require('fs');
const src = fs.readFileSync(process.argv[2], 'utf8');
function extractFunc(name) {
const re = new RegExp('function\\s+' + name + '\\s*\\(');
const start = src.search(re);
if (start < 0) throw new Error(name + ' not found');
let i = src.indexOf('{', start);
let depth = 1; i++;
while (depth > 0 && i < src.length) {
if (src[i] === '{') depth++;
else if (src[i] === '}') depth--;
i++;
}
return src.slice(start, i);
}
eval(extractFunc('_toolArgPreviewValue'));
eval(extractFunc('_toolArgPreviewKeyIsHidden'));
eval(extractFunc('_formatToolArgPreview'));
eval(extractFunc('_toolCardPreviewText'));
let buf = '';
process.stdin.on('data', c => { buf += c; });
process.stdin.on('end', () => {
const payload = JSON.parse(buf || '{}');
process.stdout.write(_toolCardPreviewText(payload.tc || {}, payload.displaySnippet || ''));
});
"""
@pytest.fixture(scope="module")
def driver_path(tmp_path_factory):
p = tmp_path_factory.mktemp("tool_preview_driver") / "driver.js"
p.write_text(_DRIVER_SRC, encoding="utf-8")
return str(p)
def _preview(driver_path: str, tc: dict, display_snippet: str = "") -> str:
assert NODE is not None
result = subprocess.run(
[NODE, driver_path, str(UI_JS_PATH)],
input=json.dumps({"tc": tc, "displaySnippet": display_snippet}),
capture_output=True,
text=True,
timeout=10,
)
if result.returncode != 0:
raise RuntimeError(result.stderr)
return result.stdout
def test_collapsed_tool_header_prefers_argument_summary_over_result_json(driver_path):
preview = _preview(
driver_path,
{
"name": "search_files",
"args": {
"target": "content",
"pattern": "Hattest du https://github.com/huggingface/speech-to-speech",
"path": "/tmp/hermes-webui",
"limit": "20",
},
"snippet": '{"total_count": 26, "matches": [{"path": "..."}]}',
"done": True,
},
'{"total_count": 26, "matches": [{"path": "..."}]}',
)
assert "pattern=" in preview
assert "/tmp/hermes-webui" in preview
assert "total_count" not in preview
assert "matches" not in preview
def test_collapsed_tool_header_uses_status_when_no_preview_or_args(driver_path):
preview = _preview(driver_path, {"name": "terminal", "done": True}, "long stdout that belongs in detail")
assert preview == "Completed"
def test_explicit_progress_preview_still_wins(driver_path):
preview = _preview(
driver_path,
{"name": "terminal", "preview": "Running command", "args": {"command": "pytest"}, "done": False},
"stdout",
)
assert preview == "Running command"
@pytest.mark.parametrize(
"secret_key",
[
"api_key", "apiKey", "API_KEY", "x-api-key",
"token", "access_token", "refresh_token", "auth_token", "bearer_token",
"authorization", "Authorization",
"secret", "secret_key", "client_secret", "clientSecret",
"password", "passwd",
"private_key", "credential", "cookie", "cookies",
],
)
def test_collapsed_preview_never_exposes_secret_shaped_arg_keys(driver_path, secret_key):
"""Regression: the collapsed-preview arg summary must never surface a
secret-shaped argument key/value, including camelCase / variant spellings.
Codex regression gate found that exact-name hiding leaked apiKey /
access_token / clientSecret / Authorization etc. into the always-visible
collapsed header (v0.51.190). The normalized `_toolArgPreviewKeyIsHidden`
predicate closes this; this test pins it so it can't silently regress.
"""
preview = _preview(
driver_path,
{"name": "mcp_tool", "args": {secret_key: "SUPER-SECRET-VALUE-xyz", "path": "/visible/ok"}, "done": True},
)
assert "SUPER-SECRET-VALUE-xyz" not in preview, f"{secret_key} value leaked into preview: {preview!r}"
# the legit, non-secret key is still allowed to render
assert "/visible/ok" in preview
def test_collapsed_preview_still_shows_legit_keys(driver_path):
"""The secret guard must not over-block ordinary tool args."""
preview = _preview(
driver_path,
{"name": "search_files", "args": {"target": "content", "pattern": "foo", "workdir": "/repo"}, "done": True},
)
assert "target=" in preview and "pattern=" in preview