from __future__ import annotations import hashlib import json import math import os import re import secrets import shutil import stat from dataclasses import dataclass from pathlib import Path from typing import Any from PIL import Image from k1link.artifacts import utc_now_iso, write_json_atomic from k1link.device_plugins.xgrids_k1.analyze.valid_fov import ( validate_k1_valid_fov_mask, ) from k1link.sessions import SessionIntegrityError from .jobs import validate_camera_compute_job from .qualification import validate_recorded_qualification_slice EVALUATION_PACK_SCHEMA = "missioncore.perception-evaluation-pack/v1" EVALUATION_PACK_IDENTITY_SCHEMA = "missioncore.perception-evaluation-pack-identity/v1" ANNOTATION_CONTRACT_SCHEMA = "missioncore.perception-annotation-contract/v1" ANNOTATION_TEMPLATE_SCHEMA = "missioncore.perception-annotation-template/v1" SELECTION_POLICY = "reviewed-anchors-plus-temporal-clips/v1" MIN_EVALUATION_FRAMES = 16 MAX_EVALUATION_FRAMES = 128 MAX_MANIFEST_BYTES = 32 * 1024 * 1024 MAX_TIMELINE_LINE_BYTES = 16 * 1024 _SHA256 = re.compile(r"^[a-f0-9]{64}$") _SAFE_GENERATION = re.compile(r"^evaluation-pack-[a-f0-9]{64}$") _SAFE_GROUP = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$") class RecordedEvaluationPackError(RuntimeError): """A recorded-perception evaluation pack is invalid.""" @dataclass(frozen=True, slots=True) class EvaluationFrameRequest: frame_index: int role: str group_id: str @dataclass(frozen=True, slots=True) class EvaluationPackFrame: image_id: int frame_index: int sequence: int segment_sha256: str session_seconds: float role: str group_id: str raw_path: Path valid_fov_fill_path: Path @dataclass(frozen=True, slots=True) class RecordedEvaluationPack: generation_id: str root: Path manifest_path: Path annotation_contract_path: Path annotation_template_path: Path job_id: str input_sha256: str qualification_generation_id: str valid_fov_generation_id: str calibration_sha256: str frames: tuple[EvaluationPackFrame, ...] def prepare_recorded_evaluation_pack( *, job_root: Path, qualification_root: Path, valid_fov_root: Path, decoded_frames_root: Path, timeline_path: Path, output_root: Path, selection: tuple[EvaluationFrameRequest, ...], decoder_version: str, selection_document_sha256: str, producer_files: tuple[tuple[str, str], ...], ) -> RecordedEvaluationPack: """Seal exact raw/fixed-fill frames and an annotation contract for model A/B. The pack is immutable. Human annotations are authored in a separate working copy of ``annotation-template.json`` and later sealed as their own generation. This keeps source imagery and the evolving review state from being conflated. """ job = validate_camera_compute_job(job_root) qualification = validate_recorded_qualification_slice( qualification_root, job_root=job.job_root, ) valid_fov = validate_k1_valid_fov_mask(valid_fov_root) if not isinstance(decoder_version, str) or not 1 <= len(decoder_version) <= 256: raise RecordedEvaluationPackError("decoder version is invalid") if _SHA256.fullmatch(selection_document_sha256) is None: raise RecordedEvaluationPackError("selection document SHA-256 is invalid") producer_identity = _validate_producer_files(producer_files) if ( valid_fov.source_id != job.source_id or valid_fov.width != 800 or valid_fov.height != 600 ): raise RecordedEvaluationPackError("valid-FOV does not match the camera job") selected = _validate_selection(selection, job.segment_count, qualification) timestamps = _read_timeline( timeline_path, job.segment_count, job.timeline_start_seconds, job.timeline_end_seconds, ) frames_root = decoded_frames_root.expanduser().resolve(strict=True) if not frames_root.is_dir(): raise RecordedEvaluationPackError("decoded frame root is not a directory") with Image.open(valid_fov.mask_path) as opened_mask: mask = opened_mask.copy() if mask.mode != "L" or mask.size != (valid_fov.width, valid_fov.height): raise RecordedEvaluationPackError("valid-FOV mask cannot be applied to the frames") source_by_index = {frame.frame_index: frame for frame in qualification.frames} job_index = _read_job_index(job.job_root, job.source_id, job.codec_epoch) decoded: list[dict[str, Any]] = [] for image_id, request in enumerate(selected, start=1): source = source_by_index.get(request.frame_index) row = job_index[request.frame_index] sequence = _nonnegative_integer(row.get("sequence"), "segment sequence") segment_sha256 = row.get("sha256") if ( sequence != request.frame_index + 1 or not isinstance(segment_sha256, str) or _SHA256.fullmatch(segment_sha256) is None or (source is not None and source.segment_sha256 != segment_sha256) ): raise RecordedEvaluationPackError("selected frame identity changed") source_path = frames_root / f"frame-{request.frame_index:06d}.png" _confined_regular_file(source_path, frames_root) try: with Image.open(source_path) as opened: raw = opened.convert("RGB") except OSError as exc: raise RecordedEvaluationPackError("decoded evaluation frame is unavailable") from exc if raw.size != (valid_fov.width, valid_fov.height): raise RecordedEvaluationPackError("decoded evaluation frame dimensions changed") fill = Image.composite(raw, Image.new("RGB", raw.size, (0, 0, 0)), mask) decoded.append( { "image_id": image_id, "request": request, "sequence": sequence, "segment_sha256": segment_sha256, "session_seconds": timestamps[request.frame_index], "raw": raw, "fill": fill, "raw_rgb_sha256": _pixel_sha256(raw), "fill_rgb_sha256": _pixel_sha256(fill), } ) identity = { "schema_version": EVALUATION_PACK_IDENTITY_SCHEMA, "job_id": job.job_id, "input_sha256": job.input_sha256, "session_id": job.session_id, "source_id": job.source_id, "codec_epoch": job.codec_epoch, "qualification_generation_id": qualification.generation_id, "qualification_policy": qualification.policy, "valid_fov_generation_id": valid_fov.generation_id, "calibration_sha256": valid_fov.calibration_sha256, "calibration_slot": valid_fov.calibration_slot, "selection_policy": SELECTION_POLICY, "selection_document_sha256": selection_document_sha256, "preprocessing_profile": "fixed-valid-fov-fill/v1", "decoder_version": decoder_version, "producer_files": producer_identity, "resolution": [valid_fov.width, valid_fov.height], "frames": [ { "image_id": item["image_id"], "frame_index": item["request"].frame_index, "sequence": item["sequence"], "segment_sha256": item["segment_sha256"], "session_seconds": item["session_seconds"], "role": item["request"].role, "group_id": item["request"].group_id, "raw_rgb_sha256": item["raw_rgb_sha256"], "valid_fov_fill_rgb_sha256": item["fill_rgb_sha256"], } for item in decoded ], } identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest() generation_id = f"evaluation-pack-{identity_sha256}" root = _prepare_private_directory(output_root) final = root / generation_id if final.exists(): existing = validate_recorded_evaluation_pack( final, job_root=job.job_root, qualification_root=qualification.root, valid_fov_root=valid_fov.root, ) if existing.input_sha256 != job.input_sha256: raise RecordedEvaluationPackError("evaluation generation collides with another input") return existing staging = root / f".{generation_id}.{secrets.token_hex(12)}.incomplete" published = False try: (staging / "images" / "raw").mkdir(mode=0o700, parents=True) (staging / "images" / "valid-fov-fill").mkdir(mode=0o700, parents=True) artifacts: list[dict[str, Any]] = [] for item in decoded: image_id = int(item["image_id"]) frame_index = item["request"].frame_index name = f"image-{image_id:03d}-frame-{frame_index:06d}.png" raw_path = staging / "images" / "raw" / name fill_path = staging / "images" / "valid-fov-fill" / name item["raw"].save(raw_path, format="PNG", optimize=False) item["fill"].save(fill_path, format="PNG", optimize=False) _fsync_file(raw_path) _fsync_file(fill_path) os.chmod(raw_path, 0o600) os.chmod(fill_path, 0o600) artifacts.extend((_artifact(raw_path, staging), _artifact(fill_path, staging))) contract = _annotation_contract(identity_sha256) template = _annotation_template(generation_id, identity["frames"]) contract_path = staging / "annotation-contract.json" template_path = staging / "annotation-template.json" write_json_atomic(contract_path, contract) write_json_atomic(template_path, template) os.chmod(contract_path, 0o600) os.chmod(template_path, 0o600) artifacts.extend((_artifact(contract_path, staging), _artifact(template_path, staging))) manifest = { "schema_version": EVALUATION_PACK_SCHEMA, "generation_id": generation_id, "identity_sha256": identity_sha256, "identity": identity, "created_at_utc": utc_now_iso(), "artifacts": artifacts, "annotation_state": { "state": "unannotated", "mutable_inside_pack": False, "workflow": ( "copy annotation-template.json to a review workspace; seal reviewed " "annotations as a separate content-addressed ground-truth generation" ), }, } write_json_atomic(staging / "manifest.json", manifest) os.chmod(staging / "manifest.json", 0o600) _fsync_tree(staging) os.replace(staging, final) _fsync_directory(root) published = True finally: if not published and staging.exists(): shutil.rmtree(staging) return validate_recorded_evaluation_pack( final, job_root=job.job_root, qualification_root=qualification.root, valid_fov_root=valid_fov.root, ) def validate_recorded_evaluation_pack( pack_root: Path, *, job_root: Path, qualification_root: Path, valid_fov_root: Path, ) -> RecordedEvaluationPack: root = pack_root.expanduser().resolve(strict=True) if not root.is_dir() or _SAFE_GENERATION.fullmatch(root.name) is None: raise RecordedEvaluationPackError("evaluation pack root is invalid") manifest = _read_json_object(root / "manifest.json", root, MAX_MANIFEST_BYTES) identity = manifest.get("identity") identity_sha256 = manifest.get("identity_sha256") if ( manifest.get("schema_version") != EVALUATION_PACK_SCHEMA or manifest.get("generation_id") != root.name or not isinstance(identity, dict) or identity.get("schema_version") != EVALUATION_PACK_IDENTITY_SCHEMA or not isinstance(identity_sha256, str) or _SHA256.fullmatch(identity_sha256) is None or root.name != f"evaluation-pack-{identity_sha256}" or hashlib.sha256(_canonical_json(identity)).hexdigest() != identity_sha256 ): raise RecordedEvaluationPackError("evaluation pack identity is inconsistent") job = validate_camera_compute_job(job_root) qualification = validate_recorded_qualification_slice( qualification_root, job_root=job.job_root, ) valid_fov = validate_k1_valid_fov_mask(valid_fov_root) if ( identity.get("job_id") != job.job_id or identity.get("input_sha256") != job.input_sha256 or identity.get("session_id") != job.session_id or identity.get("source_id") != job.source_id or identity.get("codec_epoch") != job.codec_epoch or identity.get("qualification_generation_id") != qualification.generation_id or identity.get("qualification_policy") != qualification.policy or identity.get("valid_fov_generation_id") != valid_fov.generation_id or identity.get("calibration_sha256") != valid_fov.calibration_sha256 or identity.get("calibration_slot") != valid_fov.calibration_slot or identity.get("selection_policy") != SELECTION_POLICY or not isinstance(identity.get("selection_document_sha256"), str) or _SHA256.fullmatch(str(identity["selection_document_sha256"])) is None or identity.get("preprocessing_profile") != "fixed-valid-fov-fill/v1" or not isinstance(identity.get("decoder_version"), str) or not 1 <= len(str(identity["decoder_version"])) <= 256 or identity.get("resolution") != [valid_fov.width, valid_fov.height] ): raise RecordedEvaluationPackError("evaluation pack bindings changed") producer_files = identity.get("producer_files") if not isinstance(producer_files, list): raise RecordedEvaluationPackError("evaluation producer identity is unavailable") normalized_producer = _validate_producer_files( tuple( (str(row.get("name")), str(row.get("sha256"))) for row in producer_files if isinstance(row, dict) ) ) if producer_files != normalized_producer: raise RecordedEvaluationPackError("evaluation producer identity changed") rows = identity.get("frames") if ( not isinstance(rows, list) or not MIN_EVALUATION_FRAMES <= len(rows) <= MAX_EVALUATION_FRAMES ): raise RecordedEvaluationPackError("evaluation pack frame set is invalid") artifact_rows = manifest.get("artifacts") if not isinstance(artifact_rows, list) or len(artifact_rows) != len(rows) * 2 + 2: raise RecordedEvaluationPackError("evaluation pack artifact set is invalid") artifacts: dict[str, dict[str, Any]] = {} for artifact in artifact_rows: if not isinstance(artifact, dict) or not isinstance(artifact.get("path"), str): raise RecordedEvaluationPackError("evaluation artifact descriptor is invalid") path_text = str(artifact["path"]) if path_text in artifacts: raise RecordedEvaluationPackError("evaluation artifact is duplicated") path = root / path_text metadata = _confined_regular_file(path, root) digest = artifact.get("sha256") if ( artifact.get("byte_length") != metadata.st_size or not isinstance(digest, str) or _SHA256.fullmatch(digest) is None or _sha256_file(path) != digest ): raise RecordedEvaluationPackError("evaluation artifact changed") artifacts[path_text] = artifact contract_path = root / "annotation-contract.json" template_path = root / "annotation-template.json" contract = _read_json_object(contract_path, root, MAX_MANIFEST_BYTES) template = _read_json_object(template_path, root, MAX_MANIFEST_BYTES) if ( contract.get("schema_version") != ANNOTATION_CONTRACT_SCHEMA or contract.get("evaluation_identity_sha256") != identity_sha256 or template.get("schema_version") != ANNOTATION_TEMPLATE_SCHEMA or template.get("evaluation_pack_id") != root.name or template.get("images") != rows or "annotation-contract.json" not in artifacts or "annotation-template.json" not in artifacts ): raise RecordedEvaluationPackError("annotation contract changed") qualification_indices = {frame.frame_index for frame in qualification.frames} job_index = _read_job_index(job.job_root, job.source_id, job.codec_epoch) with Image.open(valid_fov.mask_path) as opened_mask: validation_mask = opened_mask.copy() frames: list[EvaluationPackFrame] = [] previous_index = -1 temporal_groups: dict[str, list[int]] = {} for position, row in enumerate(rows, start=1): if not isinstance(row, dict): raise RecordedEvaluationPackError("evaluation frame descriptor is invalid") frame_index = row.get("frame_index") image_id = row.get("image_id") role = row.get("role") group_id = row.get("group_id") session_seconds = row.get("session_seconds") if ( image_id != position or not isinstance(frame_index, int) or isinstance(frame_index, bool) or not previous_index < frame_index < job.segment_count or role not in {"anchor", "temporal"} or not isinstance(group_id, str) or _SAFE_GROUP.fullmatch(group_id) is None or not isinstance(session_seconds, (int, float)) or isinstance(session_seconds, bool) or not math.isfinite(float(session_seconds)) or not job.timeline_start_seconds <= float(session_seconds) <= job.timeline_end_seconds ): raise RecordedEvaluationPackError("evaluation frame ordering is invalid") if role == "anchor" and frame_index not in qualification_indices: raise RecordedEvaluationPackError("anchor escaped the qualification slice") if role == "temporal": temporal_groups.setdefault(group_id, []).append(frame_index) name = f"image-{image_id:03d}-frame-{frame_index:06d}.png" raw_relative = f"images/raw/{name}" fill_relative = f"images/valid-fov-fill/{name}" if raw_relative not in artifacts or fill_relative not in artifacts: raise RecordedEvaluationPackError("evaluation image artifacts are incomplete") raw_path = root / raw_relative fill_path = root / fill_relative with Image.open(raw_path) as raw_opened, Image.open(fill_path) as fill_opened: raw = raw_opened.convert("RGB") fill = fill_opened.convert("RGB") expected_fill = Image.composite( raw, Image.new("RGB", raw.size, (0, 0, 0)), validation_mask, ) expected_segment_sha256 = job_index[frame_index].get("sha256") if ( raw.size != (valid_fov.width, valid_fov.height) or fill.size != raw.size or _pixel_sha256(raw) != row.get("raw_rgb_sha256") or _pixel_sha256(fill) != row.get("valid_fov_fill_rgb_sha256") or _pixel_sha256(expected_fill) != row.get("valid_fov_fill_rgb_sha256") or not isinstance(row.get("segment_sha256"), str) or _SHA256.fullmatch(str(row["segment_sha256"])) is None or row.get("segment_sha256") != expected_segment_sha256 or row.get("sequence") != frame_index + 1 ): raise RecordedEvaluationPackError("evaluation frame pixels or identity changed") frames.append( EvaluationPackFrame( image_id=image_id, frame_index=frame_index, sequence=frame_index + 1, segment_sha256=str(row["segment_sha256"]), session_seconds=float(session_seconds), role=role, group_id=group_id, raw_path=raw_path, valid_fov_fill_path=fill_path, ) ) previous_index = frame_index for group in temporal_groups.values(): if len(group) < 3 or any( right != left + 1 for left, right in zip(group, group[1:], strict=False) ): raise RecordedEvaluationPackError("temporal group is not a consecutive clip") if len(temporal_groups) < 2: raise RecordedEvaluationPackError("evaluation pack lacks temporal coverage") return RecordedEvaluationPack( generation_id=root.name, root=root, manifest_path=root / "manifest.json", annotation_contract_path=contract_path, annotation_template_path=template_path, job_id=job.job_id, input_sha256=job.input_sha256, qualification_generation_id=qualification.generation_id, valid_fov_generation_id=valid_fov.generation_id, calibration_sha256=valid_fov.calibration_sha256, frames=tuple(frames), ) def _validate_selection( selection: tuple[EvaluationFrameRequest, ...], source_count: int, qualification: Any, ) -> tuple[EvaluationFrameRequest, ...]: if not MIN_EVALUATION_FRAMES <= len(selection) <= MAX_EVALUATION_FRAMES: raise RecordedEvaluationPackError("evaluation selection size is outside bounds") qualification_indices = {frame.frame_index for frame in qualification.frames} previous = -1 temporal_groups: dict[str, list[int]] = {} anchors = 0 for request in selection: if ( not isinstance(request, EvaluationFrameRequest) or not isinstance(request.frame_index, int) or isinstance(request.frame_index, bool) or not previous < request.frame_index < source_count or request.role not in {"anchor", "temporal"} or _SAFE_GROUP.fullmatch(request.group_id) is None ): raise RecordedEvaluationPackError("evaluation selection is invalid") if request.role == "anchor": if request.frame_index not in qualification_indices: raise RecordedEvaluationPackError("anchor is outside the qualification slice") anchors += 1 else: temporal_groups.setdefault(request.group_id, []).append(request.frame_index) previous = request.frame_index if anchors < MIN_EVALUATION_FRAMES: raise RecordedEvaluationPackError("evaluation selection has too few anchors") if len(temporal_groups) < 2: raise RecordedEvaluationPackError("evaluation selection needs at least two temporal clips") for group in temporal_groups.values(): if len(group) < 3 or any( right != left + 1 for left, right in zip(group, group[1:], strict=False) ): raise RecordedEvaluationPackError("temporal clip frames must be consecutive") return selection def _annotation_contract(evaluation_identity_sha256: str) -> dict[str, Any]: categories = [ (1, "person", "thing"), (2, "bicycle", "thing"), (3, "motorcycle", "thing"), (4, "car", "thing"), (5, "heavy_vehicle", "thing"), (6, "building_structure", "stuff"), (7, "paved_road", "stuff"), (8, "sidewalk_curb", "stuff"), (9, "ground_dirt", "stuff"), (10, "grass_low_vegetation", "stuff"), (11, "tree_woody_vegetation", "stuff"), (12, "sky", "stuff"), (13, "static_obstacle", "thing"), (14, "animal", "thing"), (15, "other_background", "stuff"), ] return { "schema_version": ANNOTATION_CONTRACT_SCHEMA, "evaluation_identity_sha256": evaluation_identity_sha256, "task": "2d-panoptic-and-instance-qualification", "categories": [ {"id": category_id, "name": name, "kind": kind} for category_id, name, kind in categories ], "label_map": { "outside_valid_fov": 0, "ambiguous_or_unresolvable": 255, "metric_category_ids": [category_id for category_id, _name, _kind in categories], }, "policy": { "coverage": "label every resolvable pixel inside the fixed valid-FOV mask", "instances": ( "annotate every identifiable thing instance; preserve occluded and truncated " "flags; use ambiguous=255 only when class or boundary cannot be resolved" ), "lens_exterior": "must remain label 0 and is excluded from all accuracy metrics", "prelabels": ( "model-assisted prelabels are permitted only as drafts; every accepted object " "and semantic region requires human review" ), "review": "two-pass review: annotator complete, then reviewer accepted", }, "required_metrics": { "semantic": ["per_class_iou", "macro_miou_present", "frequency_weighted_iou"], "instance": ["ap_50_95", "ap50", "ap75", "ar100", "per_class_recall"], "safety_proxies": [ "person_vehicle_miss_rate", "false_large_instance_rate", "valid_fov_boundary_leakage", ], "temporal": ["class_flicker_rate", "instance_id_switches", "mask_iou_jitter"], }, "acceptance": { "unreviewed_frames": 0, "pixels_outside_valid_fov_nonzero": 0, "invalid_category_pixels": 0, "duplicate_instance_ids_per_frame": 0, "metric_reporting": "mean, p50, p95 and worst-frame identities where applicable", }, } def _validate_producer_files(files: tuple[tuple[str, str], ...]) -> list[dict[str, str]]: if not 1 <= len(files) <= 8: raise RecordedEvaluationPackError("evaluation producer file set is invalid") normalized: list[dict[str, str]] = [] seen: set[str] = set() for name, digest in files: if ( not isinstance(name, str) or _SAFE_GROUP.fullmatch(name) is None or name in seen or not isinstance(digest, str) or _SHA256.fullmatch(digest) is None ): raise RecordedEvaluationPackError("evaluation producer file identity is invalid") normalized.append({"name": name, "sha256": digest}) seen.add(name) return normalized def _annotation_template(generation_id: str, frames: Any) -> dict[str, Any]: return { "schema_version": ANNOTATION_TEMPLATE_SCHEMA, "evaluation_pack_id": generation_id, "state": "unannotated", "images": frames, "semantic_masks": [], "instances": [], "reviews": [ { "image_id": row["image_id"], "annotation_status": "unannotated", "review_status": "unreviewed", "notes": "", } for row in frames ], } def _read_timeline(path: Path, count: int, start: float, end: float) -> list[float]: root = path.expanduser().resolve(strict=True).parent resolved = path.expanduser().resolve(strict=True) _confined_regular_file(resolved, root) values: list[float] = [] try: with resolved.open("r", encoding="utf-8") as stream: for expected, line in enumerate(stream): if len(line.encode("utf-8")) > MAX_TIMELINE_LINE_BYTES: raise RecordedEvaluationPackError("decoded timeline row is too large") row = json.loads(line) value = row.get("session_seconds") if isinstance(row, dict) else None if row.get("frame_index") != expected or not isinstance(value, (int, float)): raise RecordedEvaluationPackError("decoded timeline order changed") timestamp = float(value) if not math.isfinite(timestamp) or not start <= timestamp <= end: raise RecordedEvaluationPackError("decoded timeline escapes the job") if values and timestamp <= values[-1]: raise RecordedEvaluationPackError("decoded timeline is not monotonic") values.append(timestamp) except (OSError, UnicodeDecodeError, json.JSONDecodeError, AttributeError) as exc: raise RecordedEvaluationPackError("decoded timeline is unavailable") from exc if len(values) != count: raise RecordedEvaluationPackError("decoded timeline frame count changed") return values def _read_job_index(job_root: Path, source_id: str, codec_epoch: int) -> list[dict[str, Any]]: path = job_root / "input" / "camera" / source_id / f"epoch-{codec_epoch}" / "index.jsonl" rows: list[dict[str, Any]] = [] try: with path.open("r", encoding="utf-8") as stream: for line in stream: row = json.loads(line) if not isinstance(row, dict): raise RecordedEvaluationPackError("camera index row is invalid") rows.append(row) except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc: raise RecordedEvaluationPackError("camera index is unavailable") from exc if not rows: raise RecordedEvaluationPackError("camera index is empty") return rows def _nonnegative_integer(value: Any, label: str) -> int: if not isinstance(value, int) or isinstance(value, bool) or value < 0: raise RecordedEvaluationPackError(f"{label} is invalid") return value def _pixel_sha256(image: Image.Image) -> str: digest = hashlib.sha256() digest.update(image.width.to_bytes(4, "big")) digest.update(image.height.to_bytes(4, "big")) digest.update(image.mode.encode("ascii")) digest.update(b"\x00") digest.update(image.tobytes()) return digest.hexdigest() def _artifact(path: Path, root: Path) -> dict[str, Any]: return { "path": path.relative_to(root).as_posix(), "byte_length": path.stat().st_size, "sha256": _sha256_file(path), } def _prepare_private_directory(path: Path) -> Path: candidate = path.expanduser() candidate.mkdir(mode=0o700, parents=True, exist_ok=True) metadata = candidate.lstat() if stat.S_ISLNK(metadata.st_mode) or not stat.S_ISDIR(metadata.st_mode): raise RecordedEvaluationPackError("evaluation output root must be a real directory") root = candidate.resolve(strict=True) os.chmod(root, 0o700) return root def _read_json_object(path: Path, root: Path, maximum_bytes: int) -> dict[str, Any]: metadata = _confined_regular_file(path, root) if not 0 < metadata.st_size <= maximum_bytes: raise RecordedEvaluationPackError("evaluation JSON is outside bounds") try: value = json.loads(path.read_text(encoding="utf-8")) except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc: raise RecordedEvaluationPackError("evaluation JSON is unavailable") from exc if not isinstance(value, dict): raise RecordedEvaluationPackError("evaluation JSON is not an object") return value def _confined_regular_file(path: Path, root: Path) -> os.stat_result: try: resolved_root = root.resolve(strict=True) resolved = path.resolve(strict=True) metadata = path.lstat() except OSError as exc: raise RecordedEvaluationPackError("evaluation artifact is unavailable") from exc if ( stat.S_ISLNK(metadata.st_mode) or not stat.S_ISREG(metadata.st_mode) or not resolved.is_relative_to(resolved_root) ): raise RecordedEvaluationPackError("evaluation artifact is not confined") return metadata def _sha256_file(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as stream: while chunk := stream.read(1024 * 1024): digest.update(chunk) return digest.hexdigest() def _canonical_json(value: object) -> bytes: try: return json.dumps( value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False, ).encode("utf-8") except (TypeError, ValueError) as exc: raise SessionIntegrityError("evaluation identity is not canonical JSON") from exc def _fsync_file(path: Path) -> None: with path.open("rb") as stream: os.fsync(stream.fileno()) def _fsync_tree(root: Path) -> None: for path in sorted(root.rglob("*"), reverse=True): if path.is_file(): _fsync_file(path) elif path.is_dir(): _fsync_directory(path) _fsync_directory(root) def _fsync_directory(path: Path) -> None: flags = os.O_RDONLY if hasattr(os, "O_DIRECTORY"): flags |= os.O_DIRECTORY descriptor = os.open(path, flags) try: os.fsync(descriptor) finally: os.close(descriptor)