Skip to content

Commit 4509884

Browse files
authored
fix(p0-6): redact scenario error + stderr in e2b reports (#432)
* test(e2b-report): add failing scaffold for error redaction in summary + results.json (F008 S1) * fix(e2b-report): redact scenario error in summary.md (F008 S2) * fix(e2b-report): redact scenario error in results.json (F008 S3) * fix(e2b-comprehensive): redact scenario error + stderr in transcripts (F008 S4)
1 parent c989ed1 commit 4509884

3 files changed

Lines changed: 103 additions & 14 deletions

File tree

scripts/e2b/report.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22

33
from __future__ import annotations
44

5+
import json
56
from datetime import datetime
67
from pathlib import Path
78

9+
from autosearch.core.redact import redact
10+
from scripts.e2b.lib.reporter import redacted_json_ready
811
from scripts.e2b.sandbox_runner import ScenarioResult
912

1013

@@ -85,7 +88,8 @@ def render(results: list[ScenarioResult], summary: dict, output_dir: Path) -> st
8588
if r.report_length:
8689
lines.append(f" - 报告长度:{r.report_length} 字符")
8790
if r.error:
88-
lines.append(f" - ⚠️ 错误:`{r.error[:120]}`")
91+
redacted_error = redact(str(r.error))
92+
lines.append(f" - ⚠️ 错误:`{redacted_error[:120]}`")
8993
# Notable details
9094
details = r.details
9195
if "pubmed_ok" in details:
@@ -105,7 +109,8 @@ def render(results: list[ScenarioResult], summary: dict, output_dir: Path) -> st
105109
if summary["failures"]:
106110
lines += ["## 失败场景", ""]
107111
for f in summary["failures"]:
108-
lines.append(f"- **{f['id']} {f['name']}** (score={f['score']}): {f['error']}")
112+
redacted_error = redact(str(f.get("error", "")))
113+
lines.append(f"- **{f['id']} {f['name']}** (score={f['score']}): {redacted_error}")
109114
lines.append("")
110115

111116
if summary.get("bonus_total", 0) > 0:
@@ -139,6 +144,16 @@ def render(results: list[ScenarioResult], summary: dict, output_dir: Path) -> st
139144
return report
140145

141146

147+
def render_results_json(results: list[ScenarioResult], summary: dict, output_dir: Path) -> str:
148+
payload = {
149+
"summary": summary,
150+
"results": [r.to_dict() for r in results],
151+
}
152+
content = json.dumps(redacted_json_ready(payload), indent=2, ensure_ascii=False) + "\n"
153+
(output_dir / "results.json").write_text(content, encoding="utf-8")
154+
return content
155+
156+
142157
def _conclusion(summary: dict) -> str:
143158
r = summary["readiness"]
144159
score = summary["overall_score"]

scripts/e2b/run_comprehensive_tests.py

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
import argparse
1818
import asyncio
19-
import json
2019
import os
2120
import sys
2221
import time
@@ -29,7 +28,7 @@
2928
sys.path.insert(0, str(ROOT))
3029

3130
from scripts.e2b.evaluate import compute_summary # noqa: E402
32-
from scripts.e2b.report import render # noqa: E402
31+
from scripts.e2b.report import render, render_results_json # noqa: E402
3332
from scripts.e2b.sandbox_runner import ( # noqa: E402
3433
ScenarioResult,
3534
_collect_keys,
@@ -406,6 +405,14 @@ async def run_scenario_in_sandbox(
406405
await kill_sandbox(client, sandbox_id)
407406

408407

408+
def write_outputs(results: list[ScenarioResult], output_dir: Path) -> dict:
409+
"""Write comprehensive results without leaking scenario text secrets."""
410+
summary = compute_summary(results)
411+
render_results_json(results, summary, output_dir)
412+
render(results, summary, output_dir)
413+
return summary
414+
415+
409416
# ── Main ──────────────────────────────────────────────────────────────────────
410417

411418

@@ -476,16 +483,7 @@ async def main(argv: list[str] | None = None) -> int:
476483
output_dir.mkdir(parents=True, exist_ok=True)
477484

478485
# Compute summary & write outputs
479-
summary = compute_summary(list(results))
480-
(output_dir / "results.json").write_text(
481-
json.dumps(
482-
{"summary": summary, "results": [r.to_dict() for r in results]},
483-
indent=2,
484-
ensure_ascii=False,
485-
),
486-
encoding="utf-8",
487-
)
488-
render(list(results), summary, output_dir)
486+
summary = write_outputs(list(results), output_dir)
489487

490488
# Print final summary
491489
emoji = {"READY": "🟢", "BETA": "🟡", "NOT_READY": "🔴"}.get(summary["readiness"], "⚪")
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
from __future__ import annotations
2+
3+
from pathlib import Path
4+
5+
from scripts.e2b import report, run_comprehensive_tests
6+
from scripts.e2b.evaluate import compute_summary
7+
from scripts.e2b.sandbox_runner import ScenarioResult
8+
9+
10+
FAKE_API_KEY = "sk-" + "FAKEKEY1234567890abcdef"
11+
12+
13+
def _scenario_with_secret_error() -> ScenarioResult:
14+
return ScenarioResult(
15+
scenario_id="A1",
16+
category="A",
17+
name="fake_secret_error",
18+
score=0,
19+
passed=False,
20+
details={},
21+
error=f"request failed with {FAKE_API_KEY}",
22+
duration_s=1.2,
23+
)
24+
25+
26+
def _scenario_with_secret_transcript() -> ScenarioResult:
27+
return ScenarioResult(
28+
scenario_id="A2",
29+
category="A",
30+
name="fake_secret_transcript",
31+
score=0,
32+
passed=False,
33+
details={
34+
"stderr": f"stderr leaked {FAKE_API_KEY}",
35+
"transcript": [
36+
{"role": "assistant", "content": f"traceback leaked {FAKE_API_KEY}"},
37+
],
38+
},
39+
error=f"scenario failed with {FAKE_API_KEY}",
40+
duration_s=2.3,
41+
)
42+
43+
44+
def _summary_for(result: ScenarioResult) -> dict:
45+
return compute_summary([result])
46+
47+
48+
def test_scenario_error_redacted_in_summary(tmp_path: Path) -> None:
49+
scenario = _scenario_with_secret_error()
50+
51+
report.render([scenario], _summary_for(scenario), tmp_path)
52+
53+
content = (tmp_path / "summary.md").read_text(encoding="utf-8")
54+
assert FAKE_API_KEY not in content
55+
assert "[REDACTED]" in content
56+
57+
58+
def test_scenario_error_redacted_in_results_json(tmp_path: Path) -> None:
59+
scenario = _scenario_with_secret_error()
60+
61+
report.render_results_json([scenario], _summary_for(scenario), tmp_path)
62+
63+
content = (tmp_path / "results.json").read_text(encoding="utf-8")
64+
assert FAKE_API_KEY not in content
65+
assert "[REDACTED]" in content
66+
67+
68+
def test_comprehensive_transcripts_redacted(tmp_path: Path) -> None:
69+
scenario = _scenario_with_secret_transcript()
70+
71+
run_comprehensive_tests.write_outputs([scenario], tmp_path)
72+
73+
for path in (tmp_path / "summary.md", tmp_path / "results.json"):
74+
content = path.read_text(encoding="utf-8")
75+
assert FAKE_API_KEY not in content
76+
assert "[REDACTED]" in content

0 commit comments

Comments
 (0)