feat(lab): verify and expose real evidence

This commit is contained in:
DCCONSTRUCTIONS
2026-07-26 15:36:48 +03:00
parent 453d760be4
commit dc55ff16c9
9 changed files with 2007 additions and 47 deletions
+29
View File
@@ -34,6 +34,7 @@ from k1link.sessions import (
)
from k1link.web.device_plugin_composition import load_installed_device_plugins
from k1link.web.environment_api import build_environment_router
from k1link.web.laboratory_api import build_laboratory_router
from k1link.web.lidar_api import build_lidar_router
from k1link.web.plugin_catalog import DevicePluginCatalog, PluginCatalogError
from k1link.web.plugin_runtime import (
@@ -464,6 +465,34 @@ app.include_router(
),
)
)
app.include_router(
build_laboratory_router(
e29_root_provider=lambda: (
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e29" / "results"
),
local_surface_root_provider=lambda: (
REPOSITORY_ROOT
/ ".runtime"
/ "compute-experiments"
/ "k1-local-surface-v1"
/ "models"
),
source_pack_root_provider=lambda: (
REPOSITORY_ROOT
/ ".runtime"
/ "compute-experiments"
/ "e10"
/ "lidar-packs"
),
source_result_root_provider=lambda: (
REPOSITORY_ROOT
/ ".runtime"
/ "compute-experiments"
/ "e10"
/ "worker-results"
),
)
)
frontend_dist = REPOSITORY_ROOT / "apps" / "control-station" / "dist"
+497
View File
@@ -0,0 +1,497 @@
from __future__ import annotations
import copy
import hashlib
import json
import re
from collections.abc import Callable, Mapping
from functools import lru_cache
from pathlib import Path
from typing import Any, Final
from fastapi import APIRouter, HTTPException, Query
from k1link.compute.semantic_geometry_fusion import (
CAMERA_GEOMETRY_FRAME_SCHEMA,
CAMERA_GEOMETRY_FRAMES_NAME,
CAMERA_GEOMETRY_FUSION_SCHEMA,
CAMERA_GEOMETRY_MANIFEST_NAME,
CAMERA_GEOMETRY_REPORT_NAME,
CAMERA_GEOMETRY_REPORT_SCHEMA,
)
LABORATORY_E29_CATALOG_SCHEMA: Final = (
"missioncore.laboratory-e29-catalog/v1"
)
LABORATORY_E29_DETAIL_SCHEMA: Final = "missioncore.laboratory-e29-detail/v1"
LABORATORY_E29_FRAME_SCHEMA: Final = "missioncore.laboratory-e29-frame/v1"
_SHA256 = re.compile(r"^[a-f0-9]{64}$")
_E29_RESULT_ID = re.compile(r"^e29-camera-geometry-[a-f0-9]{64}$")
_LOCAL_SURFACE_ID = re.compile(r"^k1-local-surface-[a-f0-9]{64}$")
_SOURCE_PACK_ID = re.compile(r"^e10-lidar-pack-[a-f0-9]{64}$")
_SOURCE_RESULT_ID = re.compile(r"^e10-integrated-perception-[a-f0-9]{64}$")
_SESSION_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,159}$")
_MAX_MANIFEST_BYTES: Final = 256 * 1024
_MAX_REPORT_BYTES: Final = 2 * 1024 * 1024
_MAX_FRAME_BYTES: Final = 2 * 1024 * 1024
_MAX_REVIEW_FRAMES: Final = 64
RootProvider = Callable[[], Path | None]
class LaboratoryEvidenceError(ValueError):
"""Raised when published laboratory evidence is incomplete or changed."""
def _canonical_json(value: object) -> bytes:
return json.dumps(
value,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
allow_nan=False,
).encode()
def _read_json(path: Path, *, maximum_bytes: int) -> dict[str, Any]:
if path.is_symlink() or not path.is_file():
raise LaboratoryEvidenceError("laboratory JSON artifact is unavailable")
size = path.stat().st_size
if size <= 0 or size > maximum_bytes:
raise LaboratoryEvidenceError("laboratory JSON artifact is out of bounds")
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
raise LaboratoryEvidenceError("laboratory JSON artifact is invalid") from exc
if not isinstance(value, dict):
raise LaboratoryEvidenceError("laboratory JSON artifact must be an object")
return value
def _sha256(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 _safe_artifact_path(root: Path, value: object) -> Path:
if not isinstance(value, str) or not value or Path(value).name != value:
raise LaboratoryEvidenceError("laboratory artifact path is invalid")
path = root / value
if path.is_symlink() or not path.is_file():
raise LaboratoryEvidenceError("laboratory artifact is unavailable")
return path
def _artifact_map(
root: Path,
manifest: Mapping[str, Any],
) -> dict[str, tuple[Path, Mapping[str, Any]]]:
raw = manifest.get("artifacts")
if not isinstance(raw, list) or len(raw) != 2:
raise LaboratoryEvidenceError("laboratory artifact list is invalid")
artifacts: dict[str, tuple[Path, Mapping[str, Any]]] = {}
for item in raw:
if not isinstance(item, dict):
raise LaboratoryEvidenceError("laboratory artifact entry is invalid")
role = item.get("role")
byte_length = item.get("byte_length")
digest = item.get("sha256")
if (
not isinstance(role, str)
or role in artifacts
or not isinstance(byte_length, int)
or byte_length <= 0
or not isinstance(digest, str)
or _SHA256.fullmatch(digest) is None
):
raise LaboratoryEvidenceError("laboratory artifact metadata is invalid")
path = _safe_artifact_path(root, item.get("path"))
if path.stat().st_size != byte_length:
raise LaboratoryEvidenceError("laboratory artifact size changed")
artifacts[role] = (path, item)
if set(artifacts) != {"camera-geometry-frames", "camera-geometry-report"}:
raise LaboratoryEvidenceError("laboratory artifact roles are invalid")
return artifacts
def _result_signature(root: Path) -> tuple[int, ...]:
paths = (
root / CAMERA_GEOMETRY_MANIFEST_NAME,
root / CAMERA_GEOMETRY_REPORT_NAME,
root / CAMERA_GEOMETRY_FRAMES_NAME,
)
signature: list[int] = []
for path in paths:
stat = path.stat()
signature.extend((stat.st_size, stat.st_mtime_ns))
return tuple(signature)
def _review_frame(row: Mapping[str, Any]) -> dict[str, object] | None:
observations = row.get("semantic_observations")
geometry = row.get("geometry_only_occupied")
if not isinstance(observations, list) or not isinstance(geometry, list):
raise LaboratoryEvidenceError("E29 frame collections are invalid")
conflicts = sum(
1
for observation in observations
if isinstance(observation, dict)
and observation.get("geometry_status") == "conflict"
)
if conflicts == 0:
return None
frame_index = row.get("frame_index")
session_seconds = row.get("session_seconds")
if (
not isinstance(frame_index, int)
or frame_index < 0
or not isinstance(session_seconds, (int, float))
):
raise LaboratoryEvidenceError("E29 review frame identity is invalid")
return {
"frame_index": frame_index,
"source_frame_index": row.get("source_frame_index"),
"session_seconds": float(session_seconds),
"conflict_count": conflicts,
"semantic_observation_count": len(observations),
"geometry_only_cluster_count": len(geometry),
}
@lru_cache(maxsize=16)
def _read_result_cached(
root_text: str,
signature: tuple[int, ...],
) -> tuple[dict[str, Any], tuple[int, ...]]:
del signature
root = Path(root_text)
if root.is_symlink() or not root.is_dir() or _E29_RESULT_ID.fullmatch(root.name) is None:
raise LaboratoryEvidenceError("E29 result id is invalid")
manifest = _read_json(
root / CAMERA_GEOMETRY_MANIFEST_NAME,
maximum_bytes=_MAX_MANIFEST_BYTES,
)
artifacts = _artifact_map(root, manifest)
report_path, report_artifact = artifacts["camera-geometry-report"]
frames_path, frames_artifact = artifacts["camera-geometry-frames"]
report = _read_json(report_path, maximum_bytes=_MAX_REPORT_BYTES)
identity = manifest.get("identity")
identity_sha256 = manifest.get("identity_sha256")
if (
manifest.get("schema_version") != CAMERA_GEOMETRY_FUSION_SCHEMA
or manifest.get("result_id") != root.name
or not isinstance(identity, dict)
or identity.get("schema_version") != CAMERA_GEOMETRY_FUSION_SCHEMA
or not isinstance(identity_sha256, str)
or _SHA256.fullmatch(identity_sha256) is None
or hashlib.sha256(_canonical_json(identity)).hexdigest() != identity_sha256
or root.name != f"e29-camera-geometry-{identity_sha256}"
or report.get("schema_version") != CAMERA_GEOMETRY_REPORT_SCHEMA
or report.get("result_id") != root.name
or report.get("identity") != identity
or report.get("status") != "diagnostic-replay-complete"
or report.get("ground_truth") is not False
):
raise LaboratoryEvidenceError("E29 result identity is invalid")
if _sha256(report_path) != report_artifact.get("sha256"):
raise LaboratoryEvidenceError("E29 report digest changed")
expected_frames = identity.get("frame_count")
if not isinstance(expected_frames, int) or expected_frames <= 0:
raise LaboratoryEvidenceError("E29 frame count is invalid")
offsets: list[int] = []
review_frames: list[dict[str, object]] = []
digest = hashlib.sha256()
with frames_path.open("rb") as stream:
frame_index = 0
while True:
offset = stream.tell()
raw = stream.readline(_MAX_FRAME_BYTES + 1)
if not raw:
break
if len(raw) > _MAX_FRAME_BYTES:
raise LaboratoryEvidenceError("E29 frame is out of bounds")
digest.update(raw)
try:
row = json.loads(raw)
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise LaboratoryEvidenceError("E29 frame is invalid") from exc
if (
not isinstance(row, dict)
or row.get("schema_version") != CAMERA_GEOMETRY_FRAME_SCHEMA
or row.get("frame_index") != frame_index
):
raise LaboratoryEvidenceError("E29 frame sequence changed")
offsets.append(offset)
if len(review_frames) < _MAX_REVIEW_FRAMES:
review = _review_frame(row)
if review is not None:
review_frames.append(review)
frame_index += 1
if (
len(offsets) != expected_frames
or digest.hexdigest() != frames_artifact.get("sha256")
):
raise LaboratoryEvidenceError("E29 frame evidence changed")
item = {
"result_id": root.name,
"created_at_utc": manifest.get("created_at_utc"),
"status": report["status"],
"identity": identity,
"metrics": report.get("metrics"),
"decision": report.get("decision"),
"limitations": report.get("limitations"),
"authority": report.get("authority"),
"review_frames": review_frames,
"ground_truth": False,
"access": "read-only",
}
return item, tuple(offsets)
def _linked_evidence(
item: Mapping[str, Any],
*,
local_surface_root: Path,
source_pack_root: Path,
source_result_root: Path,
) -> dict[str, object]:
identity = item.get("identity")
if not isinstance(identity, dict):
raise LaboratoryEvidenceError("E29 identity is unavailable")
model_id = identity.get("local_surface_model_id")
pack_id = identity.get("source_pack_id")
source_result_id = identity.get("source_result_id")
if (
not isinstance(model_id, str)
or _LOCAL_SURFACE_ID.fullmatch(model_id) is None
or not isinstance(pack_id, str)
or _SOURCE_PACK_ID.fullmatch(pack_id) is None
or not isinstance(source_result_id, str)
or _SOURCE_RESULT_ID.fullmatch(source_result_id) is None
):
raise LaboratoryEvidenceError("E29 linked evidence ids are invalid")
model_root = local_surface_root / model_id
pack_root = source_pack_root / pack_id
result_root = source_result_root / source_result_id
required = (
model_root / "manifest.json",
model_root / "local-surface.json",
model_root / "local-surface.npz",
pack_root / "manifest.json",
pack_root / "lidar-pack.npz",
result_root / "result.json",
result_root / "fusion-frames.jsonl",
)
if any(path.is_symlink() or not path.is_file() for path in required):
raise LaboratoryEvidenceError("E29 linked evidence is incomplete")
local_surface_report = _read_json(
model_root / "local-surface.json",
maximum_bytes=_MAX_REPORT_BYTES,
)
session_id = local_surface_report.get("session_id")
if (
local_surface_report.get("model_id") != model_id
or not isinstance(session_id, str)
or _SESSION_ID.fullmatch(session_id) is None
):
raise LaboratoryEvidenceError("E29 source session is invalid")
return {
"source_session_id": session_id,
"local_surface_model_id": model_id,
"source_pack_id": pack_id,
"source_result_id": source_result_id,
"published_on_control_plane": True,
"worker_copy_required": False,
}
def _roots(
*,
e29_root_provider: RootProvider,
local_surface_root_provider: RootProvider,
source_pack_root_provider: RootProvider,
source_result_root_provider: RootProvider,
) -> tuple[Path, Path, Path, Path] | None:
values = (
e29_root_provider(),
local_surface_root_provider(),
source_pack_root_provider(),
source_result_root_provider(),
)
if any(value is None for value in values):
return None
roots = tuple(value.resolve() for value in values if value is not None)
if len(roots) != 4 or any(not root.is_dir() for root in roots):
return None
return roots[0], roots[1], roots[2], roots[3]
def build_laboratory_router(
*,
e29_root_provider: RootProvider = lambda: None,
local_surface_root_provider: RootProvider = lambda: None,
source_pack_root_provider: RootProvider = lambda: None,
source_result_root_provider: RootProvider = lambda: None,
) -> APIRouter:
router = APIRouter(prefix="/api/v1/laboratory", tags=["laboratory"])
@router.get("/e29/results")
def list_e29_results(
limit: int = Query(default=1, ge=1, le=10),
) -> dict[str, object]:
roots = _roots(
e29_root_provider=e29_root_provider,
local_surface_root_provider=local_surface_root_provider,
source_pack_root_provider=source_pack_root_provider,
source_result_root_provider=source_result_root_provider,
)
if roots is None:
return {
"schema_version": LABORATORY_E29_CATALOG_SCHEMA,
"configured": False,
"items": [],
"candidate_total": 0,
"invalid_total": 0,
"access": "read-only",
}
e29_root, local_surface_root, source_pack_root, source_result_root = roots
candidates = sorted(
(
candidate
for candidate in e29_root.iterdir()
if candidate.is_dir()
and _E29_RESULT_ID.fullmatch(candidate.name) is not None
),
key=lambda candidate: candidate.stat().st_mtime_ns,
reverse=True,
)
items: list[dict[str, object]] = []
invalid_total = 0
for candidate in candidates:
try:
item, _ = _read_result_cached(
str(candidate.resolve()),
_result_signature(candidate),
)
document = copy.deepcopy(item)
document["linked_evidence"] = _linked_evidence(
document,
local_surface_root=local_surface_root,
source_pack_root=source_pack_root,
source_result_root=source_result_root,
)
if len(items) < limit:
items.append(document)
except (LaboratoryEvidenceError, OSError):
invalid_total += 1
return {
"schema_version": LABORATORY_E29_CATALOG_SCHEMA,
"configured": True,
"items": items,
"candidate_total": len(candidates),
"invalid_total": invalid_total,
"access": "read-only",
}
@router.get("/e29/results/{result_id}")
def get_e29_result(result_id: str) -> dict[str, object]:
roots = _roots(
e29_root_provider=e29_root_provider,
local_surface_root_provider=local_surface_root_provider,
source_pack_root_provider=source_pack_root_provider,
source_result_root_provider=source_result_root_provider,
)
if roots is None or _E29_RESULT_ID.fullmatch(result_id) is None:
raise HTTPException(status_code=404, detail="LAB E29 result не найден")
e29_root, local_surface_root, source_pack_root, source_result_root = roots
candidate = e29_root / result_id
if not candidate.is_dir():
raise HTTPException(status_code=404, detail="LAB E29 result не найден")
try:
item, _ = _read_result_cached(
str(candidate.resolve()),
_result_signature(candidate),
)
document = copy.deepcopy(item)
document["linked_evidence"] = _linked_evidence(
document,
local_surface_root=local_surface_root,
source_pack_root=source_pack_root,
source_result_root=source_result_root,
)
return {
"schema_version": LABORATORY_E29_DETAIL_SCHEMA,
"result": document,
}
except (LaboratoryEvidenceError, OSError) as exc:
raise HTTPException(
status_code=409,
detail="LAB E29 evidence не прошло проверку целостности",
) from exc
@router.get("/e29/results/{result_id}/frames/{frame_index}")
def get_e29_frame(result_id: str, frame_index: int) -> dict[str, object]:
roots = _roots(
e29_root_provider=e29_root_provider,
local_surface_root_provider=local_surface_root_provider,
source_pack_root_provider=source_pack_root_provider,
source_result_root_provider=source_result_root_provider,
)
if (
roots is None
or _E29_RESULT_ID.fullmatch(result_id) is None
or frame_index < 0
):
raise HTTPException(status_code=404, detail="LAB E29 frame не найден")
e29_root, local_surface_root, source_pack_root, source_result_root = roots
candidate = e29_root / result_id
if not candidate.is_dir():
raise HTTPException(status_code=404, detail="LAB E29 frame не найден")
try:
item, offsets = _read_result_cached(
str(candidate.resolve()),
_result_signature(candidate),
)
_linked_evidence(
item,
local_surface_root=local_surface_root,
source_pack_root=source_pack_root,
source_result_root=source_result_root,
)
if frame_index >= len(offsets):
raise HTTPException(status_code=404, detail="LAB E29 frame не найден")
frames_path = candidate / CAMERA_GEOMETRY_FRAMES_NAME
with frames_path.open("rb") as stream:
stream.seek(offsets[frame_index])
raw = stream.readline(_MAX_FRAME_BYTES + 1)
if not raw or len(raw) > _MAX_FRAME_BYTES:
raise LaboratoryEvidenceError("E29 frame is out of bounds")
row = json.loads(raw)
if (
not isinstance(row, dict)
or row.get("schema_version") != CAMERA_GEOMETRY_FRAME_SCHEMA
or row.get("frame_index") != frame_index
):
raise LaboratoryEvidenceError("E29 frame identity changed")
return {
"schema_version": LABORATORY_E29_FRAME_SCHEMA,
"result_id": result_id,
"frame": row,
"access": "read-only",
}
except HTTPException:
raise
except (LaboratoryEvidenceError, OSError, json.JSONDecodeError) as exc:
raise HTTPException(
status_code=409,
detail="LAB E29 frame не прошёл проверку целостности",
) from exc
return router
+5 -1
View File
@@ -528,6 +528,8 @@ def build_lidar_router(
reverse=True,
)
for candidate in candidates:
if len(items) >= limit:
break
try:
model = K1LocalSurfaceV1(candidate)
try:
@@ -539,9 +541,11 @@ def build_lidar_router(
return {
"schema_version": K1_LOCAL_SURFACE_CATALOG_SCHEMA,
"configured": True,
"items": items[:limit],
"items": items,
"valid_total": len(items),
"invalid_total": invalid_total,
"candidate_total": len(candidates),
"scan_complete": len(items) + invalid_total == len(candidates),
"access": "read-only",
}