refactor(platform): freeze laboratory and telemetry boundaries
This commit is contained in:
@@ -8,6 +8,7 @@ No sink is created implicitly and telemetry never grants command authority.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
@@ -30,6 +31,8 @@ SAFE_TOPIC_IDENTIFIER: Final = re.compile(
|
||||
)
|
||||
MAX_TEXT_LENGTH: Final = 256
|
||||
MAX_PAYLOAD_BYTES: Final = 1024 * 1024
|
||||
DEFAULT_JOURNAL_MAX_BYTES: Final = 64 * 1024 * 1024
|
||||
DEFAULT_JOURNAL_MAX_SEGMENTS: Final = 8
|
||||
STAGE_STATES: Final = frozenset({"started", "completed", "failed"})
|
||||
RUN_STATES: Final = STAGE_STATES
|
||||
_AUTHORITY: Final = {
|
||||
@@ -243,10 +246,27 @@ class PipelineTelemetryEmitter:
|
||||
|
||||
|
||||
class JsonlPipelineTelemetrySink:
|
||||
"""Append topic-bound telemetry records for local, auditable execution evidence."""
|
||||
"""Append to a bounded, fail-closed local outbox for auditable execution evidence.
|
||||
|
||||
def __init__(self, path: Path) -> None:
|
||||
Completed segments are content-addressed and never pruned implicitly. When the
|
||||
segment bound is reached, telemetry publication fails observably instead of
|
||||
deleting an event that Telegraf may not have acknowledged yet.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
path: Path,
|
||||
*,
|
||||
max_bytes: int = DEFAULT_JOURNAL_MAX_BYTES,
|
||||
max_segments: int = DEFAULT_JOURNAL_MAX_SEGMENTS,
|
||||
) -> None:
|
||||
if max_bytes < MAX_PAYLOAD_BYTES + 4096:
|
||||
raise PipelineTelemetryError("pipeline journal max_bytes is too small")
|
||||
if not 1 <= max_segments <= 64:
|
||||
raise PipelineTelemetryError("pipeline journal max_segments is invalid")
|
||||
self.path = path.expanduser().absolute()
|
||||
self.max_bytes = max_bytes
|
||||
self.max_segments = max_segments
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def publish(self, topic: str, payload: bytes) -> None:
|
||||
@@ -261,6 +281,10 @@ class JsonlPipelineTelemetrySink:
|
||||
encoded = _canonical_json(record) + b"\n"
|
||||
with self._lock:
|
||||
self.path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
if self.path.is_symlink():
|
||||
raise PipelineTelemetryError("pipeline journal must not be a symlink")
|
||||
if self.path.exists() and self.path.stat().st_size + len(encoded) > self.max_bytes:
|
||||
self._rotate()
|
||||
descriptor = os.open(
|
||||
self.path,
|
||||
os.O_APPEND | os.O_CREAT | os.O_WRONLY,
|
||||
@@ -272,6 +296,30 @@ class JsonlPipelineTelemetrySink:
|
||||
finally:
|
||||
os.close(descriptor)
|
||||
|
||||
def _rotate(self) -> None:
|
||||
if not self.path.is_file() or self.path.stat().st_size == 0:
|
||||
return
|
||||
segments = tuple(self.path.parent.glob(f"{self.path.stem}.*{self.path.suffix}"))
|
||||
if len(segments) >= self.max_segments:
|
||||
raise PipelineTelemetryError(
|
||||
"pipeline journal segment bound reached; acknowledged segments require review"
|
||||
)
|
||||
digest = _file_sha256(self.path)
|
||||
destination = self.path.with_name(
|
||||
f"{self.path.stem}.{digest}{self.path.suffix}"
|
||||
)
|
||||
if destination.exists() or destination.is_symlink():
|
||||
raise PipelineTelemetryError("pipeline journal segment identity already exists")
|
||||
os.replace(self.path, destination)
|
||||
|
||||
|
||||
def _file_sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
class MqttPipelineTelemetrySink:
|
||||
"""Publish through a worker-owned, already-connected Paho-compatible client."""
|
||||
|
||||
@@ -11,6 +11,18 @@ from k1link.laboratory.evidence_report import (
|
||||
LaboratoryEvidenceReportError,
|
||||
LaboratoryEvidenceReportNotFound,
|
||||
LaboratoryEvidenceReportService,
|
||||
verify_laboratory_evidence_result,
|
||||
)
|
||||
from k1link.laboratory.execution import (
|
||||
LABORATORY_EXECUTION_REGISTRY_SCHEMA,
|
||||
LABORATORY_RUN_RECEIPT_SCHEMA,
|
||||
LaboratoryAdapterResult,
|
||||
LaboratoryExecutionDefinition,
|
||||
LaboratoryExecutionError,
|
||||
LaboratoryExecutionRegistry,
|
||||
LaboratoryRunner,
|
||||
LaboratoryRunRequest,
|
||||
LaboratoryRunResult,
|
||||
)
|
||||
from k1link.laboratory.value_review_registry import (
|
||||
LABORATORY_VALUE_REVIEW_INDEX_SCHEMA,
|
||||
@@ -28,7 +40,17 @@ __all__ = [
|
||||
"LaboratoryEvidenceReportError",
|
||||
"LaboratoryEvidenceReportNotFound",
|
||||
"LaboratoryEvidenceReportService",
|
||||
"verify_laboratory_evidence_result",
|
||||
"LaboratoryRegistryError",
|
||||
"LABORATORY_EXECUTION_REGISTRY_SCHEMA",
|
||||
"LABORATORY_RUN_RECEIPT_SCHEMA",
|
||||
"LaboratoryAdapterResult",
|
||||
"LaboratoryExecutionDefinition",
|
||||
"LaboratoryExecutionError",
|
||||
"LaboratoryExecutionRegistry",
|
||||
"LaboratoryRunRequest",
|
||||
"LaboratoryRunResult",
|
||||
"LaboratoryRunner",
|
||||
"LABORATORY_VALUE_REVIEW_INDEX_SCHEMA",
|
||||
"LABORATORY_VALUE_REVIEW_REGISTRY_SCHEMA",
|
||||
"LaboratoryValueReviewEntry",
|
||||
|
||||
@@ -27,6 +27,43 @@ class LaboratoryEvidenceReportNotFound(LaboratoryEvidenceReportError):
|
||||
"""Raised when the requested evidence identity is not available."""
|
||||
|
||||
|
||||
def verify_laboratory_evidence_result(
|
||||
definition: LaboratoryEvidenceDefinition,
|
||||
result_root: Path,
|
||||
) -> dict[str, object]:
|
||||
"""Verify one immutable result without projecting it into a UI report."""
|
||||
|
||||
candidate = result_root.expanduser().absolute()
|
||||
if candidate.is_symlink():
|
||||
raise LaboratoryEvidenceReportError("LAB result must not be a symlink")
|
||||
try:
|
||||
resolved = candidate.resolve(strict=True)
|
||||
except OSError as exc:
|
||||
raise LaboratoryEvidenceReportError("LAB evidence result is unavailable") from exc
|
||||
if not resolved.is_dir() or definition.result_id_pattern.fullmatch(resolved.name) is None:
|
||||
raise LaboratoryEvidenceReportError("LAB evidence result path is invalid")
|
||||
document_path = _safe_file(resolved, definition.document_name)
|
||||
document_bytes = _read_bounded(document_path, _DOCUMENT_MAX_BYTES, "LAB document")
|
||||
document = _json_object(document_bytes, "LAB document")
|
||||
_validate_document(document, definition, resolved.name)
|
||||
identity = _object_or_none(document.get("identity"))
|
||||
identity_sha256 = document.get("identity_sha256")
|
||||
if identity is None or not isinstance(identity_sha256, str):
|
||||
raise LaboratoryEvidenceReportError("LAB identity proof is missing")
|
||||
if (
|
||||
_canonical_sha256(identity) != identity_sha256
|
||||
or not resolved.name.endswith(identity_sha256)
|
||||
):
|
||||
raise LaboratoryEvidenceReportError("LAB identity proof is invalid")
|
||||
artifacts = _verified_artifacts(resolved, document.get("artifacts"))
|
||||
return {
|
||||
"result_id": resolved.name,
|
||||
"identity_sha256": identity_sha256,
|
||||
"document_sha256": hashlib.sha256(document_bytes).hexdigest(),
|
||||
"artifact_count": len(artifacts),
|
||||
}
|
||||
|
||||
|
||||
class LaboratoryEvidenceReportService:
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -0,0 +1,544 @@
|
||||
"""Admission and execution boundary for reproducible Mission Core laboratories.
|
||||
|
||||
Legacy evidence remains readable but is not executable through this boundary. A
|
||||
canonical or experimental laboratory must declare its inputs and contracts before
|
||||
an adapter can run it. The boundary adds telemetry and one immutable receipt; it
|
||||
does not duplicate the laboratory algorithm.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import uuid
|
||||
from collections.abc import Callable, Mapping
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Final, Literal, cast
|
||||
|
||||
from k1link.compute.pipeline_telemetry import (
|
||||
PipelineTelemetryEmitter,
|
||||
PipelineTelemetryIdentity,
|
||||
PipelineTelemetrySink,
|
||||
)
|
||||
from k1link.laboratory.evidence_registry import (
|
||||
LaboratoryEvidenceRegistry,
|
||||
)
|
||||
from k1link.laboratory.evidence_report import verify_laboratory_evidence_result
|
||||
|
||||
LABORATORY_EXECUTION_REGISTRY_SCHEMA: Final = (
|
||||
"missioncore.laboratory-execution-registry/v1"
|
||||
)
|
||||
LABORATORY_RUN_RECEIPT_SCHEMA: Final = "missioncore.laboratory-run-receipt/v1"
|
||||
LABORATORY_RUN_CONTRACT: Final = "missioncore.laboratory-run/v1"
|
||||
|
||||
_MAX_REGISTRY_BYTES: Final = 128 * 1024
|
||||
_IDENTIFIER: Final = re.compile(r"^[a-z][a-z0-9-]{2,95}$")
|
||||
_INPUT_ROLE: Final = re.compile(r"^[a-z][a-z0-9_]{2,63}$")
|
||||
_RUN_IDENTIFIER: Final = re.compile(r"^[a-z0-9][a-z0-9._-]{2,95}$")
|
||||
_ADAPTER_IDENTIFIER: Final = re.compile(
|
||||
r"^(?:canonical|experimental)\.[a-z][a-z0-9-]{2,95}/v[1-9][0-9]*$"
|
||||
)
|
||||
_CONTRACT: Final = re.compile(r"^missioncore\.[a-z0-9.-]+/v[1-9][0-9]*$")
|
||||
_DEFINITION_KEYS: Final = frozenset(
|
||||
{"work_id", "lifecycle", "isolation", "adapter_id", "input_roles", "contracts"}
|
||||
)
|
||||
_CONTRACT_KEYS: Final = frozenset({"source", "provider", "graph", "run", "evidence"})
|
||||
_AUTHORITY: Final = {
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
|
||||
LaboratoryLifecycle = Literal["canonical", "experimental"]
|
||||
|
||||
|
||||
class LaboratoryExecutionError(RuntimeError):
|
||||
"""The execution registry, admission request, or immutable receipt is invalid."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class LaboratoryExecutionDefinition:
|
||||
work_id: str
|
||||
lifecycle: LaboratoryLifecycle
|
||||
isolation: str
|
||||
adapter_id: str
|
||||
input_roles: tuple[str, ...]
|
||||
source_contract: str
|
||||
provider_contract: str
|
||||
graph_contract: str
|
||||
run_contract: str
|
||||
evidence_contract: str
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_identifier(self.work_id, "work_id")
|
||||
if self.lifecycle not in {"canonical", "experimental"}:
|
||||
raise LaboratoryExecutionError("laboratory lifecycle is invalid")
|
||||
expected_isolation = (
|
||||
"core-adapter" if self.lifecycle == "canonical" else "bounded-adapter"
|
||||
)
|
||||
if self.isolation != expected_isolation:
|
||||
raise LaboratoryExecutionError(
|
||||
f"{self.lifecycle} laboratory isolation must be {expected_isolation}"
|
||||
)
|
||||
if _ADAPTER_IDENTIFIER.fullmatch(self.adapter_id) is None or not self.adapter_id.startswith(
|
||||
f"{self.lifecycle}."
|
||||
):
|
||||
raise LaboratoryExecutionError("laboratory adapter_id is invalid")
|
||||
if not self.input_roles or len(self.input_roles) != len(set(self.input_roles)):
|
||||
raise LaboratoryExecutionError("laboratory input_roles must be unique and non-empty")
|
||||
for role in self.input_roles:
|
||||
if _INPUT_ROLE.fullmatch(role) is None:
|
||||
raise LaboratoryExecutionError("input role is invalid")
|
||||
for value in (
|
||||
self.source_contract,
|
||||
self.provider_contract,
|
||||
self.graph_contract,
|
||||
self.run_contract,
|
||||
self.evidence_contract,
|
||||
):
|
||||
if _CONTRACT.fullmatch(value) is None:
|
||||
raise LaboratoryExecutionError("laboratory contract identity is invalid")
|
||||
if self.run_contract != LABORATORY_RUN_CONTRACT:
|
||||
raise LaboratoryExecutionError("laboratory run contract is unsupported")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class LaboratoryExecutionRegistry:
|
||||
definitions: tuple[LaboratoryExecutionDefinition, ...]
|
||||
legacy_work_ids: tuple[str, ...]
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
work_ids = [definition.work_id for definition in self.definitions]
|
||||
if len(work_ids) != len(set(work_ids)):
|
||||
raise LaboratoryExecutionError("duplicate executable laboratory work_id")
|
||||
if len(self.legacy_work_ids) != len(set(self.legacy_work_ids)):
|
||||
raise LaboratoryExecutionError("duplicate legacy laboratory work_id")
|
||||
for work_id in self.legacy_work_ids:
|
||||
_identifier(work_id, "legacy work_id")
|
||||
if set(work_ids).intersection(self.legacy_work_ids):
|
||||
raise LaboratoryExecutionError("a laboratory cannot be executable and legacy")
|
||||
|
||||
@classmethod
|
||||
def from_file(
|
||||
cls,
|
||||
path: Path,
|
||||
evidence_registry: LaboratoryEvidenceRegistry,
|
||||
) -> LaboratoryExecutionRegistry:
|
||||
candidate = path.expanduser().absolute()
|
||||
if candidate.is_symlink() or not candidate.is_file():
|
||||
raise LaboratoryExecutionError("laboratory execution registry must be a regular file")
|
||||
if candidate.stat().st_size > _MAX_REGISTRY_BYTES:
|
||||
raise LaboratoryExecutionError("laboratory execution registry is too large")
|
||||
try:
|
||||
payload: object = json.loads(candidate.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
raise LaboratoryExecutionError("laboratory execution registry is unreadable") from exc
|
||||
document = _object(payload, "laboratory execution registry")
|
||||
_exact_keys(
|
||||
document,
|
||||
frozenset({"schema_version", "definitions", "legacy_work_ids"}),
|
||||
"laboratory execution registry",
|
||||
)
|
||||
if document["schema_version"] != LABORATORY_EXECUTION_REGISTRY_SCHEMA:
|
||||
raise LaboratoryExecutionError("laboratory execution registry schema is invalid")
|
||||
rows = document["definitions"]
|
||||
legacy_rows = document["legacy_work_ids"]
|
||||
if not isinstance(rows, list) or not isinstance(legacy_rows, list):
|
||||
raise LaboratoryExecutionError("laboratory registry rows must be arrays")
|
||||
definitions = tuple(_definition(row) for row in rows)
|
||||
if not all(isinstance(item, str) for item in legacy_rows):
|
||||
raise LaboratoryExecutionError("legacy work IDs must be strings")
|
||||
registry = cls(definitions=definitions, legacy_work_ids=tuple(legacy_rows))
|
||||
registry.validate_evidence_registry(evidence_registry)
|
||||
return registry
|
||||
|
||||
def validate_evidence_registry(self, evidence_registry: LaboratoryEvidenceRegistry) -> None:
|
||||
evidence_by_work_id = {
|
||||
definition.work_id: definition for definition in evidence_registry.definitions
|
||||
}
|
||||
classified = {definition.work_id for definition in self.definitions}.union(
|
||||
self.legacy_work_ids
|
||||
)
|
||||
if classified != set(evidence_by_work_id):
|
||||
missing = sorted(set(evidence_by_work_id) - classified)
|
||||
unknown = sorted(classified - set(evidence_by_work_id))
|
||||
raise LaboratoryExecutionError(
|
||||
f"laboratory classification is incomplete; missing={missing}, unknown={unknown}"
|
||||
)
|
||||
for definition in self.definitions:
|
||||
if (
|
||||
evidence_by_work_id[definition.work_id].result_schema_version
|
||||
!= definition.evidence_contract
|
||||
):
|
||||
raise LaboratoryExecutionError(
|
||||
f"laboratory evidence contract mismatch: {definition.work_id}"
|
||||
)
|
||||
|
||||
def executable(self, work_id: str) -> LaboratoryExecutionDefinition:
|
||||
for definition in self.definitions:
|
||||
if definition.work_id == work_id:
|
||||
return definition
|
||||
if work_id in self.legacy_work_ids:
|
||||
raise LaboratoryExecutionError(
|
||||
f"legacy laboratory is read-only and cannot be executed: {work_id}"
|
||||
)
|
||||
raise LaboratoryExecutionError(f"laboratory work_id is not classified: {work_id}")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class LaboratoryRunRequest:
|
||||
work_id: str
|
||||
run_id: str
|
||||
request_id: str
|
||||
contour_id: str
|
||||
agent_id: str
|
||||
node_id: str
|
||||
source_id: str
|
||||
source_package_id: str
|
||||
method_id: str
|
||||
inputs: Mapping[str, Path]
|
||||
output_root: Path
|
||||
receipt_root: Path
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class LaboratoryAdapterResult:
|
||||
result_root: Path
|
||||
result_id: str
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class LaboratoryRunResult:
|
||||
result_root: Path
|
||||
result_id: str
|
||||
receipt_root: Path
|
||||
receipt_id: str
|
||||
receipt: dict[str, Any]
|
||||
|
||||
|
||||
LaboratoryAdapter = Callable[[LaboratoryRunRequest], LaboratoryAdapterResult]
|
||||
|
||||
|
||||
class LaboratoryRunner:
|
||||
"""Execute an admitted adapter and publish one common proof and telemetry shape."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
registry: LaboratoryExecutionRegistry,
|
||||
evidence_registry: LaboratoryEvidenceRegistry,
|
||||
sink: PipelineTelemetrySink,
|
||||
adapters: Mapping[str, LaboratoryAdapter] | None = None,
|
||||
clock_ns: Callable[[], int] = time.monotonic_ns,
|
||||
) -> None:
|
||||
self._registry = registry
|
||||
self._evidence = {
|
||||
definition.work_id: definition for definition in evidence_registry.definitions
|
||||
}
|
||||
self._sink = sink
|
||||
self._adapters = dict(adapters or canonical_laboratory_adapters())
|
||||
self._clock_ns = clock_ns
|
||||
|
||||
def run(self, request: LaboratoryRunRequest) -> LaboratoryRunResult:
|
||||
definition = self._registry.executable(request.work_id)
|
||||
_validate_request(request, definition)
|
||||
adapter = self._adapters.get(definition.adapter_id)
|
||||
if adapter is None:
|
||||
raise LaboratoryExecutionError(
|
||||
f"laboratory adapter is not installed: {definition.adapter_id}"
|
||||
)
|
||||
telemetry = PipelineTelemetryEmitter(
|
||||
identity=PipelineTelemetryIdentity(
|
||||
contour_id=request.contour_id,
|
||||
agent_id=request.agent_id,
|
||||
node_id=request.node_id,
|
||||
lab_id=request.work_id,
|
||||
run_id=request.run_id,
|
||||
request_id=request.request_id,
|
||||
source_id=request.source_id,
|
||||
source_package_id=request.source_package_id,
|
||||
method_id=request.method_id,
|
||||
),
|
||||
sink=self._sink,
|
||||
clock_ns=self._clock_ns,
|
||||
)
|
||||
started_ns = self._clock_ns()
|
||||
telemetry.run("started")
|
||||
try:
|
||||
with telemetry.stage("execute-adapter"):
|
||||
result = adapter(request)
|
||||
with telemetry.stage("verify-evidence") as outcome:
|
||||
proof = verify_laboratory_evidence_result(
|
||||
self._evidence[request.work_id],
|
||||
result.result_root,
|
||||
)
|
||||
if result.result_id != proof["result_id"]:
|
||||
raise LaboratoryExecutionError("adapter result_id does not match evidence")
|
||||
artifact_count = proof["artifact_count"]
|
||||
if not isinstance(artifact_count, int) or isinstance(artifact_count, bool):
|
||||
raise LaboratoryExecutionError("evidence artifact count is invalid")
|
||||
outcome.output_count = artifact_count
|
||||
with telemetry.stage("publish-receipt"):
|
||||
receipt_root, receipt_id, receipt = _publish_receipt(
|
||||
request=request,
|
||||
definition=definition,
|
||||
proof=proof,
|
||||
)
|
||||
except BaseException as exc:
|
||||
telemetry.run(
|
||||
"failed",
|
||||
duration_ms=max(0.0, (self._clock_ns() - started_ns) / 1_000_000),
|
||||
exit_code=1,
|
||||
error_type=type(exc).__name__,
|
||||
)
|
||||
raise
|
||||
telemetry.run(
|
||||
"completed",
|
||||
duration_ms=max(0.0, (self._clock_ns() - started_ns) / 1_000_000),
|
||||
exit_code=0,
|
||||
)
|
||||
return LaboratoryRunResult(
|
||||
result_root=result.result_root,
|
||||
result_id=result.result_id,
|
||||
receipt_root=receipt_root,
|
||||
receipt_id=receipt_id,
|
||||
receipt=receipt,
|
||||
)
|
||||
|
||||
|
||||
def canonical_laboratory_adapters() -> dict[str, LaboratoryAdapter]:
|
||||
return {
|
||||
"canonical.e33-worker-shadow/v1": _run_e33,
|
||||
"canonical.e35-degradation-recovery/v1": _run_e35,
|
||||
"canonical.e46j-raw-fisheye-realtime/v1": _run_e46j,
|
||||
}
|
||||
|
||||
|
||||
def _run_e33(request: LaboratoryRunRequest) -> LaboratoryAdapterResult:
|
||||
from k1link.compute.e33_worker_shadow import run_e33_worker_shadow
|
||||
|
||||
result = run_e33_worker_shadow(
|
||||
request.inputs["package_root"],
|
||||
request.output_root,
|
||||
execution_id=request.run_id,
|
||||
)
|
||||
return LaboratoryAdapterResult(result_root=result.result_root, result_id=result.result_id)
|
||||
|
||||
|
||||
def _run_e35(request: LaboratoryRunRequest) -> LaboratoryAdapterResult:
|
||||
from k1link.compute.e35_degradation_replay import build_e35_degradation_replay
|
||||
|
||||
result = build_e35_degradation_replay(
|
||||
e32_result_root=request.inputs["e32_result_root"],
|
||||
e33_result_root=request.inputs["e33_result_root"],
|
||||
e34_result_root=request.inputs["e34_result_root"],
|
||||
e34_profile_path=request.inputs["e34_profile_path"],
|
||||
profile_path=request.inputs["profile_path"],
|
||||
output_root=request.output_root,
|
||||
)
|
||||
return LaboratoryAdapterResult(result_root=result.result_root, result_id=result.result_id)
|
||||
|
||||
|
||||
def _run_e46j(request: LaboratoryRunRequest) -> LaboratoryAdapterResult:
|
||||
from k1link.compute.e46j_raw_fisheye_realtime import (
|
||||
build_e46j_raw_fisheye_realtime,
|
||||
)
|
||||
|
||||
result = build_e46j_raw_fisheye_realtime(
|
||||
raw_root=request.inputs["raw_root"],
|
||||
profile_path=request.inputs["profile_path"],
|
||||
output_root=request.output_root,
|
||||
)
|
||||
return LaboratoryAdapterResult(
|
||||
result_root=Path(result["result_root"]),
|
||||
result_id=str(result["result_id"]),
|
||||
)
|
||||
|
||||
|
||||
def _validate_request(
|
||||
request: LaboratoryRunRequest,
|
||||
definition: LaboratoryExecutionDefinition,
|
||||
) -> None:
|
||||
if _RUN_IDENTIFIER.fullmatch(request.run_id) is None:
|
||||
raise LaboratoryExecutionError("laboratory run_id is invalid")
|
||||
for value, label in (
|
||||
(request.request_id, "request_id"),
|
||||
(request.node_id, "node_id"),
|
||||
(request.source_id, "source_id"),
|
||||
(request.source_package_id, "source_package_id"),
|
||||
(request.method_id, "method_id"),
|
||||
):
|
||||
_bounded_text(value, label)
|
||||
actual_roles = set(request.inputs)
|
||||
expected_roles = set(definition.input_roles)
|
||||
if actual_roles != expected_roles:
|
||||
raise LaboratoryExecutionError(
|
||||
"laboratory input roles are invalid; "
|
||||
f"missing={sorted(expected_roles - actual_roles)}, "
|
||||
f"unexpected={sorted(actual_roles - expected_roles)}"
|
||||
)
|
||||
for role, path in request.inputs.items():
|
||||
if not isinstance(path, Path):
|
||||
raise LaboratoryExecutionError(f"laboratory input must be a Path: {role}")
|
||||
candidate = path.expanduser().absolute()
|
||||
if candidate.is_symlink():
|
||||
raise LaboratoryExecutionError(f"laboratory input must not be a symlink: {role}")
|
||||
try:
|
||||
candidate.resolve(strict=True)
|
||||
except OSError as exc:
|
||||
raise LaboratoryExecutionError(f"laboratory input is unavailable: {role}") from exc
|
||||
for path, label in (
|
||||
(request.output_root, "output_root"),
|
||||
(request.receipt_root, "receipt_root"),
|
||||
):
|
||||
if not isinstance(path, Path):
|
||||
raise LaboratoryExecutionError(f"{label} must be a Path")
|
||||
candidate = path.expanduser().absolute()
|
||||
if candidate.is_symlink():
|
||||
raise LaboratoryExecutionError(f"{label} must not be a symlink")
|
||||
|
||||
|
||||
def _publish_receipt(
|
||||
*,
|
||||
request: LaboratoryRunRequest,
|
||||
definition: LaboratoryExecutionDefinition,
|
||||
proof: dict[str, object],
|
||||
) -> tuple[Path, str, dict[str, Any]]:
|
||||
identity = {
|
||||
"schema_version": LABORATORY_RUN_RECEIPT_SCHEMA,
|
||||
"work_id": request.work_id,
|
||||
"lifecycle": definition.lifecycle,
|
||||
"adapter_id": definition.adapter_id,
|
||||
"run_id": request.run_id,
|
||||
"request_id": request.request_id,
|
||||
"contour_id": request.contour_id,
|
||||
"agent_id": request.agent_id,
|
||||
"node_id": request.node_id,
|
||||
"source_id": request.source_id,
|
||||
"source_package_id": request.source_package_id,
|
||||
"method_id": request.method_id,
|
||||
"contracts": {
|
||||
"source": definition.source_contract,
|
||||
"provider": definition.provider_contract,
|
||||
"graph": definition.graph_contract,
|
||||
"run": definition.run_contract,
|
||||
"evidence": definition.evidence_contract,
|
||||
},
|
||||
"result_id": proof["result_id"],
|
||||
"result_identity_sha256": proof["identity_sha256"],
|
||||
"evidence_document_sha256": proof["document_sha256"],
|
||||
"artifact_count": proof["artifact_count"],
|
||||
"authority": _AUTHORITY,
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
receipt_id = f"laboratory-run-receipt-{identity_sha256}"
|
||||
receipt = {
|
||||
**identity,
|
||||
"receipt_id": receipt_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
}
|
||||
destination = request.receipt_root.expanduser().absolute() / receipt_id
|
||||
document_path = destination / "receipt.json"
|
||||
encoded = _canonical_json(receipt) + b"\n"
|
||||
if destination.exists():
|
||||
if (
|
||||
destination.is_symlink()
|
||||
or document_path.is_symlink()
|
||||
or not document_path.is_file()
|
||||
):
|
||||
raise LaboratoryExecutionError("existing laboratory receipt is invalid")
|
||||
if document_path.read_bytes() != encoded:
|
||||
raise LaboratoryExecutionError("immutable laboratory receipt changed")
|
||||
return destination, receipt_id, receipt
|
||||
destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
staging = destination.parent / f".{receipt_id}.{uuid.uuid4().hex}.tmp"
|
||||
staging.mkdir(mode=0o700, exist_ok=False)
|
||||
try:
|
||||
descriptor = os.open(staging / "receipt.json", os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
|
||||
try:
|
||||
os.write(descriptor, encoded)
|
||||
os.fsync(descriptor)
|
||||
finally:
|
||||
os.close(descriptor)
|
||||
os.replace(staging, destination)
|
||||
except BaseException:
|
||||
if staging.exists():
|
||||
for child in staging.iterdir():
|
||||
child.unlink()
|
||||
staging.rmdir()
|
||||
raise
|
||||
return destination, receipt_id, receipt
|
||||
|
||||
|
||||
def _definition(value: object) -> LaboratoryExecutionDefinition:
|
||||
row = _object(value, "laboratory execution definition")
|
||||
_exact_keys(row, _DEFINITION_KEYS, "laboratory execution definition")
|
||||
contracts = _object(row["contracts"], "laboratory contracts")
|
||||
_exact_keys(contracts, _CONTRACT_KEYS, "laboratory contracts")
|
||||
input_roles = row["input_roles"]
|
||||
if not isinstance(input_roles, list) or not all(
|
||||
isinstance(item, str) for item in input_roles
|
||||
):
|
||||
raise LaboratoryExecutionError("laboratory input_roles must be strings")
|
||||
lifecycle = row["lifecycle"]
|
||||
if lifecycle not in {"canonical", "experimental"}:
|
||||
raise LaboratoryExecutionError("laboratory lifecycle is invalid")
|
||||
return LaboratoryExecutionDefinition(
|
||||
work_id=_text(row["work_id"], "work_id"),
|
||||
lifecycle=cast(LaboratoryLifecycle, lifecycle),
|
||||
isolation=_text(row["isolation"], "isolation"),
|
||||
adapter_id=_text(row["adapter_id"], "adapter_id"),
|
||||
input_roles=tuple(input_roles),
|
||||
source_contract=_text(contracts["source"], "source contract"),
|
||||
provider_contract=_text(contracts["provider"], "provider contract"),
|
||||
graph_contract=_text(contracts["graph"], "graph contract"),
|
||||
run_contract=_text(contracts["run"], "run contract"),
|
||||
evidence_contract=_text(contracts["evidence"], "evidence contract"),
|
||||
)
|
||||
|
||||
|
||||
def _object(value: object, label: str) -> dict[str, object]:
|
||||
if not isinstance(value, dict) or not all(isinstance(key, str) for key in value):
|
||||
raise LaboratoryExecutionError(f"{label} must be an object")
|
||||
return value
|
||||
|
||||
|
||||
def _exact_keys(document: dict[str, object], expected: frozenset[str], label: str) -> None:
|
||||
actual = frozenset(document)
|
||||
if actual != expected:
|
||||
raise LaboratoryExecutionError(
|
||||
f"{label} keys are invalid; missing={sorted(expected - actual)}, "
|
||||
f"unexpected={sorted(actual - expected)}"
|
||||
)
|
||||
|
||||
|
||||
def _text(value: object, label: str) -> str:
|
||||
if not isinstance(value, str) or not value or value != value.strip():
|
||||
raise LaboratoryExecutionError(f"{label} must be a non-empty trimmed string")
|
||||
return value
|
||||
|
||||
|
||||
def _identifier(value: object, label: str) -> str:
|
||||
text = _text(value, label)
|
||||
if _IDENTIFIER.fullmatch(text) is None:
|
||||
raise LaboratoryExecutionError(f"{label} is invalid")
|
||||
return text
|
||||
|
||||
|
||||
def _bounded_text(value: object, label: str) -> str:
|
||||
text = _text(value, label)
|
||||
if len(text) > 256 or any(ord(character) < 32 for character in text):
|
||||
raise LaboratoryExecutionError(f"{label} is invalid")
|
||||
return text
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
ensure_ascii=False,
|
||||
).encode("utf-8")
|
||||
@@ -910,7 +910,7 @@ def build_advanced_laboratory_router(
|
||||
if not isinstance(items, list):
|
||||
raise RuntimeError("advanced LAB index items are invalid")
|
||||
l3_identity = latest_l3_visual_identity(l3_visual_root_provider)
|
||||
if l3_identity is not None:
|
||||
if evidence_registry is None and l3_identity is not None:
|
||||
items.append(
|
||||
{
|
||||
"work_id": "l3-pointpillars-visual-audit",
|
||||
@@ -919,7 +919,7 @@ def build_advanced_laboratory_router(
|
||||
}
|
||||
)
|
||||
l31_identity = latest_l31_identity(l31_ravnoves_root_provider)
|
||||
if l31_identity is not None:
|
||||
if evidence_registry is None and l31_identity is not None:
|
||||
items.append(
|
||||
{
|
||||
"work_id": "l31-pointpillars-ravnoves",
|
||||
@@ -928,7 +928,7 @@ def build_advanced_laboratory_router(
|
||||
}
|
||||
)
|
||||
l32_identity = latest_l32_identity(l32_camera_review_root_provider)
|
||||
if l32_identity is not None:
|
||||
if evidence_registry is None and l32_identity is not None:
|
||||
items.append(
|
||||
{
|
||||
"work_id": "l32-pointpillars-camera-review",
|
||||
@@ -937,7 +937,7 @@ def build_advanced_laboratory_router(
|
||||
}
|
||||
)
|
||||
l33_identity = latest_l33_identity(l33_camera_first_review_root_provider)
|
||||
if l33_identity is not None:
|
||||
if evidence_registry is None and l33_identity is not None:
|
||||
items.append(
|
||||
{
|
||||
"work_id": "l33-camera-first-detector-review",
|
||||
|
||||
@@ -26,6 +26,7 @@ from k1link.compute import (
|
||||
from k1link.laboratory import (
|
||||
LaboratoryEvidenceRegistry,
|
||||
LaboratoryEvidenceReportService,
|
||||
LaboratoryExecutionRegistry,
|
||||
LaboratoryValueReviewRegistry,
|
||||
)
|
||||
from k1link.sessions import (
|
||||
@@ -142,6 +143,10 @@ INVALID_REQUEST_DETAIL = "Некорректные параметры запро
|
||||
LABORATORY_EVIDENCE_REGISTRY = LaboratoryEvidenceRegistry.from_directory(
|
||||
REPOSITORY_ROOT / "config" / "laboratories"
|
||||
)
|
||||
LABORATORY_EXECUTION_REGISTRY = LaboratoryExecutionRegistry.from_file(
|
||||
REPOSITORY_ROOT / "config" / "laboratory-execution.json",
|
||||
LABORATORY_EVIDENCE_REGISTRY,
|
||||
)
|
||||
LABORATORY_VALUE_REVIEW_REGISTRY = LaboratoryValueReviewRegistry.from_file(
|
||||
REPOSITORY_ROOT / "config" / "laboratory-value-review.json"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user