Files
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

370 lines
14 KiB
Python

"""Per-run provenance capture — write `provenance.json` next to `report.json`.
A reviewer auditing a published benchmark report has to answer three
questions:
1. *What code ran?* — git SHA, branch, dirty-state, list of changed files
2. *Can I reproduce it?* — full config + pre-registration content inline,
pinned model versions, Python + key package versions
3. *Did any secrets leak into the artifact?* — env-var whitelist, never
dump arbitrary env
This module is the single source of truth for those three answers. It runs
once at the start of every benchmark run, before any LLM call, and writes
its output to ``output_dir/provenance.json``. The runner cannot proceed
without succeeding here.
Security discipline:
- API-key-shaped env vars (``*_API_KEY``, ``*_TOKEN``, ``*_SECRET``,
``*_PASSWORD``, ``*_KEY``) are NEVER captured, even if explicitly named
in the whitelist
- Hostname / username are NOT captured by default (would leak who ran it)
- Uncommitted-changes capture is filename-only — no diff content
"""
from __future__ import annotations
import hashlib
import os
import subprocess
import sys
from collections.abc import Iterable
from importlib import metadata as importlib_metadata
from pathlib import Path
from typing import Any
import platform
from tests.benchmarks._framework.adapters import BenchmarkAdapter
from tests.benchmarks._framework.config import BenchmarkConfig
from tests.benchmarks._framework.cost import lookup_pricing
from tests.benchmarks._framework.llm_dispatch import LLMDispatcher
# --------------------------------------------------------------------------- #
# Constants #
# --------------------------------------------------------------------------- #
# Bumped only when the provenance schema changes in a backwards-incompatible
# way. Reviewers diffing two runs use this to know which fields they can
# compare 1:1.
PROVENANCE_SCHEMA_VERSION = 1
# Env vars whose presence + value is safe to record. Anything else is dropped.
# The values themselves are still passed through the secret-pattern filter so
# a malicious entry on the allowlist can't smuggle a key.
_ENV_ALLOWLIST: frozenset[str] = frozenset(
{
"OPENSRE_BENCH_WORKERS",
"OPENSRE_BENCH_COST_BUDGET_USD",
"BENCH_MIN_TOOL_CALLS",
"PYTHONPATH",
"LANG",
"LC_ALL",
"CI",
"GITHUB_ACTIONS",
"GITHUB_RUN_ID",
"GITHUB_SHA",
}
)
# Substring patterns that mark an env var as secret-bearing. Belt-and-braces:
# any var matching these is dropped even if it slipped onto the allowlist.
_SECRET_NAME_PATTERNS: tuple[str, ...] = (
"_API_KEY",
"_TOKEN",
"_SECRET",
"_PASSWORD",
"_KEY",
"PASSPHRASE",
"PRIVATE",
"CREDENTIALS",
"AUTH",
)
# Key packages to record versions for. Failures (PackageNotFoundError) are
# captured as None rather than raising.
_KEY_PACKAGES: tuple[str, ...] = (
"anthropic",
"openai",
"boto3",
"pydantic",
"pyyaml",
"huggingface-hub",
"datasets",
)
# --------------------------------------------------------------------------- #
# Public API #
# --------------------------------------------------------------------------- #
def capture_provenance(
*,
config: BenchmarkConfig,
adapter: BenchmarkAdapter,
run_id: str,
started_at: str,
config_path: Path | None = None,
pre_registration_path: Path | None = None,
) -> dict[str, Any]:
"""Snapshot everything needed to reproduce / audit this run.
Args:
config: the BenchmarkConfig the runner loaded
adapter: the BenchmarkAdapter the runner will call
run_id: framework-generated run id (e.g. ``2026-05-18T18-30-00Z_cloudopsbench``)
started_at: ISO-8601 UTC timestamp the runner stamped
config_path: optional path to the YAML the config was loaded from
(passed when running via the CLI; absent when constructed inline)
pre_registration_path: optional path to the pre-registration YAML
Returns:
Plain dict, JSON-serializable, suitable for ``json.dump`` to
``provenance.json`` in the run directory.
"""
provenance: dict[str, Any] = {
"schema_version": PROVENANCE_SCHEMA_VERSION,
"run_id": run_id,
"started_at": started_at,
"code": _git_state(),
"config": _config_section(config, config_path),
"pre_registration": _pre_registration_section(
config.pre_registration_path or pre_registration_path
),
"models": _models_section(config),
"environment": _environment_section(),
"dataset": _dataset_section(adapter),
"run_inputs": _run_inputs_section(config),
}
# Strategy hook — the framework's job is to assemble the standard
# sections; adapters extend with their own keys (e.g. CloudOpsBench
# adds ``min_tool_calls`` to ``run_inputs``). See BenchmarkAdapter.
return adapter.extend_provenance(provenance)
# --------------------------------------------------------------------------- #
# Code (git) section #
# --------------------------------------------------------------------------- #
def _git_state() -> dict[str, Any]:
"""Best-effort git provenance. Falls back gracefully if no git available."""
try:
sha_full = _run_git("rev-parse", "HEAD") or "(unknown)"
sha_short = _run_git("rev-parse", "--short", "HEAD") or "(unknown)"
branch = _run_git("rev-parse", "--abbrev-ref", "HEAD") or "(unknown)"
# NB: fetch porcelain UNSTRIPPED. ``git status --porcelain`` emits
# ``XY PATH`` where column 0 (X = index state) is a SIGNIFICANT space for
# unstaged-only changes (e.g. " M tools/investigation/stages/gather_evidence/agent.py"). The
# default strip in ``_run_git`` would eat that leading space on the first
# line, shifting it left so ``line[3:]`` slices into the path and drops
# its first character (" M app…" → "pp/agent/…"). Keep the raw spacing so
# the fixed-width [3:] slice lands exactly on the path.
status_porcelain = _run_git("status", "--porcelain", strip=False) or ""
changed_files = [
line[3:].strip()
for line in status_porcelain.splitlines()
if len(line) > 3 and line[3:].strip()
]
return {
"opensre_sha": sha_full,
"opensre_short_sha": sha_short,
"opensre_branch": branch,
"opensre_dirty": bool(changed_files),
"opensre_changed_files": changed_files,
}
except (FileNotFoundError, OSError):
return {
"opensre_sha": "(no-git)",
"opensre_short_sha": "(no-git)",
"opensre_branch": "(no-git)",
"opensre_dirty": False,
"opensre_changed_files": [],
}
def _run_git(*args: str, strip: bool = True) -> str | None:
"""Run a git command; return stdout (stripped by default) or None on failure.
Pass ``strip=False`` for commands whose leading/trailing whitespace is
significant — notably ``status --porcelain``, where column 0 is a meaningful
space for unstaged changes.
"""
result = subprocess.run(
["git", *args],
capture_output=True,
text=True,
check=False,
cwd=Path(__file__).parent,
)
if result.returncode != 0:
return None
return result.stdout.strip() if strip else result.stdout
# --------------------------------------------------------------------------- #
# Config / pre-registration sections — inline content so runs are self-contained
# --------------------------------------------------------------------------- #
def _config_section(config: BenchmarkConfig, config_path: Path | None) -> dict[str, Any]:
section: dict[str, Any] = {
"path": str(config_path) if config_path else None,
"sha256": None,
"content": None,
}
if config_path is not None and config_path.exists():
content = config_path.read_text(encoding="utf-8")
section["sha256"] = _sha256(content)
section["content"] = content
return section
def _pre_registration_section(pre_reg_path: Path | None) -> dict[str, Any]:
section: dict[str, Any] = {
"path": str(pre_reg_path) if pre_reg_path else None,
"sha256": None,
"content": None,
}
if pre_reg_path is not None and pre_reg_path.exists():
content = pre_reg_path.read_text(encoding="utf-8")
section["sha256"] = _sha256(content)
section["content"] = content
return section
# --------------------------------------------------------------------------- #
# Models section — per-LLM spec + pricing snapshot #
# --------------------------------------------------------------------------- #
def _models_section(config: BenchmarkConfig) -> dict[str, dict[str, Any]]:
out: dict[str, dict[str, Any]] = {}
for llm in config.llms:
configured_version = config.model_versions.get(llm, "")
entry: dict[str, Any] = {
"configured_version": configured_version,
}
try:
spec = LLMDispatcher.spec(llm)
entry["provider"] = str(spec.provider)
entry["spec_reasoning_model"] = spec.reasoning_model
entry["spec_classification_model"] = spec.classification_model
entry["spec_toolcall_model"] = spec.toolcall_model
except Exception as exc:
entry["spec_error"] = f"{type(exc).__name__}: {exc}"
try:
pricing = lookup_pricing(configured_version)
entry["pricing_snapshot"] = {
"input_usd_per_mtok": pricing.input_usd_per_mtok,
"output_usd_per_mtok": pricing.output_usd_per_mtok,
}
except Exception as exc:
entry["pricing_error"] = f"{type(exc).__name__}: {exc}"
out[llm] = entry
return out
# --------------------------------------------------------------------------- #
# Environment section — Python + key packages + safe env-var snapshot #
# --------------------------------------------------------------------------- #
def _environment_section() -> dict[str, Any]:
return {
"python_version": sys.version.split()[0],
"python_implementation": platform.python_implementation(),
"platform": platform.platform(),
"machine": platform.machine(),
"key_packages": _package_versions(_KEY_PACKAGES),
"env": _safe_env_snapshot(),
}
def _package_versions(packages: Iterable[str]) -> dict[str, str | None]:
out: dict[str, str | None] = {}
for name in packages:
try:
out[name] = importlib_metadata.version(name)
except importlib_metadata.PackageNotFoundError:
out[name] = None
return out
def _safe_env_snapshot() -> dict[str, str]:
"""Whitelist + secret-pattern filter. Drops anything risky."""
out: dict[str, str] = {}
for name in _ENV_ALLOWLIST:
if _looks_like_secret(name):
# Belt-and-braces — never let a secret-shaped allowlist entry through.
continue
value = os.environ.get(name)
if value is None:
continue
out[name] = value
return out
def _looks_like_secret(name: str) -> bool:
upper = name.upper()
return any(pattern in upper for pattern in _SECRET_NAME_PATTERNS)
# --------------------------------------------------------------------------- #
# Dataset section — best-effort from adapter attrs #
# --------------------------------------------------------------------------- #
def _dataset_section(adapter: BenchmarkAdapter) -> dict[str, Any]:
"""Adapters can expose dataset provenance via well-known attributes; we
grab what's available and leave the rest as None.
"""
return {
"adapter_name": adapter.name,
"adapter_version": adapter.version,
"hf_dataset": getattr(adapter, "hf_dataset", None),
"hf_revision": getattr(adapter, "hf_revision", None),
"local_path": _stringify_optional_path(getattr(adapter, "benchmark_dir", None)),
"data_contamination_checked": bool(getattr(adapter, "data_contamination_checked", False)),
}
def _stringify_optional_path(value: Any) -> str | None:
if value is None:
return None
return str(value)
# --------------------------------------------------------------------------- #
# Run-inputs section — echo from config for quick scanning #
# --------------------------------------------------------------------------- #
def _run_inputs_section(config: BenchmarkConfig) -> dict[str, Any]:
# Adapter-agnostic. Knobs that belong to a specific adapter (e.g.
# CloudOpsBench's ``min_tool_calls``) are injected via the adapter's
# ``extend_provenance`` hook, NOT by the framework reaching into the
# adapter's internals. See BenchmarkAdapter.extend_provenance.
return {
"modes": list(config.modes),
"llms": list(config.llms),
"model_versions": dict(config.model_versions),
"runs_per_case": config.runs_per_case,
"workers": config.workers,
"cost_budget_usd": config.cost_budget_usd,
"seed": config.seed,
"filters": config.filters.model_dump(),
"report_formats": list(config.report_formats),
}
# --------------------------------------------------------------------------- #
# Helpers #
# --------------------------------------------------------------------------- #
def _sha256(content: str) -> str:
return hashlib.sha256(content.encode("utf-8")).hexdigest()