Skip to content

Commit 81a91bc

Browse files
authored
fix(p1-11): redact CLI top-level exception text before stderr (#435)
* test(cli): add failing scaffold for top-level exception redaction (F013 S1) * fix(cli): redact top-level exception text before stderr (F013 S2) * test(first-use-flow): add smoke for redacted error path (F013 S3) * test(mcp-error-redaction): make assertion robust to stderr/stdout split + add REDACTED marker check * test(mcp-error-redaction): add --json error envelope coverage for redaction
1 parent 872de34 commit 81a91bc

3 files changed

Lines changed: 86 additions & 2 deletions

File tree

autosearch/cli/main.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from autosearch.core import secrets_store
1313
from autosearch.core.environment_probe import probe_environment
1414
from autosearch.core.models import SearchMode
15+
from autosearch.core.redact import redact
1516
from autosearch.core.scope_clarifier import ScopeClarifier
1617
from autosearch.core.search_scope import (
1718
ChannelScope,
@@ -797,18 +798,19 @@ def _is_tty() -> bool:
797798

798799

799800
def _exit_query_failure(message: str, *, exit_code: int, json_output: bool) -> None:
801+
safe_message = redact(message)
800802
if json_output:
801803
typer.echo(
802804
json.dumps(
803805
{
804806
"delivery_status": "error",
805-
"error": message,
807+
"error": safe_message,
806808
"exit_code": exit_code,
807809
}
808810
)
809811
)
810812
else:
811-
typer.echo(message, err=True)
813+
typer.echo(safe_message, err=True)
812814
raise typer.Exit(code=exit_code)
813815

814816

tests/smoke/test_first_use_flow.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from __future__ import annotations
1212

1313
import json
14+
import os
1415
import subprocess
1516

1617
import pytest
@@ -77,6 +78,47 @@ def test_query_json_loop_emits_valid_envelope() -> None:
7778
assert payload["evidence_count"] == len(payload["evidence"])
7879

7980

81+
@pytest.mark.smoke
82+
def test_first_use_error_path_redacted(tmp_path) -> None:
83+
leaked_key = "sk-FAKEKEY" + "1234567890abcdef"
84+
(tmp_path / "sitecustomize.py").write_text(
85+
"""
86+
import autosearch.cli.query_pipeline as query_pipeline
87+
from autosearch.cli.query_pipeline import QueryResult
88+
89+
_LEAKED_KEY = "sk-FAKEKEY" + "1234567890abcdef"
90+
91+
92+
async def _failing_run_query(_query: str, **_kwargs: object) -> QueryResult:
93+
raise RuntimeError(f"upstream returned token {_LEAKED_KEY}")
94+
95+
96+
query_pipeline.run_query = _failing_run_query
97+
""",
98+
encoding="utf-8",
99+
)
100+
env = smoke_env(AUTOSEARCH_LLM_MODE="dummy")
101+
env["PYTHONPATH"] = f"{tmp_path}{os.pathsep}{env['PYTHONPATH']}"
102+
103+
result = subprocess.run(
104+
[
105+
*console_script_command("autosearch", "autosearch.cli.main"),
106+
"query",
107+
"smoke redaction query",
108+
],
109+
capture_output=True,
110+
text=True,
111+
env=env,
112+
timeout=60,
113+
)
114+
115+
assert result.returncode == 1, (
116+
f"query exited {result.returncode}; stderr:\n{result.stderr}\nstdout:\n{result.stdout}"
117+
)
118+
assert leaked_key not in result.stderr
119+
assert "[REDACTED]" in result.stderr
120+
121+
80122
@pytest.mark.smoke
81123
def test_query_help_lists_subcommand() -> None:
82124
"""`autosearch query --help` proves the v2 thin-orchestration CLI is wired up."""

tests/unit/test_mcp_error_redaction.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
from __future__ import annotations
1313

1414
import asyncio
15+
import json
1516

1617
import pytest
18+
from typer.testing import CliRunner
1719

1820

1921
@pytest.fixture(autouse=True)
@@ -93,3 +95,41 @@ def test_experience_event_query_is_redacted_before_write(tmp_path, monkeypatch):
9395
).read_text(encoding="utf-8")
9496
assert "sk-ant-secretvalue" not in patterns
9597
assert "REDACTED" in patterns
98+
99+
100+
def test_cli_query_top_level_exception_redacted(monkeypatch: pytest.MonkeyPatch) -> None:
101+
from autosearch.cli.main import app
102+
from autosearch.cli.query_pipeline import QueryResult
103+
104+
leaked_key = "sk-FAKEKEY" + "1234567890abcdef"
105+
106+
async def _failing_run_query(_query: str, **_kwargs: object) -> QueryResult:
107+
raise RuntimeError(f"upstream returned token {leaked_key}")
108+
109+
monkeypatch.setattr("autosearch.cli.query_pipeline.run_query", _failing_run_query)
110+
111+
result = CliRunner().invoke(app, ["query", "redaction smoke"])
112+
113+
combined_output = (result.output or "") + (result.stderr or "")
114+
assert result.exit_code == 1
115+
assert leaked_key not in combined_output
116+
assert "REDACTED" in combined_output
117+
118+
119+
def test_cli_query_json_error_envelope_redacted(monkeypatch: pytest.MonkeyPatch) -> None:
120+
from autosearch.cli.main import app
121+
from autosearch.cli.query_pipeline import QueryResult
122+
123+
leaked_key = "sk-FAKEKEY" + "1234567890abcdef"
124+
125+
async def _failing_run_query(_query: str, **_kwargs: object) -> QueryResult:
126+
raise RuntimeError(f"upstream returned token {leaked_key}")
127+
128+
monkeypatch.setattr("autosearch.cli.query_pipeline.run_query", _failing_run_query)
129+
130+
result = CliRunner().invoke(app, ["query", "redaction smoke", "--json"])
131+
132+
assert result.exit_code == 1
133+
assert leaked_key not in result.stdout
134+
payload = json.loads(result.stdout)
135+
assert "[REDACTED]" in payload["error"]

0 commit comments

Comments
 (0)