Skip to content

Commit 27e1601

Browse files
authored
Merge pull request #17 from mogilventures/fix-parity-blindbench-ingest
fix: align parity evidence with BlindBench ingest
2 parents d90d57a + d402089 commit 27e1601

8 files changed

Lines changed: 509 additions & 18 deletions

File tree

README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,15 @@ MOGIL_RUN_DAYTONA_PARITY=1 mogil-bench run-daytona-parity \
204204

205205
Do not also export `ANTHROPIC_API_KEY` or `OPENROUTER_API_KEY`; manager preflight rejects plaintext model credentials. There is no host-Pi or mock fallback. An unavailable image, wrong in-image Python/Pi version, policy mismatch, failed attempt, incomplete evidence, or unconfirmed cleanup makes the parity command fail; retries never hide it. The image's existence and in-sandbox Python 3.12, `/bin/sh`, and Pi 0.80.6 checks occur at the real Daytona boundary, so they cannot be claimed by metadata-only validation.
206206

207-
The run root contains aggregate strict evidence with 18 lines plus one bundle per attempt. Validate and dry-run the exact uploads before adding `--confirm`:
207+
The run root contains aggregate strict evidence with 18 lines plus one bundle per attempt. Each exported `run.id` identifies one actual attempt, while `run.attempt` remains the real attempt ID and private `analysis_metadata.logical_run_id` retains the task/configuration identity for operator reconciliation.
208+
209+
A completed run can be re-exported offline from its retained, checksummed per-attempt bundles. This command does not invoke agents, providers, Daytona, or model APIs, and it does not modify bundle bytes. It validates every bundle and manifest identity, removes generated Python cache evidence, and stages and validates both aggregate files before replacement. Each file replacement uses a same-filesystem atomic rename; if the process observes a replacement failure, it restores both destinations to their prior bytes or prior absence. This is failure-safe rollback, not a crash-atomic multi-file transaction.
210+
211+
```bash
212+
mogil-bench evidence re-export /tmp/mogil-daytona-provider-parity
213+
```
214+
215+
Create a **fresh BlindBench project** for a corrected re-export; do not upload it into a project containing rows from an earlier diagnostic import. Validate and dry-run the exact fresh-project uploads before adding `--confirm`:
208216

209217
```bash
210218
mogil-bench evidence validate /tmp/mogil-daytona-provider-parity/mogil.harbor-evidence.jsonl
@@ -216,7 +224,7 @@ BLINDBENCH_AUTOMATION_TOKEN='project-token' mogil-bench evidence upload \
216224
--endpoint https://BLINDBENCH_HOST/ingest/v1/eval-runs --confirm
217225
```
218226

219-
The private envelope retains provider/model provenance. Its `reviewer` projection contains the shared task identity, blinded `isolated-sandbox` class, trajectory, objective outcomes, and bounded evidence—but no provider, model, configuration ID, secret name, or secret value—so BlindBench can group same-task attempts without exposing the comparison arm to reviewers.
227+
The private envelope retains provider/model provenance and stable logical task/configuration identity. Its `reviewer` projection contains the shared task identity, blinded `isolated-sandbox` class, trajectory, objective outcomes, and bounded evidence—but no provider, model, vendor, configuration ID, credential, canary, absolute path, secret name, or secret value—so BlindBench can group same-task attempts without exposing the comparison arm to reviewers.
220228

221229
## Command safety
222230

src/mogil_bench/cli.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from .artifacts import ArtifactError, export_run, upload_artifact, validate_artifact
1212
from .evidence import (
1313
EvidenceError,
14+
reexport_harbor_evidence,
1415
upload_evidence_artifact,
1516
validate_evidence_artifact,
1617
)
@@ -208,6 +209,16 @@ def artifact_upload(
208209
)
209210

210211

212+
@evidence_app.command("re-export")
213+
def evidence_reexport(run_dir: Path) -> None:
214+
"""Safely rebuild aggregate evidence from retained validated bundles."""
215+
try:
216+
json_path, jsonl_path = reexport_harbor_evidence(run_dir)
217+
except EvidenceError as error:
218+
_fail(str(error))
219+
typer.echo(f"wrote {json_path} and {jsonl_path}")
220+
221+
211222
@evidence_app.command("validate")
212223
def evidence_validate(path: Path) -> None:
213224
"""Strictly validate mogil.harbor-evidence v1.0 JSON or JSONL."""

