33import hashlib
44import ipaddress
55import json
6+ import os
67import re
8+ import tempfile
9+ from collections .abc import Callable
710from datetime import UTC , datetime
811from pathlib import Path , PurePosixPath
912from typing import Any , Literal
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
4450class 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+
4860def 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
5974def _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+
470721def 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 ):
0 commit comments