feat(observatory): attest recording equipment and capture profiles

This commit is contained in:
DCCONSTRUCTIONS
2026-09-02 00:57:27 +03:00
parent 7025e173a3
commit d655d6998d
17 changed files with 1089 additions and 42 deletions
@@ -48,10 +48,21 @@ export interface ObservationSessionSummary {
modalities: readonly string[]; modalities: readonly string[];
durationSeconds: number; durationSeconds: number;
replayable: boolean; replayable: boolean;
captureAttestation: ObservationCaptureAttestation | null;
preparation: ObservationSessionCatalogPreparation | null; preparation: ObservationSessionCatalogPreparation | null;
lab: ObservationLabInstance | null; lab: ObservationLabInstance | null;
} }
export interface ObservationCaptureAttestation {
readonly equipmentModelId: string;
readonly equipmentModelSha256: string;
readonly equipmentDisplayName: string;
readonly captureProfileId: string;
readonly captureProfileSha256: string;
readonly captureAttestationSha256: string;
readonly state: "attested" | "attested-with-legacy-evidence" | "unknown";
}
export interface ObservationSessionCatalog { export interface ObservationSessionCatalog {
items: readonly ObservationSessionSummary[]; items: readonly ObservationSessionSummary[];
} }
@@ -168,6 +179,7 @@ const ITEM_KEYS = new Set([
"modalities", "modalities",
"duration_seconds", "duration_seconds",
"replayable", "replayable",
"capture_attestation",
"preparation", "preparation",
"lab", "lab",
]); ]);
@@ -187,6 +199,22 @@ const LAB_V3_KEYS = new Set([
...LAB_V2_KEYS, ...LAB_V2_KEYS,
"calculation_profile", "calculation_profile",
]); ]);
const CAPTURE_ATTESTATION_KEYS = new Set([
"schema_version",
"equipment_model",
"capture_profile",
"capture_attestation_sha256",
"state",
]);
const EQUIPMENT_MODEL_KEYS = new Set([
"equipment_model_id",
"equipment_model_sha256",
"display_name",
]);
const CAPTURE_PROFILE_KEYS = new Set([
"capture_profile_id",
"capture_profile_sha256",
]);
const CATALOG_PREPARATION_KEYS = new Set([ const CATALOG_PREPARATION_KEYS = new Set([
"preparation_id", "preparation_id",
"state", "state",
@@ -400,11 +428,103 @@ function decodeItem(value: unknown, index: number): ObservationSessionSummary {
modalities: requireModalities(value.modalities), modalities: requireModalities(value.modalities),
durationSeconds: value.duration_seconds, durationSeconds: value.duration_seconds,
replayable: value.replayable, replayable: value.replayable,
captureAttestation: decodeCaptureAttestation(value.capture_attestation, id),
preparation: decodeCatalogPreparation(value.preparation, id), preparation: decodeCatalogPreparation(value.preparation, id),
lab: decodeLabInstance(value.lab, id), lab: decodeLabInstance(value.lab, id),
}; };
} }
function decodeCaptureAttestation(
value: unknown,
sessionId: string,
): ObservationCaptureAttestation | null {
if (value === null || value === undefined) return null;
if (!isRecord(value)) {
throw new ObservationSessionContractError(
`Аттестация записи ${sessionId} должна быть объектом или null.`,
);
}
assertExactKeys(value, CAPTURE_ATTESTATION_KEYS, `Аттестация записи ${sessionId}`);
if (value.schema_version !== "missioncore.recorded-capture-attestation/v1") {
throw new ObservationSessionContractError(
`Аттестация записи ${sessionId} имеет неизвестную схему.`,
);
}
if (!isRecord(value.equipment_model) || !isRecord(value.capture_profile)) {
throw new ObservationSessionContractError(
`Аттестация записи ${sessionId} не содержит оборудование или профиль записи.`,
);
}
assertExactKeys(
value.equipment_model,
EQUIPMENT_MODEL_KEYS,
`Оборудование записи ${sessionId}`,
);
assertExactKeys(
value.capture_profile,
CAPTURE_PROFILE_KEYS,
`Профиль записи ${sessionId}`,
);
const equipmentModelId = requireString(
value.equipment_model.equipment_model_id,
"equipment_model_id",
128,
);
const equipmentModelSha256 = requireString(
value.equipment_model.equipment_model_sha256,
"equipment_model_sha256",
64,
);
const captureProfileId = requireString(
value.capture_profile.capture_profile_id,
"capture_profile_id",
128,
);
const captureProfileSha256 = requireString(
value.capture_profile.capture_profile_sha256,
"capture_profile_sha256",
64,
);
const captureAttestationSha256 = requireString(
value.capture_attestation_sha256,
"capture_attestation_sha256",
64,
);
if (
!SAFE_ID.test(equipmentModelId)
|| !SAFE_ID.test(captureProfileId)
|| !SHA256.test(equipmentModelSha256)
|| !SHA256.test(captureProfileSha256)
|| !SHA256.test(captureAttestationSha256)
) {
throw new ObservationSessionContractError(
`Аттестация записи ${sessionId} содержит некорректную immutable identity.`,
);
}
if (
value.state !== "attested"
&& value.state !== "attested-with-legacy-evidence"
&& value.state !== "unknown"
) {
throw new ObservationSessionContractError(
`Аттестация записи ${sessionId} содержит неизвестное состояние.`,
);
}
return {
equipmentModelId,
equipmentModelSha256,
equipmentDisplayName: requireString(
value.equipment_model.display_name,
"equipment display_name",
128,
),
captureProfileId,
captureProfileSha256,
captureAttestationSha256,
state: value.state,
};
}
function decodeLabInstance( function decodeLabInstance(
value: unknown, value: unknown,
sessionId: string, sessionId: string,
@@ -43,7 +43,7 @@ function decodePortableSetup(value: unknown): ObservatoryLaboratorySetup {
const compatibility = record(row.source_compatibility, "portable source_compatibility"); const compatibility = record(row.source_compatibility, "portable source_compatibility");
exactKeys( exactKeys(
compatibility, compatibility,
["compatible", "outcome", "reason"], ["compatible", "outcome", "reason", "reason_code"],
"portable source_compatibility", "portable source_compatibility",
); );
const compatible = boolean(compatibility.compatible, "portable compatible"); const compatible = boolean(compatibility.compatible, "portable compatible");
@@ -53,6 +53,10 @@ function decodePortableSetup(value: unknown): ObservatoryLaboratorySetup {
"portable compatibility outcome", "portable compatibility outcome",
); );
const compatibilityReason = text(compatibility.reason, "portable compatibility reason"); const compatibilityReason = text(compatibility.reason, "portable compatibility reason");
const compatibilityReasonCode = text(
compatibility.reason_code,
"portable compatibility reason_code",
);
const executor = record(row.executor, "portable executor"); const executor = record(row.executor, "portable executor");
exactKeys( exactKeys(
@@ -134,7 +138,7 @@ function decodePortableSetup(value: unknown): ObservatoryLaboratorySetup {
compatible, compatible,
reasons: compatible reasons: compatible
? [] ? []
: [{ code: "source-capability-blocked", message: compatibilityReason }], : [{ code: compatibilityReasonCode, message: compatibilityReason }],
}, },
executor: { executor: {
contourId: text(executor.contour_id, "portable executor contour_id"), contourId: text(executor.contour_id, "portable executor contour_id"),
@@ -198,6 +198,7 @@ test("session catalog decodes canonical snake_case into a path-free camelCase mo
modalities: ["point-cloud", "pose"], modalities: ["point-cloud", "pose"],
durationSeconds: 1_450.744, durationSeconds: 1_450.744,
replayable: true, replayable: true,
captureAttestation: null,
preparation: null, preparation: null,
lab: null, lab: null,
}]); }]);
@@ -116,6 +116,7 @@ function portableSetup() {
source_compatibility: { source_compatibility: {
outcome: "pass", outcome: "pass",
compatible: true, compatible: true,
reason_code: "source-compatible",
reason: "Запись соответствует требованиям EoMT + DDRNet.", reason: "Запись соответствует требованиям EoMT + DDRNet.",
}, },
executor: { executor: {
+22
View File
@@ -0,0 +1,22 @@
{
"schema_version": "missioncore.equipment-model-registry/v1",
"models": [
{
"schema_version": "missioncore.equipment-model/v1",
"equipment_model_id": "xgrids.lixelkity-k1",
"equipment_model_version": 1,
"plugin_id": "nodedc.device.xgrids-lixelkity-k1",
"vendor": "XGRIDS",
"display_name": "XGRIDS LixelKity K1",
"category": "mobile-lidar-scanner",
"capability_ids": [
"recorded.camera.video",
"recorded.lidar.point-cloud",
"recorded.pose.trajectory"
],
"authority": {
"commands_enabled": false
}
}
]
}
@@ -0,0 +1,43 @@
{
"schema_version": "missioncore.recorded-capture-profile-registry/v1",
"profiles": [
{
"schema_version": "missioncore.recorded-capture-profile/v1",
"capture_profile_id": "xgrids-k1.viewer-live.fw-3.0.2.v1",
"capture_profile_version": 1,
"equipment_model_id": "xgrids.lixelkity-k1",
"equipment_model_sha256": "c95d7183ffd2e425892fd9ddfc4cd8dcbccc0231289127ddc14db6b52bbb92aa",
"plugin_id": "nodedc.device.xgrids-lixelkity-k1",
"archive_id": "xgrids-k1.viewer-live.evidence",
"firmware_compatibility_profile_id": "xgrids.lixelkity-k1.fw-3.0.2.local-network.v2",
"modalities": [
"point-cloud",
"trajectory",
"video"
],
"semantic_channels": [
"camera.video.recorded",
"spatial.point-cloud.recorded",
"spatial.pose.recorded"
],
"camera_media": {
"source_id": "sensor.camera.right",
"media_type": "video/mp4; codecs=\"avc1.641028\"",
"width": 800,
"height": 600,
"initialization_sha256": "e2279963e16d84c91d68e7dbb1f7efed840533387dfeb844b7398bff45fbde38"
},
"calibration": {
"slot_id": "camera_1",
"sha256": "05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9"
},
"media_epoch_policy": "exactly-one-complete-epoch",
"seekable": true,
"adapter": {
"adapter_id": "xgrids-k1-recorded-observatory-v2",
"version": 2,
"adapter_sha256": "4e12be6d2503e2e237eddb290b28d6a7d16cf983855b6d8e6b4e3b65d2feb0de"
}
}
]
}
@@ -0,0 +1,35 @@
"""Exact metadata gate for the one historical recorded-LAB migration.
This compatibility check is intentionally outside the generic Session catalog:
new device plugins and equipment profiles must not add concrete vocabulary to
the shared observation hot path.
"""
from __future__ import annotations
from typing import Final
_DISPLAY_NAME: Final = "RAVNOVES004TREE · полный маршрут восприятия"
_DURATION_SECONDS: Final = 718.0
_RUN_CREATED_AT_UTC: Final = "2026-08-29T18:05:11.329061+00:00"
_PLUGIN_ID: Final = "nodedc.device.xgrids-lixelkity-k1"
_ARCHIVE_ID: Final = "xgrids-k1.viewer-live.evidence"
def matches_historical_recorded_projection(
*,
display_name: object,
duration_seconds: object,
run_created_at_utc: object,
source_plugin_id: object,
source_archive_id: object,
) -> bool:
"""Return whether legacy rows match the only supported rolling upgrade."""
return (
display_name == _DISPLAY_NAME
and duration_seconds == _DURATION_SECONDS
and run_created_at_utc == _RUN_CREATED_AT_UTC
and source_plugin_id == _PLUGIN_ID
and source_archive_id == _ARCHIVE_ID
)
@@ -124,8 +124,8 @@ class ObservationOnlyAuthority:
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class K1RecordedSourceRequirements: class RecordedSourceRequirements:
"""Typed capability matcher for one class of recorded K1 sessions.""" """Typed capability matcher for one exact recorded capture contract."""
plugin_id: str plugin_id: str
archive_id: str archive_id: str
@@ -206,6 +206,11 @@ class K1RecordedSourceRequirements:
} }
# Compatibility name retained for promoted package readers. Canonical JSON
# and all digests remain byte-for-byte unchanged.
K1RecordedSourceRequirements = RecordedSourceRequirements
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class PortableSourceAdapterIdentity: class PortableSourceAdapterIdentity:
adapter_id: str adapter_id: str
@@ -26,6 +26,7 @@ from k1link.observatory.source_admission import (
PortableRecordedSourceCapability, PortableRecordedSourceCapability,
PortableSourceAdmissionError, PortableSourceAdmissionError,
) )
from k1link.sessions.equipment import EquipmentCaptureRegistry
from k1link.sessions.models import SessionSummary from k1link.sessions.models import SessionSummary
PORTABLE_LABORATORY_SETUP_CATALOG_SCHEMA: Final = ( PORTABLE_LABORATORY_SETUP_CATALOG_SCHEMA: Final = (
@@ -93,12 +94,14 @@ type PortableSourceCapabilityProbe = Callable[[str], PortableRecordedSourceCapab
class _SourceCompatibility: class _SourceCompatibility:
compatible: bool compatible: bool
capability: PortableRecordedSourceCapability | None capability: PortableRecordedSourceCapability | None
reason_code: str
reason: str reason: str
def as_dict(self) -> dict[str, object]: def as_dict(self) -> dict[str, object]:
return { return {
"outcome": "pass" if self.compatible else "blocked", "outcome": "pass" if self.compatible else "blocked",
"compatible": self.compatible, "compatible": self.compatible,
"reason_code": self.reason_code,
"reason": self.reason, "reason": self.reason,
} }
@@ -112,12 +115,14 @@ class PortableSetupProjector:
registry: PortableRunDefinitionRegistry, registry: PortableRunDefinitionRegistry,
capability_probe: PortableDefinitionCapabilityProbe, capability_probe: PortableDefinitionCapabilityProbe,
dispatch_available: bool = False, dispatch_available: bool = False,
equipment_capture_registry: EquipmentCaptureRegistry | None = None,
) -> None: ) -> None:
if not hasattr(capability_probe, "probe"): if not hasattr(capability_probe, "probe"):
raise PortableSetupProjectionError("portable source capability probe is unavailable") raise PortableSetupProjectionError("portable source capability probe is unavailable")
self._registry = registry self._registry = registry
self._capability_probe = capability_probe self._capability_probe = capability_probe
self._dispatch_available = dispatch_available self._dispatch_available = dispatch_available
self._equipment_capture_registry = equipment_capture_registry
for definition in registry.definitions: for definition in registry.definitions:
_validate_model_presentation(definition) _validate_model_presentation(definition)
if definition.authority.as_dict() != _OBSERVATION_ONLY_AUTHORITY: if definition.authority.as_dict() != _OBSERVATION_ONLY_AUTHORITY:
@@ -159,7 +164,7 @@ class PortableSetupProjector:
except PortableRunDefinitionRegistryError as exc: except PortableRunDefinitionRegistryError as exc:
raise PortableSetupProjectionError("portable setup is unavailable") from exc raise PortableSetupProjectionError("portable setup is unavailable") from exc
presentation = _presentation(definition) presentation = _presentation(definition)
compatibility = self._probe_source(definition, source.session_id, presentation) compatibility = self._probe_source(definition, source, presentation)
executor = definition.executor executor = definition.executor
submission_allowed = ( submission_allowed = (
compatibility.compatible and executor.ready and self._dispatch_available compatibility.compatible and executor.ready and self._dispatch_available
@@ -204,9 +209,51 @@ class PortableSetupProjector:
def _probe_source( def _probe_source(
self, self,
definition: PortableRunDefinition, definition: PortableRunDefinition,
source_session_id: str, source: SessionSummary,
presentation: dict[str, str], presentation: dict[str, str],
) -> _SourceCompatibility: ) -> _SourceCompatibility:
source_session_id = source.session_id
if self._equipment_capture_registry is not None:
expected_profile = (
self._equipment_capture_registry.compatible_profile_for_requirements(
definition.source_requirements.as_dict()
)
)
if expected_profile is None:
raise PortableSetupProjectionError(
"portable source requirements have no capture profile allowlist"
)
attestation = source.capture_attestation
if attestation is None:
return _SourceCompatibility(
compatible=False,
capability=None,
reason_code="capture-attestation-missing",
reason="У записи нет проверенной привязки к оборудованию и профилю записи.",
)
if (
attestation.equipment_model_id
!= expected_profile.equipment.equipment_model_id
or attestation.equipment_model_sha256
!= expected_profile.equipment.equipment_model_sha256
):
return _SourceCompatibility(
compatible=False,
capability=None,
reason_code="equipment-model-mismatch",
reason="Модель оборудования записи не поддерживается этим профилем.",
)
if (
attestation.capture_profile_id != expected_profile.capture_profile_id
or attestation.capture_profile_sha256
!= expected_profile.capture_profile_sha256
):
return _SourceCompatibility(
compatible=False,
capability=None,
reason_code="capture-profile-not-allowed",
reason="Профиль записи не входит в точный список совместимости.",
)
try: try:
capability = self._capability_probe.probe( capability = self._capability_probe.probe(
source_session_id=source_session_id, source_session_id=source_session_id,
@@ -217,6 +264,7 @@ class PortableSetupProjector:
return _SourceCompatibility( return _SourceCompatibility(
compatible=False, compatible=False,
capability=None, capability=None,
reason_code="source-contract-mismatch",
reason=presentation["incompatible"], reason=presentation["incompatible"],
) )
if not isinstance(capability, PortableRecordedSourceCapability): if not isinstance(capability, PortableRecordedSourceCapability):
@@ -230,6 +278,7 @@ class PortableSetupProjector:
return _SourceCompatibility( return _SourceCompatibility(
compatible=True, compatible=True,
capability=capability, capability=capability,
reason_code="source-compatible",
reason=presentation["compatible"], reason=presentation["compatible"],
) )
+7 -2
View File
@@ -76,8 +76,8 @@ class PortableSourceAdmissionStaleError(PortableSourceAdmissionIntegrityError):
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class RecordedK1SourceRequirements: class RecordedSourceRequirements:
"""Typed matcher owned by a portable RunDefinition. """Typed matcher owned by a portable recorded-source RunDefinition.
``camera_init_sha256`` is the exact ISO-BMFF initialization segment for the ``camera_init_sha256`` is the exact ISO-BMFF initialization segment for the
admitted codec/resolution profile. Width and height are asserted by that admitted codec/resolution profile. Width and height are asserted by that
@@ -173,6 +173,11 @@ class RecordedK1SourceRequirements:
return _sha256(self.adapter_document()) return _sha256(self.adapter_document())
# Compatibility import for already promoted packages. New code uses the
# equipment-neutral name; the serialized contract is unchanged.
RecordedK1SourceRequirements = RecordedSourceRequirements
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class PortableRecordedSourceCapability: class PortableRecordedSourceCapability:
"""Cheap catalog/summary attestation used by compatibility surfaces. """Cheap catalog/summary attestation used by compatibility surfaces.
+380
View File
@@ -0,0 +1,380 @@
"""Canonical equipment/capture identities for recorded observation sessions.
The registry is intentionally small and content addressed. It answers only
which equipment contract produced a recording; it does not contain Worker,
model, queue, or command authority.
"""
from __future__ import annotations
import hashlib
import json
import re
from collections.abc import Mapping
from dataclasses import dataclass
from pathlib import Path
from typing import Final, Literal
from .models import ObservationSessionCandidate
EQUIPMENT_REGISTRY_SCHEMA: Final = "missioncore.equipment-model-registry/v1"
EQUIPMENT_MODEL_SCHEMA: Final = "missioncore.equipment-model/v1"
CAPTURE_PROFILE_REGISTRY_SCHEMA: Final = (
"missioncore.recorded-capture-profile-registry/v1"
)
CAPTURE_PROFILE_SCHEMA: Final = "missioncore.recorded-capture-profile/v1"
CAPTURE_ATTESTATION_SCHEMA: Final = "missioncore.recorded-capture-attestation/v1"
_IDENTIFIER = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
_SHA256 = re.compile(r"^[a-f0-9]{64}$")
_MODALITIES: Final = frozenset({"point-cloud", "trajectory", "video"})
CaptureAttestationState = Literal[
"attested",
"attested-with-legacy-evidence",
"unknown",
]
class EquipmentRegistryError(ValueError):
"""A canonical equipment or capture registry is invalid."""
def canonical_sha256(document: Mapping[str, object]) -> str:
payload = json.dumps(
document,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
).encode("utf-8")
return hashlib.sha256(payload).hexdigest()
@dataclass(frozen=True, slots=True)
class EquipmentModel:
equipment_model_id: str
equipment_model_sha256: str
document: Mapping[str, object]
@property
def display_name(self) -> str:
return str(self.document["display_name"])
@dataclass(frozen=True, slots=True)
class RecordedCaptureProfile:
capture_profile_id: str
capture_profile_sha256: str
equipment: EquipmentModel
document: Mapping[str, object]
@dataclass(frozen=True, slots=True)
class RecordedCaptureAttestation:
equipment_model_id: str
equipment_model_sha256: str
equipment_display_name: str
capture_profile_id: str
capture_profile_sha256: str
capture_attestation_sha256: str
state: CaptureAttestationState
def as_dict(self) -> dict[str, object]:
return {
"schema_version": CAPTURE_ATTESTATION_SCHEMA,
"equipment_model": {
"equipment_model_id": self.equipment_model_id,
"equipment_model_sha256": self.equipment_model_sha256,
"display_name": self.equipment_display_name,
},
"capture_profile": {
"capture_profile_id": self.capture_profile_id,
"capture_profile_sha256": self.capture_profile_sha256,
},
"capture_attestation_sha256": self.capture_attestation_sha256,
"state": self.state,
}
class EquipmentCaptureRegistry:
"""Exact-match registry used by the host session catalog."""
def __init__(
self,
*,
equipment_models: tuple[EquipmentModel, ...],
capture_profiles: tuple[RecordedCaptureProfile, ...],
) -> None:
self.equipment_models = equipment_models
self.capture_profiles = capture_profiles
self._profiles_by_origin = {
(
str(profile.document["plugin_id"]),
str(profile.document["archive_id"]),
): profile
for profile in capture_profiles
}
if len(self._profiles_by_origin) != len(capture_profiles):
raise EquipmentRegistryError("capture profile origins must be unique")
@classmethod
def from_repository(cls, repository_root: Path) -> EquipmentCaptureRegistry | None:
equipment_path = repository_root / "config" / "observatory-equipment-models.json"
capture_path = (
repository_root / "config" / "observatory-recorded-capture-profiles.json"
)
if not equipment_path.exists() and not capture_path.exists():
return None
if not equipment_path.is_file() or not capture_path.is_file():
raise EquipmentRegistryError("equipment/capture registries must be installed together")
return cls.load(equipment_path=equipment_path, capture_path=capture_path)
@classmethod
def load(
cls,
*,
equipment_path: Path,
capture_path: Path,
) -> EquipmentCaptureRegistry:
equipment_payload = _read_json(equipment_path)
capture_payload = _read_json(capture_path)
if equipment_payload.get("schema_version") != EQUIPMENT_REGISTRY_SCHEMA:
raise EquipmentRegistryError("equipment registry schema is unsupported")
if capture_payload.get("schema_version") != CAPTURE_PROFILE_REGISTRY_SCHEMA:
raise EquipmentRegistryError("capture profile registry schema is unsupported")
equipment_rows = equipment_payload.get("models")
capture_rows = capture_payload.get("profiles")
if not isinstance(equipment_rows, list) or not equipment_rows:
raise EquipmentRegistryError("equipment registry must contain models")
if not isinstance(capture_rows, list) or not capture_rows:
raise EquipmentRegistryError("capture registry must contain profiles")
equipment_models = tuple(_equipment_model(row) for row in equipment_rows)
by_id = {item.equipment_model_id: item for item in equipment_models}
if len(by_id) != len(equipment_models):
raise EquipmentRegistryError("equipment model ids must be unique")
capture_profiles = tuple(_capture_profile(row, by_id) for row in capture_rows)
if len({item.capture_profile_id for item in capture_profiles}) != len(
capture_profiles
):
raise EquipmentRegistryError("capture profile ids must be unique")
return cls(
equipment_models=equipment_models,
capture_profiles=capture_profiles,
)
def attest(
self,
*,
session_id: str,
plugin_id: str,
archive_id: str,
candidate: ObservationSessionCandidate,
) -> RecordedCaptureAttestation | None:
profile = self._profiles_by_origin.get((plugin_id, archive_id))
if profile is None:
return None
modalities_value = profile.document["modalities"]
channels_value = profile.document["semantic_channels"]
assert isinstance(modalities_value, list)
assert isinstance(channels_value, list)
required_modalities = tuple(str(item) for item in modalities_value)
required_channels = {str(item) for item in channels_value}
actual_channels = {source.semantic_channel_id for source in candidate.sources}
camera = profile.document["camera_media"]
assert isinstance(camera, dict)
expected_camera_source = camera["source_id"]
camera_sources = {
source.source_id
for source in candidate.sources
if source.modality == "video" and source.seekable
}
if (
not set(required_modalities).issubset(candidate.modalities)
or not required_channels.issubset(actual_channels)
or expected_camera_source not in camera_sources
or not candidate.replayable
):
return None
attestation_document: dict[str, object] = {
"schema_version": CAPTURE_ATTESTATION_SCHEMA,
"session_id": session_id,
"plugin_id": plugin_id,
"archive_id": archive_id,
"equipment_model_id": profile.equipment.equipment_model_id,
"equipment_model_sha256": profile.equipment.equipment_model_sha256,
"capture_profile_id": profile.capture_profile_id,
"capture_profile_sha256": profile.capture_profile_sha256,
"observed_modalities": sorted(candidate.modalities),
"observed_semantic_channels": sorted(actual_channels),
"state": "attested-with-legacy-evidence",
}
return RecordedCaptureAttestation(
equipment_model_id=profile.equipment.equipment_model_id,
equipment_model_sha256=profile.equipment.equipment_model_sha256,
equipment_display_name=profile.equipment.display_name,
capture_profile_id=profile.capture_profile_id,
capture_profile_sha256=profile.capture_profile_sha256,
capture_attestation_sha256=canonical_sha256(attestation_document),
state="attested-with-legacy-evidence",
)
def compatible_profile_for_requirements(
self,
requirements: Mapping[str, object],
) -> RecordedCaptureProfile | None:
"""Resolve an exact capture allowlist entry without changing RunDefinition bytes."""
for profile in self.capture_profiles:
row = profile.document
camera = row["camera_media"]
calibration = row["calibration"]
semantic_channels = row["semantic_channels"]
assert isinstance(camera, dict)
assert isinstance(calibration, dict)
assert isinstance(semantic_channels, list)
if (
row["plugin_id"] == requirements.get("plugin_id")
and row["archive_id"] == requirements.get("archive_id")
and row["modalities"] == requirements.get("required_modalities")
and camera["source_id"] == requirements.get("camera_source_id")
and "camera.video.recorded" in semantic_channels
and requirements.get("camera_semantic_channel_id")
== "camera.video.recorded"
and camera["media_type"] == requirements.get("recorded_media_type")
and camera["initialization_sha256"]
== requirements.get("recorded_media_init_sha256")
and camera["width"] == requirements.get("camera_width")
and camera["height"] == requirements.get("camera_height")
and calibration["slot_id"] == requirements.get("calibration_slot")
and calibration["sha256"]
== requirements.get("calibration_identity_sha256")
and requirements.get("exactly_one_media_epoch") is True
and requirements.get("seekable") is True
):
return profile
return None
def _read_json(path: Path) -> dict[str, object]:
try:
payload = json.loads(path.read_text(encoding="utf-8"))
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
raise EquipmentRegistryError("equipment/capture registry is unreadable") from exc
if not isinstance(payload, dict):
raise EquipmentRegistryError("equipment/capture registry must be an object")
return payload
def _equipment_model(value: object) -> EquipmentModel:
if not isinstance(value, dict) or value.get("schema_version") != EQUIPMENT_MODEL_SCHEMA:
raise EquipmentRegistryError("equipment model schema is unsupported")
equipment_model_id = _identifier(value.get("equipment_model_id"), "equipment model id")
plugin_id = _identifier(value.get("plugin_id"), "equipment plugin id")
if not plugin_id.startswith("nodedc.device."):
raise EquipmentRegistryError("equipment plugin id is not canonical")
version = value.get("equipment_model_version")
if isinstance(version, bool) or not isinstance(version, int) or version < 1:
raise EquipmentRegistryError("equipment model version is invalid")
capabilities = value.get("capability_ids")
if (
not isinstance(capabilities, list)
or not capabilities
or capabilities != sorted(capabilities)
or len(set(capabilities)) != len(capabilities)
or any(
not isinstance(item, str) or _IDENTIFIER.fullmatch(item) is None
for item in capabilities
)
):
raise EquipmentRegistryError("equipment capabilities are invalid")
authority = value.get("authority")
if authority != {"commands_enabled": False}:
raise EquipmentRegistryError("equipment registry must be observation-only")
for field in ("vendor", "display_name", "category"):
if not isinstance(value.get(field), str) or not str(value[field]).strip():
raise EquipmentRegistryError(f"equipment {field} is invalid")
return EquipmentModel(
equipment_model_id=equipment_model_id,
equipment_model_sha256=canonical_sha256(value),
document=value,
)
def _capture_profile(
value: object,
equipment_models: Mapping[str, EquipmentModel],
) -> RecordedCaptureProfile:
if not isinstance(value, dict) or value.get("schema_version") != CAPTURE_PROFILE_SCHEMA:
raise EquipmentRegistryError("capture profile schema is unsupported")
profile_id = _identifier(value.get("capture_profile_id"), "capture profile id")
version = value.get("capture_profile_version")
if isinstance(version, bool) or not isinstance(version, int) or version < 1:
raise EquipmentRegistryError("capture profile version is invalid")
equipment_id = _identifier(value.get("equipment_model_id"), "capture equipment id")
equipment = equipment_models.get(equipment_id)
if equipment is None:
raise EquipmentRegistryError("capture profile references unknown equipment")
equipment_sha256 = value.get("equipment_model_sha256")
if equipment_sha256 != equipment.equipment_model_sha256:
raise EquipmentRegistryError("capture profile equipment digest does not match")
_identifier(value.get("plugin_id"), "capture plugin id")
_identifier(value.get("archive_id"), "capture archive id")
_identifier(
value.get("firmware_compatibility_profile_id"),
"firmware compatibility profile id",
)
modalities = value.get("modalities")
if (
not isinstance(modalities, list)
or not modalities
or modalities != sorted(modalities)
or not set(modalities).issubset(_MODALITIES)
):
raise EquipmentRegistryError("capture modalities are invalid")
channels = value.get("semantic_channels")
if (
not isinstance(channels, list)
or not channels
or channels != sorted(channels)
or any(
not isinstance(item, str) or _IDENTIFIER.fullmatch(item) is None
for item in channels
)
):
raise EquipmentRegistryError("capture semantic channels are invalid")
camera = value.get("camera_media")
calibration = value.get("calibration")
adapter = value.get("adapter")
if (
not isinstance(camera, dict)
or not isinstance(calibration, dict)
or not isinstance(adapter, dict)
):
raise EquipmentRegistryError("capture nested contracts are invalid")
_identifier(camera.get("source_id"), "capture camera source")
_digest(camera.get("initialization_sha256"), "capture camera initialization")
_identifier(calibration.get("slot_id"), "capture calibration slot")
_digest(calibration.get("sha256"), "capture calibration")
_identifier(adapter.get("adapter_id"), "capture adapter id")
_digest(adapter.get("adapter_sha256"), "capture adapter")
if value.get("media_epoch_policy") != "exactly-one-complete-epoch":
raise EquipmentRegistryError("capture media epoch policy is unsupported")
if value.get("seekable") is not True:
raise EquipmentRegistryError("capture profile must be seekable")
return RecordedCaptureProfile(
capture_profile_id=profile_id,
capture_profile_sha256=canonical_sha256(value),
equipment=equipment,
document=value,
)
def _identifier(value: object, label: str) -> str:
if not isinstance(value, str) or _IDENTIFIER.fullmatch(value) is None:
raise EquipmentRegistryError(f"{label} is invalid")
return value
def _digest(value: object, label: str) -> str:
if not isinstance(value, str) or _SHA256.fullmatch(value) is None:
raise EquipmentRegistryError(f"{label} digest is invalid")
return value
+39 -14
View File
@@ -2,11 +2,17 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Any, Literal from typing import TYPE_CHECKING, Any, Literal
if TYPE_CHECKING:
from .equipment import RecordedCaptureAttestation
SessionStatus = Literal["ready", "interrupted", "failed"] SessionStatus = Literal["ready", "interrupted", "failed"]
SessionModality = Literal["point-cloud", "trajectory", "video"] SessionModality = Literal["point-cloud", "trajectory", "video"]
LAB_REPLAY_CAPABILITY_SCHEMA = "missioncore.observation-lab-replay-capability/v1" LAB_REPLAY_CAPABILITY_SCHEMA = "missioncore.observation-lab-replay-capability/v1"
PORTABLE_LAB_REPLAY_CAPABILITY_SCHEMA = (
"missioncore.observation-lab-replay-capability/v2"
)
class SessionStoreError(RuntimeError): class SessionStoreError(RuntimeError):
@@ -33,28 +39,41 @@ class LayoutConflictError(SessionStoreError):
class LabReplayCapability: class LabReplayCapability:
"""Explicit, observation-only admission for a derived recorded replay.""" """Explicit, observation-only admission for a derived recorded replay."""
schema_version: Literal["missioncore.observation-lab-replay-capability/v1"] schema_version: Literal[
kind: Literal["canonical-recorded-rerun"] "missioncore.observation-lab-replay-capability/v1",
viewer_profile: Literal["recorded-session"] "missioncore.observation-lab-replay-capability/v2",
timeline: Literal["session_time"] ]
kind: Literal["canonical-recorded-rerun", "portable-result-review"]
viewer_profile: Literal["recorded-session", "portable-result"]
timeline: Literal["session_time", "result-defined"]
activation: Literal["explicit"] activation: Literal["explicit"]
commands_enabled: Literal[False] commands_enabled: Literal[False]
def __post_init__(self) -> None: def __post_init__(self) -> None:
expected_text = ( identity = (
LAB_REPLAY_CAPABILITY_SCHEMA,
"canonical-recorded-rerun",
"recorded-session",
"session_time",
"explicit",
)
if (
self.schema_version, self.schema_version,
self.kind, self.kind,
self.viewer_profile, self.viewer_profile,
self.timeline, self.timeline,
self.activation, self.activation,
) != expected_text or self.commands_enabled is not False: )
admitted = {
(
LAB_REPLAY_CAPABILITY_SCHEMA,
"canonical-recorded-rerun",
"recorded-session",
"session_time",
"explicit",
),
(
PORTABLE_LAB_REPLAY_CAPABILITY_SCHEMA,
"portable-result-review",
"portable-result",
"result-defined",
"explicit",
),
}
if identity not in admitted or self.commands_enabled is not False:
raise ValueError("LAB replay capability is invalid") raise ValueError("LAB replay capability is invalid")
def as_dict(self) -> dict[str, object]: def as_dict(self) -> dict[str, object]:
@@ -160,6 +179,7 @@ class SessionSummary:
total_bytes: int total_bytes: int
replayable: bool replayable: bool
origin: str origin: str
capture_attestation: RecordedCaptureAttestation | None = None
lab: LabSessionBinding | None = None lab: LabSessionBinding | None = None
def as_dict(self) -> dict[str, Any]: def as_dict(self) -> dict[str, Any]:
@@ -176,6 +196,11 @@ class SessionSummary:
"total_bytes": self.total_bytes, "total_bytes": self.total_bytes,
"replayable": self.replayable, "replayable": self.replayable,
"origin": self.origin, "origin": self.origin,
"capture_attestation": (
None
if self.capture_attestation is None
else self.capture_attestation.as_dict()
),
} }
if self.lab is not None: if self.lab is not None:
document["lab"] = self.lab.as_dict() document["lab"] = self.lab.as_dict()
+155 -15
View File
@@ -16,7 +16,15 @@ from typing import Any, Literal, cast
from uuid import uuid4 from uuid import uuid4
from k1link.artifacts import utc_now_iso from k1link.artifacts import utc_now_iso
from k1link.laboratory.canonical_recorded_migration import (
matches_historical_recorded_projection,
)
from .equipment import (
CaptureAttestationState,
EquipmentCaptureRegistry,
RecordedCaptureAttestation,
)
from .models import ( from .models import (
LabReplayCapability, LabReplayCapability,
LabSessionBinding, LabSessionBinding,
@@ -72,6 +80,16 @@ CREATE TABLE IF NOT EXISTS observation_sessions (
session_root TEXT NOT NULL, session_root TEXT NOT NULL,
created_at_utc TEXT NOT NULL, created_at_utc TEXT NOT NULL,
updated_at_utc TEXT NOT NULL updated_at_utc TEXT NOT NULL
,equipment_model_id TEXT
,equipment_model_sha256 TEXT
,equipment_display_name TEXT
,capture_profile_id TEXT
,capture_profile_sha256 TEXT
,capture_attestation_sha256 TEXT
,capture_attestation_state TEXT
CHECK (capture_attestation_state IN (
'attested', 'attested-with-legacy-evidence', 'unknown'
))
); );
CREATE INDEX IF NOT EXISTS observation_sessions_recent CREATE INDEX IF NOT EXISTS observation_sessions_recent
@@ -167,8 +185,15 @@ class SessionStore:
self.data_dir.chmod(0o700) self.data_dir.chmod(0o700)
self.database_path = self.data_dir / DATABASE_NAME self.database_path = self.data_dir / DATABASE_NAME
self._lock = threading.RLock() self._lock = threading.RLock()
self._equipment_registry = EquipmentCaptureRegistry.from_repository(
self.repository_root
)
self._initialize() self._initialize()
@property
def equipment_capture_registry(self) -> EquipmentCaptureRegistry | None:
return self._equipment_registry
def reconcile_archive(self, source: ObservationArchiveSource) -> tuple[str, ...]: def reconcile_archive(self, source: ObservationArchiveSource) -> tuple[str, ...]:
"""Reconcile one plugin-owned evidence namespace into the host catalog.""" """Reconcile one plugin-owned evidence namespace into the host catalog."""
@@ -538,6 +563,9 @@ class SessionStore:
else duration_seconds else duration_seconds
), ),
include_recorded_media=include_recorded_media, include_recorded_media=include_recorded_media,
require_recorded_replay=(
replay_capability.viewer_profile == "recorded-session"
),
) )
if stored_recorded_media_policy is None: if stored_recorded_media_policy is None:
connection.execute( connection.execute(
@@ -565,8 +593,12 @@ class SessionStore:
"replayable, origin, source_count, total_bytes, " "replayable, origin, source_count, total_bytes, "
"primary_replay_artifact_id, timeline_origin_epoch_ns, " "primary_replay_artifact_id, timeline_origin_epoch_ns, "
"timeline_origin_monotonic_ns, allowed_root, session_root, " "timeline_origin_monotonic_ns, allowed_root, session_root, "
"created_at_utc, updated_at_utc) " "created_at_utc, updated_at_utc, equipment_model_id, "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", "equipment_model_sha256, equipment_display_name, capture_profile_id, "
"capture_profile_sha256, capture_attestation_sha256, "
"capture_attestation_state) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, "
"?, ?, ?, ?, ?, ?, ?)",
( (
session_id, session_id,
source["plugin_id"], source["plugin_id"],
@@ -592,6 +624,13 @@ class SessionStore:
source["session_root"], source["session_root"],
published_at, published_at,
published_at, published_at,
source["equipment_model_id"],
source["equipment_model_sha256"],
source["equipment_display_name"],
source["capture_profile_id"],
source["capture_profile_sha256"],
source["capture_attestation_sha256"],
source["capture_attestation_state"],
), ),
) )
connection.execute( connection.execute(
@@ -972,6 +1011,17 @@ class SessionStore:
("primary_replay_artifact_id", "TEXT"), ("primary_replay_artifact_id", "TEXT"),
("timeline_origin_epoch_ns", "INTEGER"), ("timeline_origin_epoch_ns", "INTEGER"),
("timeline_origin_monotonic_ns", "INTEGER"), ("timeline_origin_monotonic_ns", "INTEGER"),
("equipment_model_id", "TEXT"),
("equipment_model_sha256", "TEXT"),
("equipment_display_name", "TEXT"),
("capture_profile_id", "TEXT"),
("capture_profile_sha256", "TEXT"),
("capture_attestation_sha256", "TEXT"),
(
"capture_attestation_state",
"TEXT CHECK (capture_attestation_state IN ("
"'attested', 'attested-with-legacy-evidence', 'unknown'))",
),
): ):
if name in session_columns: if name in session_columns:
continue continue
@@ -1054,6 +1104,16 @@ class SessionStore:
_validate_candidate_replay(candidate, artifacts) _validate_candidate_replay(candidate, artifacts)
now = utc_now_iso() now = utc_now_iso()
modalities_json = json.dumps(list(candidate.modalities), separators=(",", ":")) modalities_json = json.dumps(list(candidate.modalities), separators=(",", ":"))
capture_attestation = (
None
if self._equipment_registry is None
else self._equipment_registry.attest(
session_id=candidate.session_id,
plugin_id=source.plugin_id,
archive_id=source.archive_id,
candidate=candidate,
)
)
with self._lock, self._connect() as connection: with self._lock, self._connect() as connection:
connection.execute("BEGIN IMMEDIATE") connection.execute("BEGIN IMMEDIATE")
@@ -1086,8 +1146,12 @@ class SessionStore:
"duration_seconds, modalities_json, replayable, origin, source_count, " "duration_seconds, modalities_json, replayable, origin, source_count, "
"total_bytes, primary_replay_artifact_id, timeline_origin_epoch_ns, " "total_bytes, primary_replay_artifact_id, timeline_origin_epoch_ns, "
"timeline_origin_monotonic_ns, allowed_root, " "timeline_origin_monotonic_ns, allowed_root, "
"session_root, created_at_utc, updated_at_utc) " "session_root, created_at_utc, updated_at_utc, equipment_model_id, "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) " "equipment_model_sha256, equipment_display_name, capture_profile_id, "
"capture_profile_sha256, capture_attestation_sha256, "
"capture_attestation_state) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, "
"?, ?, ?, ?, ?, ?, ?) "
"ON CONFLICT(session_id) DO UPDATE SET " "ON CONFLICT(session_id) DO UPDATE SET "
"plugin_id = excluded.plugin_id, archive_id = excluded.archive_id, " "plugin_id = excluded.plugin_id, archive_id = excluded.archive_id, "
"display_name = excluded.display_name, status = excluded.status, " "display_name = excluded.display_name, status = excluded.status, "
@@ -1100,6 +1164,13 @@ class SessionStore:
"primary_replay_artifact_id = excluded.primary_replay_artifact_id, " "primary_replay_artifact_id = excluded.primary_replay_artifact_id, "
"timeline_origin_epoch_ns = excluded.timeline_origin_epoch_ns, " "timeline_origin_epoch_ns = excluded.timeline_origin_epoch_ns, "
"timeline_origin_monotonic_ns = excluded.timeline_origin_monotonic_ns, " "timeline_origin_monotonic_ns = excluded.timeline_origin_monotonic_ns, "
"equipment_model_id = excluded.equipment_model_id, "
"equipment_model_sha256 = excluded.equipment_model_sha256, "
"equipment_display_name = excluded.equipment_display_name, "
"capture_profile_id = excluded.capture_profile_id, "
"capture_profile_sha256 = excluded.capture_profile_sha256, "
"capture_attestation_sha256 = excluded.capture_attestation_sha256, "
"capture_attestation_state = excluded.capture_attestation_state, "
"updated_at_utc = excluded.updated_at_utc", "updated_at_utc = excluded.updated_at_utc",
( (
candidate.session_id, candidate.session_id,
@@ -1122,6 +1193,37 @@ class SessionStore:
str(session_root), str(session_root),
created_at, created_at,
now, now,
(
None
if capture_attestation is None
else capture_attestation.equipment_model_id
),
(
None
if capture_attestation is None
else capture_attestation.equipment_model_sha256
),
(
None
if capture_attestation is None
else capture_attestation.equipment_display_name
),
(
None
if capture_attestation is None
else capture_attestation.capture_profile_id
),
(
None
if capture_attestation is None
else capture_attestation.capture_profile_sha256
),
(
None
if capture_attestation is None
else capture_attestation.capture_attestation_sha256
),
None if capture_attestation is None else capture_attestation.state,
), ),
) )
connection.execute( connection.execute(
@@ -1246,6 +1348,7 @@ def _validate_existing_lab_projection(
run_created_at_utc: str, run_created_at_utc: str,
duration_seconds: float | None, duration_seconds: float | None,
include_recorded_media: bool, include_recorded_media: bool,
require_recorded_replay: bool = True,
) -> None: ) -> None:
"""Validate the immutable companion snapshot without rereading a mutable source row.""" """Validate the immutable companion snapshot without rereading a mutable source row."""
@@ -1282,7 +1385,7 @@ def _validate_existing_lab_projection(
raise SessionIntegrityError( raise SessionIntegrityError(
"LAB session id is already bound to different catalog metadata" "LAB session id is already bound to different catalog metadata"
) )
if ( if require_recorded_replay and (
summary["status"] != "ready" summary["status"] != "ready"
or summary["replayable"] != 1 or summary["replayable"] != 1
or summary["primary_replay_artifact_id"] is None or summary["primary_replay_artifact_id"] is None
@@ -1546,16 +1649,12 @@ def _migrate_canonical_replay_capabilities(connection: sqlite3.Connection) -> No
"SELECT * FROM observation_sessions WHERE session_id = ?", "SELECT * FROM observation_sessions WHERE session_id = ?",
(row["source_session_id"],), (row["source_session_id"],),
).fetchone() ).fetchone()
if ( if source_summary is None or not matches_historical_recorded_projection(
summary["display_name"] display_name=summary["display_name"],
!= "RAVNOVES004TREE · полный маршрут восприятия" duration_seconds=summary["duration_seconds"],
or summary["duration_seconds"] != 718.0 run_created_at_utc=row["run_created_at_utc"],
or row["run_created_at_utc"] source_plugin_id=source_summary["plugin_id"],
!= "2026-08-29T18:05:11.329061+00:00" source_archive_id=source_summary["archive_id"],
or source_summary is None
or source_summary["plugin_id"]
!= "nodedc.device.xgrids-lixelkity-k1"
or source_summary["archive_id"] != "xgrids-k1.viewer-live.evidence"
): ):
raise SessionIntegrityError( raise SessionIntegrityError(
"canonical rolling LAB projection metadata is invalid" "canonical rolling LAB projection metadata is invalid"
@@ -1725,6 +1824,9 @@ def _require_capability_owned_lab_projection(
run_created_at_utc=binding.run_created_at_utc, run_created_at_utc=binding.run_created_at_utc,
duration_seconds=summary["duration_seconds"], duration_seconds=summary["duration_seconds"],
include_recorded_media=bool(lab["include_recorded_media"]), include_recorded_media=bool(lab["include_recorded_media"]),
require_recorded_replay=(
binding.replay_capability.viewer_profile == "recorded-session"
),
) )
return summary, lab return summary, lab
@@ -1749,10 +1851,48 @@ def _summary_from_row(
total_bytes=row["total_bytes"], total_bytes=row["total_bytes"],
replayable=bool(row["replayable"]), replayable=bool(row["replayable"]),
origin=row["origin"], origin=row["origin"],
capture_attestation=_capture_attestation_from_row(row),
lab=lab, lab=lab,
) )
def _capture_attestation_from_row(
row: sqlite3.Row,
) -> RecordedCaptureAttestation | None:
values = (
row["equipment_model_id"],
row["equipment_model_sha256"],
row["equipment_display_name"],
row["capture_profile_id"],
row["capture_profile_sha256"],
row["capture_attestation_sha256"],
row["capture_attestation_state"],
)
if all(value is None for value in values):
return None
if any(not isinstance(value, str) or not value for value in values):
raise SessionIntegrityError("stored capture attestation is incomplete")
state = row["capture_attestation_state"]
if state not in {"attested", "attested-with-legacy-evidence", "unknown"}:
raise SessionIntegrityError("stored capture attestation state is invalid")
for digest in (
row["equipment_model_sha256"],
row["capture_profile_sha256"],
row["capture_attestation_sha256"],
):
if SHA256_PATTERN.fullmatch(digest) is None:
raise SessionIntegrityError("stored capture attestation digest is invalid")
return RecordedCaptureAttestation(
equipment_model_id=row["equipment_model_id"],
equipment_model_sha256=row["equipment_model_sha256"],
equipment_display_name=row["equipment_display_name"],
capture_profile_id=row["capture_profile_id"],
capture_profile_sha256=row["capture_profile_sha256"],
capture_attestation_sha256=row["capture_attestation_sha256"],
state=cast(CaptureAttestationState, state),
)
def _lab_binding_from_row(row: sqlite3.Row) -> LabSessionBinding: def _lab_binding_from_row(row: sqlite3.Row) -> LabSessionBinding:
try: try:
provenance = json.loads(row["provenance_json"]) provenance = json.loads(row["provenance_json"])
+5
View File
@@ -383,6 +383,11 @@ def build_session_router(
"modalities": list(item.modalities), "modalities": list(item.modalities),
"duration_seconds": item.duration_seconds or 0.0, "duration_seconds": item.duration_seconds or 0.0,
"replayable": item.replayable, "replayable": item.replayable,
"capture_attestation": (
None
if item.capture_attestation is None
else item.capture_attestation.as_dict()
),
**( **(
{ {
"lab": lab_catalog_document(item, lab_contract) "lab": lab_catalog_document(item, lab_contract)
@@ -0,0 +1,209 @@
from __future__ import annotations
from pathlib import Path
from k1link.observatory.portable_run_definitions import PortableRunDefinitionRegistry
from k1link.observatory.portable_setup_projection import PortableSetupProjector
from k1link.sessions.equipment import (
EquipmentCaptureRegistry,
RecordedCaptureAttestation,
)
from k1link.sessions.models import (
ObservationArtifactCandidate,
ObservationSessionCandidate,
SessionSource,
SessionSummary,
)
from k1link.sessions.plugin_contract import ObservationArchiveSource
from k1link.sessions.store import SessionStore
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
def _equipment_registry() -> EquipmentCaptureRegistry:
registry = EquipmentCaptureRegistry.from_repository(REPOSITORY_ROOT)
assert registry is not None
return registry
def _definition_registry() -> PortableRunDefinitionRegistry:
return PortableRunDefinitionRegistry.from_file(
REPOSITORY_ROOT / "config" / "observatory-portable-run-definitions.json"
)
def test_k1_equipment_and_capture_profiles_have_exact_content_identities() -> None:
registry = _equipment_registry()
assert len(registry.equipment_models) == 1
assert len(registry.capture_profiles) == 1
equipment = registry.equipment_models[0]
capture = registry.capture_profiles[0]
assert equipment.equipment_model_id == "xgrids.lixelkity-k1"
assert equipment.equipment_model_sha256 == (
"c95d7183ffd2e425892fd9ddfc4cd8dcbccc0231289127ddc14db6b52bbb92aa"
)
assert capture.equipment == equipment
for definition in _definition_registry().definitions:
assert (
registry.compatible_profile_for_requirements(
definition.source_requirements.as_dict()
)
== capture
)
def test_session_store_persists_immutable_k1_capture_attestation(tmp_path: Path) -> None:
archive_root = tmp_path / "evidence"
session_root = archive_root / "recording-001"
session_root.mkdir(parents=True)
raw = session_root / "capture.bin"
video = session_root / "camera.mp4"
raw.write_bytes(b"sealed spatial capture")
video.write_bytes(b"sealed camera capture")
candidate = _k1_candidate(archive_root, session_root, raw, video)
source = ObservationArchiveSource(
plugin_id="nodedc.device.xgrids-lixelkity-k1",
archive_id="xgrids-k1.viewer-live.evidence",
root=archive_root,
discover=lambda _root: (candidate,),
)
data_dir = tmp_path / "data"
store = SessionStore(REPOSITORY_ROOT, data_dir=data_dir)
assert store.reconcile_archive(source) == ("recording-001",)
attestation = store.get_session("recording-001").summary.capture_attestation
assert attestation is not None
assert attestation.equipment_model_id == "xgrids.lixelkity-k1"
assert attestation.capture_profile_id == "xgrids-k1.viewer-live.fw-3.0.2.v1"
assert attestation.state == "attested-with-legacy-evidence"
reopened = SessionStore(REPOSITORY_ROOT, data_dir=data_dir)
assert reopened.get_session("recording-001").summary.capture_attestation == attestation
def test_other_equipment_is_blocked_before_any_profile_probe() -> None:
registry = _equipment_registry()
calls: list[str] = []
class Probe:
def probe(self, **kwargs: str) -> object:
calls.append(kwargs["source_session_id"])
raise AssertionError("incompatible equipment must not reach source probing")
projector = PortableSetupProjector(
registry=_definition_registry(),
capability_probe=Probe(),
dispatch_available=True,
equipment_capture_registry=registry,
)
source = _summary(
RecordedCaptureAttestation(
equipment_model_id="example.other-scanner",
equipment_model_sha256="1" * 64,
equipment_display_name="Other scanner",
capture_profile_id="example.other.capture.v1",
capture_profile_sha256="2" * 64,
capture_attestation_sha256="3" * 64,
state="attested",
)
)
catalog = projector.catalog(source)
assert calls == []
assert len(catalog["setups"]) == 2
for setup in catalog["setups"]:
assert setup["source_compatibility"]["compatible"] is False
assert setup["source_compatibility"]["reason_code"] == (
"equipment-model-mismatch"
)
assert setup["preflight"]["submission_allowed"] is False
def _summary(attestation: RecordedCaptureAttestation) -> SessionSummary:
return SessionSummary(
session_id="other-equipment-recording-001",
display_name="Other equipment",
status="ready",
started_at_utc="2026-09-01T10:00:00Z",
completed_at_utc="2026-09-01T10:01:00Z",
duration_seconds=60.0,
modalities=("point-cloud", "trajectory", "video"),
source_count=3,
total_bytes=1,
replayable=True,
origin="example.other.archive",
capture_attestation=attestation,
)
def _k1_candidate(
archive_root: Path,
session_root: Path,
raw: Path,
video: Path,
) -> ObservationSessionCandidate:
return ObservationSessionCandidate(
session_id="recording-001",
display_name="K1 recording",
status="ready",
started_at_utc="2026-09-01T10:00:00Z",
completed_at_utc="2026-09-01T10:01:00Z",
duration_seconds=60.0,
modalities=("point-cloud", "trajectory", "video"),
replayable=True,
total_bytes=raw.stat().st_size + video.stat().st_size,
allowed_root=archive_root,
session_root=session_root,
primary_replay_artifact_id="raw-transport-primary",
timeline_origin_epoch_ns=1,
timeline_origin_monotonic_ns=1,
sources=(
SessionSource(
source_id="sensor.lidar.primary",
semantic_channel_id="spatial.point-cloud.recorded",
modality="point-cloud",
status="recorded",
seekable=True,
artifact_id="raw-transport-primary",
),
SessionSource(
source_id="spatial.trajectory",
semantic_channel_id="spatial.pose.recorded",
modality="trajectory",
status="recorded",
seekable=True,
artifact_id="raw-transport-primary",
),
SessionSource(
source_id="sensor.camera.right",
semantic_channel_id="camera.video.recorded",
modality="video",
status="recorded",
seekable=True,
artifact_id="camera-right",
),
),
artifacts=(
ObservationArtifactCandidate(
artifact_id="raw-transport-primary",
kind="native-recording",
media_type="application/x-k1-recording",
locator=raw,
byte_length=raw.stat().st_size,
replay_byte_length=raw.stat().st_size,
sha256=None,
integrity_status="validated-structure",
),
ObservationArtifactCandidate(
artifact_id="camera-right",
kind="recorded-video",
media_type="video/mp4",
locator=video,
byte_length=video.stat().st_size,
replay_byte_length=0,
sha256=None,
integrity_status="validated-structure",
),
),
)
@@ -174,22 +174,23 @@ def test_calculation_profile_policies_cover_both_exact_portable_definitions() ->
assert resolved["m49-tgs-portable-v2"].display_name == PORTABLE_M49_DISPLAY_NAME assert resolved["m49-tgs-portable-v2"].display_name == PORTABLE_M49_DISPLAY_NAME
def test_new_compatible_source_passes_capability_but_uninstalled_executor_blocks() -> None: def test_new_compatible_source_passes_capability_but_disabled_dispatch_blocks() -> None:
source = _source(NEW_SESSION_ID) source = _source(NEW_SESSION_ID)
setup = _projector(lambda session_id: _capability(session_id)).project(source) setup = _projector(lambda session_id: _capability(session_id)).project(source)
assert setup["source_compatibility"] == { assert setup["source_compatibility"] == {
"outcome": "pass", "outcome": "pass",
"compatible": True, "compatible": True,
"reason_code": "source-compatible",
"reason": "Запись соответствует требованиям EoMT + DDRNet.", "reason": "Запись соответствует требованиям EoMT + DDRNet.",
} }
assert setup["executor"]["state"] == "not-installed" assert setup["executor"]["state"] == "ready"
assert setup["executor"]["ready"] is False assert setup["executor"]["ready"] is True
assert setup["existing_results"] == [] assert setup["existing_results"] == []
assert setup["preflight"] == { assert setup["preflight"] == {
"outcome": "blocked", "outcome": "blocked",
"action": "blocked", "action": "blocked",
"reason": "Вычислительный контур LAB V1 пока недоступен.", "reason": "Server-side проверка и постановка portable-сетапа в очередь недоступны.",
"submission_allowed": False, "submission_allowed": False,
"existing_result_ids": [], "existing_result_ids": [],
} }
@@ -207,7 +208,7 @@ def test_legacy_vegetation_result_is_never_existing_for_portable_v2() -> None:
assert setup["preflight"] == { assert setup["preflight"] == {
"outcome": "blocked", "outcome": "blocked",
"action": "blocked", "action": "blocked",
"reason": "Вычислительный контур LAB V1 пока недоступен.", "reason": "Server-side проверка и постановка portable-сетапа в очередь недоступны.",
"submission_allowed": False, "submission_allowed": False,
"existing_result_ids": [], "existing_result_ids": [],
} }
@@ -225,6 +226,7 @@ def test_real_capability_probe_rejection_wins_over_summary() -> None:
assert setup["source_compatibility"] == { assert setup["source_compatibility"] == {
"outcome": "blocked", "outcome": "blocked",
"compatible": False, "compatible": False,
"reason_code": "source-contract-mismatch",
"reason": "Запись не соответствует требованиям EoMT + DDRNet.", "reason": "Запись не соответствует требованиям EoMT + DDRNet.",
} }
assert setup["existing_results"] == [] assert setup["existing_results"] == []
+1
View File
@@ -292,6 +292,7 @@ def test_session_router_lists_details_and_dispatches_opaque_replay(tmp_path: Pat
"modalities": ["point-cloud", "trajectory"], "modalities": ["point-cloud", "trajectory"],
"duration_seconds": 1440.1, "duration_seconds": 1440.1,
"replayable": True, "replayable": True,
"capture_attestation": None,
} }
assert detail["timeline"]["seekable"] is True assert detail["timeline"]["seekable"] is True
assert detail["modalities"] == ["point-cloud", "trajectory"] assert detail["modalities"] == ["point-cloud", "trajectory"]