src/mogil_bench/evidence.py

Lines changed: 258 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
import hashlib
44
import ipaddress
55
import json
6+
import os
67
import re
8+
import tempfile
9+
from collections.abc import Callable
710
from datetime import UTC, datetime
811
from pathlib import Path, PurePosixPath
912
from typing import Any, Literal
@@ -39,12 +42,21 @@
3942
r"(?<![A-Za-z0-9_.-])/(?:workspace|artifacts/candidate-workspace)(?:/[A-Za-z0-9_.@+-]+)+"
4043
)
4144
_ABSOLUTE_PATH = re.compile(r"(?<![:A-Za-z0-9_.-])/(?!/)(?:[A-Za-z0-9_.@+-]+/)*[A-Za-z0-9_.@+-]+")
45+
_LINE_ENDING_COLON = re.compile(r"(?<=[A-Za-z]):(?=\r?\n)")
46+
_PYTHON_CACHE_SUFFIXES = (".pyc", ".pyo")
47+
_DIFF_HEADER = re.compile(r"^diff --git a/(.+) b/(.+)$")
4248

4349

4450
class EvidenceError(ValueError):
4551
pass
4652

4753

54+
def evidence_run_id(logical_run_id: str, attempt_id: str) -> str:
55+
"""Return a stable BlindBench run identity for one actual attempt."""
56+
identity = f"{logical_run_id}\0{attempt_id}".encode()
57+
return f"mogil-attempt-{hashlib.sha256(identity).hexdigest()}"
58+
59+
4860
def redact_text(value: str) -> str:
4961
result = value
5062
for pattern in _SECRET_PATTERNS:
@@ -53,7 +65,10 @@ def redact_text(value: str) -> str:
5365
)
5466
result = _HOST_PATH.sub("[HOST_PATH]", result)
5567
result = _WORKSPACE_PATH.sub("[WORKSPACE_PATH]", result)
56-
return _ABSOLUTE_PATH.sub("[ABSOLUTE_PATH]", result)
68+
result = _ABSOLUTE_PATH.sub("[ABSOLUTE_PATH]", result)
69+
# BlindBench scans JSON serialization for Windows paths. A line ending in an
70+
# ASCII letter plus ':' serializes as e.g. ``s:\\n`` and trips that check.
71+
return _LINE_ENDING_COLON.sub(": ", result)
5772

5873

5974
def _redact(value: Any) -> Any:
@@ -467,6 +482,242 @@ def build_harbor_evidence(
467482
)
468483

469484

