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.
145 lines
5.0 KiB
Python
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
|