benchflow 0.7.2.dev1994__py3-none-any.whl → 0.7.2.dev2000__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- benchflow/cli/_traj_upload_ui.py +29 -1
- benchflow/cli/main.py +15 -1
- benchflow/cli/traj.py +47 -22
- benchflow/publish/redact.py +138 -23
- benchflow/publish/traj_capture.py +24 -4
- benchflow/publish/traj_report.py +14 -1
- benchflow/trajectories/types.py +86 -18
- benchflow/trajectories/viewer.py +124 -44
- {benchflow-0.7.2.dev1994.dist-info → benchflow-0.7.2.dev2000.dist-info}/METADATA +1 -1
- {benchflow-0.7.2.dev1994.dist-info → benchflow-0.7.2.dev2000.dist-info}/RECORD +13 -13
- {benchflow-0.7.2.dev1994.dist-info → benchflow-0.7.2.dev2000.dist-info}/WHEEL +0 -0
- {benchflow-0.7.2.dev1994.dist-info → benchflow-0.7.2.dev2000.dist-info}/entry_points.txt +0 -0
- {benchflow-0.7.2.dev1994.dist-info → benchflow-0.7.2.dev2000.dist-info}/licenses/LICENSE +0 -0
benchflow/cli/_traj_upload_ui.py
CHANGED
|
@@ -20,10 +20,29 @@ from rich.progress import (
|
|
|
20
20
|
from rich.table import Table
|
|
21
21
|
from rich.text import Text
|
|
22
22
|
|
|
23
|
-
from benchflow.publish.redact import REDACTED
|
|
23
|
+
from benchflow.publish.redact import REDACTED, format_redaction_breakdown
|
|
24
24
|
from benchflow.publish.traj_capture import StagedFile
|
|
25
25
|
from benchflow.publish.traj_report import PREVIEW_WORD_LIMIT, TrajectoryReport
|
|
26
26
|
|
|
27
|
+
# Redaction-transparency copy shown with the masked-value count. The wording is
|
|
28
|
+
# deliberately honest: redaction is a local structural pass (no agents, no
|
|
29
|
+
# token spend), and the server independently rescans staged artifacts.
|
|
30
|
+
NO_MASKING_NEEDED_COPY = (
|
|
31
|
+
"No secrets or personal identifiers detected — nothing needed masking."
|
|
32
|
+
)
|
|
33
|
+
REDACTION_REASSURANCE_COPY = (
|
|
34
|
+
"Redaction ran locally before anything was staged; "
|
|
35
|
+
"the server independently rescans and rejects any survivor."
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def masked_for_you_line(report: TrajectoryReport) -> str:
|
|
40
|
+
"""Itemized ``Masked for you`` copy for a report with masked values."""
|
|
41
|
+
breakdown = format_redaction_breakdown(dict(report.masked_categories))
|
|
42
|
+
if not breakdown: # category data unavailable — fall back to the total
|
|
43
|
+
breakdown = f"{report.masked_values} secret-like values"
|
|
44
|
+
return f"{breakdown} — originals never leave this machine"
|
|
45
|
+
|
|
27
46
|
|
|
28
47
|
def render_trajectory_report(report: TrajectoryReport, *, console: Console) -> None:
|
|
29
48
|
"""Render upload facts and a bounded, already-redacted trajectory preview."""
|
|
@@ -43,8 +62,17 @@ def render_trajectory_report(report: TrajectoryReport, *, console: Console) -> N
|
|
|
43
62
|
"API keys / secrets masked",
|
|
44
63
|
Text(str(report.masked_values), style="bold green"),
|
|
45
64
|
)
|
|
65
|
+
if report.masked_values:
|
|
66
|
+
facts.add_row(
|
|
67
|
+
"Masked for you",
|
|
68
|
+
Text(masked_for_you_line(report), style="green"),
|
|
69
|
+
)
|
|
70
|
+
else:
|
|
71
|
+
facts.add_row("Masked for you", Text(NO_MASKING_NEEDED_COPY, style="dim"))
|
|
46
72
|
facts.add_row("Safe replacement", Text(REDACTED))
|
|
47
73
|
console.print(Panel(facts, title="Trajectory report", border_style="cyan"))
|
|
74
|
+
if report.masked_values:
|
|
75
|
+
console.print(f"[dim]{REDACTION_REASSURANCE_COPY}[/dim]")
|
|
48
76
|
|
|
49
77
|
if not report.preview:
|
|
50
78
|
return
|
benchflow/cli/main.py
CHANGED
|
@@ -1239,11 +1239,25 @@ def eval_view(
|
|
|
1239
1239
|
),
|
|
1240
1240
|
),
|
|
1241
1241
|
] = False,
|
|
1242
|
+
redaction_summary: Annotated[
|
|
1243
|
+
str | None,
|
|
1244
|
+
typer.Option(
|
|
1245
|
+
"--redaction-summary",
|
|
1246
|
+
help=(
|
|
1247
|
+
"Masked-secret breakdown shown in the --confirm bar, e.g. "
|
|
1248
|
+
"'2 API keys, 1 bearer token' (lift it from "
|
|
1249
|
+
"`bench traj upload PATH --dry-run`). Display-only; no effect "
|
|
1250
|
+
"without --confirm."
|
|
1251
|
+
),
|
|
1252
|
+
),
|
|
1253
|
+
] = None,
|
|
1242
1254
|
) -> None:
|
|
1243
1255
|
"""View a trial or session trajectory in the browser."""
|
|
1244
1256
|
from benchflow.trajectories import viewer
|
|
1245
1257
|
|
|
1246
|
-
decision = viewer.serve(
|
|
1258
|
+
decision = viewer.serve(
|
|
1259
|
+
str(rollout_dir), port, confirm=confirm, redaction_summary=redaction_summary
|
|
1260
|
+
)
|
|
1247
1261
|
# Exit-code contract for --confirm: 0 approved, 3 rejected. 3 is chosen
|
|
1248
1262
|
# so a rejection never collides with the CLI's existing error exits
|
|
1249
1263
|
# (1 = errors, 2 = Typer usage errors).
|
benchflow/cli/traj.py
CHANGED
|
@@ -23,6 +23,7 @@ from benchflow.cli._traj_upload_ui import (
|
|
|
23
23
|
render_trajectory_report,
|
|
24
24
|
upload_progress,
|
|
25
25
|
)
|
|
26
|
+
from benchflow.publish.redact import format_redaction_breakdown
|
|
26
27
|
from benchflow.publish.traj_capture import (
|
|
27
28
|
StagedCapture,
|
|
28
29
|
default_source_id,
|
|
@@ -291,17 +292,23 @@ def register_traj(app: typer.Typer) -> None:
|
|
|
291
292
|
def _run_upload(options: _UploadOptions) -> None:
|
|
292
293
|
prompted = options.path is None
|
|
293
294
|
path = options.path or _prompt_for_path()
|
|
294
|
-
|
|
295
|
+
detected: _DetectedRepo | None = None
|
|
295
296
|
if options.source_id is not None:
|
|
296
297
|
source_id = options.source_id
|
|
297
298
|
else:
|
|
298
299
|
if options.repo:
|
|
299
|
-
|
|
300
|
-
source_id = f"repo/{
|
|
301
|
-
if
|
|
302
|
-
# Contributor-visible metadata: surface the tag
|
|
303
|
-
# sessions can opt out before anything leaves
|
|
304
|
-
|
|
300
|
+
detected = _detect_repo_slug(path)
|
|
301
|
+
source_id = f"repo/{detected.slug}" if detected else default_source_id(path)
|
|
302
|
+
if detected:
|
|
303
|
+
# Contributor-visible metadata: surface the tag and where it came
|
|
304
|
+
# from so private-repo sessions can opt out before anything leaves
|
|
305
|
+
# the machine. The local path stays terminal-only; the uploaded
|
|
306
|
+
# source id is just repo/<owner>/<name>. Plain print keeps the line
|
|
307
|
+
# one physical line (no Rich wrapping) so the path stays selectable.
|
|
308
|
+
print(
|
|
309
|
+
f"Repo: {detected.slug} "
|
|
310
|
+
f"(from session cwd {detected.session_cwd}; use --no-repo to omit)"
|
|
311
|
+
)
|
|
305
312
|
destination = _resolve_destination(options)
|
|
306
313
|
|
|
307
314
|
with (
|
|
@@ -314,6 +321,7 @@ def _run_upload(options: _UploadOptions) -> None:
|
|
|
314
321
|
artifacts.files,
|
|
315
322
|
masked_values=artifacts.redaction_replacements,
|
|
316
323
|
preview_steps=options.preview_steps,
|
|
324
|
+
masked_categories=artifacts.redaction_categories,
|
|
317
325
|
)
|
|
318
326
|
status.stop()
|
|
319
327
|
render_trajectory_report(report, console=console)
|
|
@@ -417,25 +425,35 @@ def _infer_email() -> str:
|
|
|
417
425
|
_SESSION_CWD_SCAN_LINES = 50
|
|
418
426
|
|
|
419
427
|
|
|
420
|
-
|
|
428
|
+
@dataclass(frozen=True)
|
|
429
|
+
class _DetectedRepo:
|
|
430
|
+
slug: str
|
|
431
|
+
session_cwd: Path
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _detect_repo_slug(path: Path) -> _DetectedRepo | None:
|
|
421
435
|
"""Best-effort ``owner/name`` for the repository the session was about.
|
|
422
436
|
|
|
423
437
|
Reads the working directory the session recorded (Claude events carry a
|
|
424
|
-
``cwd`` field; Codex ``session_meta`` payloads do too)
|
|
425
|
-
directory's git for the ``origin`` remote
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
438
|
+
``cwd`` field; Codex ``session_meta`` payloads do too) and asks that
|
|
439
|
+
directory's git for the ``origin`` remote. The trajectory's own recorded
|
|
440
|
+
cwd is the ONLY provenance source: there is deliberately no fallback to
|
|
441
|
+
the upload invocation directory, which mis-attributed sessions recorded
|
|
442
|
+
outside a repo to whatever checkout the contributor happened to upload
|
|
443
|
+
from. No session cwd, a missing directory, or no GitHub remote all mean
|
|
444
|
+
no tag. Every failure is silent — repo tagging must never break an
|
|
445
|
+
upload — and local-path remotes never produce a tag, so no local
|
|
446
|
+
absolute path can leak into the manifest.
|
|
429
447
|
"""
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
448
|
+
session_cwd = _session_cwd(path)
|
|
449
|
+
if session_cwd is None or not session_cwd.is_dir():
|
|
450
|
+
return None
|
|
451
|
+
remote = _command_stdout(
|
|
452
|
+
"git", "-C", str(session_cwd), "remote", "get-url", "origin"
|
|
453
|
+
)
|
|
454
|
+
slug = _repo_slug_from_remote(remote) if remote else None
|
|
455
|
+
if slug:
|
|
456
|
+
return _DetectedRepo(slug=slug, session_cwd=session_cwd)
|
|
439
457
|
return None
|
|
440
458
|
|
|
441
459
|
|
|
@@ -633,6 +651,13 @@ def _print_dry_run(staged: StagedCapture) -> None:
|
|
|
633
651
|
if staged.ignored:
|
|
634
652
|
console.print(f"Ignored: {escape(', '.join(staged.ignored))}")
|
|
635
653
|
console.print(f"Redactions: {staged.redaction_replacements}")
|
|
654
|
+
# One plain, greppable line so the upload skill can lift the breakdown into
|
|
655
|
+
# the viewer's confirm bar (bench eval view --redaction-summary "...").
|
|
656
|
+
breakdown = format_redaction_breakdown(dict(staged.artifact_redaction_categories))
|
|
657
|
+
if breakdown:
|
|
658
|
+
print(f"Masked for you: {breakdown}")
|
|
659
|
+
else:
|
|
660
|
+
print("Masked for you: nothing — no secrets detected")
|
|
636
661
|
|
|
637
662
|
|
|
638
663
|
def _print_contributor_prompt() -> None:
|
benchflow/publish/redact.py
CHANGED
|
@@ -4,10 +4,19 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import re
|
|
6
6
|
import shlex
|
|
7
|
+
from collections import Counter
|
|
7
8
|
from collections.abc import Mapping, Sequence
|
|
8
9
|
from typing import Any, NamedTuple
|
|
9
10
|
|
|
10
|
-
from benchflow.trajectories.types import
|
|
11
|
+
from benchflow.trajectories.types import (
|
|
12
|
+
REDACTION_CATEGORY_API_KEY,
|
|
13
|
+
REDACTION_CATEGORY_BEARER_TOKEN,
|
|
14
|
+
REDACTION_CATEGORY_CREDENTIAL_FIELD,
|
|
15
|
+
REDACTION_CATEGORY_PASSWORD,
|
|
16
|
+
REDACTION_CATEGORY_PRIVATE_KEY,
|
|
17
|
+
REDACTION_CATEGORY_URL_CREDENTIAL,
|
|
18
|
+
redact_trajectory_text_with_categories,
|
|
19
|
+
)
|
|
11
20
|
|
|
12
21
|
REDACTED = "<XXX-benchflow-key-values-XXX>"
|
|
13
22
|
_CANONICAL_REDACTED = "***REDACTED***"
|
|
@@ -67,23 +76,81 @@ COMPACT_SENSITIVE_KEY_SUFFIXES = tuple(
|
|
|
67
76
|
class RedactionPattern(NamedTuple):
|
|
68
77
|
pattern: re.Pattern[str]
|
|
69
78
|
replacement: str
|
|
79
|
+
category: str
|
|
70
80
|
|
|
71
81
|
|
|
72
82
|
VALUE_PATTERNS = (
|
|
73
83
|
# Google AI Studio's newer token format is not covered by the canonical
|
|
74
84
|
# trajectory redactor yet.
|
|
75
|
-
RedactionPattern(
|
|
85
|
+
RedactionPattern(
|
|
86
|
+
re.compile(r"AQ\.[0-9A-Za-z_-]{20,}"), REDACTED, REDACTION_CATEGORY_API_KEY
|
|
87
|
+
),
|
|
76
88
|
RedactionPattern(
|
|
77
89
|
re.compile(r"Bearer\s+[A-Za-z0-9._~+/=-]{16,}", re.IGNORECASE),
|
|
78
90
|
f"Bearer {REDACTED}",
|
|
91
|
+
REDACTION_CATEGORY_BEARER_TOKEN,
|
|
79
92
|
),
|
|
80
93
|
)
|
|
81
94
|
|
|
95
|
+
# Display order and pluralized labels for the per-category breakdown shown to
|
|
96
|
+
# contributors ("Masked for you: 2 API keys, 1 bearer token"). Keys are the
|
|
97
|
+
# canonical category names from ``benchflow.trajectories.types``; the taxonomy
|
|
98
|
+
# names what the redaction rules actually detect — nothing more.
|
|
99
|
+
REDACTION_CATEGORY_LABELS: tuple[tuple[str, str], ...] = (
|
|
100
|
+
(REDACTION_CATEGORY_API_KEY, "API keys"),
|
|
101
|
+
(REDACTION_CATEGORY_BEARER_TOKEN, "bearer tokens"),
|
|
102
|
+
(REDACTION_CATEGORY_PRIVATE_KEY, "private key blocks"),
|
|
103
|
+
(REDACTION_CATEGORY_PASSWORD, "passwords"),
|
|
104
|
+
(REDACTION_CATEGORY_URL_CREDENTIAL, "URL credentials"),
|
|
105
|
+
(REDACTION_CATEGORY_CREDENTIAL_FIELD, "credential-bearing field values"),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def redaction_breakdown(counts: Mapping[str, int]) -> tuple[tuple[str, int], ...]:
|
|
110
|
+
"""Return non-zero category counts in canonical display order."""
|
|
111
|
+
ordered = [
|
|
112
|
+
(category, counts[category])
|
|
113
|
+
for category, _ in REDACTION_CATEGORY_LABELS
|
|
114
|
+
if counts.get(category)
|
|
115
|
+
]
|
|
116
|
+
# Unknown categories cannot arise from this module's own accumulation, but
|
|
117
|
+
# persisted/forwarded counts must never be dropped silently.
|
|
118
|
+
ordered.extend(
|
|
119
|
+
(category, count)
|
|
120
|
+
for category, count in counts.items()
|
|
121
|
+
if count and category not in dict(REDACTION_CATEGORY_LABELS)
|
|
122
|
+
)
|
|
123
|
+
return tuple(ordered)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def format_redaction_breakdown(counts: Mapping[str, int]) -> str:
|
|
127
|
+
"""Render category counts as prose, e.g. ``2 API keys, 1 bearer token``."""
|
|
128
|
+
plurals = dict(REDACTION_CATEGORY_LABELS)
|
|
129
|
+
parts = []
|
|
130
|
+
for category, count in redaction_breakdown(counts):
|
|
131
|
+
label = category if count == 1 else plurals.get(category, f"{category}s")
|
|
132
|
+
parts.append(f"{count} {label}")
|
|
133
|
+
return ", ".join(parts)
|
|
82
134
|
|
|
83
|
-
|
|
84
|
-
|
|
135
|
+
|
|
136
|
+
def redact_value(
|
|
137
|
+
value: Any,
|
|
138
|
+
*,
|
|
139
|
+
field_name: str | None = None,
|
|
140
|
+
categories: Counter[str] | None = None,
|
|
141
|
+
) -> tuple[Any, int]:
|
|
142
|
+
"""Return a structurally redacted JSON value and replacement count.
|
|
143
|
+
|
|
144
|
+
When *categories* is given, every replacement also increments the matching
|
|
145
|
+
per-category counter (see ``REDACTION_CATEGORY_LABELS``); the counter's
|
|
146
|
+
total always equals the returned replacement count.
|
|
147
|
+
"""
|
|
85
148
|
if field_name is not None and _is_sensitive_key(field_name):
|
|
86
|
-
|
|
149
|
+
if _is_redacted_value(value):
|
|
150
|
+
return value, 0
|
|
151
|
+
if categories is not None:
|
|
152
|
+
categories[_field_category(field_name)] += 1
|
|
153
|
+
return REDACTED, 1
|
|
87
154
|
|
|
88
155
|
if isinstance(value, Mapping):
|
|
89
156
|
redacted: dict[Any, Any] = {}
|
|
@@ -104,7 +171,7 @@ def redact_value(value: Any, *, field_name: str | None = None) -> tuple[Any, int
|
|
|
104
171
|
for key, item in value.items():
|
|
105
172
|
clean_key = key
|
|
106
173
|
if isinstance(key, str):
|
|
107
|
-
_, key_replacements = _redact_text(key)
|
|
174
|
+
_, key_replacements = _redact_text(key, categories=categories)
|
|
108
175
|
if key_replacements:
|
|
109
176
|
secret_key_index += 1
|
|
110
177
|
clean_key = f"***REDACTED_KEY_{secret_key_index}***"
|
|
@@ -122,73 +189,102 @@ def redact_value(value: Any, *, field_name: str | None = None) -> tuple[Any, int
|
|
|
122
189
|
if isinstance(key, str)
|
|
123
190
|
else None
|
|
124
191
|
),
|
|
192
|
+
categories=categories,
|
|
125
193
|
)
|
|
126
194
|
redacted[clean_key] = clean
|
|
127
195
|
replacements += count
|
|
128
196
|
return redacted, replacements
|
|
129
197
|
|
|
130
198
|
if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
|
|
131
|
-
return _redact_argv(value, redact_other_values=True)
|
|
199
|
+
return _redact_argv(value, redact_other_values=True, categories=categories)
|
|
132
200
|
|
|
133
201
|
if not isinstance(value, str):
|
|
134
202
|
return value, 0
|
|
135
203
|
|
|
136
|
-
return _redact_text(value)
|
|
204
|
+
return _redact_text(value, categories=categories)
|
|
137
205
|
|
|
138
206
|
|
|
139
|
-
def redact_value_to_stability(
|
|
207
|
+
def redact_value_to_stability(
|
|
208
|
+
value: Any, *, categories: Counter[str] | None = None
|
|
209
|
+
) -> tuple[Any, int]:
|
|
140
210
|
"""Redact until another server-side pass would make no replacements."""
|
|
141
211
|
redacted = value
|
|
142
212
|
replacements = 0
|
|
143
213
|
for _ in range(_MAX_REDACTION_PASSES):
|
|
144
|
-
redacted, count = redact_value(redacted)
|
|
214
|
+
redacted, count = redact_value(redacted, categories=categories)
|
|
145
215
|
replacements += count
|
|
146
216
|
if count == 0:
|
|
147
217
|
return redacted, replacements
|
|
148
218
|
raise ValueError("trajectory secret redaction did not converge")
|
|
149
219
|
|
|
150
220
|
|
|
151
|
-
def _redact_text(
|
|
221
|
+
def _redact_text(
|
|
222
|
+
value: str, *, categories: Counter[str] | None = None
|
|
223
|
+
) -> tuple[str, int]:
|
|
152
224
|
# The canonical carrier patterns intentionally ignore their asterisk marker.
|
|
153
225
|
# Protect our public-upload marker with that value during the scan so a second
|
|
154
226
|
# client/server pass is idempotent even inside text such as ``API_KEY=...``.
|
|
155
227
|
protected = value.replace(REDACTED, _CANONICAL_REDACTED)
|
|
156
|
-
redacted_text,
|
|
228
|
+
redacted_text, text_categories = redact_trajectory_text_with_categories(protected)
|
|
229
|
+
replacements = sum(text_categories.values())
|
|
230
|
+
if categories is not None:
|
|
231
|
+
categories.update(text_categories)
|
|
157
232
|
redacted_text = redacted_text.replace(_CANONICAL_REDACTED, REDACTED)
|
|
158
|
-
for pattern, replacement in VALUE_PATTERNS:
|
|
233
|
+
for pattern, replacement, category in VALUE_PATTERNS:
|
|
159
234
|
redacted_text, count = pattern.subn(replacement, redacted_text)
|
|
160
235
|
replacements += count
|
|
161
|
-
|
|
236
|
+
if count and categories is not None:
|
|
237
|
+
categories[category] += count
|
|
238
|
+
cli_redacted, cli_replacements = _redact_cli_text(
|
|
239
|
+
redacted_text, categories=categories
|
|
240
|
+
)
|
|
162
241
|
redacted_text = cli_redacted
|
|
163
242
|
replacements += cli_replacements
|
|
164
243
|
return redacted_text, replacements
|
|
165
244
|
|
|
166
245
|
|
|
167
|
-
def _redact_cli_text(
|
|
246
|
+
def _redact_cli_text(
|
|
247
|
+
value: str, *, categories: Counter[str] | None = None
|
|
248
|
+
) -> tuple[str, int]:
|
|
168
249
|
if "--" not in value:
|
|
169
250
|
return value, 0
|
|
170
251
|
try:
|
|
171
252
|
argv = shlex.split(value)
|
|
172
253
|
except ValueError:
|
|
173
254
|
return value, 0
|
|
174
|
-
|
|
175
|
-
|
|
255
|
+
# Count categories only for the accepted result: a zero-replacement parse
|
|
256
|
+
# returns the original text, so nothing may be attributed for it.
|
|
257
|
+
argv_categories: Counter[str] | None = None if categories is None else Counter()
|
|
258
|
+
redacted, replacements = _redact_argv(
|
|
259
|
+
argv, redact_other_values=False, categories=argv_categories
|
|
260
|
+
)
|
|
261
|
+
if not replacements:
|
|
262
|
+
return value, 0
|
|
263
|
+
if categories is not None and argv_categories is not None:
|
|
264
|
+
categories.update(argv_categories)
|
|
265
|
+
return shlex.join(redacted), replacements
|
|
176
266
|
|
|
177
267
|
|
|
178
268
|
def _redact_argv(
|
|
179
|
-
values: Sequence[Any],
|
|
269
|
+
values: Sequence[Any],
|
|
270
|
+
*,
|
|
271
|
+
redact_other_values: bool,
|
|
272
|
+
categories: Counter[str] | None = None,
|
|
180
273
|
) -> tuple[list[Any], int]:
|
|
181
274
|
redacted: list[Any] = []
|
|
182
275
|
replacements = 0
|
|
183
|
-
|
|
276
|
+
sensitive_option: str | None = None
|
|
184
277
|
for item in values:
|
|
185
|
-
if
|
|
278
|
+
if sensitive_option is not None:
|
|
279
|
+
option = sensitive_option
|
|
280
|
+
sensitive_option = None
|
|
186
281
|
if _is_redacted_value(item):
|
|
187
282
|
redacted.append(item)
|
|
188
283
|
else:
|
|
189
284
|
redacted.append(REDACTED)
|
|
190
285
|
replacements += 1
|
|
191
|
-
|
|
286
|
+
if categories is not None:
|
|
287
|
+
categories[_field_category(option)] += 1
|
|
192
288
|
continue
|
|
193
289
|
|
|
194
290
|
if isinstance(item, str) and item.startswith("-"):
|
|
@@ -200,13 +296,16 @@ def _redact_argv(
|
|
|
200
296
|
else:
|
|
201
297
|
redacted.append(f"{option}={REDACTED}")
|
|
202
298
|
replacements += 1
|
|
299
|
+
if categories is not None:
|
|
300
|
+
categories[_field_category(option)] += 1
|
|
203
301
|
else:
|
|
204
302
|
redacted.append(item)
|
|
205
|
-
|
|
303
|
+
if not separator:
|
|
304
|
+
sensitive_option = option
|
|
206
305
|
continue
|
|
207
306
|
|
|
208
307
|
if redact_other_values:
|
|
209
|
-
clean, count = redact_value(item)
|
|
308
|
+
clean, count = redact_value(item, categories=categories)
|
|
210
309
|
redacted.append(clean)
|
|
211
310
|
replacements += count
|
|
212
311
|
else:
|
|
@@ -220,6 +319,22 @@ def _is_redacted_value(value: Any) -> bool:
|
|
|
220
319
|
)
|
|
221
320
|
|
|
222
321
|
|
|
322
|
+
def _field_category(field_name: str) -> str:
|
|
323
|
+
"""Categorize a masked value by the credential-bearing name that matched.
|
|
324
|
+
|
|
325
|
+
Structural redaction fires on the field/option NAME, so the name is the
|
|
326
|
+
only honest signal for what kind of secret the value was.
|
|
327
|
+
"""
|
|
328
|
+
compact = _normalize_key(field_name).replace("_", "")
|
|
329
|
+
if compact.endswith(("password", "passwd")):
|
|
330
|
+
return REDACTION_CATEGORY_PASSWORD
|
|
331
|
+
if compact.endswith("apikey"):
|
|
332
|
+
return REDACTION_CATEGORY_API_KEY
|
|
333
|
+
if compact.endswith("authorization") or "bearertoken" in compact:
|
|
334
|
+
return REDACTION_CATEGORY_BEARER_TOKEN
|
|
335
|
+
return REDACTION_CATEGORY_CREDENTIAL_FIELD
|
|
336
|
+
|
|
337
|
+
|
|
223
338
|
def _is_sensitive_key(field_name: str) -> bool:
|
|
224
339
|
normalized = _normalize_key(field_name)
|
|
225
340
|
return (
|
|
@@ -7,6 +7,7 @@ import json
|
|
|
7
7
|
import re
|
|
8
8
|
import shutil
|
|
9
9
|
import tempfile
|
|
10
|
+
from collections import Counter
|
|
10
11
|
from collections.abc import Iterator, Mapping
|
|
11
12
|
from contextlib import contextmanager
|
|
12
13
|
from dataclasses import dataclass
|
|
@@ -15,7 +16,11 @@ from pathlib import Path
|
|
|
15
16
|
from typing import Any, Never
|
|
16
17
|
|
|
17
18
|
from benchflow import __version__
|
|
18
|
-
from benchflow.publish.redact import
|
|
19
|
+
from benchflow.publish.redact import (
|
|
20
|
+
redact_value,
|
|
21
|
+
redact_value_to_stability,
|
|
22
|
+
redaction_breakdown,
|
|
23
|
+
)
|
|
19
24
|
|
|
20
25
|
MAX_FILE_BYTES = 128 * 1024**2
|
|
21
26
|
MAX_ARTIFACTS = 8
|
|
@@ -81,6 +86,10 @@ class StagedTrajectoryArtifacts:
|
|
|
81
86
|
_metadata_dir: Path | None
|
|
82
87
|
_staging_dir: Path
|
|
83
88
|
_redact: bool
|
|
89
|
+
# Per-category artifact replacement counts in display order (display-only:
|
|
90
|
+
# the manifest/server contract stays untouched). Sums to
|
|
91
|
+
# ``redaction_replacements``.
|
|
92
|
+
redaction_categories: tuple[tuple[str, int], ...] = ()
|
|
84
93
|
|
|
85
94
|
|
|
86
95
|
@dataclass(frozen=True)
|
|
@@ -92,6 +101,8 @@ class StagedCapture:
|
|
|
92
101
|
ignored: tuple[str, ...]
|
|
93
102
|
artifact_redaction_replacements: int
|
|
94
103
|
redaction_replacements: int
|
|
104
|
+
# Display-only per-category counts for the artifact replacements.
|
|
105
|
+
artifact_redaction_categories: tuple[tuple[str, int], ...] = ()
|
|
95
106
|
|
|
96
107
|
|
|
97
108
|
@dataclass(frozen=True)
|
|
@@ -199,12 +210,15 @@ def stage_trajectory_artifacts(
|
|
|
199
210
|
staging_dir = Path(temp_name)
|
|
200
211
|
payloads: list[StagedFile] = []
|
|
201
212
|
replacement_count = 0
|
|
213
|
+
replacement_categories: Counter[str] = Counter()
|
|
202
214
|
for source in resolved.files:
|
|
203
215
|
relname = validate_artifact_name(f"trajectory/{source.name}")
|
|
204
216
|
target = staging_dir / relname
|
|
205
217
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
206
218
|
if redact:
|
|
207
|
-
replacements = _redact_jsonl(
|
|
219
|
+
replacements = _redact_jsonl(
|
|
220
|
+
source, target, categories=replacement_categories
|
|
221
|
+
)
|
|
208
222
|
replacement_count += replacements
|
|
209
223
|
else:
|
|
210
224
|
shutil.copyfile(source, target)
|
|
@@ -240,6 +254,7 @@ def stage_trajectory_artifacts(
|
|
|
240
254
|
_metadata_dir=resolved.metadata_dir,
|
|
241
255
|
_staging_dir=staging_dir,
|
|
242
256
|
_redact=redact,
|
|
257
|
+
redaction_categories=redaction_breakdown(replacement_categories),
|
|
243
258
|
)
|
|
244
259
|
|
|
245
260
|
|
|
@@ -305,6 +320,7 @@ def finalize_trajectory_capture(
|
|
|
305
320
|
ignored=artifacts.ignored,
|
|
306
321
|
artifact_redaction_replacements=artifacts.redaction_replacements,
|
|
307
322
|
redaction_replacements=replacement_count,
|
|
323
|
+
artifact_redaction_categories=artifacts.redaction_categories,
|
|
308
324
|
)
|
|
309
325
|
|
|
310
326
|
|
|
@@ -430,7 +446,9 @@ def _validate_jsonl(path: Path) -> None:
|
|
|
430
446
|
raise ValueError(f"trajectory JSONL has no records: {path}")
|
|
431
447
|
|
|
432
448
|
|
|
433
|
-
def _redact_jsonl(
|
|
449
|
+
def _redact_jsonl(
|
|
450
|
+
source: Path, target: Path, *, categories: Counter[str] | None = None
|
|
451
|
+
) -> int:
|
|
434
452
|
replacements = 0
|
|
435
453
|
with (
|
|
436
454
|
source.open(encoding="utf-8", newline="") as input_stream,
|
|
@@ -443,7 +461,9 @@ def _redact_jsonl(source: Path, target: Path) -> int:
|
|
|
443
461
|
continue
|
|
444
462
|
value = strict_json_loads(body)
|
|
445
463
|
try:
|
|
446
|
-
redacted, count = redact_value_to_stability(
|
|
464
|
+
redacted, count = redact_value_to_stability(
|
|
465
|
+
value, categories=categories
|
|
466
|
+
)
|
|
447
467
|
except RecursionError as exc: # defense in depth after complexity gate
|
|
448
468
|
raise ValueError(
|
|
449
469
|
f"{source}: line {line_number}: trajectory JSONL nesting "
|
benchflow/publish/traj_report.py
CHANGED
|
@@ -54,9 +54,20 @@ class TrajectoryReport:
|
|
|
54
54
|
created_at_source: str
|
|
55
55
|
masked_values: int
|
|
56
56
|
preview: tuple[TrajectoryPreviewStep, ...]
|
|
57
|
+
# Per-category masked-value counts, display order, summing to
|
|
58
|
+
# ``masked_values``. Display-only: the manifest ``trajectory_report``
|
|
59
|
+
# contract (``TrajectoryReportInfo``, ``extra="forbid"``, exact
|
|
60
|
+
# recompute-equality on the server) must not gain fields, so
|
|
61
|
+
# ``as_manifest_metadata`` deliberately excludes this.
|
|
62
|
+
masked_categories: tuple[tuple[str, int], ...] = ()
|
|
57
63
|
|
|
58
64
|
def as_manifest_metadata(self) -> dict[str, Any]:
|
|
59
|
-
"""Serialize every displayed report field for the upload manifest.
|
|
65
|
+
"""Serialize every displayed report field for the upload manifest.
|
|
66
|
+
|
|
67
|
+
``masked_categories`` is intentionally absent: the server validates
|
|
68
|
+
``trajectory_report`` with ``extra="forbid"`` and an exact equality
|
|
69
|
+
check against its own recomputation, so new fields would be rejected.
|
|
70
|
+
"""
|
|
60
71
|
return {
|
|
61
72
|
"primary_file": self.primary_file,
|
|
62
73
|
"format": self.format.value,
|
|
@@ -133,6 +144,7 @@ def build_trajectory_report(
|
|
|
133
144
|
*,
|
|
134
145
|
masked_values: int,
|
|
135
146
|
preview_steps: int = DEFAULT_PREVIEW_STEPS,
|
|
147
|
+
masked_categories: tuple[tuple[str, int], ...] = (),
|
|
136
148
|
) -> TrajectoryReport:
|
|
137
149
|
"""Summarize one canonical trajectory view from staged, redacted artifacts."""
|
|
138
150
|
if not artifacts:
|
|
@@ -183,6 +195,7 @@ def build_trajectory_report(
|
|
|
183
195
|
created_at_source=created_at_source,
|
|
184
196
|
masked_values=masked_values,
|
|
185
197
|
preview=tuple(analysis.preview),
|
|
198
|
+
masked_categories=masked_categories,
|
|
186
199
|
)
|
|
187
200
|
|
|
188
201
|
|
benchflow/trajectories/types.py
CHANGED
|
@@ -179,6 +179,18 @@ def _exchange_token_usage(exchange: "LLMExchange") -> TokenUsage:
|
|
|
179
179
|
)
|
|
180
180
|
|
|
181
181
|
|
|
182
|
+
# Human-facing redaction categories. Every canonical pattern below is tagged
|
|
183
|
+
# with the kind of secret its rule actually detects, so the upload preview can
|
|
184
|
+
# tell contributors WHAT was masked ("2 API keys, 1 bearer token") instead of
|
|
185
|
+
# only how many values. The taxonomy names the real rules — there is no
|
|
186
|
+
# PII/email detection, so no such category exists.
|
|
187
|
+
REDACTION_CATEGORY_API_KEY = "API key"
|
|
188
|
+
REDACTION_CATEGORY_BEARER_TOKEN = "bearer token"
|
|
189
|
+
REDACTION_CATEGORY_PRIVATE_KEY = "private key block"
|
|
190
|
+
REDACTION_CATEGORY_PASSWORD = "password"
|
|
191
|
+
REDACTION_CATEGORY_URL_CREDENTIAL = "URL credential"
|
|
192
|
+
REDACTION_CATEGORY_CREDENTIAL_FIELD = "credential-bearing field value"
|
|
193
|
+
|
|
182
194
|
# Quote atom for the header/key-value carriers below: a run of 0-8 optional
|
|
183
195
|
# backslashes followed by an optional quote. This makes every carrier fire on raw
|
|
184
196
|
# text (``"k": "v"``), Python dict-repr (``'k': 'v'``), AND json.dumps-escaped
|
|
@@ -202,7 +214,7 @@ _SECVAL = r"[^\"'\s,}\\*&]+"
|
|
|
202
214
|
# ``{0,64}`` makes each match attempt O(1) → overall O(n) (#830 ReDoS fix).
|
|
203
215
|
_NAME = r"[A-Za-z0-9_]{0,64}"
|
|
204
216
|
|
|
205
|
-
_REDACTION_PATTERNS: list[tuple[re.Pattern[str], str]] = [
|
|
217
|
+
_REDACTION_PATTERNS: list[tuple[re.Pattern[str], str, str]] = [
|
|
206
218
|
# PEM private-key blocks (GCP/Vertex service-account JSON, RSA keys). Redact
|
|
207
219
|
# the whole armored block incl. the base64 body; the carriers below stop at
|
|
208
220
|
# the first space and would leave the key material. Lazy + length-capped body
|
|
@@ -214,17 +226,23 @@ _REDACTION_PATTERNS: list[tuple[re.Pattern[str], str]] = [
|
|
|
214
226
|
r"-----END [A-Z0-9 ]{0,40}PRIVATE KEY-----"
|
|
215
227
|
),
|
|
216
228
|
"***REDACTED***",
|
|
229
|
+
REDACTION_CATEGORY_PRIVATE_KEY,
|
|
217
230
|
),
|
|
218
231
|
# --- Token families: redacted WHOLE (prefix included) so the v0.5 leak audit
|
|
219
232
|
# greps (``AIzaSy``/``dtn_``…) see no live-key shape (#537/#585). ---
|
|
220
233
|
# Anthropic: sk-ant-api03-...
|
|
221
|
-
(
|
|
234
|
+
(
|
|
235
|
+
re.compile(r"sk-ant-[a-zA-Z0-9_-]{12,}"),
|
|
236
|
+
"***REDACTED***",
|
|
237
|
+
REDACTION_CATEGORY_API_KEY,
|
|
238
|
+
),
|
|
222
239
|
# BenchFlow proxy master key (sk-benchflow-<token_urlsafe(24)>), OpenAI service
|
|
223
240
|
# account (sk-svcacct-), OpenRouter (sk-or-v1-): rare labels that won't appear
|
|
224
241
|
# in a normal kebab slug, so the hyphen-permitting entropy class is safe here.
|
|
225
242
|
(
|
|
226
243
|
re.compile(r"sk-(?:benchflow|svcacct|or-v1)-[A-Za-z0-9_-]{12,}"),
|
|
227
244
|
"***REDACTED***",
|
|
245
|
+
REDACTION_CATEGORY_API_KEY,
|
|
228
246
|
),
|
|
229
247
|
# OpenAI org-scoped sk-proj-/sk-admin-: `proj`/`admin` ARE common identifier
|
|
230
248
|
# words, so a bare hyphen class would redact kebab slugs like
|
|
@@ -236,33 +254,58 @@ _REDACTION_PATTERNS: list[tuple[re.Pattern[str], str]] = [
|
|
|
236
254
|
r"sk-(?:proj|admin)-(?=[A-Za-z0-9_-]{0,200}[A-Z])[A-Za-z0-9_-]{12,}"
|
|
237
255
|
),
|
|
238
256
|
"***REDACTED***",
|
|
257
|
+
REDACTION_CATEGORY_API_KEY,
|
|
239
258
|
),
|
|
240
259
|
# OpenAI / generic sk- (alphanumeric only — widening to include `-` would
|
|
241
260
|
# match common slugs like `task-sk-us-east-1-...`)
|
|
242
|
-
(re.compile(r"sk-[a-zA-Z0-9]{12,}"), "***REDACTED***"),
|
|
261
|
+
(re.compile(r"sk-[a-zA-Z0-9]{12,}"), "***REDACTED***", REDACTION_CATEGORY_API_KEY),
|
|
243
262
|
# Google AI / Gemini: AIzaSy... (≥20 char suffix avoids matching `AIzaSy`
|
|
244
263
|
# alone). Prefix is redacted too so the audit grep for `AIzaSy` is clean.
|
|
245
|
-
(
|
|
264
|
+
(
|
|
265
|
+
re.compile(r"AIzaSy[A-Za-z0-9_-]{20,}"),
|
|
266
|
+
"***REDACTED***",
|
|
267
|
+
REDACTION_CATEGORY_API_KEY,
|
|
268
|
+
),
|
|
246
269
|
# AWS access keys: AKIA/ASIA + exactly 16 chars; length anchor avoids
|
|
247
270
|
# matching English words like "ASIAPACIFIC".
|
|
248
|
-
(
|
|
271
|
+
(
|
|
272
|
+
re.compile(r"(?:AKIA|ASIA)[A-Z0-9]{16}(?![A-Z0-9])"),
|
|
273
|
+
"***REDACTED***",
|
|
274
|
+
REDACTION_CATEGORY_API_KEY,
|
|
275
|
+
),
|
|
249
276
|
# Daytona SDK tokens: dtn_... — ≥16 char suffix avoids short ids (`dtn_v2`).
|
|
250
|
-
(
|
|
277
|
+
(
|
|
278
|
+
re.compile(r"dtn_[A-Za-z0-9_]{16,}"),
|
|
279
|
+
"***REDACTED***",
|
|
280
|
+
REDACTION_CATEGORY_API_KEY,
|
|
281
|
+
),
|
|
251
282
|
# GitHub tokens: PATs / OAuth / app / refresh (ghp_/gho_/ghu_/ghs_/ghr_) and
|
|
252
283
|
# fine-grained PATs (github_pat_...). ≥20 char suffix avoids short slugs.
|
|
253
|
-
(
|
|
254
|
-
|
|
284
|
+
(
|
|
285
|
+
re.compile(r"(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}"),
|
|
286
|
+
"***REDACTED***",
|
|
287
|
+
REDACTION_CATEGORY_API_KEY,
|
|
288
|
+
),
|
|
289
|
+
(
|
|
290
|
+
re.compile(r"github_pat_[A-Za-z0-9_]{20,}"),
|
|
291
|
+
"***REDACTED***",
|
|
292
|
+
REDACTION_CATEGORY_API_KEY,
|
|
293
|
+
),
|
|
255
294
|
# Slack tokens: bot/user/app/refresh/legacy (xoxb-/xoxp-/xoxa-/xoxr-/xoxs-).
|
|
256
|
-
(
|
|
295
|
+
(
|
|
296
|
+
re.compile(r"xox[baprs]-[A-Za-z0-9-]{10,}"),
|
|
297
|
+
"***REDACTED***",
|
|
298
|
+
REDACTION_CATEGORY_API_KEY,
|
|
299
|
+
),
|
|
257
300
|
# Other provider key families with distinctive prefixes — caught here for the
|
|
258
301
|
# bare-token-in-prose form that no NAME=value carrier sees (a provider
|
|
259
302
|
# exception that echoes the key). Groq gsk_, xAI xai-, Replicate r8_,
|
|
260
303
|
# HuggingFace hf_, Fireworks fw_. ≥20-char anchors avoid short ids (#830).
|
|
261
|
-
(re.compile(r"gsk_[A-Za-z0-9]{20,}"), "***REDACTED***"),
|
|
262
|
-
(re.compile(r"xai-[A-Za-z0-9]{20,}"), "***REDACTED***"),
|
|
263
|
-
(re.compile(r"r8_[A-Za-z0-9]{20,}"), "***REDACTED***"),
|
|
264
|
-
(re.compile(r"hf_[A-Za-z0-9]{20,}"), "***REDACTED***"),
|
|
265
|
-
(re.compile(r"fw_[A-Za-z0-9]{20,}"), "***REDACTED***"),
|
|
304
|
+
(re.compile(r"gsk_[A-Za-z0-9]{20,}"), "***REDACTED***", REDACTION_CATEGORY_API_KEY),
|
|
305
|
+
(re.compile(r"xai-[A-Za-z0-9]{20,}"), "***REDACTED***", REDACTION_CATEGORY_API_KEY),
|
|
306
|
+
(re.compile(r"r8_[A-Za-z0-9]{20,}"), "***REDACTED***", REDACTION_CATEGORY_API_KEY),
|
|
307
|
+
(re.compile(r"hf_[A-Za-z0-9]{20,}"), "***REDACTED***", REDACTION_CATEGORY_API_KEY),
|
|
308
|
+
(re.compile(r"fw_[A-Za-z0-9]{20,}"), "***REDACTED***", REDACTION_CATEGORY_API_KEY),
|
|
266
309
|
# JSON Web Tokens (session/bearer creds): three base64url segments split by
|
|
267
310
|
# dots. The ``eyJ`` prefix is base64url of ``{"`` — a JWT header. The 3rd
|
|
268
311
|
# (signature) segment requires ≥20 chars (real HS256/RS256 sigs are 43+) so
|
|
@@ -275,6 +318,7 @@ _REDACTION_PATTERNS: list[tuple[re.Pattern[str], str]] = [
|
|
|
275
318
|
r"eyJ[A-Za-z0-9_-]{10,512}\.[A-Za-z0-9_-]{6,512}\.[A-Za-z0-9_-]{20,512}"
|
|
276
319
|
),
|
|
277
320
|
"***REDACTED***",
|
|
321
|
+
REDACTION_CATEGORY_BEARER_TOKEN,
|
|
278
322
|
),
|
|
279
323
|
# --- URL credentials ---
|
|
280
324
|
# Credentials in a URL's userinfo (scheme://user:pass@host) for KNOWN
|
|
@@ -293,6 +337,7 @@ _REDACTION_PATTERNS: list[tuple[re.Pattern[str], str]] = [
|
|
|
293
337
|
r"[^\s/?#:@]{0,256}:[^\s/?#]{0,256}@"
|
|
294
338
|
),
|
|
295
339
|
r"\1***REDACTED***@",
|
|
340
|
+
REDACTION_CATEGORY_URL_CREDENTIAL,
|
|
296
341
|
),
|
|
297
342
|
# Secrets carried as URL query params. Curated EXACT param names (anchored to
|
|
298
343
|
# `?`/`&` with `=` right after the name) so non-secret params (`?page=`,
|
|
@@ -317,6 +362,7 @@ _REDACTION_PATTERNS: list[tuple[re.Pattern[str], str]] = [
|
|
|
317
362
|
re.IGNORECASE,
|
|
318
363
|
),
|
|
319
364
|
r"\1***REDACTED***",
|
|
365
|
+
REDACTION_CATEGORY_URL_CREDENTIAL,
|
|
320
366
|
),
|
|
321
367
|
# Azure SAS `?sig=<token>` — bare `sig` is excluded from the curated list above
|
|
322
368
|
# (a `?sig=v2` scheme-version flag is a common non-secret), so gate on a
|
|
@@ -326,6 +372,7 @@ _REDACTION_PATTERNS: list[tuple[re.Pattern[str], str]] = [
|
|
|
326
372
|
(
|
|
327
373
|
re.compile(r"([?&]sig=)[^&\s\"'*]{16,}", re.IGNORECASE),
|
|
328
374
|
r"\1***REDACTED***",
|
|
375
|
+
REDACTION_CATEGORY_URL_CREDENTIAL,
|
|
329
376
|
),
|
|
330
377
|
# --- Header / key-value secret carriers ---
|
|
331
378
|
# Match `name: value` / `name=value` in raw, JSON, Python dict-repr
|
|
@@ -340,6 +387,7 @@ _REDACTION_PATTERNS: list[tuple[re.Pattern[str], str]] = [
|
|
|
340
387
|
re.IGNORECASE,
|
|
341
388
|
),
|
|
342
389
|
r"\1***REDACTED***",
|
|
390
|
+
REDACTION_CATEGORY_API_KEY,
|
|
343
391
|
),
|
|
344
392
|
# Underscore form `api_key`. No leading boundary: namespaced env dumps like
|
|
345
393
|
# `GEMINI_API_KEY=secret` / JSON keys such as `"openai_api_key"` must redact
|
|
@@ -349,6 +397,7 @@ _REDACTION_PATTERNS: list[tuple[re.Pattern[str], str]] = [
|
|
|
349
397
|
rf"({_ESCQ}api_key{_ESCQ}\s*[:=]\s*{_ESCQ}){_SECVAL}", re.IGNORECASE
|
|
350
398
|
),
|
|
351
399
|
r"\1***REDACTED***",
|
|
400
|
+
REDACTION_CATEGORY_API_KEY,
|
|
352
401
|
),
|
|
353
402
|
# `master_key`/`master-key` and `private_key`/`private-key` carriers — the
|
|
354
403
|
# BenchFlow proxy master key and GCP/Vertex service-account private-key field
|
|
@@ -359,6 +408,7 @@ _REDACTION_PATTERNS: list[tuple[re.Pattern[str], str]] = [
|
|
|
359
408
|
re.IGNORECASE,
|
|
360
409
|
),
|
|
361
410
|
r"\1***REDACTED***",
|
|
411
|
+
REDACTION_CATEGORY_CREDENTIAL_FIELD,
|
|
362
412
|
),
|
|
363
413
|
# Bearer-token env vars whose NAME has BEARER_TOKEN in the MIDDLE, e.g.
|
|
364
414
|
# `AWS_BEARER_TOKEN_BEDROCK=` — the secret-suffix rule below anchors its marker
|
|
@@ -371,6 +421,7 @@ _REDACTION_PATTERNS: list[tuple[re.Pattern[str], str]] = [
|
|
|
371
421
|
re.IGNORECASE,
|
|
372
422
|
),
|
|
373
423
|
r"\1***REDACTED***",
|
|
424
|
+
REDACTION_CATEGORY_BEARER_TOKEN,
|
|
374
425
|
),
|
|
375
426
|
# Generic secret-NAME-suffix carriers: names ending in TOKEN/SECRET/PASSWORD or
|
|
376
427
|
# a specific *secret* key suffix (ACCESS_KEY/SECRET_KEY/ACCOUNT_KEY/…). The KEY
|
|
@@ -388,6 +439,7 @@ _REDACTION_PATTERNS: list[tuple[re.Pattern[str], str]] = [
|
|
|
388
439
|
re.IGNORECASE,
|
|
389
440
|
),
|
|
390
441
|
r"\1***REDACTED***",
|
|
442
|
+
REDACTION_CATEGORY_CREDENTIAL_FIELD,
|
|
391
443
|
),
|
|
392
444
|
# authorization header WITH a scheme (Bearer/Token/Basic/…): keep scheme.
|
|
393
445
|
(
|
|
@@ -397,6 +449,7 @@ _REDACTION_PATTERNS: list[tuple[re.Pattern[str], str]] = [
|
|
|
397
449
|
re.IGNORECASE,
|
|
398
450
|
),
|
|
399
451
|
r"\1***REDACTED***",
|
|
452
|
+
REDACTION_CATEGORY_BEARER_TOKEN,
|
|
400
453
|
),
|
|
401
454
|
# authorization header with a bare value (no recognized scheme). The negative
|
|
402
455
|
# lookahead skips scheme-prefixed values already handled above, so
|
|
@@ -409,6 +462,7 @@ _REDACTION_PATTERNS: list[tuple[re.Pattern[str], str]] = [
|
|
|
409
462
|
re.IGNORECASE,
|
|
410
463
|
),
|
|
411
464
|
r"\1***REDACTED***",
|
|
465
|
+
REDACTION_CATEGORY_BEARER_TOKEN,
|
|
412
466
|
),
|
|
413
467
|
]
|
|
414
468
|
|
|
@@ -428,11 +482,25 @@ def redact_trajectory_text(text: str) -> str:
|
|
|
428
482
|
|
|
429
483
|
def redact_trajectory_text_with_count(text: str) -> tuple[str, int]:
|
|
430
484
|
"""Apply the canonical patterns and report how many matches were replaced."""
|
|
431
|
-
|
|
432
|
-
|
|
485
|
+
text, categories = redact_trajectory_text_with_categories(text)
|
|
486
|
+
return text, sum(categories.values())
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def redact_trajectory_text_with_categories(text: str) -> tuple[str, dict[str, int]]:
|
|
490
|
+
"""Apply the canonical patterns and report replacements per category.
|
|
491
|
+
|
|
492
|
+
Categories name the kind of secret each rule actually detects (API key,
|
|
493
|
+
bearer token, private key block, URL credential, credential-bearing field
|
|
494
|
+
value) so upload previews can itemize what was masked without inventing
|
|
495
|
+
detection that does not exist. The total replacement count is the sum of
|
|
496
|
+
the returned values.
|
|
497
|
+
"""
|
|
498
|
+
categories: dict[str, int] = {}
|
|
499
|
+
for pattern, replacement, category in _REDACTION_PATTERNS:
|
|
433
500
|
text, count = pattern.subn(replacement, text)
|
|
434
|
-
|
|
435
|
-
|
|
501
|
+
if count:
|
|
502
|
+
categories[category] = categories.get(category, 0) + count
|
|
503
|
+
return text, categories
|
|
436
504
|
|
|
437
505
|
|
|
438
506
|
def redact_trajectory_obj(obj: Any) -> Any:
|
benchflow/trajectories/viewer.py
CHANGED
|
@@ -97,9 +97,11 @@ body { padding-bottom: 104px; }
|
|
|
97
97
|
.confirm-btn.approve:hover { background: #262626; }
|
|
98
98
|
.confirm-btn.reject { background: var(--card); color: var(--ink); border: 1px solid var(--rule-strong); }
|
|
99
99
|
.confirm-btn.reject:hover { background: var(--secondary); }
|
|
100
|
+
.confirm-note { flex-basis: 100%; font-size: 12.5px; color: var(--muted); }
|
|
100
101
|
</style>
|
|
101
102
|
<div class="confirm-bar">
|
|
102
103
|
<div class="confirm-inner" id="confirm-inner">
|
|
104
|
+
<!--BENCHFLOW-REDACTION-NOTE-->
|
|
103
105
|
<span class="confirm-question">Submit this trajectory to the BenchFlow eval prize?</span>
|
|
104
106
|
<div class="confirm-actions">
|
|
105
107
|
<button class="confirm-btn approve" onclick="__benchDecide('approve')">Approve & submit</button>
|
|
@@ -124,11 +126,33 @@ async function __benchDecide(choice) {
|
|
|
124
126
|
"""
|
|
125
127
|
|
|
126
128
|
|
|
127
|
-
|
|
129
|
+
_REDACTION_NOTE_MARKER = "<!--BENCHFLOW-REDACTION-NOTE-->"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _confirm_bar_html(redaction_summary: str | None) -> str:
|
|
133
|
+
"""Confirm-bar snippet, optionally carrying the redaction-summary note.
|
|
134
|
+
|
|
135
|
+
The note is presentation-only text the caller composed (the upload skill
|
|
136
|
+
lifts it from ``bench traj upload --dry-run``); the viewer itself never
|
|
137
|
+
redacts, so without a summary the bar is byte-identical to the plain
|
|
138
|
+
``--confirm`` bar.
|
|
139
|
+
"""
|
|
140
|
+
note = ""
|
|
141
|
+
if redaction_summary:
|
|
142
|
+
note = (
|
|
143
|
+
'<div class="confirm-note">Before upload, BenchFlow masks: '
|
|
144
|
+
f"{html.escape(redaction_summary)}. "
|
|
145
|
+
"Originals never leave this machine.</div>"
|
|
146
|
+
)
|
|
147
|
+
return _CONFIRM_BAR_HTML.replace(_REDACTION_NOTE_MARKER, note, 1)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _inject_confirm_bar(page: str, redaction_summary: str | None = None) -> str:
|
|
128
151
|
"""Append the confirm bar just before </body> (or at the end as fallback)."""
|
|
152
|
+
bar = _confirm_bar_html(redaction_summary)
|
|
129
153
|
if "</body>" in page:
|
|
130
|
-
return page.replace("</body>", f"{
|
|
131
|
-
return page +
|
|
154
|
+
return page.replace("</body>", f"{bar}</body>", 1)
|
|
155
|
+
return page + bar
|
|
132
156
|
|
|
133
157
|
|
|
134
158
|
# Small BenchFlow wordmark header (inline SVG logo from the site's icon set —
|
|
@@ -407,26 +431,18 @@ def render_rollout(rollout_dir: Path, prompts: list[str] | None = None) -> str:
|
|
|
407
431
|
while len(prompts) < len(turn_files):
|
|
408
432
|
prompts.append("")
|
|
409
433
|
|
|
410
|
-
|
|
411
|
-
sys_event = next((e for e in first_events if e.get("type") == "system"), {})
|
|
412
|
-
total_cost = 0
|
|
413
|
-
total_turns_count = 0
|
|
414
|
-
|
|
434
|
+
all_events: list[dict] = []
|
|
415
435
|
all_blocks = []
|
|
416
436
|
for i, tf in enumerate(turn_files):
|
|
417
437
|
events = _parse_jsonl(tf.read_text())
|
|
438
|
+
all_events.extend(events)
|
|
418
439
|
all_blocks.append(render_turn(events, i + 1, prompts[i]))
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
# stream-json (e.g. "session_id": null) bypasses the default and would crash
|
|
426
|
-
# html.escape() / the [:16] slice below with a raw TypeError.
|
|
427
|
-
session_id = str(sys_event.get("session_id") or "?")
|
|
428
|
-
model = str(sys_event.get("model") or "?")
|
|
429
|
-
version = str(sys_event.get("claude_code_version") or "?")
|
|
440
|
+
|
|
441
|
+
badges = _stream_header_badges(all_events)
|
|
442
|
+
badges.append(("turns", str(len(turn_files))))
|
|
443
|
+
total_cost = _result_cost(all_events)
|
|
444
|
+
if total_cost is not None:
|
|
445
|
+
badges.append(("total cost", f"${total_cost:.4f}"))
|
|
430
446
|
|
|
431
447
|
return f"""<!DOCTYPE html>
|
|
432
448
|
<html>
|
|
@@ -440,14 +456,7 @@ def render_rollout(rollout_dir: Path, prompts: list[str] | None = None) -> str:
|
|
|
440
456
|
<div class="header">
|
|
441
457
|
{_WORDMARK_HTML}
|
|
442
458
|
<h1>{html.escape(rollout_dir.name)}</h1>
|
|
443
|
-
|
|
444
|
-
<span>model: {html.escape(model)}</span>
|
|
445
|
-
<span>session: {html.escape(session_id[:16])}...</span>
|
|
446
|
-
<span>claude code: {html.escape(version)}</span>
|
|
447
|
-
<span>turns: {len(turn_files)}</span>
|
|
448
|
-
<span>total cost: ${total_cost:.4f}</span>
|
|
449
|
-
</div>
|
|
450
|
-
</div>
|
|
459
|
+
{_meta_badges_html(badges)}</div>
|
|
451
460
|
{_join_with_divider(all_blocks)}
|
|
452
461
|
</body>
|
|
453
462
|
</html>"""
|
|
@@ -648,18 +657,89 @@ def render_jsonl_file(path: Path) -> str:
|
|
|
648
657
|
body = render_turn(events, 1, "")
|
|
649
658
|
if not body.strip():
|
|
650
659
|
return _NO_TRAJECTORIES_HTML
|
|
651
|
-
return _stream_json_page(path.name, events, [body])
|
|
660
|
+
return _stream_json_page(path.name, events, [body], session_fallback=path.stem)
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def _stream_header_badges(
|
|
664
|
+
events: list[dict], *, session_fallback: str | None = None
|
|
665
|
+
) -> list[tuple[str, str]]:
|
|
666
|
+
"""Header badges derivable from what the stream actually contains.
|
|
667
|
+
|
|
668
|
+
``claude -p`` stream-json carries a ``type: system`` init event
|
|
669
|
+
(``session_id`` / ``model`` / ``claude_code_version``); real ``~/.claude``
|
|
670
|
+
session files don't — their metadata lives per event (``sessionId``,
|
|
671
|
+
``version``) and on assistant events (``message.model``). Pull from
|
|
672
|
+
whichever is present and omit anything unknown: a header of ``?`` badges
|
|
673
|
+
at the approve moment reads as a broken viewer, not as missing data.
|
|
674
|
+
Truthiness (not ``.get`` defaults) also swallows present-but-null values
|
|
675
|
+
like ``"session_id": null``, which previously needed an explicit guard to
|
|
676
|
+
avoid a TypeError.
|
|
677
|
+
"""
|
|
678
|
+
sys_event = next((e for e in events if e.get("type") == "system"), {})
|
|
652
679
|
|
|
680
|
+
def _first(values) -> object | None:
|
|
681
|
+
return next((value for value in values if value), None)
|
|
653
682
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
683
|
+
model = sys_event.get("model") or _first(
|
|
684
|
+
event["message"].get("model")
|
|
685
|
+
for event in events
|
|
686
|
+
if event.get("type") == "assistant" and isinstance(event.get("message"), dict)
|
|
687
|
+
)
|
|
688
|
+
session_id = (
|
|
689
|
+
sys_event.get("session_id")
|
|
690
|
+
or _first(event.get("sessionId") for event in events)
|
|
691
|
+
or session_fallback
|
|
692
|
+
)
|
|
693
|
+
version = sys_event.get("claude_code_version") or _first(
|
|
694
|
+
event.get("version") for event in events
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
badges: list[tuple[str, str]] = []
|
|
698
|
+
if model:
|
|
699
|
+
badges.append(("model", str(model)))
|
|
700
|
+
if session_id:
|
|
701
|
+
sid = str(session_id)
|
|
702
|
+
badges.append(("session", sid[:16] + ("..." if len(sid) > 16 else "")))
|
|
703
|
+
if version:
|
|
704
|
+
badges.append(("claude code", str(version)))
|
|
705
|
+
return badges
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
def _result_cost(events: list[dict]) -> float | None:
|
|
709
|
+
"""Total cost summed from result events, or ``None`` when no event
|
|
710
|
+
carries cost data — so the header can hide the badge instead of
|
|
711
|
+
asserting a fictional ``$0.0000``."""
|
|
712
|
+
total = 0.0
|
|
713
|
+
seen = False
|
|
657
714
|
for event in events:
|
|
658
|
-
if event.get("type") == "result":
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
715
|
+
if event.get("type") == "result" and event.get("total_cost_usd") is not None:
|
|
716
|
+
total += float(event.get("total_cost_usd") or 0)
|
|
717
|
+
seen = True
|
|
718
|
+
return total if seen else None
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
def _meta_badges_html(badges: list[tuple[str, str]]) -> str:
|
|
722
|
+
"""The header's ``.meta`` badge row; empty string when nothing is known."""
|
|
723
|
+
if not badges:
|
|
724
|
+
return ""
|
|
725
|
+
spans = "\n".join(
|
|
726
|
+
f"<span>{html.escape(label)}: {html.escape(value)}</span>"
|
|
727
|
+
for label, value in badges
|
|
728
|
+
)
|
|
729
|
+
return f'<div class="meta">\n{spans}\n</div>\n'
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def _stream_json_page(
|
|
733
|
+
title: str,
|
|
734
|
+
events: list[dict],
|
|
735
|
+
turn_blocks: list[str],
|
|
736
|
+
*,
|
|
737
|
+
session_fallback: str | None = None,
|
|
738
|
+
) -> str:
|
|
739
|
+
badges = _stream_header_badges(events, session_fallback=session_fallback)
|
|
740
|
+
total_cost = _result_cost(events)
|
|
741
|
+
if total_cost is not None:
|
|
742
|
+
badges.append(("total cost", f"${total_cost:.4f}"))
|
|
663
743
|
return f"""<!DOCTYPE html>
|
|
664
744
|
<html>
|
|
665
745
|
<head>
|
|
@@ -672,13 +752,7 @@ def _stream_json_page(title: str, events: list[dict], turn_blocks: list[str]) ->
|
|
|
672
752
|
<div class="header">
|
|
673
753
|
{_WORDMARK_HTML}
|
|
674
754
|
<h1>{html.escape(title)}</h1>
|
|
675
|
-
|
|
676
|
-
<span>model: {html.escape(model)}</span>
|
|
677
|
-
<span>session: {html.escape(session_id[:16])}...</span>
|
|
678
|
-
<span>claude code: {html.escape(version)}</span>
|
|
679
|
-
<span>total cost: ${total_cost:.4f}</span>
|
|
680
|
-
</div>
|
|
681
|
-
</div>
|
|
755
|
+
{_meta_badges_html(badges)}</div>
|
|
682
756
|
{_join_with_divider(turn_blocks)}
|
|
683
757
|
</body>
|
|
684
758
|
</html>"""
|
|
@@ -689,6 +763,7 @@ def serve(
|
|
|
689
763
|
port: int = 8888,
|
|
690
764
|
prompts: list[str] | None = None,
|
|
691
765
|
confirm: bool = False,
|
|
766
|
+
redaction_summary: str | None = None,
|
|
692
767
|
) -> str | None:
|
|
693
768
|
"""Serve a trial directory or a session JSONL file as a web page.
|
|
694
769
|
|
|
@@ -698,6 +773,11 @@ def serve(
|
|
|
698
773
|
machine-readable ``DECISION: <value>`` line to stdout. Without it the
|
|
699
774
|
server runs until Ctrl+C and the return value is ``None`` — exactly the
|
|
700
775
|
pre-confirm behavior (no bar, no POST endpoint).
|
|
776
|
+
|
|
777
|
+
``redaction_summary`` is an optional caller-composed line (e.g. ``"2 API
|
|
778
|
+
keys, 1 bearer token"``) rendered inside the confirm bar so the reviewer
|
|
779
|
+
sees what upload-time redaction would mask. Presentation-only; it has no
|
|
780
|
+
effect without ``confirm=True``.
|
|
701
781
|
"""
|
|
702
782
|
import threading
|
|
703
783
|
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
|
@@ -723,7 +803,7 @@ def serve(
|
|
|
723
803
|
# interaction against this live server, not part of the artifact.
|
|
724
804
|
(path / "trajectory.html").write_text(html_content)
|
|
725
805
|
if confirm:
|
|
726
|
-
html_content = _inject_confirm_bar(html_content)
|
|
806
|
+
html_content = _inject_confirm_bar(html_content, redaction_summary)
|
|
727
807
|
|
|
728
808
|
print(f"Trajectory viewer: http://localhost:{port}")
|
|
729
809
|
print(f"Trial: {path}")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: benchflow
|
|
3
|
-
Version: 0.7.2.
|
|
3
|
+
Version: 0.7.2.dev2000
|
|
4
4
|
Summary: Multi-turn agent benchmarking with ACP — run any agent, any model, any provider.
|
|
5
5
|
Project-URL: Homepage, https://github.com/benchflow-ai/benchflow
|
|
6
6
|
Project-URL: Repository, https://github.com/benchflow-ai/benchflow
|
|
@@ -100,7 +100,7 @@ benchflow/cli/_hosted_env.py,sha256=RUwmeITDiDCNl2zimyHJwENOcFkt12uPTmWAYbPL3a4,
|
|
|
100
100
|
benchflow/cli/_live_progress.py,sha256=tjvU8jjp6AxsJ7qXomk6Jach_nWitYFjO4nw79tjMeE,18707
|
|
101
101
|
benchflow/cli/_options.py,sha256=dG2R5-jjNzvAj513Dscj1TFbH5ukM_ONxYg9vELnE7M,1237
|
|
102
102
|
benchflow/cli/_shared.py,sha256=gE1sCuTUw0SzIK3H3Unpirvs9Rt28fDaVPAyS6Oyhdk,11744
|
|
103
|
-
benchflow/cli/_traj_upload_ui.py,sha256=
|
|
103
|
+
benchflow/cli/_traj_upload_ui.py,sha256=2mewoumFvlKGPIz6fR_RNqyPKxmqiGRnpBlc3DBfSa0,6173
|
|
104
104
|
benchflow/cli/adopt.py,sha256=HbsTrQPfxlfnq88lO3MZj1iQzwhjzor8XHoHOUDnyj0,7259
|
|
105
105
|
benchflow/cli/agent.py,sha256=B4X36VIN-KUZSOKVveoJBIm2dsVx9D142CnC0ClIfLM,3547
|
|
106
106
|
benchflow/cli/continue_cmd.py,sha256=I93JosVb3HJitDFMJLOhb7_jihz3UFzTvpbkf1y-kKQ,10098
|
|
@@ -108,7 +108,7 @@ benchflow/cli/environment.py,sha256=q3G8G2i0rs_wqGIWzULnnc6Nt8TlJLO12zuYlro-5-A,
|
|
|
108
108
|
benchflow/cli/eval_artifacts.py,sha256=4CbS8aIaynCk2PUeWA4Qo31F8SH7_rxtwB7po8_O3v4,11275
|
|
109
109
|
benchflow/cli/eval_lift.py,sha256=0Tyn9ZVIzAIeZ9vm0T7onBLre48gliKf0K8NrD7m1j0,3072
|
|
110
110
|
benchflow/cli/hub.py,sha256=uX5O-q_9CMRb9VlW06HkA5EbKUa6xORnNNTNmMM2H_0,7029
|
|
111
|
-
benchflow/cli/main.py,sha256=
|
|
111
|
+
benchflow/cli/main.py,sha256=NQwSGLDGDttzcllFP7z3hY3KojnnelXk_49xd2aghoc,48795
|
|
112
112
|
benchflow/cli/monitor.py,sha256=-0C4OzZqgUj7Lvi5obJVsK-M81OZwaZNEpis71Kqyj4,3427
|
|
113
113
|
benchflow/cli/review.py,sha256=ysR6No9oFu0TgcbucNG9YmXY5FkMUHobdr6F4rHQmQw,7892
|
|
114
114
|
benchflow/cli/sandbox.py,sha256=QrLASbsfzPGVosW_s5XxE3_mcXqTzbU2ZnnC9MADpXM,9849
|
|
@@ -116,7 +116,7 @@ benchflow/cli/skills.py,sha256=2Apv1iqvmstWEhY6O-PRdxKc8RYwR6f7MvbYfl2dWm0,7497
|
|
|
116
116
|
benchflow/cli/tasks.py,sha256=b1XP1EZ79ZtK_1shF65qZyv27EcKKFZYV35M4DzxYKI,18534
|
|
117
117
|
benchflow/cli/trace_import.py,sha256=NHDieFdiKQ-2Sr1-GF8Z5EDcbGsz2qq_kFE0rXacC2o,14705
|
|
118
118
|
benchflow/cli/train.py,sha256=MIP_JxptitH348bWWURpcWt-TmDAw1s8d-RUU7jBaIg,23635
|
|
119
|
-
benchflow/cli/traj.py,sha256=
|
|
119
|
+
benchflow/cli/traj.py,sha256=uisYWo-f_R_e06sKJPMObYbvDAPJac0zzyU4byGHU7U,25515
|
|
120
120
|
benchflow/continue_run/__init__.py,sha256=dWidng7kJphVUtbP1a1Ma0Lyjxj5yLgQZIApaKCJUew,1479
|
|
121
121
|
benchflow/continue_run/batch.py,sha256=8BNSInyp_Ol6Fcrg4ezBJdi3cBS12nC8mf963_x4fIg,4146
|
|
122
122
|
benchflow/continue_run/orchestrator.py,sha256=wYoseBNE_rZ0EjNwWDyqfave54xNRe43sCd_O4VJjyk,27676
|
|
@@ -159,9 +159,9 @@ benchflow/publish/_progress.py,sha256=TWkIH5CVtBj7TL0eqCX57_asrqoHzIYeSLn86NtueB
|
|
|
159
159
|
benchflow/publish/azure_blob.py,sha256=CpjRdmWc2XGb2q8j8hj2G0hCEoAvHUK8-wmoUU4uZm4,5583
|
|
160
160
|
benchflow/publish/broker.py,sha256=DkJPe8btYMJi5AJZmuQduUh4d9DvTloNsvtiH74VKgk,8842
|
|
161
161
|
benchflow/publish/huggingface.py,sha256=V34xuQYFmL7DPi9W62LESKgzPFmTBdlJxLTZxGmpGBM,3772
|
|
162
|
-
benchflow/publish/redact.py,sha256=
|
|
163
|
-
benchflow/publish/traj_capture.py,sha256=
|
|
164
|
-
benchflow/publish/traj_report.py,sha256=
|
|
162
|
+
benchflow/publish/redact.py,sha256=spO8zTd0Q5PZ3BAs5ZYqhdNiGKUtC8NouaDO1RfDMKI,12153
|
|
163
|
+
benchflow/publish/traj_capture.py,sha256=Z8rBDsUf0Hyv2e6AL9RhGVLV1IxERk97hTEa5ghM-mw,22797
|
|
164
|
+
benchflow/publish/traj_report.py,sha256=2iYeipPrfQcTlAeNhDhWk-vbBTdA5RdXCjcWdEi_VWo,25559
|
|
165
165
|
benchflow/review/__init__.py,sha256=vl7jnLnHGG9aKBux2F9Ni5rf_EPsC-tvauHL-QnyRa4,2080
|
|
166
166
|
benchflow/review/config.py,sha256=WGxH4dRd-qCQVf8hDf4AwXKKGVPx2zkPpna6dgwxFMk,9002
|
|
167
167
|
benchflow/review/default-rubric.json,sha256=Ed5cTrt_q3e7TGERWbjJgNWrrhkTsB_ToP0UMn1vobg,1843
|
|
@@ -290,10 +290,10 @@ benchflow/trajectories/results.py,sha256=VAHktYYwMN8ikjSr2OlAgP8IsDpDJp5AJiZGrYi
|
|
|
290
290
|
benchflow/trajectories/sessions.py,sha256=u_6x1WyT8YtKUVOb6b3myTCHhqn58a8NHCLAvQqKP_Y,4437
|
|
291
291
|
benchflow/trajectories/tree.py,sha256=c0jyoP9OurDQq-zRtPc66edzWUvLGwQQz0jgtyqSw2U,5576
|
|
292
292
|
benchflow/trajectories/trl_sft_tokenization.py,sha256=-Ef0bP9cuzfzOz5nIv2mEF3taQIEw68h6SM1YO-Mwn4,8643
|
|
293
|
-
benchflow/trajectories/types.py,sha256=
|
|
294
|
-
benchflow/trajectories/viewer.py,sha256=
|
|
295
|
-
benchflow-0.7.2.
|
|
296
|
-
benchflow-0.7.2.
|
|
297
|
-
benchflow-0.7.2.
|
|
298
|
-
benchflow-0.7.2.
|
|
299
|
-
benchflow-0.7.2.
|
|
293
|
+
benchflow/trajectories/types.py,sha256=EG_TDTh0JveXk5WJX4YMcEDBjfe3526lC7JjDvQmiw8,30067
|
|
294
|
+
benchflow/trajectories/viewer.py,sha256=L-tMiR15qdGBaTlVNgY4dOS1pIrf4PB24MvNic6lj4s,37715
|
|
295
|
+
benchflow-0.7.2.dev2000.dist-info/METADATA,sha256=IYq47CWBBGAdRObAxld55c_Nbt_mma-kzn0JFrADhbw,15565
|
|
296
|
+
benchflow-0.7.2.dev2000.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
297
|
+
benchflow-0.7.2.dev2000.dist-info/entry_points.txt,sha256=qPMBVtH4qz3P4nMALEAdTbvpxvsVch9RhqshZKQn1dE,84
|
|
298
|
+
benchflow-0.7.2.dev2000.dist-info/licenses/LICENSE,sha256=ohnbHkuBylrTtOvaZJk9Hz7dRwmaEEGAb5ZcBV4Xuhw,10779
|
|
299
|
+
benchflow-0.7.2.dev2000.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|