485+
def _generated_python_cache_path(value: str) -> bool:
486+
path = PurePosixPath(value)
487+
return "__pycache__" in path.parts or path.name.endswith(_PYTHON_CACHE_SUFFIXES)
488+
489+
490+
def _sanitize_reviewer_value(value: Any) -> Any:
491+
if isinstance(value, str):
492+
return redact_text(value)
493+
if isinstance(value, list):
494+
return [_sanitize_reviewer_value(item) for item in value]
495+
if isinstance(value, dict):
496+
return {key: _sanitize_reviewer_value(item) for key, item in value.items()}
497+
return value
498+
499+
500+
def _reviewer_safe_patch(value: str) -> str:
501+
blocks = re.split(r"(?=^diff --git )", value, flags=re.MULTILINE)
502+
retained: list[str] = []
503+
for block in blocks:
504+
first_line = block.splitlines()[0] if block else ""
505+
header = _DIFF_HEADER.fullmatch(first_line)
506+
if header and any(_generated_python_cache_path(path) for path in header.groups()):
507+
continue
508+
retained.append(block)
509+
return redact_text("".join(retained))
510+
511+
512+
def _bundle_from_manifest(run_dir: Path, reference_value: object) -> Path:
513+
if not isinstance(reference_value, str):
514+
raise EvidenceError("manifest bundle reference must be a string")
515+
reference = Path(reference_value)
516+
if reference.is_absolute() or not reference.parts or any(
517+
part in {"", ".", ".."} for part in reference.parts
518+
):
519+
raise EvidenceError("manifest bundle reference must be a safe relative path")
520+
root = run_dir.resolve(strict=True)
521+
current = run_dir
522+
for part in reference.parts:
523+
current /= part
524+
if current.is_symlink():
525+
raise EvidenceError("manifest bundle path must not contain symlinks")
526+
try:
527+
bundle = current.resolve(strict=True)
528+
except OSError as error:
529+
raise EvidenceError("manifest bundle path cannot be resolved") from error
530+
if not bundle.is_relative_to(root):
531+
raise EvidenceError("manifest bundle path escapes run root")
532+
return bundle
533+
534+
535+
def _write_staged_file(run_dir: Path, *, prefix: str, data: bytes) -> Path:
536+
descriptor, temporary = tempfile.mkstemp(dir=run_dir, prefix=prefix, suffix=".tmp")
537+
path = Path(temporary)
538+
with os.fdopen(descriptor, "wb") as stream:
539+
stream.write(data)
540+
stream.flush()
541+
os.fsync(stream.fileno())
542+
return path
543+
544+
545+
def _replace_evidence_pair(
546+
run_dir: Path,
547+
json_bytes: bytes,
548+
jsonl_bytes: bytes,
549+
*,
550+
replace_file: Callable[[Path, Path], object] = os.replace,
551+
) -> tuple[Path, Path]:
552+
"""Replace both files with rollback on observed errors; not crash-atomic as a pair."""
553+
destinations = (
554+
run_dir / "mogil.harbor-evidence.json",
555+
run_dir / "mogil.harbor-evidence.jsonl",
556+
)
557+
staged: list[Path] = []
558+
backups: dict[Path, Path | None] = {}
559+
try:
560+
for destination, data in zip(destinations, (json_bytes, jsonl_bytes), strict=True):
561+
staged.append(
562+
_write_staged_file(
563+
run_dir,
564+
prefix=f".{destination.name}.new.",
565+
data=data,
566+
)
567+
)
568+
backups[destination] = (
569+
_write_staged_file(
570+
run_dir,
571+
prefix=f".{destination.name}.backup.",
572+
data=destination.read_bytes(),
573+
)
574+
if destination.exists()
575+
else None
576+
)
577+
try:
578+
for staged_path, destination in zip(staged, destinations, strict=True):
579+
replace_file(staged_path, destination)
580+
except OSError as error:
581+
rollback_errors: list[OSError] = []
582+
for destination in destinations:
583+
backup = backups[destination]
584+
try:
585+
if backup is None:
586+
destination.unlink(missing_ok=True)
587+
else:
588+
replace_file(backup, destination)
589+
except OSError as rollback_error:
590+
rollback_errors.append(rollback_error)
591+
if rollback_errors:
592+
raise EvidenceError(
593+
"aggregate replacement failed and prior pair could not be fully restored"
594+
) from error
595+
raise EvidenceError(
596+
"aggregate replacement failed; prior destination bytes and absence restored"
597+
) from error
598+
return destinations
599+
finally:
600+
for path in (*staged, *(backup for backup in backups.values() if backup is not None)):
601+
path.unlink(missing_ok=True)
602+
603+
604+
def reexport_harbor_evidence(run_dir: Path) -> tuple[Path, Path]:
605+
"""Rebuild aggregates with validated staging and rollback on replacement errors."""
606+
try:
607+
manifest = json.loads((run_dir / "manifest.json").read_text(encoding="utf-8"))
608+
except (OSError, UnicodeError, json.JSONDecodeError) as error:
609+
raise EvidenceError("cannot read re-export manifest") from error
610+
if not isinstance(manifest, dict) or not isinstance(manifest.get("results"), list):
611+
raise EvidenceError("re-export manifest has an invalid shape")
612+
results = manifest["results"]
613+
expected = manifest.get("result_count")
614+
if not isinstance(expected, int) or isinstance(expected, bool) or expected != len(results):
615+
raise EvidenceError("re-export manifest count does not match results")
616+
if expected < 1:
617+
raise EvidenceError("re-export manifest count must be positive")
618+
619+
from .run_bundle import validate_checksums
620+
621+
artifacts: list[HarborEvidence] = []
622+
manifest_attempts: set[str] = set()
623+
for result in results:
624+
if not isinstance(result, dict):
625+
raise EvidenceError("re-export manifest result is invalid")
626+
logical_run_id = result.get("logical_run_id")
627+
attempt_id = result.get("attempt_id")
628+
task_id = result.get("task_id")
629+
if (
630+
not isinstance(logical_run_id, str)
631+
or not logical_run_id
632+
or not isinstance(attempt_id, str)
633+
or not attempt_id
634+
or attempt_id in manifest_attempts
635+
or not isinstance(task_id, str)
636+
or not task_id
637+
):
638+
raise EvidenceError("re-export manifest identity is invalid")
639+
manifest_attempts.add(attempt_id)
640+
bundle = _bundle_from_manifest(run_dir, result.get("bundle"))
641+
if not validate_checksums(bundle):
642+
raise EvidenceError("retained bundle checksum validation failed")
643+
private_path = bundle / "mogil.harbor-evidence.json"
644+
if validate_evidence_artifact(private_path) != 1:
645+
raise EvidenceError("retained bundle evidence validation failed")
646+
try:
647+
artifact = HarborEvidence.model_validate_json(
648+
private_path.read_text(encoding="utf-8")
649+
)
650+
except (OSError, UnicodeError, ValidationError) as error:
651+
raise EvidenceError("retained bundle evidence is invalid") from error
652+
desired_run_id = evidence_run_id(logical_run_id, attempt_id)
653+
retained_logical = artifact.analysis_metadata.get("logical_run_id")
654+
if (
655+
artifact.run.attempt != attempt_id
656+
or artifact.run.id not in {logical_run_id, desired_run_id}
657+
or (retained_logical is not None and retained_logical != logical_run_id)
658+
or artifact.reviewer.task.id != task_id
659+
):
660+
raise EvidenceError("retained bundle identity does not match manifest")
661+
662+
value = artifact.model_dump(mode="json", by_alias=True, exclude_none=True)
663+
value["run"]["id"] = desired_run_id
664+
value["analysis_metadata"]["logical_run_id"] = logical_run_id
665+
value["reviewer"] = _sanitize_reviewer_value(value["reviewer"])
666+
reviewer_evidence = value["reviewer"]["evidence"]
667+
changed_files = [
668+
item
669+
for item in reviewer_evidence["changed_files"]
670+
if not _generated_python_cache_path(item["path"])
671+
]
672+
patch = _reviewer_safe_patch(reviewer_evidence["patch"])
673+
reviewer_evidence["changed_files"] = changed_files
674+
reviewer_evidence["changed_files_reference"]["reviewer_sha256"] = (
675+
_reviewer_sha256(changed_files)
676+
)
677+
reviewer_evidence["patch"] = patch
678+
reviewer_evidence["patch_reference"]["reviewer_sha256"] = _reviewer_sha256(patch)
679+
verifier_values = {
680+
"verifier/stdout.txt": reviewer_evidence["verifier_stdout"],
681+
"verifier/stderr.txt": reviewer_evidence["verifier_stderr"],
682+
}
683+
for reference in reviewer_evidence["verifier_references"]:
684+
reference["reviewer_sha256"] = _reviewer_sha256(
685+
verifier_values[reference["path"]]
686+
)
687+
artifacts.append(HarborEvidence.model_validate(value))
688+
689+
values = [
690+
artifact.model_dump(mode="json", by_alias=True, exclude_none=True)
691+
for artifact in artifacts
692+
]
693+
if (
694+
len(values) != expected
695+
or len({artifact.run.id for artifact in artifacts}) != expected
696+
or len({artifact.run.attempt for artifact in artifacts}) != expected
697+
):
698+
raise EvidenceError("re-export count or identity validation failed")
699+
json_bytes = (
700+
json.dumps(values, indent=2, sort_keys=True, ensure_ascii=False) + "\n"
701+
).encode("utf-8")
702+
jsonl_bytes = "".join(
703+
json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
704+
+ "\n"
705+
for value in values
706+
).encode("utf-8")
707+
708+
with tempfile.TemporaryDirectory(dir=run_dir, prefix=".evidence-validate-") as temporary:
709+
staged_json = Path(temporary) / "mogil.harbor-evidence.json"
710+
staged_jsonl = Path(temporary) / "mogil.harbor-evidence.jsonl"
711+
staged_json.write_bytes(json_bytes)
712+
staged_jsonl.write_bytes(jsonl_bytes)
713+
if (
714+
validate_evidence_artifact(staged_json) != expected
715+
or validate_evidence_artifact(staged_jsonl) != expected
716+
):
717+
raise EvidenceError("re-export aggregate validation failed")
718+
return _replace_evidence_pair(run_dir, json_bytes, jsonl_bytes)
719+
720+
470721
def validate_evidence_endpoint(endpoint: str) -> None:
471722
parsed = urlparse(endpoint)
472723
loopback = False
@@ -556,9 +807,12 @@ def validate_evidence_artifact(path: Path) -> int:
556807
if not values:
557808
raise EvidenceError("artifact is empty")
558809
artifacts = [HarborEvidence.model_validate(value) for value in values]
559-
identities = {(artifact.run.id, artifact.run.attempt) for artifact in artifacts}
560-
if len(identities) != len(artifacts):
561-
raise EvidenceError("artifact contains duplicate run/attempt identities")
810+
run_ids = {artifact.run.id for artifact in artifacts}
811+
attempts = {artifact.run.attempt for artifact in artifacts}
812+
if len(run_ids) != len(artifacts):
813+
raise EvidenceError("artifact contains duplicate run ids")
814+
if len(attempts) != len(artifacts):
815+
raise EvidenceError("artifact contains duplicate attempts")
562816
return len(artifacts)
563817
except (OSError, UnicodeError, json.JSONDecodeError, ValidationError, ValueError) as error:
564818
if isinstance(error, EvidenceError):

