From 104e54f4dd841e25bd976de36fffcd5975d38e8c Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Mon, 31 Aug 2026 19:46:58 +0300 Subject: [PATCH] refactor(lab): separate release contract identity --- ...lab-v1-eomt-ddrnet-executor-candidate.json | 14 +++--- .../observatory/portable_lab_v1_executor.py | 38 +++++++++------- .../observatory/portable_lab_v1_worker.py | 7 ++- .../observatory/portable_run_definitions.py | 32 ++++++++++++++ ...st_observatory_portable_lab_v1_executor.py | 44 ++++++++++++++++++- ...st_observatory_portable_run_definitions.py | 2 + 6 files changed, 111 insertions(+), 26 deletions(-) diff --git a/experiments/perception/worker/observatory_portable/lab-v1-eomt-ddrnet-executor-candidate.json b/experiments/perception/worker/observatory_portable/lab-v1-eomt-ddrnet-executor-candidate.json index 312a5f6..b03c824 100644 --- a/experiments/perception/worker/observatory_portable/lab-v1-eomt-ddrnet-executor-candidate.json +++ b/experiments/perception/worker/observatory_portable/lab-v1-eomt-ddrnet-executor-candidate.json @@ -142,17 +142,17 @@ }, { "asset_id": "lab-v1-portable-contracts", - "byte_length": 114905, + "byte_length": 115196, "kind": "repository-file", "repository_path": "src/k1link/observatory/portable_lab_v1_executor.py", - "sha256": "c8ba210336138617a1cdb02ed2795935b25a265ae7b905f75cd0338774338c55" + "sha256": "2a18fe862f0934c78e86156708d8b80a3c31644a68829c8e673a0f48538f3473" }, { "asset_id": "lab-v1-portable-worker", - "byte_length": 46859, + "byte_length": 47008, "kind": "repository-file", "repository_path": "src/k1link/observatory/portable_lab_v1_worker.py", - "sha256": "ad00418d59b793328a281fa432e450919690fe4957f024f296e2d125c1b3c03a" + "sha256": "0a954b5d4d2f3cdd588d58ba4234141c32992b4b46380a03b621060c25c4ac23" }, { "asset_id": "portable-result-contracts", @@ -189,7 +189,7 @@ "navigation_or_safety_accepted": false, "production_accepted": false }, - "candidate_sha256": "6e7adbf25acbc3a9daedf8671ee57a7c00043ea73897bc9abf1e9947eaa5d0bd", + "candidate_sha256": "546c74fb88b9571996cecc41fa915044e3907365b8891b7fceb297fb74a837c7", "declared_blockers": [ "combined-executor-entrypoint-uninstalled", "combined-executor-image-unsealed", @@ -200,7 +200,7 @@ "worker-installation-receipt-unavailable" ], "definition_id": "lab-v1-eomt-ddrnet-portable", - "definition_sha256": "3692d41cec3949f348a36eb60a501fb2cd483fed1645679b0ec58061a2fc6dc2", + "definition_contract_sha256": "504e197db72e8bad258a2e4ec117d6d8247297847bd651470f8d43601e2e1fc7", "definition_version": 2, "executor_image_sha256": null, "phases": [ @@ -213,6 +213,6 @@ ], "release_id": "lab-v1-eomt-ddrnet-worker006-candidate-v2", "result_contract_sha256": "b3dfaa8e20a0f22fc510d062ac469f010a3281c650059d9ea134f0b3ccb38d9a", - "schema_version": "missioncore.observatory-portable-lab-v1-executor-candidate/v1", + "schema_version": "missioncore.observatory-portable-lab-v1-executor-candidate/v2", "setup_id": "lab-v1-eomt-ddrnet-portable-v1" } diff --git a/src/k1link/observatory/portable_lab_v1_executor.py b/src/k1link/observatory/portable_lab_v1_executor.py index 5dd561f..38054ce 100644 --- a/src/k1link/observatory/portable_lab_v1_executor.py +++ b/src/k1link/observatory/portable_lab_v1_executor.py @@ -65,13 +65,13 @@ PORTABLE_LAB_V1_RESULT_IDENTITY_SCHEMA: Final = ( "missioncore.recorded-eomt-ddrnet-review-identity/v2" ) PORTABLE_LAB_V1_RELEASE_SCHEMA: Final = ( - "missioncore.observatory-portable-lab-v1-executor-candidate/v1" + "missioncore.observatory-portable-lab-v1-executor-candidate/v2" ) PORTABLE_LAB_V1_RELEASE_IDENTITY_SCHEMA: Final = ( - "missioncore.observatory-portable-lab-v1-executor-candidate-identity/v1" + "missioncore.observatory-portable-lab-v1-executor-candidate-identity/v2" ) PORTABLE_LAB_V1_EXECUTOR_SEAL_SCHEMA: Final = ( - "missioncore.observatory-portable-lab-v1-executor-seal/v1" + "missioncore.observatory-portable-lab-v1-executor-seal/v2" ) PORTABLE_LAB_V1_DDRNET_PROFILE_SCHEMA: Final = ( "missioncore.lab-v1-eomt-ddrnet-portable-profile/v2" @@ -452,7 +452,7 @@ class PortableLabV1ReleaseCandidate: setup_id: str definition_id: str definition_version: int - definition_sha256: str + definition_contract_sha256: str result_contract_sha256: str executor_image_sha256: str | None assets: tuple[PortableLabV1ReleaseAsset, ...] @@ -474,7 +474,10 @@ class PortableLabV1ReleaseCandidate: or self.definition_version < 1 ): raise PortableLabV1ReleaseError("release definition version is invalid") - _digest(self.definition_sha256, "release definition sha256") + _digest( + self.definition_contract_sha256, + "release definition contract sha256", + ) _digest(self.result_contract_sha256, "release result contract sha256") if self.executor_image_sha256 is not None: _digest(self.executor_image_sha256, "release executor image sha256") @@ -523,7 +526,7 @@ class PortableLabV1ReleaseCandidate: "setup_id", "definition_id", "definition_version", - "definition_sha256", + "definition_contract_sha256", "result_contract_sha256", "executor_image_sha256", "assets", @@ -563,8 +566,9 @@ class PortableLabV1ReleaseCandidate: definition_version=_positive_int( document["definition_version"], "release definition version" ), - definition_sha256=_string( - document["definition_sha256"], "release definition sha256" + definition_contract_sha256=_string( + document["definition_contract_sha256"], + "release definition contract sha256", ), result_contract_sha256=_string( document["result_contract_sha256"], @@ -587,7 +591,7 @@ class PortableLabV1ReleaseCandidate: "setup_id": self.setup_id, "definition_id": self.definition_id, "definition_version": self.definition_version, - "definition_sha256": self.definition_sha256, + "definition_contract_sha256": self.definition_contract_sha256, "result_contract_sha256": self.result_contract_sha256, "executor_image_sha256": self.executor_image_sha256, "assets": [asset.as_dict() for asset in self.assets], @@ -601,7 +605,8 @@ class PortableLabV1ReleaseCandidate: definition.setup_id != self.setup_id or definition.definition_id != self.definition_id or definition.version != self.definition_version - or definition.definition_sha256 != self.definition_sha256 + or definition.executable_contract_sha256 + != self.definition_contract_sha256 or definition.result_contract.contract_sha256 != self.result_contract_sha256 ): @@ -661,7 +666,7 @@ class PortableLabV1ReleaseCandidate: "schema_version": PORTABLE_LAB_V1_EXECUTOR_SEAL_SCHEMA, "release_id": self.release_id, "candidate_sha256": self.candidate_sha256, - "definition_sha256": self.definition_sha256, + "definition_contract_sha256": self.definition_contract_sha256, "executor_image_sha256": self.executor_image_sha256, "asset_sha256s": [asset.sha256 for asset in self.assets], "authority": dict(OBSERVATION_ONLY_AUTHORITY), @@ -669,7 +674,7 @@ class PortableLabV1ReleaseCandidate: return PortableLabV1ExecutorSeal( release_id=self.release_id, candidate_sha256=self.candidate_sha256, - definition_sha256=self.definition_sha256, + definition_contract_sha256=self.definition_contract_sha256, executor_image_sha256=self.executor_image_sha256, release_sha256=canonical_sha256(identity), ) @@ -681,7 +686,7 @@ class PortableLabV1ExecutorSeal: release_id: str candidate_sha256: str - definition_sha256: str + definition_contract_sha256: str executor_image_sha256: str release_sha256: str @@ -689,7 +694,10 @@ class PortableLabV1ExecutorSeal: _pattern(self.release_id, _IDENTIFIER, "executor seal release id") for value, label in ( (self.candidate_sha256, "executor seal candidate sha256"), - (self.definition_sha256, "executor seal definition sha256"), + ( + self.definition_contract_sha256, + "executor seal definition contract sha256", + ), (self.executor_image_sha256, "executor seal image sha256"), (self.release_sha256, "executor seal release sha256"), ): @@ -700,7 +708,7 @@ class PortableLabV1ExecutorSeal: "schema_version": PORTABLE_LAB_V1_EXECUTOR_SEAL_SCHEMA, "release_id": self.release_id, "candidate_sha256": self.candidate_sha256, - "definition_sha256": self.definition_sha256, + "definition_contract_sha256": self.definition_contract_sha256, "executor_image_sha256": self.executor_image_sha256, "release_sha256": self.release_sha256, "authority": dict(OBSERVATION_ONLY_AUTHORITY), diff --git a/src/k1link/observatory/portable_lab_v1_worker.py b/src/k1link/observatory/portable_lab_v1_worker.py index 1c91617..1eed1c3 100644 --- a/src/k1link/observatory/portable_lab_v1_worker.py +++ b/src/k1link/observatory/portable_lab_v1_worker.py @@ -505,7 +505,7 @@ def compose_lab_v1_portable_executor_adapter( ) -> PortableWorkerExecutorAdapter: """Compose the shared Worker ports without enabling or registering them.""" - _verify_candidate_release(candidate, installation) + _verify_candidate_release(candidate, installation, definition) return PortableWorkerExecutorAdapter( candidate=candidate, definition=definition, @@ -560,6 +560,7 @@ def _verify_runtime_plan( def _verify_candidate_release( candidate: PortableWorkerRuntimeCandidate, installation: PortableLabV1RunnerInstallation, + definition: PortableRunDefinition, ) -> None: release = installation.release release_assets = {asset.asset_id: asset for asset in release.assets} @@ -569,7 +570,9 @@ def _verify_candidate_release( candidate.setup_id != release.setup_id or candidate.definition_id != release.definition_id or candidate.definition_version != release.definition_version - or candidate.definition_sha256 != release.definition_sha256 + or candidate.definition_sha256 != definition.definition_sha256 + or release.definition_contract_sha256 + != definition.executable_contract_sha256 or candidate.result_contract_sha256 != release.result_contract_sha256 or tuple(phase.phase_id for phase in candidate.phases) != PORTABLE_LAB_V1_RUNTIME_PHASES or executor is None diff --git a/src/k1link/observatory/portable_run_definitions.py b/src/k1link/observatory/portable_run_definitions.py index 834c2ff..a1cf39f 100644 --- a/src/k1link/observatory/portable_run_definitions.py +++ b/src/k1link/observatory/portable_run_definitions.py @@ -32,6 +32,9 @@ PORTABLE_RUN_DEFINITION_REGISTRY_SCHEMA: Final = ( PORTABLE_RUN_DEFINITION_IDENTITY_SCHEMA: Final = ( "missioncore.observatory-portable-run-definition-identity/v2" ) +PORTABLE_RUN_DEFINITION_EXECUTABLE_CONTRACT_SCHEMA: Final = ( + "missioncore.observatory-portable-run-definition-executable-contract/v1" +) PORTABLE_SOURCE_ADAPTER_IDENTITY_SCHEMA: Final = "missioncore.portable-source-adapter/v1" PORTABLE_MODEL_MANIFEST_SCHEMA: Final = "missioncore.observatory-portable-model-manifest/v2" PORTABLE_RESOURCE_PROFILE_SCHEMA: Final = "missioncore.observatory-portable-resource-profile/v2" @@ -571,6 +574,35 @@ class PortableRunDefinition: def learned_models(self) -> tuple[str, ...]: return tuple(model.release_id for model in self.models) + @property + def executable_contract_sha256(self) -> str: + """Return the stable executable contract, independent of installation state. + + Executor availability is deliberately excluded. An executor release may + bind this digest before the resulting release identity is written back to + the full RunDefinition without creating a definition/release digest cycle. + """ + + return canonical_sha256(self.executable_contract_document()) + + def executable_contract_document(self) -> dict[str, object]: + """Return the canonical computation contract without executor availability.""" + + return { + "schema_version": PORTABLE_RUN_DEFINITION_EXECUTABLE_CONTRACT_SCHEMA, + "setup_id": self.setup_id, + "definition_id": self.definition_id, + "version": self.version, + "source_requirements": self.source_requirements.as_dict(), + "source_adapter": self.source_adapter.as_dict(), + "components": [component.as_dict() for component in self.components], + "models": [model.as_dict() for model in self.models], + "model_manifest_sha256": self.model_manifest_sha256, + "resource_profile": self.resource_profile.as_dict(), + "result_contract": self.result_contract.as_dict(), + "authority": self.authority.as_dict(), + } + def identity_document(self) -> dict[str, object]: """Return the complete canonical executable identity.""" diff --git a/tests/test_observatory_portable_lab_v1_executor.py b/tests/test_observatory_portable_lab_v1_executor.py index 3a64373..c610bb4 100644 --- a/tests/test_observatory_portable_lab_v1_executor.py +++ b/tests/test_observatory_portable_lab_v1_executor.py @@ -840,14 +840,16 @@ def _release_for_definition( ) identity = release.identity_document() identity["definition_version"] = definition.version - identity["definition_sha256"] = definition.definition_sha256 + identity["definition_contract_sha256"] = ( + definition.executable_contract_sha256 + ) identity["executor_image_sha256"] = "f" * 64 identity["assets"] = [asset.as_dict() for asset in assets] identity["declared_blockers"] = [] return replace( release, definition_version=definition.version, - definition_sha256=definition.definition_sha256, + definition_contract_sha256=definition.executable_contract_sha256, executor_image_sha256="f" * 64, assets=assets, declared_blockers=(), @@ -895,6 +897,39 @@ def test_release_candidate_matches_repository_but_stays_honestly_blocked( ) +def test_release_seal_survives_full_definition_executor_promotion() -> None: + blocked = _definition() + release = _release_for_definition(blocked) + inspection = PortableLabV1ReleaseInspection( + candidate_sha256=release.candidate_sha256, + matched_assets=tuple(asset.asset_id for asset in release.assets), + blockers=(), + ready=True, + ) + seal = release.seal(inspection) + ready_executor = replace( + blocked.executor, + state="ready", + release_id=seal.release_id, + release_sha256=seal.release_sha256, + image_sha256=seal.executor_image_sha256, + reason_code=None, + reason=None, + ) + identity = blocked.identity_document() + identity["executor"] = ready_executor.identity_document() + promoted = replace( + blocked, + executor=ready_executor, + definition_sha256=canonical_sha256(identity), + ) + + assert promoted.definition_sha256 != blocked.definition_sha256 + assert promoted.executable_contract_sha256 == blocked.executable_contract_sha256 + assert seal.definition_contract_sha256 == blocked.executable_contract_sha256 + release.bind_definition(promoted) + + def test_source_materialization_and_effective_config_are_recording_independent( tmp_path: Path, ) -> None: @@ -1034,6 +1069,11 @@ def test_release_manifest_is_digest_fenced() -> None: repository_root=REPOSITORY_ROOT, ) + assert document["schema_version"].endswith("/v2") + assert document["definition_contract_sha256"] == ( + _definition().executable_contract_sha256 + ) + assert "definition_sha256" not in document assert document["executor_image_sha256"] is None with pytest.raises(Exception, match="identity digest changed"): replace(release, candidate_sha256="f" * 64) diff --git a/tests/test_observatory_portable_run_definitions.py b/tests/test_observatory_portable_run_definitions.py index 90b0a43..2807f23 100644 --- a/tests/test_observatory_portable_run_definitions.py +++ b/tests/test_observatory_portable_run_definitions.py @@ -324,6 +324,8 @@ def test_conversion_to_recorded_definition_requires_and_preserves_sealed_identit recorded = ready.to_recorded_run_definition() + assert ready.definition_sha256 != blocked.definition_sha256 + assert ready.executable_contract_sha256 == blocked.executable_contract_sha256 assert recorded.setup_id == ready.setup_id assert recorded.definition_sha256 == ready.definition_sha256 assert recorded.source_adapter_sha256 == ready.source_adapter.contract_sha256