feat(lab): publish fail-closed TGS evidence
This commit is contained in:
@@ -0,0 +1,440 @@
|
||||
"""Seal and verify bounded gravity-aligned TRAVEL TGS evidence."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Final
|
||||
|
||||
M49_TGS_RESULT_SCHEMA: Final = "missioncore.m49-tgs-fail-closed-result/v1"
|
||||
M49_TGS_REPORT_SCHEMA: Final = "missioncore.m49-tgs-fail-closed-report/v1"
|
||||
M49_TGS_WORKER_RESULT_SCHEMA: Final = "missioncore.m49-tgs-fail-closed-evidence-result/v1"
|
||||
M49_TGS_PREFIX: Final = "m49-tgs-fail-closed-"
|
||||
M49_TGS_PROFILE_ID: Final = "m49-ravnoves00-tgs-fail-closed-evidence/v1"
|
||||
M49_TGS_ANCHORS: Final = (171, 306, 368, 402, 450, 509, 525, 744, 1122, 1856)
|
||||
M49_TGS_PROFILES: Final = ("current_increment", "causal_rolling_1s")
|
||||
_MAX_JSON_BYTES: Final = 1024 * 1024
|
||||
_HASH_CHUNK_BYTES: Final = 1024 * 1024
|
||||
|
||||
|
||||
class M49TgsFailClosedError(RuntimeError):
|
||||
"""The TGS evidence pack is unavailable or failed its immutable contract."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class M49TgsFailClosedResult:
|
||||
result_id: str
|
||||
root: Path
|
||||
manifest: dict[str, Any]
|
||||
report: dict[str, Any]
|
||||
|
||||
@property
|
||||
def evidence_path(self) -> Path:
|
||||
return self.root / "evidence.npz"
|
||||
|
||||
|
||||
def seal_m49_tgs_fail_closed(
|
||||
*,
|
||||
source_root: Path,
|
||||
destination_root: Path,
|
||||
profile_path: Path,
|
||||
linked_visual_result_id: str,
|
||||
created_at_utc: str | None = None,
|
||||
) -> M49TgsFailClosedResult:
|
||||
source = _real_directory(source_root, "M49 Worker evidence")
|
||||
destination = destination_root.expanduser().absolute()
|
||||
destination.mkdir(parents=True, exist_ok=True)
|
||||
if destination.is_symlink():
|
||||
raise M49TgsFailClosedError("M49 destination must not be a symlink")
|
||||
profile = _json_file(profile_path, "M49 profile")
|
||||
worker_result = _json_file(source / "result.json", "M49 Worker result")
|
||||
worker_summary = _json_file(source / "worker-summary.json", "M49 Worker summary")
|
||||
input_manifest = _json_file(source / "input-manifest.json", "M49 input manifest")
|
||||
timing = _timing_metrics(source / "tgs-timing.tsv")
|
||||
_validate_source(profile, worker_result, worker_summary, input_manifest, source)
|
||||
if (
|
||||
not linked_visual_result_id.startswith("m4-threat-replay-")
|
||||
or len(linked_visual_result_id) != len("m4-threat-replay-") + 64
|
||||
):
|
||||
raise M49TgsFailClosedError("M49 linked visual result is invalid")
|
||||
|
||||
evidence_sha = _file_sha256(source / "evidence.npz")
|
||||
identity = {
|
||||
"schema_version": M49_TGS_RESULT_SCHEMA,
|
||||
"source": {
|
||||
"source_id": "RAVNOVES00",
|
||||
"source_session_id": "20260720T065719Z_viewer_live",
|
||||
"source_pack_sha256": worker_result["source_pack_sha256"],
|
||||
"input_manifest_sha256": worker_result["input_manifest_sha256"],
|
||||
"linked_visual_result_id": linked_visual_result_id,
|
||||
"anchor_frame_indices": list(M49_TGS_ANCHORS),
|
||||
},
|
||||
"configuration": {
|
||||
"profile_id": M49_TGS_PROFILE_ID,
|
||||
"config_sha256": worker_result["config_sha256"],
|
||||
"coordinate_frame": "map-gravity-local",
|
||||
"primary_profile": "causal_rolling_1s",
|
||||
"cell_size_m": worker_result["costmap"]["cell_size_m"],
|
||||
"radius_m": worker_result["costmap"]["radius_m"],
|
||||
},
|
||||
"method": {
|
||||
"execution_class": "deterministic",
|
||||
"pipeline_id": "travel-tgs-gravity-aligned-fail-closed/v1",
|
||||
"travel_revision": profile["source"]["travel_revision"],
|
||||
"aos_used": False,
|
||||
"missing_support_means_free": False,
|
||||
"eligible_point_accounting": "exact-multiset-complement",
|
||||
},
|
||||
"evidence": {
|
||||
"sha256": evidence_sha,
|
||||
"byte_length": (source / "evidence.npz").stat().st_size,
|
||||
"state_codes": profile["state_codes"],
|
||||
},
|
||||
"authority": {
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
"visual_quality_accepted": False,
|
||||
},
|
||||
}
|
||||
identity_sha256 = _canonical_sha256(identity)
|
||||
result_id = f"{M49_TGS_PREFIX}{identity_sha256}"
|
||||
target = destination / result_id
|
||||
if target.exists():
|
||||
return read_m49_tgs_fail_closed(target)
|
||||
|
||||
created = created_at_utc or datetime.now(tz=UTC).isoformat().replace("+00:00", "Z")
|
||||
anchors = worker_result["anchors"]
|
||||
primary = [row for row in anchors if row["profile_id"] == "causal_rolling_1s"]
|
||||
report = {
|
||||
"schema_version": M49_TGS_REPORT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"created_at_utc": created,
|
||||
"source": identity["source"],
|
||||
"configuration": {
|
||||
**identity["configuration"],
|
||||
"state_priority": profile["costmap"]["state_priority"],
|
||||
"tgs": profile["tgs"],
|
||||
},
|
||||
"method": {
|
||||
**identity["method"],
|
||||
"components": [
|
||||
{
|
||||
"kind": "algorithm",
|
||||
"name": "TRAVEL GroundSeg",
|
||||
"version": profile["source"]["travel_revision"],
|
||||
"role": "gravity-aligned ground/nonground separation",
|
||||
},
|
||||
{
|
||||
"kind": "algorithm",
|
||||
"name": "fail-closed complement adapter",
|
||||
"version": "v1",
|
||||
"role": "retain rejected points and explicit unobserved cells",
|
||||
},
|
||||
{
|
||||
"kind": "runtime",
|
||||
"name": "Worker 006 CPU qualification",
|
||||
"version": worker_summary["code_revision"],
|
||||
"role": "20 bounded TGS invocations without GPU",
|
||||
},
|
||||
],
|
||||
},
|
||||
"execution": {
|
||||
"worker": "Worker 006",
|
||||
"device": "cpu",
|
||||
"gpu_used": False,
|
||||
"wrapper_elapsed_seconds": worker_summary["wall_seconds"],
|
||||
"canonical_triton_id": worker_summary["canonical_triton_id"],
|
||||
"canonical_triton_health": worker_summary["canonical_triton_health"],
|
||||
"free_memory_gib_before": worker_summary["free_memory_gib_before"],
|
||||
},
|
||||
"metrics": {
|
||||
"anchor_count": len(M49_TGS_ANCHORS),
|
||||
"anchor_profile_count": len(anchors),
|
||||
"all_eligible_points_accounted": True,
|
||||
"primary_profile": "causal_rolling_1s",
|
||||
"primary": primary,
|
||||
"costmap_cell_count": worker_result["costmap"]["cell_count"],
|
||||
"process_wall_current_p50_ms": timing["current_increment"]["p50_ms"],
|
||||
"process_wall_current_max_ms": timing["current_increment"]["max_ms"],
|
||||
"process_wall_rolling_p50_ms": timing["causal_rolling_1s"]["p50_ms"],
|
||||
"process_wall_rolling_max_ms": timing["causal_rolling_1s"]["max_ms"],
|
||||
"process_max_rss_kib": timing["max_rss_kib"],
|
||||
},
|
||||
"acceptance": {
|
||||
"representation_complete": True,
|
||||
"all_points_accounted": True,
|
||||
"aos_absent": True,
|
||||
"gpu_absent": True,
|
||||
"visual_quality_accepted": False,
|
||||
"traversability_accepted": False,
|
||||
},
|
||||
"decision": {
|
||||
"state": "visual-review-required",
|
||||
"candidate_retained": True,
|
||||
"next_action": (
|
||||
"Review exact gravity-aligned points and fail-closed costmap "
|
||||
"on the ten immutable anchors."
|
||||
),
|
||||
},
|
||||
"limitations": [
|
||||
"The ten anchors are bounded diagnostic evidence, not a full 4,489-frame replay.",
|
||||
"No independent terrain or traversability truth is available.",
|
||||
"No vehicle envelope exists, so occupied cells do not grant or deny physical passage.",
|
||||
"Visual quality, realtime integration, navigation and actuation remain unaccepted.",
|
||||
],
|
||||
"authority": {
|
||||
"mode": "replay-simulated",
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
"visual_quality_accepted": False,
|
||||
},
|
||||
"visual_review": {
|
||||
"instrument": "m4-canonical-reference-graph",
|
||||
"linked_visual_result_id": linked_visual_result_id,
|
||||
"anchors": list(M49_TGS_ANCHORS),
|
||||
"profiles": list(M49_TGS_PROFILES),
|
||||
"default_profile": "causal_rolling_1s",
|
||||
"point_states": profile["state_codes"],
|
||||
},
|
||||
}
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="mission-core-m49-tgs-", dir=destination) as raw:
|
||||
staging = Path(raw) / result_id
|
||||
staging.mkdir()
|
||||
for name in (
|
||||
"evidence.npz",
|
||||
"worker-summary.json",
|
||||
"input-manifest.json",
|
||||
"tgs-timing.tsv",
|
||||
):
|
||||
shutil.copyfile(source / name, staging / name)
|
||||
_write_json(staging / "report.json", report)
|
||||
artifacts = [
|
||||
_artifact(staging / "report.json", "report", M49_TGS_REPORT_SCHEMA, "application/json"),
|
||||
_artifact(
|
||||
staging / "evidence.npz", "visual-spatial-evidence", None, "application/x-npz"
|
||||
),
|
||||
_artifact(staging / "worker-summary.json", "runtime-summary", None, "application/json"),
|
||||
_artifact(staging / "input-manifest.json", "source-manifest", None, "application/json"),
|
||||
_artifact(
|
||||
staging / "tgs-timing.tsv", "runtime-timing", None, "text/tab-separated-values"
|
||||
),
|
||||
]
|
||||
manifest = {
|
||||
"schema_version": M49_TGS_RESULT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"created_at_utc": created,
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"artifacts": artifacts,
|
||||
"authority": report["authority"],
|
||||
"ground_truth": False,
|
||||
}
|
||||
_write_json(staging / "manifest.json", manifest)
|
||||
os.replace(staging, target)
|
||||
return read_m49_tgs_fail_closed(target)
|
||||
|
||||
|
||||
def read_m49_tgs_fail_closed(root: Path) -> M49TgsFailClosedResult:
|
||||
candidate = _real_directory(root, "M49 result")
|
||||
if not candidate.name.startswith(M49_TGS_PREFIX):
|
||||
raise M49TgsFailClosedError("M49 result identity is invalid")
|
||||
manifest = _json_file(candidate / "manifest.json", "M49 manifest")
|
||||
report = _json_file(candidate / "report.json", "M49 report")
|
||||
if (
|
||||
manifest.get("schema_version") != M49_TGS_RESULT_SCHEMA
|
||||
or manifest.get("result_id") != candidate.name
|
||||
or report.get("schema_version") != M49_TGS_REPORT_SCHEMA
|
||||
or report.get("result_id") != candidate.name
|
||||
):
|
||||
raise M49TgsFailClosedError("M49 result contract changed")
|
||||
identity = manifest.get("identity")
|
||||
identity_sha = manifest.get("identity_sha256")
|
||||
if (
|
||||
not isinstance(identity, dict)
|
||||
or not isinstance(identity_sha, str)
|
||||
or _canonical_sha256(identity) != identity_sha
|
||||
or candidate.name != f"{M49_TGS_PREFIX}{identity_sha}"
|
||||
):
|
||||
raise M49TgsFailClosedError("M49 identity proof changed")
|
||||
artifacts = manifest.get("artifacts")
|
||||
if not isinstance(artifacts, list) or len(artifacts) != 5:
|
||||
raise M49TgsFailClosedError("M49 artifact manifest changed")
|
||||
for item in artifacts:
|
||||
if not isinstance(item, dict):
|
||||
raise M49TgsFailClosedError("M49 artifact descriptor changed")
|
||||
path = candidate / str(item.get("path", ""))
|
||||
if (
|
||||
path.is_symlink()
|
||||
or not path.is_file()
|
||||
or path.parent != candidate
|
||||
or path.stat().st_size != item.get("byte_length")
|
||||
or _file_sha256(path) != item.get("sha256")
|
||||
):
|
||||
raise M49TgsFailClosedError("M49 artifact proof changed")
|
||||
return M49TgsFailClosedResult(candidate.name, candidate, manifest, report)
|
||||
|
||||
|
||||
def _validate_source(
|
||||
profile: dict[str, Any],
|
||||
worker_result: dict[str, Any],
|
||||
worker_summary: dict[str, Any],
|
||||
input_manifest: dict[str, Any],
|
||||
source: Path,
|
||||
) -> None:
|
||||
if (
|
||||
profile.get("schema_version") != "missioncore.m49-tgs-fail-closed-evidence-profile/v1"
|
||||
or profile.get("profile_id") != M49_TGS_PROFILE_ID
|
||||
or tuple(profile.get("anchors", ())) != M49_TGS_ANCHORS
|
||||
or profile.get("invariants", {}).get("aos_allowed") is not False
|
||||
or profile.get("invariants", {}).get("missing_support_means_free") is not False
|
||||
):
|
||||
raise M49TgsFailClosedError("M49 profile changed")
|
||||
if (
|
||||
worker_result.get("schema_version") != M49_TGS_WORKER_RESULT_SCHEMA
|
||||
or worker_result.get("status") != "passed"
|
||||
or worker_result.get("summary", {}).get("aos_used") is not False
|
||||
or worker_result.get("summary", {}).get("all_eligible_points_accounted") is not True
|
||||
or worker_result.get("summary", {}).get("primary_profile") != "causal_rolling_1s"
|
||||
or len(worker_result.get("anchors", ())) != 20
|
||||
):
|
||||
raise M49TgsFailClosedError("M49 Worker result changed")
|
||||
if (
|
||||
input_manifest.get("schema_version") != "missioncore.m49-tgs-fail-closed-input/v1"
|
||||
or input_manifest.get("coordinate_frame") != "map-gravity-local"
|
||||
or len(input_manifest.get("records", ())) != 20
|
||||
):
|
||||
raise M49TgsFailClosedError("M49 input manifest changed")
|
||||
if (
|
||||
worker_summary.get("gpu_requested") is not False
|
||||
and worker_summary.get("gpu_requested") is not None
|
||||
):
|
||||
raise M49TgsFailClosedError("M49 Worker GPU contract changed")
|
||||
if (
|
||||
worker_summary.get("aos_used") is not False
|
||||
or worker_summary.get("all_eligible_points_accounted") is not True
|
||||
or worker_summary.get("canonical_triton_health") != "healthy"
|
||||
):
|
||||
raise M49TgsFailClosedError("M49 Worker cleanup changed")
|
||||
evidence = worker_result.get("evidence", {})
|
||||
if (
|
||||
evidence.get("path") != "evidence.npz"
|
||||
or evidence.get("bytes") != (source / "evidence.npz").stat().st_size
|
||||
or evidence.get("sha256") != _file_sha256(source / "evidence.npz")
|
||||
or worker_result.get("input_manifest_sha256")
|
||||
!= _file_sha256(source / "input-manifest.json")
|
||||
):
|
||||
raise M49TgsFailClosedError("M49 evidence proof changed")
|
||||
|
||||
|
||||
def _artifact(
|
||||
path: Path,
|
||||
role: str,
|
||||
schema_version: str | None,
|
||||
media_type: str,
|
||||
) -> dict[str, object]:
|
||||
result: dict[str, object] = {
|
||||
"role": role,
|
||||
"path": path.name,
|
||||
"byte_length": path.stat().st_size,
|
||||
"sha256": _file_sha256(path),
|
||||
"media_type": media_type,
|
||||
}
|
||||
if schema_version is not None:
|
||||
result["schema_version"] = schema_version
|
||||
return result
|
||||
|
||||
|
||||
def _timing_metrics(path: Path) -> dict[str, Any]:
|
||||
if path.is_symlink() or not path.is_file():
|
||||
raise M49TgsFailClosedError("M49 timing evidence is unavailable")
|
||||
profiles: dict[str, list[float]] = {name: [] for name in M49_TGS_PROFILES}
|
||||
maximum_rss = 0
|
||||
lines = path.read_text(encoding="utf-8-sig").splitlines()
|
||||
if not lines or lines[0] != "profile\tslot\twall_seconds\tmax_rss_kib":
|
||||
raise M49TgsFailClosedError("M49 timing evidence changed")
|
||||
for line in lines[1:]:
|
||||
fields = line.split("\t")
|
||||
if len(fields) != 4 or fields[0] not in profiles:
|
||||
raise M49TgsFailClosedError("M49 timing row changed")
|
||||
profiles[fields[0]].append(float(fields[2]))
|
||||
maximum_rss = max(maximum_rss, int(fields[3]))
|
||||
if any(len(values) != len(M49_TGS_ANCHORS) for values in profiles.values()):
|
||||
raise M49TgsFailClosedError("M49 timing coverage changed")
|
||||
result: dict[str, Any] = {"max_rss_kib": maximum_rss}
|
||||
for name, values in profiles.items():
|
||||
ordered = sorted(values)
|
||||
result[name] = {
|
||||
"p50_ms": ordered[len(ordered) // 2] * 1000,
|
||||
"max_ms": max(ordered) * 1000,
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
def _real_directory(path: Path, label: str) -> Path:
|
||||
candidate = path.expanduser().absolute()
|
||||
if candidate.is_symlink():
|
||||
raise M49TgsFailClosedError(f"{label} must not be a symlink")
|
||||
try:
|
||||
resolved = candidate.resolve(strict=True)
|
||||
except OSError as exc:
|
||||
raise M49TgsFailClosedError(f"{label} is unavailable") from exc
|
||||
if not resolved.is_dir():
|
||||
raise M49TgsFailClosedError(f"{label} is unavailable")
|
||||
return resolved
|
||||
|
||||
|
||||
def _json_file(path: Path, label: str) -> dict[str, Any]:
|
||||
if path.is_symlink() or not path.is_file() or path.stat().st_size > _MAX_JSON_BYTES:
|
||||
raise M49TgsFailClosedError(f"{label} is unavailable")
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8-sig"))
|
||||
except (json.JSONDecodeError, OSError) as exc:
|
||||
raise M49TgsFailClosedError(f"{label} is invalid") from exc
|
||||
if not isinstance(value, dict):
|
||||
raise M49TgsFailClosedError(f"{label} is invalid")
|
||||
return value
|
||||
|
||||
|
||||
def _write_json(path: Path, value: object) -> None:
|
||||
path.write_bytes(_canonical_json(value) + b"\n")
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def _canonical_sha256(value: object) -> str:
|
||||
return hashlib.sha256(_canonical_json(value)).hexdigest()
|
||||
|
||||
|
||||
def _file_sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for chunk in iter(lambda: stream.read(_HASH_CHUNK_BYTES), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
__all__ = [
|
||||
"M49_TGS_ANCHORS",
|
||||
"M49_TGS_PREFIX",
|
||||
"M49_TGS_REPORT_SCHEMA",
|
||||
"M49_TGS_RESULT_SCHEMA",
|
||||
"M49TgsFailClosedError",
|
||||
"M49TgsFailClosedResult",
|
||||
"read_m49_tgs_fail_closed",
|
||||
"seal_m49_tgs_fail_closed",
|
||||
]
|
||||
Reference in New Issue
Block a user