src/mogil_bench/parity.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66
from pathlib import Path
77
from typing import Any
88

9-
from .evidence import EvidenceError, HarborEvidence, validate_evidence_artifact
9+
from .evidence import (
10+
EvidenceError,
11+
HarborEvidence,
12+
evidence_run_id,
13+
validate_evidence_artifact,
14+
)
1015
from .models import EnvironmentType, Pack
1116
from .packs import load_pack, pack_fingerprint
1217

@@ -244,7 +249,7 @@ def validate_parity_output(
244249
raise ValueError("parity manifest attempt identity is invalid")
245250
cells.setdefault(cell, set()).add(number)
246251
logical_ids.setdefault(cell, set()).add(logical)
247-
manifest_attempts[(logical, attempt)] = cell[1]
252+
manifest_attempts[(evidence_run_id(logical, attempt), attempt)] = cell[1]
248253
if set(cells) != expected_cells or any(numbers != {1, 2, 3} for numbers in cells.values()):
249254
raise ValueError("parity manifest does not contain 3 attempts for every matrix cell")
250255
stable_logical_ids = {
@@ -254,11 +259,19 @@ def validate_parity_output(
254259
raise ValueError("parity logical identity is not stable and distinct by matrix cell")
255260

256261
evidence_attempts: dict[tuple[str, str], str] = {}
262+
evidence_run_ids: set[str] = set()
263+
evidence_attempt_ids: set[str] = set()
257264
for value in values:
258265
artifact = HarborEvidence.model_validate(value)
259266
identity = (artifact.run.id, artifact.run.attempt)
260-
if artifact.run.status != "quality_eligible" or identity in evidence_attempts:
261-
raise ValueError("parity evidence attempts must be distinct and quality eligible")
267+
if (
268+
artifact.run.status != "quality_eligible"
269+
or artifact.run.id in evidence_run_ids
270+
or artifact.run.attempt in evidence_attempt_ids
271+
):
272+
raise ValueError("parity evidence run ids and attempts must be independently distinct")
273+
evidence_run_ids.add(artifact.run.id)
274+
evidence_attempt_ids.add(artifact.run.attempt)
262275
if _contains_provenance(value.get("reviewer") if isinstance(value, dict) else None):
263276
raise ValueError("reviewer projection contains provenance")
264277
evidence_attempts[identity] = artifact.reviewer.task.id

0 commit comments

Comments
 (0)