From 11a146011f7c59756d33e272737c9c84977223bc Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Mon, 31 Aug 2026 20:56:29 +0300 Subject: [PATCH] feat(worker): install combined observatory profiles --- .../Install-Worker006AgentImage.ps1 | 698 ++++++++ ...lab-v1-eomt-ddrnet-executor-candidate.json | 6 +- .../promote_portable_lab_v1_ready.py | 1407 +++++++++++++++++ ...orker-006-agent-install-plan.template.json | 120 +- src/k1link/observatory/m49_worker_service.py | 439 ++++- .../observatory/portable_lab_v1_worker.py | 9 + .../portable_lab_v1_worker_service.py | 63 +- tests/test_observatory_m49_lab_v1_wiring.py | 237 +++ .../test_observatory_m49_worker_entrypoint.py | 84 +- ...t_observatory_portable_lab_v1_promotion.py | 435 +++++ ...ervatory_portable_lab_v1_runtime_anchor.py | 95 ++ ...ervatory_portable_lab_v1_worker_service.py | 43 + ...observatory_worker_agent_image_artifact.py | 138 +- .../test_worker_006_agent_image_installer.py | 163 ++ 14 files changed, 3878 insertions(+), 59 deletions(-) create mode 100644 experiments/perception/worker/observatory_portable/Install-Worker006AgentImage.ps1 create mode 100644 experiments/perception/worker/observatory_portable/promote_portable_lab_v1_ready.py create mode 100644 tests/test_observatory_m49_lab_v1_wiring.py create mode 100644 tests/test_observatory_portable_lab_v1_promotion.py create mode 100644 tests/test_observatory_portable_lab_v1_runtime_anchor.py create mode 100644 tests/test_worker_006_agent_image_installer.py diff --git a/experiments/perception/worker/observatory_portable/Install-Worker006AgentImage.ps1 b/experiments/perception/worker/observatory_portable/Install-Worker006AgentImage.ps1 new file mode 100644 index 0000000..e44af87 --- /dev/null +++ b/experiments/perception/worker/observatory_portable/Install-Worker006AgentImage.ps1 @@ -0,0 +1,698 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [ValidatePattern("^[a-f0-9]{40}$")] + [string]$SourceRevision, + + [Parameter(Mandatory = $true)] + [ValidatePattern("^[a-f0-9]{64}$")] + [string]$ExpectedGitArchiveSha256, + + [Parameter(Mandatory = $true)] + [ValidatePattern("^[a-f0-9]{64}$")] + [string]$ExpectedStagedSnapshotSha256 +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" +$ProgressPreference = "SilentlyContinue" + +$RuntimeRoot = [IO.Path]::GetFullPath("D:\NDC_MISSIONCORE\runtime").TrimEnd("\") +$StagedSnapshotRoot = Join-Path ( + $RuntimeRoot +) "staging\observatory-worker-agent-$SourceRevision" +$ContextManifestRelativePath = ( + "experiments/perception/worker/observatory_portable/" + + "worker-006-agent-build-context.json" +) +$DockerfileRelativePath = ( + "experiments/perception/worker/observatory_portable/" + + "Dockerfile.worker-006-agent" +) +$SourceTreeRelativePath = "src/k1link" +$BaseImageSha256 = ( + "58df7489c3f2276f9591d500a012dee03e23d35543ce3c390b4c001e6bf90794" +) +$BaseImageReference = "sha256:$BaseImageSha256" +$BuildMethod = "docker-commit-exact-layer-v1" +$MaximumLayerBytes = [int64](32MB) +$ImageWorkdir = "/opt/nodedc/mission-core" +$ImageEntrypoint = '["python3","-m","k1link.observatory.m49_worker_container_main"]' +$ImageCommand = '[]' +$FixedEnvironment = @( + "PYTHONPATH=/opt/nodedc/mission-core/src", + "PYTHONNOUSERSITE=1", + "PYTHONDONTWRITEBYTECODE=1", + "PYTHONUNBUFFERED=1" +) + +function Get-FileSha256 { + param([string]$Path) + + $item = Get-Item -LiteralPath $Path -Force + if ( + -not ($item -is [IO.FileInfo]) -or + ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) + ) { + throw "Worker 006 staged source is not a regular file" + } + return (Get-FileHash -LiteralPath $item.FullName -Algorithm SHA256).Hash.ToLowerInvariant() +} + +function Assert-ExactDirectoryChildren { + param( + [string]$Path, + [string[]]$ExpectedNames + ) + + $directory = Get-Item -LiteralPath $Path -Force + if ( + -not ($directory -is [IO.DirectoryInfo]) -or + ($directory.Attributes -band [IO.FileAttributes]::ReparsePoint) + ) { + throw "Worker 006 staged snapshot directory is invalid" + } + $actualNames = @( + Get-ChildItem -LiteralPath $directory.FullName -Force | + ForEach-Object { $_.Name } + ) + [Array]::Sort($actualNames, [StringComparer]::Ordinal) + $expected = @($ExpectedNames) + [Array]::Sort($expected, [StringComparer]::Ordinal) + if ( + $actualNames.Count -ne $expected.Count -or + ([string]::Join("`n", $actualNames) -cne [string]::Join("`n", $expected)) + ) { + throw "Worker 006 staged snapshot contains unexpected entries" + } +} + +function Get-StagedSnapshotInspection { + param([string]$Root) + + Assert-ExactDirectoryChildren $Root @("experiments", "src") + $srcRoot = Join-Path $Root "src" + Assert-ExactDirectoryChildren $srcRoot @("k1link") + $experimentsRoot = Join-Path $Root "experiments" + Assert-ExactDirectoryChildren $experimentsRoot @("perception") + $perceptionRoot = Join-Path $experimentsRoot "perception" + Assert-ExactDirectoryChildren $perceptionRoot @("worker") + $workerRoot = Join-Path $perceptionRoot "worker" + Assert-ExactDirectoryChildren $workerRoot @("observatory_portable") + $portableRoot = Join-Path $workerRoot "observatory_portable" + Assert-ExactDirectoryChildren $portableRoot @( + "Dockerfile.worker-006-agent", + "worker-006-agent-build-context.json" + ) + + $sourceRoot = Join-Path $Root ($SourceTreeRelativePath.Replace("/", "\")) + $sourceItems = @(Get-ChildItem -LiteralPath $sourceRoot -Recurse -Force) + foreach ($item in $sourceItems) { + if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { + throw "Worker 006 staged snapshot contains a reparse point" + } + if (-not ($item -is [IO.FileInfo]) -and -not ($item -is [IO.DirectoryInfo])) { + throw "Worker 006 staged snapshot contains an unsupported entry" + } + } + + $manifestPath = Join-Path $Root ($ContextManifestRelativePath.Replace("/", "\")) + $dockerfilePath = Join-Path $Root ($DockerfileRelativePath.Replace("/", "\")) + $requiredSourceFiles = @( + Join-Path $sourceRoot "__init__.py", + Join-Path $sourceRoot "observatory\m49_worker_container_main.py", + Join-Path $sourceRoot "observatory\m49_worker_service.py" + ) + foreach ($requiredPath in $requiredSourceFiles + @($manifestPath, $dockerfilePath)) { + if (-not (Test-Path -LiteralPath $requiredPath -PathType Leaf)) { + throw "Worker 006 staged snapshot is incomplete" + } + [void](Get-FileSha256 $requiredPath) + } + + $context = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json + if ( + [string]$context.schema_version -cne + "missioncore.observatory-worker-agent-build-context/v1" -or + [string]$context.worker_id -cne "worker-006" -or + [string]$context.base_image.sha256 -cne $BaseImageSha256 -or + [bool]$context.base_image.pull_allowed + ) { + throw "Worker 006 staged build-context contract is invalid" + } + $declaredPaths = @($context.context_entries | ForEach-Object { [string]$_.path }) + $expectedDeclaredPaths = @( + $SourceTreeRelativePath, + $DockerfileRelativePath, + $ContextManifestRelativePath + ) + if ( + $declaredPaths.Count -ne $expectedDeclaredPaths.Count -or + ([string]::Join("`n", $declaredPaths) -cne + [string]::Join("`n", $expectedDeclaredPaths)) + ) { + throw "Worker 006 staged build-context entries changed" + } + + $files = New-Object "Collections.Generic.List[object]" + foreach ($item in $sourceItems) { + if ($item -is [IO.FileInfo]) { + $files.Add($item) + } + } + $files.Add((Get-Item -LiteralPath $dockerfilePath -Force)) + $files.Add((Get-Item -LiteralPath $manifestPath -Force)) + + $rows = New-Object "Collections.Generic.List[object]" + foreach ($file in $files) { + $relative = $file.FullName.Substring($Root.Length).TrimStart("\") + $relative = $relative.Replace("\", "/") + if (-not $relative -or $relative -match "[\x00\r\n]") { + throw "Worker 006 staged snapshot path is invalid" + } + $rows.Add([pscustomobject]@{ + path = $relative + byte_length = [int64]$file.Length + sha256 = Get-FileSha256 $file.FullName + }) + } + $orderedRows = @($rows | Sort-Object -Property path -CaseSensitive) + $canonical = New-Object Text.StringBuilder + [int64]$totalBytes = 0 + foreach ($row in $orderedRows) { + [void]$canonical.Append([string]$row.path) + [void]$canonical.Append([char]0) + [void]$canonical.Append(([int64]$row.byte_length).ToString([Globalization.CultureInfo]::InvariantCulture)) + [void]$canonical.Append([char]0) + [void]$canonical.Append([string]$row.sha256) + [void]$canonical.Append("`n") + $totalBytes += [int64]$row.byte_length + } + $utf8 = New-Object Text.UTF8Encoding($false) + $canonicalBytes = $utf8.GetBytes($canonical.ToString()) + $hasher = [Security.Cryptography.SHA256]::Create() + try { + $digestBytes = $hasher.ComputeHash($canonicalBytes) + } + finally { + $hasher.Dispose() + } + $digest = ([BitConverter]::ToString($digestBytes)).Replace("-", "").ToLowerInvariant() + return [pscustomobject]@{ + sha256 = $digest + file_count = [int64]$orderedRows.Count + byte_length = $totalBytes + canonicalization = "utf8-path-nul-length-nul-sha256-lf-v1" + files = @($orderedRows) + } +} + +function New-StagedSnapshotManifest { + param([object]$SnapshotInspection) + + $stagingRoot = Split-Path -Parent $StagedSnapshotRoot + $manifestPath = Join-Path ( + $stagingRoot + ) ("observatory-worker-agent-embedded-manifest-{0}.json" -f [Guid]::NewGuid().ToString("N")) + $payload = [ordered]@{ + schema_version = "missioncore.observatory-worker-agent-embedded-snapshot/v1" + snapshot_sha256 = [string]$SnapshotInspection.sha256 + canonicalization = [string]$SnapshotInspection.canonicalization + file_count = [int64]$SnapshotInspection.file_count + byte_length = [int64]$SnapshotInspection.byte_length + files = @($SnapshotInspection.files) + } + $json = $payload | ConvertTo-Json -Compress -Depth 5 + $utf8 = New-Object Text.UTF8Encoding($false) + try { + [IO.File]::WriteAllText($manifestPath, $json, $utf8) + return [pscustomobject]@{ + path = $manifestPath + sha256 = Get-FileSha256 $manifestPath + byte_length = [int64](Get-Item -LiteralPath $manifestPath -Force).Length + } + } + catch { + if (Test-Path -LiteralPath $manifestPath -PathType Leaf) { + Remove-Item -LiteralPath $manifestPath -Force + } + throw + } +} + +function Get-ImageInspection { + param([string]$Reference) + + $payload = docker image inspect $Reference + if ($LASTEXITCODE -ne 0) { + throw "Worker 006 image is unavailable: $Reference" + } + $rows = @($payload | ConvertFrom-Json) + if ($rows.Count -ne 1 -or [string]$rows[0].Id -notmatch "^sha256:[a-f0-9]{64}$") { + throw "Worker 006 image inspection is invalid" + } + return $rows[0] +} + +function Assert-ImageContract { + param( + [object]$Image, + [object]$BaseImage, + [string]$EmbeddedManifestSha256 + ) + + $labels = $Image.Config.Labels + $expectedLabels = [ordered]@{ + "org.opencontainers.image.title" = "NODE.DC Observatory Worker 006 agent" + "org.opencontainers.image.source" = "NODEDC_MISSION_CORE" + "org.opencontainers.image.revision" = $SourceRevision + "com.nodedc.product" = "mission-core" + "com.nodedc.stack" = "ndc-mission-core-observatory" + "com.nodedc.role" = "observatory-worker-agent" + "com.nodedc.managed-by" = "mission-core-worker-release" + "com.nodedc.worker-contour" = "worker-006" + "com.nodedc.authority" = "observation-only" + "com.nodedc.models" = "external" + "com.nodedc.runtime-registries" = "external-read-only" + "com.nodedc.base-image.sha256" = $BaseImageSha256 + "com.nodedc.build-context.sha256" = $ExpectedGitArchiveSha256 + "com.nodedc.staged-snapshot.sha256" = $ExpectedStagedSnapshotSha256 + "com.nodedc.embedded-snapshot-manifest.sha256" = $EmbeddedManifestSha256 + "com.nodedc.build-method" = $BuildMethod + } + foreach ($key in $expectedLabels.Keys) { + if ([string]$labels.$key -cne [string]$expectedLabels[$key]) { + throw "Worker 006 installed image label identity changed: $key" + } + } + if ([string]$Image.Config.WorkingDir -cne $ImageWorkdir) { + throw "Worker 006 installed image workdir changed" + } + if ([string]::Join([char]0, @($Image.Config.Entrypoint)) -cne + [string]::Join([char]0, @("python3", "-m", "k1link.observatory.m49_worker_container_main"))) { + throw "Worker 006 installed image entrypoint changed" + } + if (@($Image.Config.Cmd).Count -ne 0) { + throw "Worker 006 installed image command changed" + } + if ([string]$Image.Config.User -cne "0:0") { + throw "Worker 006 installed image user changed" + } + $actualEnvironment = @($Image.Config.Env) + foreach ($value in $FixedEnvironment) { + if ($actualEnvironment -cnotcontains $value) { + throw "Worker 006 installed image environment changed" + } + } + + if ( + [string]$BaseImage.RootFS.Type -cne "layers" -or + [string]$Image.RootFS.Type -cne "layers" + ) { + throw "Worker 006 image RootFS type changed" + } + $baseLayers = @($BaseImage.RootFS.Layers) + $imageLayers = @($Image.RootFS.Layers) + if ($baseLayers.Count -eq 0 -or $imageLayers.Count -ne ($baseLayers.Count + 1)) { + throw "Worker 006 image is not exactly one layer above the pinned base" + } + for ($index = 0; $index -lt $baseLayers.Count; $index += 1) { + if ([string]$imageLayers[$index] -cne [string]$baseLayers[$index]) { + throw "Worker 006 image RootFS does not extend the pinned base layer chain" + } + } + $derivedLayerDiffId = [string]$imageLayers[$baseLayers.Count] + if ($derivedLayerDiffId -notmatch "^sha256:[a-f0-9]{64}$") { + throw "Worker 006 derived layer identity is invalid" + } + + $layerBytes = [int64]$Image.Size - [int64]$BaseImage.Size + if ($layerBytes -lt 0 -or $layerBytes -gt $MaximumLayerBytes) { + throw "Worker 006 image is not within the thin-layer bound" + } + return [pscustomobject]@{ + layer_bytes = $layerBytes + base_layer_count = [int64]$baseLayers.Count + derived_layer_count = [int64]1 + derived_layer_diff_id = $derivedLayerDiffId + } +} + +function Invoke-InstalledImageSmoke { + param( + [string]$Tag, + [string]$SnapshotRoot, + [object]$EmbeddedManifest + ) + + $smokeScript = ( + "set -eu; " + + "test ! -e /opt/nodedc/mission-core/experiments; " + + "test -z `"`$(find /opt/nodedc/mission-core -perm /022 -print -quit)`"; " + + "test -z `"`$(find /opt/nodedc/mission-core/src/k1link " + + "! -type d ! -type f -print -quit)`"; " + + "test -z `"`$(find /nodedc-verify-source/src/k1link " + + "! -type d ! -type f -print -quit)`"; " + + "cmp -s /nodedc-verify-snapshot.json " + + "/opt/nodedc/mission-core/release/worker-006-agent-staged-snapshot.json; " + + "test `"`$(sha256sum " + + "/opt/nodedc/mission-core/release/worker-006-agent-staged-snapshot.json " + + "| cut -d' ' -f1)`" = $($EmbeddedManifest.sha256); " + + "cmp -s /nodedc-verify-source/$ContextManifestRelativePath " + + "/opt/nodedc/mission-core/release/worker-006-agent-build-context.json; " + + "find /nodedc-verify-source/src/k1link -type d -exec sh -c '" + + "for source_path do relative=`${source_path#/nodedc-verify-source/}; " + + "test -d `"/opt/nodedc/mission-core/`$relative`" || exit 70; " + + "done' sh {} +; " + + "find /opt/nodedc/mission-core/src/k1link -type d -exec sh -c '" + + "for image_path do relative=`${image_path#/opt/nodedc/mission-core/}; " + + "test -d `"/nodedc-verify-source/`$relative`" || exit 71; " + + "done' sh {} +; " + + "find /nodedc-verify-source/src/k1link -type f -exec sh -c '" + + "for source_path do relative=`${source_path#/nodedc-verify-source/}; " + + "cmp -s `"`$source_path`" `"/opt/nodedc/mission-core/`$relative`" || exit 72; " + + "done' sh {} +; " + + "find /opt/nodedc/mission-core/src/k1link -type f -exec sh -c '" + + "for image_path do relative=`${image_path#/opt/nodedc/mission-core/}; " + + "cmp -s `"`$image_path`" `"/nodedc-verify-source/`$relative`" || exit 73; " + + "done' sh {} +; " + + "cd /opt/nodedc/mission-core; " + + "python3 -B -c 'import k1link.observatory.m49_worker_container_main as entrypoint; " + + "import k1link.observatory.m49_worker_service as composition; " + + "assert callable(entrypoint.main); " + + "assert callable(composition.compose_installed_m49_worker_service)'" + ) + $sourceMount = ( + "type=bind,source=$SnapshotRoot," + + "target=/nodedc-verify-source,readonly" + ) + $manifestMount = ( + "type=bind,source=$($EmbeddedManifest.path)," + + "target=/nodedc-verify-snapshot.json,readonly" + ) + docker run ` + --rm ` + --network none ` + --read-only ` + --tmpfs "/tmp:rw,noexec,nosuid,size=16m" ` + --cap-drop ALL ` + --security-opt no-new-privileges ` + --mount $sourceMount ` + --mount $manifestMount ` + --entrypoint /bin/sh ` + $Tag ` + -c $smokeScript + if ($LASTEXITCODE -ne 0) { + throw "Installed Worker 006 image smoke failed" + } +} + +function New-OutputContract { + param( + [string]$Status, + [string]$Tag, + [object]$Image, + [object]$LayerInspection, + [object]$SnapshotInspection, + [object]$EmbeddedManifest + ) + + return [ordered]@{ + schema_version = "missioncore.observatory-worker-agent-image-installation/v1" + status = $Status + worker_id = "worker-006" + build_method = $BuildMethod + source_revision = $SourceRevision + provenance = [ordered]@{ + git_archive_sha256 = $ExpectedGitArchiveSha256 + git_archive_verification = "external-before-extract" + staged_snapshot_sha256 = [string]$SnapshotInspection.sha256 + staged_snapshot_file_count = [int64]$SnapshotInspection.file_count + staged_snapshot_byte_length = [int64]$SnapshotInspection.byte_length + staged_snapshot_canonicalization = [string]$SnapshotInspection.canonicalization + embedded_snapshot_manifest_sha256 = [string]$EmbeddedManifest.sha256 + embedded_snapshot_manifest_byte_length = [int64]$EmbeddedManifest.byte_length + } + base_image_sha256 = $BaseImageSha256 + derived_image_sha256 = ([string]$Image.Id).Substring(7) + image = [ordered]@{ + tag = $Tag + id = [string]$Image.Id + size_bytes = [int64]$Image.Size + thin_layer_bytes = [int64]$LayerInspection.layer_bytes + maximum_thin_layer_bytes = $MaximumLayerBytes + rootfs = [ordered]@{ + base_layer_count = [int64]$LayerInspection.base_layer_count + derived_layer_count = [int64]$LayerInspection.derived_layer_count + derived_layer_diff_id = [string]$LayerInspection.derived_layer_diff_id + pinned_base_is_exact_prefix = $true + } + } + runtime_contract = [ordered]@{ + workdir = $ImageWorkdir + entrypoint = @("python3", "-m", "k1link.observatory.m49_worker_container_main") + command = @() + authority = "observation-only" + models = "external" + runtime_registries = "external-read-only" + } + smoke = [ordered]@{ + network = "none" + read_only_rootfs = $true + staged_source_bytes = "matched" + embedded_context_bytes = "matched" + embedded_snapshot_manifest = "matched" + result = "passed" + } + } +} + +if (-not (Test-Path -LiteralPath $StagedSnapshotRoot -PathType Container)) { + throw "Worker 006 staged snapshot root is unavailable" +} +$snapshotBefore = Get-StagedSnapshotInspection $StagedSnapshotRoot +if ([string]$snapshotBefore.sha256 -cne $ExpectedStagedSnapshotSha256) { + throw "Worker 006 staged snapshot identity changed" +} +$embeddedManifest = New-StagedSnapshotManifest $snapshotBefore +try { + $base = Get-ImageInspection $BaseImageReference + if ([string]$base.Id -cne $BaseImageReference) { + throw "Worker 006 base image identity changed" + } + + $shortRevision = $SourceRevision.Substring(0, 12) + $tag = "ndc/mission-core-observatory-worker-agent:$shortRevision" + $existingIds = @( + docker image ls ` + --quiet ` + --no-trunc ` + --filter "reference=$tag" + ) + if ($LASTEXITCODE -ne 0) { + throw "Worker 006 image lookup failed" + } + if ($existingIds.Count -gt 1) { + throw "Worker 006 image tag resolves ambiguously" + } + if ($existingIds.Count -eq 1) { + $image = Get-ImageInspection $tag + if ([string]$image.Id -cne [string]$existingIds[0]) { + throw "Worker 006 existing image lookup changed" + } + $layerInspection = Assert-ImageContract ` + $image ` + $base ` + ([string]$embeddedManifest.sha256) + Invoke-InstalledImageSmoke $tag $StagedSnapshotRoot $embeddedManifest + New-OutputContract ` + "already-installed" ` + $tag ` + $image ` + $layerInspection ` + $snapshotBefore ` + $embeddedManifest | + ConvertTo-Json -Compress -Depth 9 + return + } + + $containerName = "ndc-observatory-worker-agent-image-build-$shortRevision" + $occupiedContainerIds = @( + docker ps -a --filter "name=^/$containerName$" --format "{{.ID}}" + ) + if ($LASTEXITCODE -ne 0) { + throw "Worker 006 temporary build container lookup failed" + } + if ($occupiedContainerIds.Count -ne 0) { + throw "Worker 006 temporary build container name is already occupied" + } + + $copyScript = ( + "set -eu; " + + "test ! -e /opt/nodedc/mission-core; " + + "mkdir -p /opt/nodedc/mission-core/src /opt/nodedc/mission-core/release; " + + "mkdir -p /run/nodedc/registries; " + + "cp -a /nodedc-build-source/src/k1link /opt/nodedc/mission-core/src/k1link; " + + "cp /nodedc-build-source/$ContextManifestRelativePath " + + "/opt/nodedc/mission-core/release/worker-006-agent-build-context.json; " + + "cp /nodedc-build-snapshot.json " + + "/opt/nodedc/mission-core/release/worker-006-agent-staged-snapshot.json; " + + "find /nodedc-build-source/src/k1link -type f -exec sh -c '" + + "for source_path do relative=`${source_path#/nodedc-build-source/}; " + + "cmp -s `"`$source_path`" `"/opt/nodedc/mission-core/`$relative`" || exit 70; " + + "done' sh {} +; " + + "cmp -s /nodedc-build-source/$ContextManifestRelativePath " + + "/opt/nodedc/mission-core/release/worker-006-agent-build-context.json; " + + "cmp -s /nodedc-build-snapshot.json " + + "/opt/nodedc/mission-core/release/worker-006-agent-staged-snapshot.json; " + + "test ! -e /opt/nodedc/mission-core/experiments; " + + "find /opt/nodedc/mission-core -type d -exec chmod 0555 {} +; " + + "find /opt/nodedc/mission-core -type f -exec chmod 0444 {} +; " + + "chmod 0555 /run/nodedc /run/nodedc/registries; " + + "cd /opt/nodedc/mission-core; " + + "PYTHONPATH=/opt/nodedc/mission-core/src PYTHONNOUSERSITE=1 " + + "PYTHONDONTWRITEBYTECODE=1 python3 -B -c '" + + "import k1link.observatory.m49_worker_container_main as entrypoint; " + + "import k1link.observatory.m49_worker_service as composition; " + + "assert callable(entrypoint.main); " + + "assert callable(composition.compose_installed_m49_worker_service)'" + ) + $sourceMount = ( + "type=bind,source=$StagedSnapshotRoot," + + "target=/nodedc-build-source,readonly" + ) + $manifestMount = ( + "type=bind,source=$($embeddedManifest.path)," + + "target=/nodedc-build-snapshot.json,readonly" + ) + $containerId = $null + $imageCommitted = $false + $committedImageId = $null + try { + try { + $createOutput = @( + docker create ` + --name $containerName ` + --network none ` + --cap-drop ALL ` + --security-opt no-new-privileges ` + --mount $sourceMount ` + --mount $manifestMount ` + --entrypoint /bin/sh ` + $BaseImageReference ` + -c $copyScript + ) + $createdIds = @( + $createOutput | Where-Object { $_ -match "^[a-f0-9]{64}$" } + ) + if ($createdIds.Count -eq 1) { + $containerId = [string]$createdIds[0] + } + if ($LASTEXITCODE -ne 0 -or $null -eq $containerId) { + throw "Worker 006 temporary build container creation failed" + } + docker start --attach $containerId + if ($LASTEXITCODE -ne 0) { + throw "Worker 006 source installation command failed" + } + $snapshotAfter = Get-StagedSnapshotInspection $StagedSnapshotRoot + if ( + [string]$snapshotAfter.sha256 -cne $ExpectedStagedSnapshotSha256 -or + [int64]$snapshotAfter.file_count -ne [int64]$snapshotBefore.file_count -or + [int64]$snapshotAfter.byte_length -ne [int64]$snapshotBefore.byte_length + ) { + throw "Worker 006 staged snapshot changed during installation" + } + if ((Get-FileSha256 $embeddedManifest.path) -cne [string]$embeddedManifest.sha256) { + throw "Worker 006 embedded snapshot manifest changed during installation" + } + + $changes = @( + "--change", "WORKDIR $ImageWorkdir", + "--change", "USER 0:0", + "--change", "ENTRYPOINT $ImageEntrypoint", + "--change", "CMD $ImageCommand", + "--change", 'LABEL org.opencontainers.image.title="NODE.DC Observatory Worker 006 agent"', + "--change", "LABEL org.opencontainers.image.source=NODEDC_MISSION_CORE", + "--change", "LABEL org.opencontainers.image.revision=$SourceRevision", + "--change", "LABEL com.nodedc.product=mission-core", + "--change", "LABEL com.nodedc.stack=ndc-mission-core-observatory", + "--change", "LABEL com.nodedc.role=observatory-worker-agent", + "--change", "LABEL com.nodedc.managed-by=mission-core-worker-release", + "--change", "LABEL com.nodedc.worker-contour=worker-006", + "--change", "LABEL com.nodedc.authority=observation-only", + "--change", "LABEL com.nodedc.models=external", + "--change", "LABEL com.nodedc.runtime-registries=external-read-only", + "--change", "LABEL com.nodedc.base-image.sha256=$BaseImageSha256", + "--change", "LABEL com.nodedc.build-context.sha256=$ExpectedGitArchiveSha256", + "--change", "LABEL com.nodedc.staged-snapshot.sha256=$ExpectedStagedSnapshotSha256", + "--change", ( + "LABEL com.nodedc.embedded-snapshot-manifest.sha256=" + + [string]$embeddedManifest.sha256 + ), + "--change", "LABEL com.nodedc.build-method=$BuildMethod" + ) + foreach ($value in $FixedEnvironment) { + $changes += @("--change", "ENV $value") + } + $commitOutput = docker commit --pause=true @changes $containerId $tag + $committedIds = @( + $commitOutput | + ForEach-Object { ([string]$_).Trim() } | + Where-Object { $_ -match "^sha256:[a-f0-9]{64}$" } + ) + if ($LASTEXITCODE -ne 0 -or $committedIds.Count -ne 1) { + throw "Worker 006 image commit failed" + } + $committedImageId = [string]$committedIds[0] + $imageCommitted = $true + } + finally { + if ($null -ne $containerId -and $containerId -match "^[a-f0-9]{64}$") { + docker rm -f $containerId | Out-Null + if ($LASTEXITCODE -ne 0) { + throw "Worker 006 temporary build container cleanup failed" + } + } + } + + $image = Get-ImageInspection $tag + if ([string]$image.Id -cne $committedImageId) { + throw "Worker 006 committed image tag identity changed" + } + $layerInspection = Assert-ImageContract ` + $image ` + $base ` + ([string]$embeddedManifest.sha256) + Invoke-InstalledImageSmoke $tag $StagedSnapshotRoot $embeddedManifest + New-OutputContract ` + "installed" ` + $tag ` + $image ` + $layerInspection ` + $snapshotBefore ` + $embeddedManifest | + ConvertTo-Json -Compress -Depth 9 + } + catch { + $installationFailure = $_ + if ($imageCommitted -and $committedImageId -match "^sha256:[a-f0-9]{64}$") { + docker image rm --force $committedImageId | Out-Null + if ($LASTEXITCODE -ne 0) { + throw ( + "Worker 006 image verification failed and committed image cleanup failed: " + + [string]$installationFailure + ) + } + } + throw $installationFailure + } +} +finally { + if ( + $null -ne $embeddedManifest -and + (Test-Path -LiteralPath $embeddedManifest.path -PathType Leaf) + ) { + Remove-Item -LiteralPath $embeddedManifest.path -Force + } +} 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 b03c824..146a908 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 @@ -149,10 +149,10 @@ }, { "asset_id": "lab-v1-portable-worker", - "byte_length": 47008, + "byte_length": 47392, "kind": "repository-file", "repository_path": "src/k1link/observatory/portable_lab_v1_worker.py", - "sha256": "0a954b5d4d2f3cdd588d58ba4234141c32992b4b46380a03b621060c25c4ac23" + "sha256": "56aa40390b8413fdf7f7293329e89420305914abd478821c08bfe8b0a2ec3df8" }, { "asset_id": "portable-result-contracts", @@ -189,7 +189,7 @@ "navigation_or_safety_accepted": false, "production_accepted": false }, - "candidate_sha256": "546c74fb88b9571996cecc41fa915044e3907365b8891b7fceb297fb74a837c7", + "candidate_sha256": "9d40369e393aea4b1a09a5784336442a082b1b6126afaac379f1ecece39b54f9", "declared_blockers": [ "combined-executor-entrypoint-uninstalled", "combined-executor-image-unsealed", diff --git a/experiments/perception/worker/observatory_portable/promote_portable_lab_v1_ready.py b/experiments/perception/worker/observatory_portable/promote_portable_lab_v1_ready.py new file mode 100644 index 0000000..75488be --- /dev/null +++ b/experiments/perception/worker/observatory_portable/promote_portable_lab_v1_ready.py @@ -0,0 +1,1407 @@ +"""Generate a sealed, ready LAB V1 registry set without deploying it. + +The generator writes only to a new output root. It deliberately keeps the +installation receipt out of the release candidate: the receipt is created +after the release and the ready RunDefinition, then becomes a runtime-only +asset. This ordering prevents both definition/release and release/receipt +self-cycles. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import re +import shutil +import tempfile +from collections.abc import Mapping, Sequence +from dataclasses import dataclass +from pathlib import Path +from typing import Final, cast + +from k1link.observatory.portable_lab_v1_executor import ( + PORTABLE_LAB_V1_RELEASE_SCHEMA, + PortableLabV1ExecutorSeal, + PortableLabV1ReleaseAsset, + PortableLabV1ReleaseCandidate, + ReleaseAssetKind, +) +from k1link.observatory.portable_lab_v1_local_runners import ( + PortableLabV1AssetKind, + PortableLabV1AssetVerification, + PortableLabV1Component, + PortableLabV1ComponentInstallation, + PortableLabV1HostAsset, + PortableLabV1RunnerInstallation, + PortableLabV1WorkRootBinding, +) +from k1link.observatory.portable_lab_v1_worker import ( + PORTABLE_LAB_V1_RUNTIME_PHASES, +) +from k1link.observatory.portable_lab_v1_worker_service import ( + PORTABLE_LAB_V1_ADAPTER_ID, + PORTABLE_LAB_V1_COMPONENT_IMAGE_BUILD_METHOD, + PORTABLE_LAB_V1_SETUP_ID, + PORTABLE_LAB_V1_WORKER_INSTALLATION_RECEIPT_ASSET_ID, + PortableLabV1ComponentImageBuildSeal, + PortableLabV1WorkerInstallationReceipt, + load_portable_lab_v1_worker_installation_receipt, + portable_lab_v1_worker_receipt_document, +) +from k1link.observatory.portable_result_contract import ( + OBSERVATION_ONLY_AUTHORITY, + canonical_json, +) +from k1link.observatory.portable_run_definitions import ( + PORTABLE_RUN_DEFINITION_REGISTRY_SCHEMA, + PortableExecutorAvailability, + PortableRunDefinition, + PortableRunDefinitionRegistry, + canonical_sha256, +) +from k1link.observatory.portable_worker_runtime import ( + PORTABLE_WORKER_RUNTIME_CANDIDATE_SCHEMA, + PORTABLE_WORKER_RUNTIME_REGISTRY_SCHEMA, + PortableWorkerAssetRequirement, + PortableWorkerExecutorSeal, + PortableWorkerRuntimeCandidate, + PortableWorkerRuntimePhase, + PortableWorkerRuntimeRegistry, +) + +PROMOTION_INPUT_SCHEMA: Final = ( + "missioncore.observatory-portable-lab-v1-promotion-input/v2" +) +INSTALLATION_EVIDENCE_SCHEMA: Final = ( + "missioncore.observatory-portable-lab-v1-installation-evidence/v1" +) +WORKER_AGENT_IMAGE_INSTALLATION_SCHEMA: Final = ( + "missioncore.observatory-worker-agent-image-installation/v1" +) +LAB_V1_RELEASE_ID: Final = "lab-v1-eomt-ddrnet-worker006-candidate-v2" +LAB_V1_WORKER_AGENT_IMAGE_ASSET_ID: Final = "worker-006-agent-image" +DDRNET_PORTABLE_CONFIG_ASSET_ID: Final = "ddrnet-portable-config" +DDRNET_PORTABLE_CONFIG_COMPONENT_ID: Final = ( + "ddrnet-portable-runtime-config-v2" +) +DDRNET_PORTABLE_CONFIG_RELATIVE_PATH: Final = ( + "config/perception/lab-v1-eomt-ddrnet-portable-v2.json" +) +RELEASE_FILE_NAME: Final = "lab-v1-executor-release.json" +RELEASE_SEAL_FILE_NAME: Final = "lab-v1-executor-seal.json" +DEFINITION_REGISTRY_FILE_NAME: Final = ( + "observatory-portable-run-definitions.json" +) +INSTALLATION_RECEIPT_FILE_NAME: Final = ( + "lab-v1-worker-installation-receipt.json" +) +RUNTIME_REGISTRY_FILE_NAME: Final = ( + "observatory-worker-runtime-candidates.json" +) + +_SOURCE_REVISION: Final = re.compile(r"^[a-f0-9]{40}$") +_MAX_INPUT_BYTES: Final = 512 * 1024 +_COORDINATOR_BASE_IMAGE_SHA256: Final = ( + "58df7489c3f2276f9591d500a012dee03e23d35543ce3c390b4c001e6bf90794" +) +_COMPONENT_ENTRYPOINTS: Final = { + "eomt": ("python3",), + "ddrnet": ("conda", "run", "--no-capture-output", "--name", "goose", "python"), +} +_COMPONENT_COMMANDS: Final = { + "eomt": ("/opt/nodedc/adapter/run_portable_lab_v1_eomt_component.py",), + "ddrnet": ("/opt/nodedc/adapter/run_portable_lab_v1_ddrnet_component.py",), +} +_RELEASE_PHASES: Final = ( + "source-materialization", + "eomt-full-session", + "ddrnet-full-session", + "result-v2-assembly", + "result-v2-validation", + "portable-result-packaging", +) + + +class PortableLabV1PromotionError(RuntimeError): + """Ready artifact generation failed before any output was promoted.""" + + +@dataclass(frozen=True, slots=True) +class LabV1InstallationEvidence: + """Identity of external installers that completed offline import smoke.""" + + component_receipt_path: Path + coordinator_receipt_path: Path + component_receipt_sha256: str + coordinator_receipt_sha256: str + evidence_sha256: str + + def __post_init__(self) -> None: + for path in (self.component_receipt_path, self.coordinator_receipt_path): + if not path.is_absolute(): + raise PortableLabV1PromotionError( + "installation evidence path is not absolute" + ) + for digest, label in ( + (self.component_receipt_sha256, "component installation evidence"), + (self.coordinator_receipt_sha256, "coordinator installation evidence"), + (self.evidence_sha256, "combined installation evidence"), + ): + _require_digest(digest, f"{label} SHA-256") + + +@dataclass(frozen=True, slots=True) +class ComponentPromotionInput: + component: PortableLabV1Component + base_image_sha256: str + derived_image_sha256: str + dockerfile_sha256: str + installer_sha256: str + shared_adapter_sha256: str + component_adapter_sha256: str + assets: tuple[PortableLabV1HostAsset, ...] + + def build_seal(self) -> PortableLabV1ComponentImageBuildSeal: + return PortableLabV1ComponentImageBuildSeal.seal( + component=self.component, + base_image_sha256=self.base_image_sha256, + derived_image_sha256=self.derived_image_sha256, + dockerfile_sha256=self.dockerfile_sha256, + build_method=PORTABLE_LAB_V1_COMPONENT_IMAGE_BUILD_METHOD, + installer_sha256=self.installer_sha256, + shared_adapter_sha256=self.shared_adapter_sha256, + component_adapter_sha256=self.component_adapter_sha256, + ) + + def installation(self) -> PortableLabV1ComponentInstallation: + return PortableLabV1ComponentInstallation.seal( + component=self.component, + image_sha256=self.derived_image_sha256, + entrypoint=_COMPONENT_ENTRYPOINTS[self.component], + command=_COMPONENT_COMMANDS[self.component], + assets=self.assets, + timeout_seconds=3600.0, + memory_bytes=16 * 1024**3, + nano_cpus=4_000_000_000, + ) + + +@dataclass(frozen=True, slots=True) +class LabV1PromotionInput: + source_revision: str + component_source_revision: str + coordinator_image_sha256: str + work_root: PortableLabV1WorkRootBinding + eomt: ComponentPromotionInput + ddrnet: ComponentPromotionInput + installation_evidence: LabV1InstallationEvidence + + def __post_init__(self) -> None: + if _SOURCE_REVISION.fullmatch(self.source_revision) is None: + raise PortableLabV1PromotionError("source revision is invalid") + if _SOURCE_REVISION.fullmatch(self.component_source_revision) is None: + raise PortableLabV1PromotionError("component source revision is invalid") + if self.eomt.component != "eomt" or self.ddrnet.component != "ddrnet": + raise PortableLabV1PromotionError("component promotion inputs are misbound") + _require_digest(self.coordinator_image_sha256, "coordinator image SHA-256") + + +@dataclass(frozen=True, slots=True) +class LabV1ReadyArtifacts: + root: Path + release_candidate_path: Path + release_seal_path: Path + definition_registry_path: Path + installation_receipt_path: Path + runtime_registry_path: Path + release_candidate_sha256: str + release_sha256: str + definition_sha256: str + installation_receipt_file_sha256: str + runtime_candidate_sha256: str + + +def load_promotion_input(path: Path) -> LabV1PromotionInput: + """Load one strict local promotion input document.""" + + document = _read_json_object(path, maximum=_MAX_INPUT_BYTES) + _exact_keys( + document, + { + "schema_version", + "source_revision", + "component_source_revision", + "coordinator_image_sha256", + "work_root", + "components", + "installation_evidence", + }, + "promotion input", + ) + if document["schema_version"] != PROMOTION_INPUT_SCHEMA: + raise PortableLabV1PromotionError("promotion input schema is invalid") + work = _object(document["work_root"], "promotion work root") + _exact_keys(work, {"controller_root", "engine_host_root"}, "promotion work root") + components = _object(document["components"], "promotion components") + _exact_keys(components, {"eomt", "ddrnet"}, "promotion components") + source_revision = _string(document["source_revision"], "source revision") + component_source_revision = _string( + document["component_source_revision"], + "component source revision", + ) + coordinator_image_sha256 = _string( + document["coordinator_image_sha256"], + "coordinator image SHA-256", + ) + eomt = _component_input(components["eomt"], "eomt") + ddrnet = _component_input(components["ddrnet"], "ddrnet") + evidence_paths = _object( + document["installation_evidence"], + "promotion installation evidence", + ) + _exact_keys( + evidence_paths, + { + "component_image_installer_receipt_file", + "coordinator_image_installer_receipt_file", + }, + "promotion installation evidence", + ) + installation_evidence = _load_installation_evidence( + component_receipt_path=Path( + _string( + evidence_paths["component_image_installer_receipt_file"], + "component image installer receipt file", + ) + ), + coordinator_receipt_path=Path( + _string( + evidence_paths["coordinator_image_installer_receipt_file"], + "coordinator image installer receipt file", + ) + ), + source_revision=source_revision, + component_source_revision=component_source_revision, + coordinator_image_sha256=coordinator_image_sha256, + eomt_image_sha256=eomt.derived_image_sha256, + ddrnet_image_sha256=ddrnet.derived_image_sha256, + ) + return LabV1PromotionInput( + source_revision=source_revision, + component_source_revision=component_source_revision, + coordinator_image_sha256=coordinator_image_sha256, + work_root=PortableLabV1WorkRootBinding( + controller_root=Path( + _string(work["controller_root"], "controller work root") + ), + engine_host_root=_string( + work["engine_host_root"], + "engine host work root", + ), + ), + eomt=eomt, + ddrnet=ddrnet, + installation_evidence=installation_evidence, + ) + + +def generate_ready_lab_v1_artifacts( + *, + promotion: LabV1PromotionInput, + source_definition_registry: Path, + source_runtime_registry: Path, + ddrnet_portable_config: Path, + output_root: Path, +) -> LabV1ReadyArtifacts: + """Generate and round-trip validate one additive ready LAB V1 artifact set.""" + + _verify_installation_evidence(promotion) + output = output_root.expanduser().absolute() + if not output_root.is_absolute(): + raise PortableLabV1PromotionError("output root must be absolute") + if output.exists(): + raise PortableLabV1PromotionError("output root already exists") + parent = output.parent + if parent.is_symlink() or not parent.is_dir(): + raise PortableLabV1PromotionError("output parent is not a real directory") + + source_definitions_document = _read_json_object(source_definition_registry) + source_runtime_document = _read_json_object(source_runtime_registry) + source_definitions = PortableRunDefinitionRegistry.from_file( + source_definition_registry + ) + PortableWorkerRuntimeRegistry.from_file( + source_runtime_registry, + definitions=source_definitions, + ) + blocked_definition = source_definitions.resolve_setup(PORTABLE_LAB_V1_SETUP_ID) + if blocked_definition.executor.ready: + raise PortableLabV1PromotionError("source LAB V1 definition is already ready") + config_payload = _read_regular_file(ddrnet_portable_config) + config_component = next( + ( + component + for component in blocked_definition.components + if component.component_id == DDRNET_PORTABLE_CONFIG_COMPONENT_ID + ), + None, + ) + if ( + config_component is None + or hashlib.sha256(config_payload).hexdigest() != config_component.sha256 + ): + raise PortableLabV1PromotionError( + "portable DDRNet config differs from the source definition" + ) + + staging = Path(tempfile.mkdtemp(prefix=f".{output.name}.staging-", dir=parent)) + try: + config_output = staging / DDRNET_PORTABLE_CONFIG_RELATIVE_PATH + config_output.parent.mkdir(parents=True) + config_output.write_bytes(config_payload) + + eomt_build = promotion.eomt.build_seal() + ddrnet_build = promotion.ddrnet.build_seal() + release = _ready_release_candidate( + repository_root=staging, + definition=blocked_definition, + config_byte_length=len(config_payload), + promotion=promotion, + eomt_build=eomt_build, + ddrnet_build=ddrnet_build, + ) + release_path = staging / RELEASE_FILE_NAME + _write_canonical(release_path, _release_document(release)) + release = PortableLabV1ReleaseCandidate.from_file( + release_path, + repository_root=staging, + ) + release.bind_definition(blocked_definition) + inspection = release.inspect( + { + asset.asset_id: asset.sha256 + for asset in release.assets + if asset.repository_path is None + } + ) + seal = release.seal(inspection) + _write_canonical(staging / RELEASE_SEAL_FILE_NAME, seal.as_dict()) + + ready_definition = _ready_definition(blocked_definition, seal) + definition_document = _replace_registry_row( + source_definitions_document, + collection_key="definitions", + identity_key="setup_id", + identity_value=PORTABLE_LAB_V1_SETUP_ID, + replacement=_definition_document(ready_definition), + schema=PORTABLE_RUN_DEFINITION_REGISTRY_SCHEMA, + ) + definition_path = staging / DEFINITION_REGISTRY_FILE_NAME + _write_canonical(definition_path, definition_document) + + runner_installation = PortableLabV1RunnerInstallation.seal( + definition_sha256=ready_definition.definition_sha256, + release_candidate_sha256=release.candidate_sha256, + work_root=promotion.work_root, + eomt=promotion.eomt.installation(), + ddrnet=promotion.ddrnet.installation(), + ) + receipt_document = portable_lab_v1_worker_receipt_document( + source_revision=promotion.source_revision, + release_candidate_sha256=release.candidate_sha256, + runner_installation=runner_installation, + eomt_image_build=eomt_build, + ddrnet_image_build=ddrnet_build, + installation_evidence_sha256=( + promotion.installation_evidence.evidence_sha256 + ), + ) + receipt_path = staging / INSTALLATION_RECEIPT_FILE_NAME + _write_canonical(receipt_path, receipt_document) + receipt = load_portable_lab_v1_worker_installation_receipt(receipt_path) + + runtime_candidate = _ready_runtime_candidate( + definition=ready_definition, + seal=seal, + receipt=receipt, + config_byte_length=len(config_payload), + promotion=promotion, + ) + runtime_document = _replace_registry_row( + source_runtime_document, + collection_key="candidates", + identity_key="setup_id", + identity_value=PORTABLE_LAB_V1_SETUP_ID, + replacement=_runtime_candidate_document(runtime_candidate), + schema=PORTABLE_WORKER_RUNTIME_REGISTRY_SCHEMA, + ) + runtime_path = staging / RUNTIME_REGISTRY_FILE_NAME + _write_canonical(runtime_path, runtime_document) + + _validate_output( + root=staging, + source_definitions=source_definitions_document, + source_runtime=source_runtime_document, + promotion=promotion, + ) + _verify_installation_evidence(promotion) + os.replace(staging, output) + _validate_output( + root=output, + source_definitions=source_definitions_document, + source_runtime=source_runtime_document, + promotion=promotion, + ) + _verify_installation_evidence(promotion) + except Exception: + if staging.exists(): + shutil.rmtree(staging) + if output.exists(): + shutil.rmtree(output) + raise + + definitions = PortableRunDefinitionRegistry.from_file( + output / DEFINITION_REGISTRY_FILE_NAME + ) + definition = definitions.resolve_setup(PORTABLE_LAB_V1_SETUP_ID) + runtime = PortableWorkerRuntimeRegistry.from_file( + output / RUNTIME_REGISTRY_FILE_NAME, + definitions=definitions, + ).resolve(PORTABLE_LAB_V1_SETUP_ID, definition.definition_sha256) + release = PortableLabV1ReleaseCandidate.from_file( + output / RELEASE_FILE_NAME, + repository_root=output, + ) + receipt = load_portable_lab_v1_worker_installation_receipt( + output / INSTALLATION_RECEIPT_FILE_NAME + ) + inspection = release.inspect( + { + asset.asset_id: asset.sha256 + for asset in release.assets + if asset.repository_path is None + } + ) + seal = release.seal(inspection) + return LabV1ReadyArtifacts( + root=output, + release_candidate_path=output / RELEASE_FILE_NAME, + release_seal_path=output / RELEASE_SEAL_FILE_NAME, + definition_registry_path=output / DEFINITION_REGISTRY_FILE_NAME, + installation_receipt_path=output / INSTALLATION_RECEIPT_FILE_NAME, + runtime_registry_path=output / RUNTIME_REGISTRY_FILE_NAME, + release_candidate_sha256=release.candidate_sha256, + release_sha256=seal.release_sha256, + definition_sha256=definition.definition_sha256, + installation_receipt_file_sha256=receipt.file_sha256, + runtime_candidate_sha256=runtime.candidate_sha256, + ) + + +def _ready_release_candidate( + *, + repository_root: Path, + definition: PortableRunDefinition, + config_byte_length: int, + promotion: LabV1PromotionInput, + eomt_build: PortableLabV1ComponentImageBuildSeal, + ddrnet_build: PortableLabV1ComponentImageBuildSeal, +) -> PortableLabV1ReleaseCandidate: + assets = [ + PortableLabV1ReleaseAsset( + asset_id=DDRNET_PORTABLE_CONFIG_ASSET_ID, + kind="repository-file", + sha256=next( + component.sha256 + for component in definition.components + if component.component_id == DDRNET_PORTABLE_CONFIG_COMPONENT_ID + ), + byte_length=config_byte_length, + repository_path=DDRNET_PORTABLE_CONFIG_RELATIVE_PATH, + ), + PortableLabV1ReleaseAsset( + asset_id=LAB_V1_WORKER_AGENT_IMAGE_ASSET_ID, + kind="container-image", + sha256=promotion.coordinator_image_sha256, + byte_length=None, + repository_path=None, + ), + ] + for component in (promotion.eomt, promotion.ddrnet): + for asset in component.assets: + assets.append( + PortableLabV1ReleaseAsset( + asset_id=asset.asset_id, + kind=( + "model-artifact" + if asset.asset_id in {"eomt-model-cache", "ddrnet-checkpoint"} + else "runtime-artifact" + ), + sha256=asset.identity_sha256, + byte_length=None, + repository_path=None, + ) + ) + for component, build in ((promotion.eomt, eomt_build), (promotion.ddrnet, ddrnet_build)): + prefix = component.component + for suffix, digest, kind in ( + ("base-image", build.base_image_sha256, "container-image"), + ("derived-image", build.derived_image_sha256, "container-image"), + ("adapter-dockerfile", build.dockerfile_sha256, "runtime-artifact"), + ("adapter-installer", build.installer_sha256, "runtime-artifact"), + ("shared-adapter", build.shared_adapter_sha256, "runtime-artifact"), + ("component-adapter", build.component_adapter_sha256, "runtime-artifact"), + ("component-image-build-seal", build.seal_sha256, "runtime-artifact"), + ): + assets.append( + PortableLabV1ReleaseAsset( + asset_id=f"{prefix}-{suffix}", + kind=cast(ReleaseAssetKind, kind), + sha256=digest, + byte_length=None, + repository_path=None, + ) + ) + ordered = tuple(sorted(assets, key=lambda asset: asset.asset_id)) + identity = { + "schema_version": ( + "missioncore.observatory-portable-lab-v1-executor-candidate-identity/v2" + ), + "release_id": LAB_V1_RELEASE_ID, + "setup_id": definition.setup_id, + "definition_id": definition.definition_id, + "definition_version": definition.version, + "definition_contract_sha256": definition.executable_contract_sha256, + "result_contract_sha256": definition.result_contract.contract_sha256, + "executor_image_sha256": promotion.coordinator_image_sha256, + "assets": [asset.as_dict() for asset in ordered], + "phases": list(_RELEASE_PHASES), + "declared_blockers": [], + "authority": dict(OBSERVATION_ONLY_AUTHORITY), + } + return PortableLabV1ReleaseCandidate( + release_id=LAB_V1_RELEASE_ID, + setup_id=definition.setup_id, + definition_id=definition.definition_id, + definition_version=definition.version, + definition_contract_sha256=definition.executable_contract_sha256, + result_contract_sha256=definition.result_contract.contract_sha256, + executor_image_sha256=promotion.coordinator_image_sha256, + assets=ordered, + phases=_RELEASE_PHASES, + declared_blockers=(), + candidate_sha256=canonical_sha256(identity), + repository_root=repository_root, + ) + + +def _ready_definition( + blocked: PortableRunDefinition, + seal: PortableLabV1ExecutorSeal, +) -> PortableRunDefinition: + executor = PortableExecutorAvailability( + contour_id=blocked.executor.contour_id, + 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(), + "executor": executor.identity_document(), + } + return PortableRunDefinition( + setup_id=blocked.setup_id, + definition_id=blocked.definition_id, + version=blocked.version, + definition_sha256=canonical_sha256(identity), + source_requirements=blocked.source_requirements, + source_adapter=blocked.source_adapter, + components=blocked.components, + models=blocked.models, + resource_profile=blocked.resource_profile, + result_contract=blocked.result_contract, + executor=executor, + authority=blocked.authority, + ) + + +def _ready_runtime_candidate( + *, + definition: PortableRunDefinition, + seal: PortableLabV1ExecutorSeal, + receipt: PortableLabV1WorkerInstallationReceipt, + config_byte_length: int, + promotion: LabV1PromotionInput, +) -> PortableWorkerRuntimeCandidate: + config_component = next( + component + for component in definition.components + if component.component_id == DDRNET_PORTABLE_CONFIG_COMPONENT_ID + ) + assets = tuple( + sorted( + ( + PortableWorkerAssetRequirement( + asset_id=DDRNET_PORTABLE_CONFIG_ASSET_ID, + kind="definition-component", + sha256=config_component.sha256, + byte_length=config_byte_length, + component_id=config_component.component_id, + model_release_id=None, + model_artifact_role=None, + ), + PortableWorkerAssetRequirement( + asset_id="eomt-derived-image", + kind="container-image", + sha256=promotion.eomt.derived_image_sha256, + byte_length=None, + component_id=None, + model_release_id=None, + model_artifact_role=None, + ), + PortableWorkerAssetRequirement( + asset_id="ddrnet-derived-image", + kind="container-image", + sha256=promotion.ddrnet.derived_image_sha256, + byte_length=None, + component_id=None, + model_release_id=None, + model_artifact_role=None, + ), + PortableWorkerAssetRequirement( + asset_id=LAB_V1_WORKER_AGENT_IMAGE_ASSET_ID, + kind="container-image", + sha256=promotion.coordinator_image_sha256, + byte_length=None, + component_id=None, + model_release_id=None, + model_artifact_role=None, + ), + PortableWorkerAssetRequirement( + asset_id=PORTABLE_LAB_V1_WORKER_INSTALLATION_RECEIPT_ASSET_ID, + kind="local-file", + sha256=receipt.file_sha256, + byte_length=receipt.file_byte_length, + component_id=None, + model_release_id=None, + model_artifact_role=None, + ), + ), + key=lambda asset: asset.asset_id, + ) + ) + phases = tuple( + PortableWorkerRuntimePhase(phase_id=phase, state="implemented") + for phase in PORTABLE_LAB_V1_RUNTIME_PHASES + ) + executor = PortableWorkerExecutorSeal( + release_id=seal.release_id, + release_sha256=seal.release_sha256, + image_sha256=seal.executor_image_sha256, + ) + identity = { + "schema_version": PORTABLE_WORKER_RUNTIME_CANDIDATE_SCHEMA, + "adapter_id": PORTABLE_LAB_V1_ADAPTER_ID, + "setup_id": definition.setup_id, + "definition_id": definition.definition_id, + "definition_version": definition.version, + "definition_sha256": definition.definition_sha256, + "source_adapter_sha256": definition.source_adapter.contract_sha256, + "model_manifest_sha256": definition.model_manifest_sha256, + "resource_profile_sha256": definition.resource_profile.profile_sha256, + "result_contract_sha256": definition.result_contract.contract_sha256, + "state": "ready", + "executor": executor.as_dict(), + "reusable_assets": [asset.as_dict() for asset in assets], + "phases": [phase.as_dict() for phase in phases], + "blockers": [], + "authority": dict(OBSERVATION_ONLY_AUTHORITY), + } + return PortableWorkerRuntimeCandidate( + adapter_id=PORTABLE_LAB_V1_ADAPTER_ID, + setup_id=definition.setup_id, + definition_id=definition.definition_id, + definition_version=definition.version, + definition_sha256=definition.definition_sha256, + source_adapter_sha256=definition.source_adapter.contract_sha256, + model_manifest_sha256=definition.model_manifest_sha256, + resource_profile_sha256=definition.resource_profile.profile_sha256, + result_contract_sha256=definition.result_contract.contract_sha256, + state="ready", + executor=executor, + reusable_assets=assets, + phases=phases, + blockers=(), + candidate_sha256=canonical_sha256(identity), + ) + + +def _validate_output( + *, + root: Path, + source_definitions: Mapping[str, object], + source_runtime: Mapping[str, object], + promotion: LabV1PromotionInput, +) -> None: + definitions = PortableRunDefinitionRegistry.from_file( + root / DEFINITION_REGISTRY_FILE_NAME + ) + definition = definitions.resolve_setup(PORTABLE_LAB_V1_SETUP_ID) + release = PortableLabV1ReleaseCandidate.from_file( + root / RELEASE_FILE_NAME, + repository_root=root, + ) + release.bind_definition(definition) + inspection = release.inspect( + { + asset.asset_id: asset.sha256 + for asset in release.assets + if asset.repository_path is None + } + ) + seal = release.seal(inspection) + receipt = load_portable_lab_v1_worker_installation_receipt( + root / INSTALLATION_RECEIPT_FILE_NAME + ) + runtime = PortableWorkerRuntimeRegistry.from_file( + root / RUNTIME_REGISTRY_FILE_NAME, + definitions=definitions, + ).resolve(PORTABLE_LAB_V1_SETUP_ID, definition.definition_sha256) + if ( + not definition.executor.ready + or not runtime.ready + or receipt.release_candidate_sha256 != release.candidate_sha256 + or receipt.runner_installation.definition_sha256 + != definition.definition_sha256 + or receipt.installation_evidence_sha256 + != promotion.installation_evidence.evidence_sha256 + or runtime.executor is None + or runtime.executor.release_sha256 != seal.release_sha256 + or runtime.executor.image_sha256 != promotion.coordinator_image_sha256 + ): + raise PortableLabV1PromotionError("generated LAB V1 identities disagree") + release_assets = {asset.asset_id: asset for asset in release.assets} + repository_assets = [ + asset for asset in release.assets if asset.kind == "repository-file" + ] + if ( + [asset.asset_id for asset in repository_assets] + != [DDRNET_PORTABLE_CONFIG_ASSET_ID] + or PORTABLE_LAB_V1_WORKER_INSTALLATION_RECEIPT_ASSET_ID + in release_assets + ): + raise PortableLabV1PromotionError("release/receipt cycle guard changed") + for requirement in runtime.reusable_assets: + if ( + requirement.asset_id + == PORTABLE_LAB_V1_WORKER_INSTALLATION_RECEIPT_ASSET_ID + ): + if ( + requirement.sha256 != receipt.file_sha256 + or requirement.byte_length != receipt.file_byte_length + ): + raise PortableLabV1PromotionError("runtime receipt anchor changed") + continue + asset = release_assets.get(requirement.asset_id) + if ( + asset is None + or asset.sha256 != requirement.sha256 + or asset.byte_length != requirement.byte_length + ): + raise PortableLabV1PromotionError("runtime/release asset binding changed") + _assert_non_lab_rows_unchanged( + source_definitions, + _read_json_object(root / DEFINITION_REGISTRY_FILE_NAME), + collection_key="definitions", + ) + _assert_non_lab_rows_unchanged( + source_runtime, + _read_json_object(root / RUNTIME_REGISTRY_FILE_NAME), + collection_key="candidates", + ) + + +def _release_document(release: PortableLabV1ReleaseCandidate) -> dict[str, object]: + return { + "schema_version": PORTABLE_LAB_V1_RELEASE_SCHEMA, + "release_id": release.release_id, + "setup_id": release.setup_id, + "definition_id": release.definition_id, + "definition_version": release.definition_version, + "definition_contract_sha256": release.definition_contract_sha256, + "result_contract_sha256": release.result_contract_sha256, + "executor_image_sha256": release.executor_image_sha256, + "assets": [asset.as_dict() for asset in release.assets], + "phases": list(release.phases), + "declared_blockers": list(release.declared_blockers), + "authority": dict(OBSERVATION_ONLY_AUTHORITY), + "candidate_sha256": release.candidate_sha256, + } + + +def _definition_document(definition: PortableRunDefinition) -> dict[str, object]: + return { + "setup_id": definition.setup_id, + "definition_id": definition.definition_id, + "version": definition.version, + "definition_sha256": definition.definition_sha256, + "source_requirements": definition.source_requirements.as_dict(), + "source_adapter": definition.source_adapter.as_dict(), + "components": [component.as_dict() for component in definition.components], + "models": [model.as_dict() for model in definition.models], + "resource_profile": definition.resource_profile.as_dict(), + "result_contract": definition.result_contract.as_dict(), + "executor": { + **definition.executor.identity_document(), + "reason_code": definition.executor.reason_code, + "reason": definition.executor.reason, + }, + "authority": definition.authority.as_dict(), + } + + +def _runtime_candidate_document( + candidate: PortableWorkerRuntimeCandidate, +) -> dict[str, object]: + return {**candidate.identity_document(), "candidate_sha256": candidate.candidate_sha256} + + +def _replace_registry_row( + source: Mapping[str, object], + *, + collection_key: str, + identity_key: str, + identity_value: str, + replacement: Mapping[str, object], + schema: str, +) -> dict[str, object]: + if source.get("schema_version") != schema: + raise PortableLabV1PromotionError("source registry schema changed") + rows = _array(source.get(collection_key), f"source registry {collection_key}") + matches = [ + row + for row in rows + if isinstance(row, dict) and row.get(identity_key) == identity_value + ] + if len(matches) != 1: + raise PortableLabV1PromotionError("source LAB V1 registry row is not unique") + return { + "schema_version": schema, + collection_key: [ + dict(replacement) + if isinstance(row, dict) and row.get(identity_key) == identity_value + else row + for row in rows + ], + } + + +def _assert_non_lab_rows_unchanged( + before: Mapping[str, object], + after: Mapping[str, object], + *, + collection_key: str, +) -> None: + before_rows = _array(before.get(collection_key), collection_key) + after_rows = _array(after.get(collection_key), collection_key) + before_other = [ + row + for row in before_rows + if not isinstance(row, dict) or row.get("setup_id") != PORTABLE_LAB_V1_SETUP_ID + ] + after_other = [ + row + for row in after_rows + if not isinstance(row, dict) or row.get("setup_id") != PORTABLE_LAB_V1_SETUP_ID + ] + if before_other != after_other: + raise PortableLabV1PromotionError("non-LAB registry rows changed") + + +def _load_installation_evidence( + *, + component_receipt_path: Path, + coordinator_receipt_path: Path, + source_revision: str, + component_source_revision: str, + coordinator_image_sha256: str, + eomt_image_sha256: str, + ddrnet_image_sha256: str, +) -> LabV1InstallationEvidence: + """Bind promotion to external installer outputs produced after real smoke.""" + + component_payload, component_value = _read_json_value( + component_receipt_path, + maximum=_MAX_INPUT_BYTES, + ) + if not isinstance(component_value, list) or len(component_value) != 2: + raise PortableLabV1PromotionError( + "component image installer receipt inventory is invalid" + ) + component_rows: dict[str, dict[str, object]] = {} + for value in component_value: + row = _object(value, "component image installer receipt row") + _exact_keys( + row, + { + "component", + "status", + "tag", + "base_image_sha256", + "derived_image_sha256", + "build_method", + }, + "component image installer receipt row", + ) + component = _string(row["component"], "installed component") + if component not in {"eomt", "ddrnet"} or component in component_rows: + raise PortableLabV1PromotionError( + "component image installer receipt inventory is invalid" + ) + expected_image = ( + eomt_image_sha256 if component == "eomt" else ddrnet_image_sha256 + ) + expected_base = ( + _COORDINATOR_BASE_IMAGE_SHA256 + if component == "eomt" + else "591cb382c099eeb05e7ec16e2371e0b2da54d2bb5c49ec0f4ac88dbf72b0f0cd" + ) + if ( + row["status"] not in {"installed", "already-installed"} + or row["tag"] + != ( + f"ndc/mission-core-lab-v1-{component}-adapter:" + f"{component_source_revision[:12]}" + ) + or row["base_image_sha256"] != expected_base + or row["derived_image_sha256"] != expected_image + or row["build_method"] != PORTABLE_LAB_V1_COMPONENT_IMAGE_BUILD_METHOD + ): + raise PortableLabV1PromotionError( + "component image installer evidence does not bind the promotion" + ) + component_rows[component] = row + if set(component_rows) != {"eomt", "ddrnet"}: + raise PortableLabV1PromotionError( + "component image installer receipt inventory is invalid" + ) + + coordinator_payload = _read_regular_file( + coordinator_receipt_path, + maximum=_MAX_INPUT_BYTES, + ) + coordinator = _read_json_object( + coordinator_receipt_path, + maximum=_MAX_INPUT_BYTES, + ) + _exact_keys( + coordinator, + { + "schema_version", + "status", + "worker_id", + "build_method", + "source_revision", + "provenance", + "base_image_sha256", + "derived_image_sha256", + "image", + "runtime_contract", + "smoke", + }, + "coordinator image installer receipt", + ) + provenance = _object(coordinator["provenance"], "coordinator provenance") + _exact_keys( + provenance, + { + "git_archive_sha256", + "git_archive_verification", + "staged_snapshot_sha256", + "staged_snapshot_file_count", + "staged_snapshot_byte_length", + "staged_snapshot_canonicalization", + "embedded_snapshot_manifest_sha256", + "embedded_snapshot_manifest_byte_length", + }, + "coordinator provenance", + ) + image = _object(coordinator["image"], "coordinator installed image") + _exact_keys( + image, + { + "tag", + "id", + "size_bytes", + "thin_layer_bytes", + "maximum_thin_layer_bytes", + "rootfs", + }, + "coordinator installed image", + ) + rootfs = _object(image["rootfs"], "coordinator installed image RootFS") + _exact_keys( + rootfs, + { + "base_layer_count", + "derived_layer_count", + "derived_layer_diff_id", + "pinned_base_is_exact_prefix", + }, + "coordinator installed image RootFS", + ) + runtime = _object(coordinator["runtime_contract"], "coordinator runtime contract") + _exact_keys( + runtime, + { + "workdir", + "entrypoint", + "command", + "authority", + "models", + "runtime_registries", + }, + "coordinator runtime contract", + ) + smoke = _object(coordinator["smoke"], "coordinator installation smoke") + _exact_keys( + smoke, + { + "network", + "read_only_rootfs", + "staged_source_bytes", + "embedded_context_bytes", + "embedded_snapshot_manifest", + "result", + }, + "coordinator installation smoke", + ) + file_count = _integer( + provenance["staged_snapshot_file_count"], + "coordinator staged snapshot file count", + ) + snapshot_bytes = _integer( + provenance["staged_snapshot_byte_length"], + "coordinator staged snapshot byte length", + ) + embedded_manifest_bytes = _integer( + provenance["embedded_snapshot_manifest_byte_length"], + "coordinator embedded snapshot manifest byte length", + ) + size_bytes = _integer(image["size_bytes"], "coordinator image size") + layer_bytes = _integer(image["thin_layer_bytes"], "coordinator thin layer size") + maximum_layer_bytes = _integer( + image["maximum_thin_layer_bytes"], + "coordinator maximum thin layer size", + ) + base_layer_count = _integer( + rootfs["base_layer_count"], + "coordinator base layer count", + ) + derived_layer_count = _integer( + rootfs["derived_layer_count"], + "coordinator derived layer count", + ) + for key in ( + "git_archive_sha256", + "staged_snapshot_sha256", + "embedded_snapshot_manifest_sha256", + ): + _require_digest( + _string(provenance[key], f"coordinator {key}"), + f"coordinator {key}", + ) + if ( + coordinator["schema_version"] != WORKER_AGENT_IMAGE_INSTALLATION_SCHEMA + or coordinator["status"] not in {"installed", "already-installed"} + or coordinator["worker_id"] != "worker-006" + or coordinator["build_method"] + != PORTABLE_LAB_V1_COMPONENT_IMAGE_BUILD_METHOD + or coordinator["source_revision"] != source_revision + or coordinator["base_image_sha256"] != _COORDINATOR_BASE_IMAGE_SHA256 + or coordinator["derived_image_sha256"] != coordinator_image_sha256 + or provenance["git_archive_verification"] != "external-before-extract" + or provenance["staged_snapshot_canonicalization"] + != "utf8-path-nul-length-nul-sha256-lf-v1" + or file_count <= 0 + or snapshot_bytes <= 0 + or embedded_manifest_bytes <= 0 + or image["tag"] + != f"ndc/mission-core-observatory-worker-agent:{source_revision[:12]}" + or image["id"] != f"sha256:{coordinator_image_sha256}" + or size_bytes <= 0 + or layer_bytes < 0 + or maximum_layer_bytes <= 0 + or layer_bytes > maximum_layer_bytes + or base_layer_count <= 0 + or derived_layer_count != 1 + or rootfs["pinned_base_is_exact_prefix"] is not True + or re.fullmatch( + r"sha256:[a-f0-9]{64}", + _string(rootfs["derived_layer_diff_id"], "derived layer diff ID"), + ) + is None + or runtime["workdir"] != "/opt/nodedc/mission-core" + or runtime["entrypoint"] + != ["python3", "-m", "k1link.observatory.m49_worker_container_main"] + or runtime["command"] != [] + or runtime["authority"] != "observation-only" + or runtime["models"] != "external" + or runtime["runtime_registries"] != "external-read-only" + or smoke + != { + "network": "none", + "read_only_rootfs": True, + "staged_source_bytes": "matched", + "embedded_context_bytes": "matched", + "embedded_snapshot_manifest": "matched", + "result": "passed", + } + ): + raise PortableLabV1PromotionError( + "coordinator image installer evidence does not bind the promotion" + ) + + component_sha256 = hashlib.sha256(component_payload).hexdigest() + coordinator_sha256 = hashlib.sha256(coordinator_payload).hexdigest() + identity = { + "schema_version": INSTALLATION_EVIDENCE_SCHEMA, + "source_revision": source_revision, + "component_source_revision": component_source_revision, + "coordinator_image_sha256": coordinator_image_sha256, + "eomt_image_sha256": eomt_image_sha256, + "ddrnet_image_sha256": ddrnet_image_sha256, + "component_image_installer_receipt_sha256": component_sha256, + "coordinator_image_installer_receipt_sha256": coordinator_sha256, + "smoke_scope": "offline-import-only", + "result": "passed", + } + return LabV1InstallationEvidence( + component_receipt_path=component_receipt_path.expanduser().absolute(), + coordinator_receipt_path=coordinator_receipt_path.expanduser().absolute(), + component_receipt_sha256=component_sha256, + coordinator_receipt_sha256=coordinator_sha256, + evidence_sha256=hashlib.sha256(canonical_json(identity)).hexdigest(), + ) + + +def _verify_installation_evidence(promotion: LabV1PromotionInput) -> None: + current = _load_installation_evidence( + component_receipt_path=( + promotion.installation_evidence.component_receipt_path + ), + coordinator_receipt_path=( + promotion.installation_evidence.coordinator_receipt_path + ), + source_revision=promotion.source_revision, + component_source_revision=promotion.component_source_revision, + coordinator_image_sha256=promotion.coordinator_image_sha256, + eomt_image_sha256=promotion.eomt.derived_image_sha256, + ddrnet_image_sha256=promotion.ddrnet.derived_image_sha256, + ) + if current != promotion.installation_evidence: + raise PortableLabV1PromotionError( + "external installation evidence changed after admission" + ) + + +def _component_input( + value: object, + component: PortableLabV1Component, +) -> ComponentPromotionInput: + row = _object(value, f"{component} promotion input") + _exact_keys( + row, + { + "base_image_sha256", + "derived_image_sha256", + "dockerfile_sha256", + "installer_sha256", + "shared_adapter_sha256", + "component_adapter_sha256", + "assets", + }, + f"{component} promotion input", + ) + assets = tuple( + sorted( + (_host_asset(item) for item in _array(row["assets"], "component assets")), + key=lambda asset: asset.asset_id, + ) + ) + return ComponentPromotionInput( + component=component, + base_image_sha256=_string(row["base_image_sha256"], "base image SHA-256"), + derived_image_sha256=_string( + row["derived_image_sha256"], + "derived image SHA-256", + ), + dockerfile_sha256=_string(row["dockerfile_sha256"], "Dockerfile SHA-256"), + installer_sha256=_string(row["installer_sha256"], "installer SHA-256"), + shared_adapter_sha256=_string( + row["shared_adapter_sha256"], + "shared adapter SHA-256", + ), + component_adapter_sha256=_string( + row["component_adapter_sha256"], + "component adapter SHA-256", + ), + assets=assets, + ) + + +def _host_asset(value: object) -> PortableLabV1HostAsset: + row = _object(value, "host asset") + _exact_keys( + row, + { + "asset_id", + "host_path", + "container_path", + "kind", + "verification", + "identity_sha256", + "byte_length", + }, + "host asset", + ) + length = row["byte_length"] + if length is not None and (isinstance(length, bool) or not isinstance(length, int)): + raise PortableLabV1PromotionError("host asset byte length is invalid") + return PortableLabV1HostAsset( + asset_id=_string(row["asset_id"], "host asset id"), + host_path=_string(row["host_path"], "host asset path"), + container_path=_string(row["container_path"], "container asset path"), + kind=cast(PortableLabV1AssetKind, _string(row["kind"], "host asset kind")), + verification=cast( + PortableLabV1AssetVerification, + _string(row["verification"], "host asset verification"), + ), + identity_sha256=_string(row["identity_sha256"], "host asset SHA-256"), + byte_length=length, + ) + + +def _read_regular_file(path: Path, *, maximum: int = 8 * 1024 * 1024) -> bytes: + candidate = path.expanduser().absolute() + if not path.is_absolute() or candidate.is_symlink() or not candidate.is_file(): + raise PortableLabV1PromotionError("promotion input file is not a regular file") + payload = candidate.read_bytes() + if not 0 < len(payload) <= maximum: + raise PortableLabV1PromotionError("promotion input file size is invalid") + return payload + + +def _read_json_object(path: Path, *, maximum: int = 1024 * 1024) -> dict[str, object]: + _payload, value = _read_json_value(path, maximum=maximum) + return _object(value, "promotion JSON") + + +def _read_json_value( + path: Path, + *, + maximum: int = 1024 * 1024, +) -> tuple[bytes, object]: + payload = _read_regular_file(path, maximum=maximum) + try: + value = json.loads( + payload.decode("utf-8"), + object_pairs_hook=_unique_object, + parse_constant=_reject_constant, + ) + except (UnicodeDecodeError, json.JSONDecodeError, ValueError) as exc: + raise PortableLabV1PromotionError("promotion JSON is invalid") from exc + return payload, value + + +def _write_canonical(path: Path, value: object) -> None: + path.write_bytes(canonical_json(value)) + + +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 PortableLabV1PromotionError(f"{label} must be an object") + return cast(dict[str, object], value) + + +def _array(value: object, label: str) -> list[object]: + if not isinstance(value, list): + raise PortableLabV1PromotionError(f"{label} must be an array") + return value + + +def _string(value: object, label: str) -> str: + if not isinstance(value, str) or not value: + raise PortableLabV1PromotionError(f"{label} must be a non-empty string") + return value + + +def _integer(value: object, label: str) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise PortableLabV1PromotionError(f"{label} must be an integer") + return value + + +def _exact_keys(row: Mapping[str, object], expected: set[str], label: str) -> None: + if set(row) != expected: + raise PortableLabV1PromotionError(f"{label} fields are invalid") + + +def _require_digest(value: str, label: str) -> None: + if re.fullmatch(r"[a-f0-9]{64}", value) is None: + raise PortableLabV1PromotionError(f"{label} is invalid") + + +def _unique_object(pairs: Sequence[tuple[str, object]]) -> dict[str, object]: + result: dict[str, object] = {} + for key, value in pairs: + if key in result: + raise ValueError("duplicate JSON key") + result[key] = value + return result + + +def _reject_constant(value: str) -> object: + raise ValueError(f"invalid JSON constant: {value}") + + +def main(arguments: Sequence[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--input", type=Path, required=True) + parser.add_argument("--definition-registry", type=Path, required=True) + parser.add_argument("--runtime-registry", type=Path, required=True) + parser.add_argument("--ddrnet-portable-config", type=Path, required=True) + parser.add_argument("--output-root", type=Path, required=True) + options = parser.parse_args(arguments) + result = generate_ready_lab_v1_artifacts( + promotion=load_promotion_input(options.input), + source_definition_registry=options.definition_registry, + source_runtime_registry=options.runtime_registry, + ddrnet_portable_config=options.ddrnet_portable_config, + output_root=options.output_root, + ) + print( + json.dumps( + { + "root": str(result.root), + "release_candidate_sha256": result.release_candidate_sha256, + "release_sha256": result.release_sha256, + "definition_sha256": result.definition_sha256, + "installation_receipt_file_sha256": ( + result.installation_receipt_file_sha256 + ), + "runtime_candidate_sha256": result.runtime_candidate_sha256, + }, + sort_keys=True, + ) + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/experiments/perception/worker/observatory_portable/worker-006-agent-install-plan.template.json b/experiments/perception/worker/observatory_portable/worker-006-agent-install-plan.template.json index ad55fbf..727f96c 100644 --- a/experiments/perception/worker/observatory_portable/worker-006-agent-install-plan.template.json +++ b/experiments/perception/worker/observatory_portable/worker-006-agent-install-plan.template.json @@ -6,17 +6,31 @@ "build": { "source_revision": null, "source_revision_requirement": "full clean committed Git revision", - "source_date_epoch": null, - "build_context_sha256": null, + "git_archive_sha256": null, + "staged_snapshot_sha256": null, + "installer": "experiments/perception/worker/observatory_portable/Install-Worker006AgentImage.ps1", "context_manifest": "experiments/perception/worker/observatory_portable/worker-006-agent-build-context.json", "dockerfile": "experiments/perception/worker/observatory_portable/Dockerfile.worker-006-agent", + "dockerfile_role": "source-contract evidence included in the staged snapshot; not executed by the exact-layer installer", "materialization": { "method": "git archive", "include_only_context_manifest_entries": true, "reject_dirty_worktree": true, "archive_format": "tar", "archive_paths_source": "context manifest context_entries in declared order", - "build_context_sha256_subject": "exact git-archive tar bytes" + "git_archive_sha256_subject": "exact git-archive tar bytes", + "extraction_target": "D:\\NDC_MISSIONCORE\\runtime\\staging\\observatory-worker-agent-" + }, + "staged_snapshot": { + "sha256_subject": "sorted regular-file rows for src/k1link, Dockerfile.worker-006-agent and worker-006-agent-build-context.json", + "canonicalization": "utf8-path-nul-length-nul-sha256-lf-v1", + "reject_reparse_points": true, + "reject_unexpected_entries": true, + "verify_timing": [ + "before temporary container creation", + "after source installation and before image commit" + ], + "embedded_manifest": "/opt/nodedc/mission-core/release/worker-006-agent-staged-snapshot.json" }, "base_image": { "reference": "nvcr.io/nvidia/tritonserver:26.06-py3@sha256:58df7489c3f2276f9591d500a012dee03e23d35543ce3c390b4c001e6bf90794", @@ -24,17 +38,20 @@ "must_exist_locally": true, "pull_allowed": false }, - "docker_build": { + "image_materialization": { + "method": "docker-commit-exact-layer-v1", + "dockerfile_executed": false, "network": "none", "pull": false, - "no_cache": true, - "provenance": false, "platform": "linux/amd64", - "context_input": "exact git-archive tar bytes", - "required_build_args": [ - "NODEDC_SOURCE_REVISION", - "NODEDC_BUILD_CONTEXT_SHA256", - "SOURCE_DATE_EPOCH" + "source_input": "exact read-only staged snapshot", + "base_container": "docker create by exact pinned base image ID", + "commit": "docker commit --pause=true with fixed config changes", + "maximum_thin_layer_bytes": 33554432, + "embedded_payload": [ + "/opt/nodedc/mission-core/src/k1link", + "/opt/nodedc/mission-core/release/worker-006-agent-build-context.json", + "/opt/nodedc/mission-core/release/worker-006-agent-staged-snapshot.json" ] }, "required_preflight": [ @@ -42,7 +59,9 @@ "selected revision resolves to exactly one commit", "selected revision equals HEAD", "base image inspect ID equals the pinned SHA-256", - "context archive contains exactly the context manifest entries" + "context archive contains exactly the context manifest entries", + "git archive SHA-256 was verified externally before extraction", + "staged snapshot SHA-256 matches the exact canonical regular-file inventory" ], "image_tag_template": "ndc/mission-core-observatory-worker-agent:" }, @@ -57,7 +76,21 @@ "com.nodedc.models": "external", "com.nodedc.runtime-registries": "external-read-only", "com.nodedc.base-image.sha256": "58df7489c3f2276f9591d500a012dee03e23d35543ce3c390b4c001e6bf90794", - "com.nodedc.build-context.sha256": "" + "com.nodedc.build-context.sha256": "", + "com.nodedc.staged-snapshot.sha256": "", + "com.nodedc.embedded-snapshot-manifest.sha256": "", + "com.nodedc.build-method": "docker-commit-exact-layer-v1" + }, + "rootfs": { + "base_layer_chain": "exact prefix of the pinned base image RootFS.Layers", + "derived_layer_count": 1, + "derived_layer_diff_id_required": true, + "maximum_thin_layer_bytes": 33554432 + }, + "embedded_identity": { + "source_tree": "byte-for-byte and bidirectional path match against staged src/k1link", + "context_manifest": "byte-for-byte match against the staged context manifest", + "snapshot_manifest": "byte-for-byte match against the generated manifest covering every staged file, including the Dockerfile" }, "smoke": { "network": "none", @@ -68,6 +101,9 @@ "-c", "import k1link.observatory.m49_worker_container_main as e; import k1link.observatory.m49_worker_service as c; assert callable(e.main); assert callable(c.compose_installed_m49_worker_service)" ], + "staged_source_bytes": "matched", + "embedded_context_bytes": "matched", + "embedded_snapshot_manifest": "matched", "expected_result": "exit-0" } }, @@ -80,7 +116,8 @@ "MISSIONCORE_OBSERVATORY_WORKER_DEFINITIONS_FILE": "/run/nodedc/registries/observatory-portable-run-definitions.json", "MISSIONCORE_OBSERVATORY_WORKER_RUNTIME_REGISTRY_FILE": "/run/nodedc/registries/observatory-worker-runtime-candidates.json", "MISSIONCORE_OBSERVATORY_M49_INSTALLATION_RECEIPT_FILE": "/release/worker-installation-receipt.json", - "MISSIONCORE_OBSERVATORY_LAB_V1_INSTALLATION_RECEIPT_FILE": "/release/lab-v1-worker-installation-receipt.json" + "MISSIONCORE_OBSERVATORY_LAB_V1_INSTALLATION_RECEIPT_FILE": "/release/lab-v1-worker-installation-receipt.json", + "MISSIONCORE_OBSERVATORY_LAB_V1_RELEASE_CANDIDATE_FILE": "/release/lab-v1-executor-release.json" }, "runtime_registry_files": { "binding": "individual read-only bind files", @@ -238,6 +275,9 @@ "state": "external-installed-receipt-required", "environment_variable": "MISSIONCORE_OBSERVATORY_LAB_V1_INSTALLATION_RECEIPT_FILE", "container_path": "/release/lab-v1-worker-installation-receipt.json", + "release_candidate_environment_variable": "MISSIONCORE_OBSERVATORY_LAB_V1_RELEASE_CANDIDATE_FILE", + "release_candidate_container_path": "/release/lab-v1-executor-release.json", + "release_repository_root": "/release", "mode": "read-only", "owns_component_image_identities": true, "queued_jobs_may_override_component_images": false @@ -249,29 +289,55 @@ ] }, "receipt_skeleton": { - "schema_version": "missioncore.observatory-worker-agent-image-receipt/v1", - "receipt_state": "not-built", + "schema_version": "missioncore.observatory-worker-agent-image-installation/v1", + "status": null, "worker_id": "worker-006", + "build_method": "docker-commit-exact-layer-v1", "source_revision": null, - "source_date_epoch": null, - "build_context_sha256": null, + "provenance": { + "git_archive_sha256": null, + "git_archive_verification": "external-before-extract", + "staged_snapshot_sha256": null, + "staged_snapshot_file_count": null, + "staged_snapshot_byte_length": null, + "staged_snapshot_canonicalization": "utf8-path-nul-length-nul-sha256-lf-v1", + "embedded_snapshot_manifest_sha256": null, + "embedded_snapshot_manifest_byte_length": null + }, "base_image_sha256": "58df7489c3f2276f9591d500a012dee03e23d35543ce3c390b4c001e6bf90794", + "derived_image_sha256": null, "image": { "tag": null, "id": null, - "size_bytes": null + "size_bytes": null, + "thin_layer_bytes": null, + "maximum_thin_layer_bytes": 33554432, + "rootfs": { + "base_layer_count": null, + "derived_layer_count": 1, + "derived_layer_diff_id": null, + "pinned_base_is_exact_prefix": true + } + }, + "runtime_contract": { + "workdir": "/opt/nodedc/mission-core", + "entrypoint": [ + "python3", + "-m", + "k1link.observatory.m49_worker_container_main" + ], + "command": [], + "authority": "observation-only", + "models": "external", + "runtime_registries": "external-read-only" }, "smoke": { "network": "none", + "read_only_rootfs": true, + "staged_source_bytes": null, + "embedded_context_bytes": null, + "embedded_snapshot_manifest": null, "result": "not-run" - }, - "models_baked_into_image": false, - "runtime_registries_baked_into_image": false, - "authority": { - "commands_enabled": false, - "actuation_allowed": false, - "navigation_or_safety_accepted": false, - "production_accepted": false } } } diff --git a/src/k1link/observatory/m49_worker_service.py b/src/k1link/observatory/m49_worker_service.py index ed6c460..205aec4 100644 --- a/src/k1link/observatory/m49_worker_service.py +++ b/src/k1link/observatory/m49_worker_service.py @@ -1,7 +1,7 @@ -"""Fixed POSIX service composition for the sealed portable M4.9 executor. +"""Fixed POSIX service composition for installed M4.9 and LAB V1 executors. -The entrypoint reads only immutable registries, one installation receipt and -path-only Worker service settings. Jobs cannot select commands, providers, +The entrypoint reads only immutable registries, exact external release files +and path-only Worker service settings. Jobs cannot select commands, providers, modules, images or filesystem locations. Every runtime asset is resolved from the fixed release layout and re-verified against the ready runtime candidate before the first queue claim. @@ -15,6 +15,7 @@ import json import os import re import signal +import socket import stat from collections.abc import Iterator, Mapping, Sequence from contextlib import contextmanager @@ -35,6 +36,22 @@ from k1link.observatory.m49_portable_executor import ( M49PortableRunnerInstallation, compose_m49_portable_executor_adapter, ) +from k1link.observatory.portable_lab_v1_executor import ( + PortableLabV1ReleaseAsset, + PortableLabV1ReleaseCandidate, + PortableLabV1ReleaseInspection, +) +from k1link.observatory.portable_lab_v1_worker import ( + PortableLabV1RunnerInstallation as PortableLabV1ReleaseInstallation, +) +from k1link.observatory.portable_lab_v1_worker_service import ( + PORTABLE_LAB_V1_ADAPTER_ID, + PORTABLE_LAB_V1_SETUP_ID, + PORTABLE_LAB_V1_WORKER_INSTALLATION_RECEIPT_ASSET_ID, + PortableLabV1WorkerInstallationReceipt, + compose_installed_lab_v1_executor_builder, + load_portable_lab_v1_worker_installation_receipt, +) from k1link.observatory.portable_result_contract import OBSERVATION_ONLY_AUTHORITY from k1link.observatory.portable_run_definitions import ( PortableRunDefinition, @@ -64,6 +81,12 @@ M49_WORKER_RUNTIME_REGISTRY_FILE_ENV: Final = "MISSIONCORE_OBSERVATORY_WORKER_RU M49_WORKER_INSTALLATION_RECEIPT_FILE_ENV: Final = ( "MISSIONCORE_OBSERVATORY_M49_INSTALLATION_RECEIPT_FILE" ) +LAB_V1_WORKER_INSTALLATION_RECEIPT_FILE_ENV: Final = ( + "MISSIONCORE_OBSERVATORY_LAB_V1_INSTALLATION_RECEIPT_FILE" +) +LAB_V1_WORKER_RELEASE_CANDIDATE_FILE_ENV: Final = ( + "MISSIONCORE_OBSERVATORY_LAB_V1_RELEASE_CANDIDATE_FILE" +) M49_WORKER_INSTALLATION_RECEIPT_SCHEMA: Final = ( "missioncore.m49-tgs-portable-worker-installation-ready-receipt/v1" @@ -74,12 +97,24 @@ M49_WORKER_RELEASE_ID: Final = "m49-tgs-portable-executor-v1" M49_EXECUTOR_RELEASE_ASSET_ID: Final = "m49-portable-executor-release" M49_WORKER_INSTALLATION_RECEIPT_ASSET_ID: Final = "m49-portable-worker-installation-receipt" +LAB_V1_PORTABLE_CONFIG_ASSET_ID: Final = "ddrnet-portable-config" +LAB_V1_WORKER_AGENT_IMAGE_ASSET_ID: Final = "worker-006-agent-image" _MAX_RECEIPT_BYTES: Final = 256 * 1024 +_MAX_DOCKER_INSPECTION_BYTES: Final = 1024 * 1024 _SHA256: Final = re.compile(r"^[a-f0-9]{64}$") +_CONTAINER_HOSTNAME: Final = re.compile(r"^[a-f0-9]{12,64}$") _SOURCE_REVISION: Final = re.compile(r"^[a-f0-9]{40}$") _WORKER_COMPUTER_NAME: Final = "DESKTOP-OPJ8J04" +_DOCKER_SOCKET: Final = Path("/var/run/docker.sock") +_DOCKER_API_VERSION: Final = "v1.47" _FIXED_RUNTIME_RELEASE_ROOT: Final = Path("/release") +_FIXED_LAB_V1_INSTALLATION_RECEIPT_FILE: Final = ( + _FIXED_RUNTIME_RELEASE_ROOT / "lab-v1-worker-installation-receipt.json" +) +_FIXED_LAB_V1_RELEASE_CANDIDATE_FILE: Final = ( + _FIXED_RUNTIME_RELEASE_ROOT / "lab-v1-executor-release.json" +) _PROTECTED_RUNTIME_NAMES: Final = ( "ndc-mission-core-triton", "ndc-mission-core-perception-worker", @@ -119,6 +154,8 @@ class M49WorkerEntrypointConfiguration: definitions_file: Path runtime_registry_file: Path installation_receipt_file: Path + lab_v1_installation_receipt_file: Path | None = None + lab_v1_release_candidate_file: Path | None = None def __post_init__(self) -> None: for path, label in ( @@ -127,6 +164,33 @@ class M49WorkerEntrypointConfiguration: (self.installation_receipt_file, "M4.9 installation receipt"), ): _absolute_path(path, label) + if (self.lab_v1_installation_receipt_file is None) != ( + self.lab_v1_release_candidate_file is None + ): + raise M49WorkerCompositionError( + "LAB V1 installation receipt and release candidate must be configured together" + ) + for optional_path, label in ( + ( + self.lab_v1_installation_receipt_file, + "LAB V1 installation receipt", + ), + (self.lab_v1_release_candidate_file, "LAB V1 release candidate"), + ): + if optional_path is not None: + _absolute_path(optional_path, label) + if ( + self.lab_v1_installation_receipt_file is not None + and ( + self.lab_v1_installation_receipt_file + != _FIXED_LAB_V1_INSTALLATION_RECEIPT_FILE + or self.lab_v1_release_candidate_file + != _FIXED_LAB_V1_RELEASE_CANDIDATE_FILE + ) + ): + raise M49WorkerCompositionError( + "LAB V1 production inputs must use the fixed /release files" + ) @classmethod def from_environment( @@ -148,6 +212,14 @@ class M49WorkerEntrypointConfiguration: values, M49_WORKER_INSTALLATION_RECEIPT_FILE_ENV, ), + lab_v1_installation_receipt_file=_required_environment_path( + values, + LAB_V1_WORKER_INSTALLATION_RECEIPT_FILE_ENV, + ), + lab_v1_release_candidate_file=_required_environment_path( + values, + LAB_V1_WORKER_RELEASE_CANDIDATE_FILE_ENV, + ), ) @@ -483,15 +555,374 @@ def compose_installed_m49_worker_service( installation=installation, ), ) + installed_builders = list(executor_builders) + if configuration.lab_v1_installation_receipt_file is not None: + installed_builders.insert( + 0, + _compose_installed_lab_v1_builder( + configuration=configuration, + definitions=definitions, + runtime_registry=runtime_registry, + ), + ) return compose_installed_observatory_worker_service_from_builders( configuration=configuration.worker, definitions=definitions, runtime_registry=runtime_registry, - builders=(m49_builder, *executor_builders), + builders=(m49_builder, *installed_builders), http_transport=http_transport, ) +def _compose_installed_lab_v1_builder( + *, + configuration: M49WorkerEntrypointConfiguration, + definitions: PortableRunDefinitionRegistry, + runtime_registry: PortableWorkerRuntimeRegistry, +) -> ObservatoryWorkerExecutorBuilderRegistration: + receipt_path = configuration.lab_v1_installation_receipt_file + release_path = configuration.lab_v1_release_candidate_file + if receipt_path is None or release_path is None: + raise M49WorkerCompositionError("LAB V1 production inputs are incomplete") + definition = definitions.resolve_setup(PORTABLE_LAB_V1_SETUP_ID) + candidate = runtime_registry.resolve( + definition.setup_id, + definition.definition_sha256, + ) + _verify_ready_lab_v1_identity(definitions, definition, candidate) + receipt = load_portable_lab_v1_worker_installation_receipt(receipt_path) + release_file = _regular_file(release_path, "LAB V1 release candidate") + release = PortableLabV1ReleaseCandidate.from_file( + release_file, + repository_root=_real_directory( + release_file.parent, + "LAB V1 release repository root", + ), + ) + if receipt.release_candidate_sha256 != release.candidate_sha256: + raise M49WorkerCompositionError( + "LAB V1 installation receipt belongs to another release candidate" + ) + release.bind_definition(definition) + inspection = _inspect_installed_lab_v1_release( + release=release, + receipt=receipt, + definition=definition, + running_worker_image_sha256=( + _inspect_running_worker_container_image_sha256() + ), + ) + bindings = _lab_v1_runtime_asset_bindings( + candidate=candidate, + release=release, + receipt=receipt, + ) + admission = inspect_runtime_candidate(candidate, bindings) + if not admission.ready: + raise M49WorkerCompositionError( + "portable LAB V1 local asset admission is not ready" + ) + portable_config_path = _lab_v1_release_repository_file( + release, + LAB_V1_PORTABLE_CONFIG_ASSET_ID, + ) + release_installation = PortableLabV1ReleaseInstallation( + release=release, + inspection=inspection, + portable_ddrnet_config_path=portable_config_path, + output_parent=( + configuration.worker.work_root / "lab-v1-portable" / "runner-output" + ), + ) + return compose_installed_lab_v1_executor_builder( + receipt=receipt, + definition=definition, + candidate=candidate, + admission=admission, + release_installation=release_installation, + ) + + +def _verify_ready_lab_v1_identity( + definitions: PortableRunDefinitionRegistry, + definition: PortableRunDefinition, + candidate: PortableWorkerRuntimeCandidate, +) -> None: + ready_identities = { + (item.setup_id, item.definition_sha256) + for item in definitions.ready_recorded_definitions() + } + if ( + (definition.setup_id, definition.definition_sha256) not in ready_identities + or definition.setup_id != PORTABLE_LAB_V1_SETUP_ID + or definition.executor.contour_id != WORKER_006_CONTOUR_ID + or candidate.adapter_id != PORTABLE_LAB_V1_ADAPTER_ID + or not definition.executor.ready + or not candidate.ready + ): + raise M49WorkerCompositionError( + "fixed Worker requires the exact ready LAB V1 definition" + ) + + +def _lab_v1_runtime_asset_bindings( + *, + candidate: PortableWorkerRuntimeCandidate, + release: PortableLabV1ReleaseCandidate, + receipt: PortableLabV1WorkerInstallationReceipt, +) -> dict[str, PortableWorkerLocalAssetBinding]: + release_assets = {asset.asset_id: asset for asset in release.assets} + bindings: dict[str, PortableWorkerLocalAssetBinding] = {} + for requirement in candidate.reusable_assets: + if ( + requirement.asset_id + == PORTABLE_LAB_V1_WORKER_INSTALLATION_RECEIPT_ASSET_ID + ): + if ( + requirement.kind != "local-file" + or requirement.sha256 != receipt.file_sha256 + or requirement.byte_length != receipt.file_byte_length + ): + raise M49WorkerCompositionError( + "LAB V1 runtime installation receipt anchor changed" + ) + bindings[requirement.asset_id] = PortableWorkerLocalAssetBinding( + asset_id=requirement.asset_id, + file_path=receipt.path, + ) + continue + asset = release_assets.get(requirement.asset_id) + if ( + asset is None + or asset.sha256 != requirement.sha256 + or asset.byte_length != requirement.byte_length + ): + raise M49WorkerCompositionError( + "LAB V1 runtime asset differs from its exact release candidate" + ) + if requirement.kind == "container-image" and asset.kind == "container-image": + bindings[requirement.asset_id] = PortableWorkerLocalAssetBinding( + asset_id=requirement.asset_id, + image_sha256=asset.sha256, + ) + continue + if asset.repository_path is None: + raise M49WorkerCompositionError( + "LAB V1 non-image runtime asset has no exact release file" + ) + bindings[requirement.asset_id] = PortableWorkerLocalAssetBinding( + asset_id=requirement.asset_id, + file_path=_lab_v1_release_repository_file( + release, + requirement.asset_id, + ), + ) + return bindings + + +def _inspect_installed_lab_v1_release( + *, + release: PortableLabV1ReleaseCandidate, + receipt: PortableLabV1WorkerInstallationReceipt, + definition: PortableRunDefinition, + running_worker_image_sha256: str, +) -> PortableLabV1ReleaseInspection: + if release.declared_blockers or release.executor_image_sha256 is None: + raise M49WorkerCompositionError("LAB V1 release candidate is not ready") + _digest( + running_worker_image_sha256, + "running Worker container image SHA-256", + ) + worker_image_assets = tuple( + asset + for asset in release.assets + if asset.asset_id == LAB_V1_WORKER_AGENT_IMAGE_ASSET_ID + ) + if ( + len(worker_image_assets) != 1 + or worker_image_assets[0].kind != "container-image" + or worker_image_assets[0].sha256 != running_worker_image_sha256 + or worker_image_assets[0].byte_length is not None + or worker_image_assets[0].repository_path is not None + or release.executor_image_sha256 != running_worker_image_sha256 + ): + raise M49WorkerCompositionError( + "LAB V1 executor image differs from the running Worker container" + ) + installed_evidence = _lab_v1_installed_release_evidence(receipt) + installed_images = { + receipt.eomt_image_build.base_image_sha256, + receipt.eomt_image_build.derived_image_sha256, + receipt.ddrnet_image_build.base_image_sha256, + receipt.ddrnet_image_build.derived_image_sha256, + running_worker_image_sha256, + } + definition_components = {component.sha256 for component in definition.components} + matched: list[str] = [] + for asset in release.assets: + if ( + asset.asset_id + == PORTABLE_LAB_V1_WORKER_INSTALLATION_RECEIPT_ASSET_ID + ): + raise M49WorkerCompositionError( + "LAB V1 installation receipt must remain a runtime-only anchor" + ) + if asset.kind == "repository-file": + _lab_v1_release_repository_file(release, asset.asset_id) + elif asset.kind == "container-image": + if asset.byte_length is not None or asset.sha256 not in installed_images: + raise M49WorkerCompositionError( + "LAB V1 release contains an uninstalled container image" + ) + elif asset.kind == "definition-component": + if asset.byte_length is not None or asset.sha256 not in definition_components: + raise M49WorkerCompositionError( + "LAB V1 release contains an unknown definition component" + ) + elif not _lab_v1_release_evidence_matches(asset, installed_evidence): + raise M49WorkerCompositionError( + "LAB V1 release contains an asset absent from its installation receipt" + ) + matched.append(asset.asset_id) + return PortableLabV1ReleaseInspection( + candidate_sha256=release.candidate_sha256, + matched_assets=tuple(matched), + blockers=(), + ready=True, + ) + + +def _inspect_running_worker_container_image_sha256( + *, + container_hostname: str | None = None, + transport: httpx.BaseTransport | None = None, + docker_socket: Path = _DOCKER_SOCKET, +) -> str: + """Return the exact image ID of this running Worker container.""" + + hostname = socket.gethostname() if container_hostname is None else container_hostname + if _CONTAINER_HOSTNAME.fullmatch(hostname) is None: + raise M49WorkerCompositionError( + "running Worker container hostname is not an exact container ID" + ) + selected_transport = transport + if selected_transport is None: + candidate = docker_socket.expanduser().absolute() + try: + metadata = candidate.lstat() + except OSError as exc: + raise M49WorkerCompositionError( + "running Worker Docker Engine socket is unavailable" + ) from exc + if stat.S_ISLNK(metadata.st_mode) or not stat.S_ISSOCK(metadata.st_mode): + raise M49WorkerCompositionError( + "running Worker Docker Engine socket is unsafe" + ) + selected_transport = httpx.HTTPTransport(uds=str(candidate)) + try: + with httpx.Client( + base_url="http://docker", + transport=selected_transport, + timeout=5.0, + ) as client: + response = client.get( + f"/{_DOCKER_API_VERSION}/containers/{hostname}/json" + ) + payload = response.content + except (httpx.HTTPError, OSError) as exc: + raise M49WorkerCompositionError( + "running Worker container inspection failed" + ) from exc + if response.status_code != 200 or not 0 < len(payload) <= _MAX_DOCKER_INSPECTION_BYTES: + raise M49WorkerCompositionError( + "running Worker container inspection is unavailable" + ) + try: + document = json.loads(payload.decode("utf-8")) + except (UnicodeDecodeError, json.JSONDecodeError) as exc: + raise M49WorkerCompositionError( + "running Worker container inspection is invalid" + ) from exc + row = _object(document, "running Worker container inspection") + container_id = _string(row.get("Id"), "running Worker container ID") + image_reference = _string( + row.get("Image"), + "running Worker container image ID", + ) + state = _object(row.get("State"), "running Worker container state") + image_sha256 = image_reference.removeprefix("sha256:") + if ( + _SHA256.fullmatch(container_id) is None + or not container_id.startswith(hostname) + or image_reference != f"sha256:{image_sha256}" + or _SHA256.fullmatch(image_sha256) is None + or state.get("Running") is not True + ): + raise M49WorkerCompositionError( + "running Worker container identity changed" + ) + return image_sha256 + + +def _lab_v1_installed_release_evidence( + receipt: PortableLabV1WorkerInstallationReceipt, +) -> dict[str, set[int | None]]: + evidence: dict[str, set[int | None]] = {} + + def add(digest: str, byte_length: int | None) -> None: + evidence.setdefault(digest, set()).add(byte_length) + + for component in ( + receipt.runner_installation.eomt, + receipt.runner_installation.ddrnet, + ): + add(component.image_sha256, None) + add(component.installation_sha256, None) + for asset in component.assets: + add(asset.identity_sha256, asset.byte_length) + add(receipt.runner_installation.receipt_sha256, None) + for build in (receipt.eomt_image_build, receipt.ddrnet_image_build): + for key, value in build.identity_document().items(): + if key.endswith("_sha256") and isinstance(value, str): + add(value, None) + add(build.seal_sha256, None) + return evidence + + +def _lab_v1_release_evidence_matches( + asset: PortableLabV1ReleaseAsset, + evidence: Mapping[str, set[int | None]], +) -> bool: + lengths = evidence.get(asset.sha256) + if lengths is None: + return False + return asset.byte_length is None or asset.byte_length in lengths + + +def _lab_v1_release_repository_file( + release: PortableLabV1ReleaseCandidate, + asset_id: str, +) -> Path: + asset = next((row for row in release.assets if row.asset_id == asset_id), None) + if asset is None or asset.kind != "repository-file" or asset.repository_path is None: + raise M49WorkerCompositionError("LAB V1 exact release file is unavailable") + relative = _safe_relative_path(asset.repository_path) + path = _regular_file( + release.repository_root.joinpath(*relative.parts), + "LAB V1 release file", + ) + if not path.is_relative_to(release.repository_root): + raise M49WorkerCompositionError("LAB V1 release file escapes its release root") + if ( + (asset.byte_length is not None and path.stat().st_size != asset.byte_length) + or _sha256_file(path) != asset.sha256 + ): + raise M49WorkerCompositionError( + "LAB V1 release file differs from its release candidate" + ) + return path + + def run_installed_m49_worker( service: InstalledObservatoryWorkerService, *, diff --git a/src/k1link/observatory/portable_lab_v1_worker.py b/src/k1link/observatory/portable_lab_v1_worker.py index 1eed1c3..c39b2d8 100644 --- a/src/k1link/observatory/portable_lab_v1_worker.py +++ b/src/k1link/observatory/portable_lab_v1_worker.py @@ -94,6 +94,9 @@ _EXPECTED_RESULT_CONTRACT_SHA256: Final = ( "b3dfaa8e20a0f22fc510d062ac469f010a3281c650059d9ea134f0b3ccb38d9a" ) _PORTABLE_CONFIG_ASSET_ID: Final = "ddrnet-portable-config" +_WORKER_INSTALLATION_RECEIPT_ASSET_ID: Final = ( + "lab-v1-worker-installation-receipt" +) _MAX_SOURCE_DOCUMENT_BYTES: Final = 8 * 1024 * 1024 _MAX_MATERIALIZATION_MANIFEST_BYTES: Final = 64 * 1024 * 1024 _MAX_SOURCE_MEMBERS: Final = 100_000 @@ -582,6 +585,12 @@ def _verify_candidate_release( ): raise PortableLabV1WorkerError("portable LAB V1 runtime and release candidates disagree") for requirement in candidate.reusable_assets: + if requirement.asset_id == _WORKER_INSTALLATION_RECEIPT_ASSET_ID: + if requirement.kind != "local-file": + raise PortableLabV1WorkerError( + "portable LAB V1 installation receipt runtime anchor changed" + ) + continue asset = release_assets.get(requirement.asset_id) if ( asset is None diff --git a/src/k1link/observatory/portable_lab_v1_worker_service.py b/src/k1link/observatory/portable_lab_v1_worker_service.py index 1b0e333..d15ad1c 100644 --- a/src/k1link/observatory/portable_lab_v1_worker_service.py +++ b/src/k1link/observatory/portable_lab_v1_worker_service.py @@ -63,10 +63,13 @@ from k1link.observatory.worker_service import ( ) PORTABLE_LAB_V1_WORKER_INSTALLATION_RECEIPT_SCHEMA: Final = ( - "missioncore.observatory-portable-lab-v1-worker-installation-ready-receipt/v1" + "missioncore.observatory-portable-lab-v1-worker-installation-ready-receipt/v2" ) PORTABLE_LAB_V1_COMPONENT_IMAGE_BUILD_SEAL_SCHEMA: Final = ( - "missioncore.observatory-portable-lab-v1-component-image-build-seal/v1" + "missioncore.observatory-portable-lab-v1-component-image-build-seal/v2" +) +PORTABLE_LAB_V1_COMPONENT_IMAGE_BUILD_METHOD: Final = ( + "docker-commit-exact-layer-v1" ) PORTABLE_LAB_V1_WORKER_INSTALLATION_RECEIPT_ASSET_ID: Final = ( "lab-v1-worker-installation-receipt" @@ -98,6 +101,8 @@ class PortableLabV1ComponentImageBuildSeal: base_image_sha256: str derived_image_sha256: str dockerfile_sha256: str + build_method: str + installer_sha256: str shared_adapter_sha256: str component_adapter_sha256: str network: str @@ -112,6 +117,7 @@ class PortableLabV1ComponentImageBuildSeal: (self.base_image_sha256, "LAB V1 component base image SHA-256"), (self.derived_image_sha256, "LAB V1 derived component image SHA-256"), (self.dockerfile_sha256, "LAB V1 component Dockerfile SHA-256"), + (self.installer_sha256, "LAB V1 component image installer SHA-256"), (self.shared_adapter_sha256, "LAB V1 shared adapter SHA-256"), (self.component_adapter_sha256, "LAB V1 component adapter SHA-256"), (self.seal_sha256, "LAB V1 component image build seal SHA-256"), @@ -120,6 +126,8 @@ class PortableLabV1ComponentImageBuildSeal: if ( self.base_image_sha256 != _COMPONENT_BASE_IMAGE_SHA256S[self.component] + or self.build_method + != PORTABLE_LAB_V1_COMPONENT_IMAGE_BUILD_METHOD or self.network != "none" or self.seal_sha256 != hashlib.sha256(canonical_json(self.identity_document())).hexdigest() @@ -136,6 +144,8 @@ class PortableLabV1ComponentImageBuildSeal: base_image_sha256: str, derived_image_sha256: str, dockerfile_sha256: str, + build_method: str, + installer_sha256: str, shared_adapter_sha256: str, component_adapter_sha256: str, ) -> PortableLabV1ComponentImageBuildSeal: @@ -144,6 +154,8 @@ class PortableLabV1ComponentImageBuildSeal: base_image_sha256=base_image_sha256, derived_image_sha256=derived_image_sha256, dockerfile_sha256=dockerfile_sha256, + build_method=build_method, + installer_sha256=installer_sha256, shared_adapter_sha256=shared_adapter_sha256, component_adapter_sha256=component_adapter_sha256, ) @@ -152,6 +164,8 @@ class PortableLabV1ComponentImageBuildSeal: base_image_sha256=base_image_sha256, derived_image_sha256=derived_image_sha256, dockerfile_sha256=dockerfile_sha256, + build_method=build_method, + installer_sha256=installer_sha256, shared_adapter_sha256=shared_adapter_sha256, component_adapter_sha256=component_adapter_sha256, network="none", @@ -164,6 +178,8 @@ class PortableLabV1ComponentImageBuildSeal: base_image_sha256=self.base_image_sha256, derived_image_sha256=self.derived_image_sha256, dockerfile_sha256=self.dockerfile_sha256, + build_method=self.build_method, + installer_sha256=self.installer_sha256, shared_adapter_sha256=self.shared_adapter_sha256, component_adapter_sha256=self.component_adapter_sha256, ) @@ -181,6 +197,7 @@ class PortableLabV1WorkerInstallationReceipt: runner_installation: PortableLabV1ComponentRunnerInstallation eomt_image_build: PortableLabV1ComponentImageBuildSeal ddrnet_image_build: PortableLabV1ComponentImageBuildSeal + installation_evidence_sha256: str receipt_sha256: str def __post_init__(self) -> None: @@ -193,6 +210,10 @@ class PortableLabV1WorkerInstallationReceipt: "LAB V1 installation receipt size is invalid" ) _digest(self.file_sha256, "LAB V1 installation receipt file SHA-256") + _digest( + self.installation_evidence_sha256, + "LAB V1 external installation evidence SHA-256", + ) if _SOURCE_REVISION.fullmatch(self.source_revision) is None: raise PortableLabV1WorkerCompositionError( "LAB V1 installation receipt source revision is invalid" @@ -223,6 +244,7 @@ class PortableLabV1WorkerInstallationReceipt: runner_installation=self.runner_installation, eomt_image_build=self.eomt_image_build, ddrnet_image_build=self.ddrnet_image_build, + installation_evidence_sha256=self.installation_evidence_sha256, ): raise PortableLabV1WorkerCompositionError( "LAB V1 installation receipt identity changed" @@ -309,7 +331,8 @@ def load_portable_lab_v1_worker_installation_receipt( "runner_installation", "component_installations", "component_image_build_seals", - "fixture_smoke", + "installation_evidence_sha256", + "installation_smoke", "blockers", "authority", "receipt_sha256", @@ -322,7 +345,7 @@ def load_portable_lab_v1_worker_installation_receipt( != PORTABLE_LAB_V1_WORKER_INSTALLATION_RECEIPT_SCHEMA or row["receipt_state"] != "installed-ready" or row["worker_id"] != _WORKER_ID - or row["fixture_smoke"] != "passed" + or row["installation_smoke"] != "offline-import-passed" or blockers or row["authority"] != OBSERVATION_ONLY_AUTHORITY ): @@ -385,6 +408,10 @@ def load_portable_lab_v1_worker_installation_receipt( runner_installation=runner, eomt_image_build=eomt_image_build, ddrnet_image_build=ddrnet_image_build, + installation_evidence_sha256=_string( + row["installation_evidence_sha256"], + "LAB V1 external installation evidence SHA-256", + ), receipt_sha256=_string( row["receipt_sha256"], "LAB V1 installation receipt SHA-256", @@ -501,6 +528,7 @@ def portable_lab_v1_worker_receipt_document( runner_installation: PortableLabV1ComponentRunnerInstallation, eomt_image_build: PortableLabV1ComponentImageBuildSeal, ddrnet_image_build: PortableLabV1ComponentImageBuildSeal, + installation_evidence_sha256: str, ) -> dict[str, object]: """Return the canonical serializable receipt document for an installer.""" @@ -510,6 +538,7 @@ def portable_lab_v1_worker_receipt_document( runner_installation=runner_installation, eomt_image_build=eomt_image_build, ddrnet_image_build=ddrnet_image_build, + installation_evidence_sha256=installation_evidence_sha256, ) return {**identity, "receipt_sha256": hashlib.sha256(canonical_json(identity)).hexdigest()} @@ -521,6 +550,7 @@ def _receipt_identity_sha256( runner_installation: PortableLabV1ComponentRunnerInstallation, eomt_image_build: PortableLabV1ComponentImageBuildSeal, ddrnet_image_build: PortableLabV1ComponentImageBuildSeal, + installation_evidence_sha256: str, ) -> str: return hashlib.sha256( canonical_json( @@ -530,6 +560,7 @@ def _receipt_identity_sha256( runner_installation=runner_installation, eomt_image_build=eomt_image_build, ddrnet_image_build=ddrnet_image_build, + installation_evidence_sha256=installation_evidence_sha256, ) ) ).hexdigest() @@ -542,7 +573,12 @@ def _receipt_identity_document( runner_installation: PortableLabV1ComponentRunnerInstallation, eomt_image_build: PortableLabV1ComponentImageBuildSeal, ddrnet_image_build: PortableLabV1ComponentImageBuildSeal, + installation_evidence_sha256: str, ) -> dict[str, object]: + _digest( + installation_evidence_sha256, + "LAB V1 external installation evidence SHA-256", + ) return { "schema_version": PORTABLE_LAB_V1_WORKER_INSTALLATION_RECEIPT_SCHEMA, "receipt_state": "installed-ready", @@ -561,7 +597,8 @@ def _receipt_identity_document( "eomt": _component_image_build_document(eomt_image_build), "ddrnet": _component_image_build_document(ddrnet_image_build), }, - "fixture_smoke": "passed", + "installation_evidence_sha256": installation_evidence_sha256, + "installation_smoke": "offline-import-passed", "blockers": [], "authority": dict(OBSERVATION_ONLY_AUTHORITY), } @@ -582,6 +619,8 @@ def _component_image_build_identity( base_image_sha256: str, derived_image_sha256: str, dockerfile_sha256: str, + build_method: str, + installer_sha256: str, shared_adapter_sha256: str, component_adapter_sha256: str, ) -> dict[str, object]: @@ -591,6 +630,8 @@ def _component_image_build_identity( "base_image_sha256": base_image_sha256, "derived_image_sha256": derived_image_sha256, "dockerfile_sha256": dockerfile_sha256, + "build_method": build_method, + "installer_sha256": installer_sha256, "shared_adapter_sha256": shared_adapter_sha256, "component_adapter_sha256": component_adapter_sha256, "network": "none", @@ -618,6 +659,8 @@ def _component_image_build_seal( "base_image_sha256", "derived_image_sha256", "dockerfile_sha256", + "build_method", + "installer_sha256", "shared_adapter_sha256", "component_adapter_sha256", "network", @@ -649,6 +692,14 @@ def _component_image_build_seal( row["dockerfile_sha256"], "LAB V1 component Dockerfile SHA-256", ), + build_method=_string( + row["build_method"], + "LAB V1 component image build method", + ), + installer_sha256=_string( + row["installer_sha256"], + "LAB V1 component image installer SHA-256", + ), shared_adapter_sha256=_string( row["shared_adapter_sha256"], "LAB V1 shared adapter SHA-256", @@ -694,6 +745,7 @@ def _verify_release_covers_runner_installation( for build in image_builds for digest in ( build.dockerfile_sha256, + build.installer_sha256, build.shared_adapter_sha256, build.component_adapter_sha256, build.seal_sha256, @@ -969,6 +1021,7 @@ def _utc_now() -> str: __all__ = [ + "PORTABLE_LAB_V1_COMPONENT_IMAGE_BUILD_METHOD", "PORTABLE_LAB_V1_COMPONENT_IMAGE_BUILD_SEAL_SCHEMA", "PORTABLE_LAB_V1_ADAPTER_ID", "PORTABLE_LAB_V1_SETUP_ID", diff --git a/tests/test_observatory_m49_lab_v1_wiring.py b/tests/test_observatory_m49_lab_v1_wiring.py new file mode 100644 index 0000000..0b5c301 --- /dev/null +++ b/tests/test_observatory_m49_lab_v1_wiring.py @@ -0,0 +1,237 @@ +from __future__ import annotations + +import hashlib +from pathlib import Path +from types import SimpleNamespace +from typing import cast + +import httpx +import pytest + +from k1link.observatory import m49_worker_service as service_module +from k1link.observatory.portable_lab_v1_executor import ( + PortableLabV1ReleaseAsset, + PortableLabV1ReleaseCandidate, +) +from k1link.observatory.portable_lab_v1_worker_service import ( + PortableLabV1WorkerInstallationReceipt, +) +from k1link.observatory.portable_run_definitions import PortableRunDefinition + + +def _sha256(payload: bytes) -> str: + return hashlib.sha256(payload).hexdigest() + + +def _receipt(asset_sha256: str, byte_length: int) -> object: + host_asset = SimpleNamespace( + identity_sha256=asset_sha256, + byte_length=byte_length, + ) + eomt = SimpleNamespace( + image_sha256="1" * 64, + installation_sha256="c" * 64, + assets=(host_asset,), + ) + ddrnet = SimpleNamespace( + image_sha256="2" * 64, + installation_sha256="d" * 64, + assets=(), + ) + eomt_build = SimpleNamespace( + base_image_sha256="3" * 64, + derived_image_sha256=eomt.image_sha256, + seal_sha256="4" * 64, + identity_document=lambda: { + "base_image_sha256": "3" * 64, + "derived_image_sha256": eomt.image_sha256, + "adapter_sha256": "5" * 64, + }, + ) + ddrnet_build = SimpleNamespace( + base_image_sha256="6" * 64, + derived_image_sha256=ddrnet.image_sha256, + seal_sha256="7" * 64, + identity_document=lambda: { + "base_image_sha256": "6" * 64, + "derived_image_sha256": ddrnet.image_sha256, + "adapter_sha256": "8" * 64, + }, + ) + return SimpleNamespace( + runner_installation=SimpleNamespace( + eomt=eomt, + ddrnet=ddrnet, + receipt_sha256="e" * 64, + ), + eomt_image_build=eomt_build, + ddrnet_image_build=ddrnet_build, + ) + + +def _release( + tmp_path: Path, + *, + installed_asset_sha256: str, + installed_asset_length: int, + include_unknown: bool = False, + include_worker_image: bool = True, + executor_image_sha256: str = "b" * 64, +) -> object: + config_payload = b'{"profile":"lab-v1"}' + config_path = tmp_path / "config.json" + config_path.write_bytes(config_payload) + assets = [ + PortableLabV1ReleaseAsset( + asset_id="ddrnet-portable-config", + kind="repository-file", + sha256=_sha256(config_payload), + byte_length=len(config_payload), + repository_path="config.json", + ), + PortableLabV1ReleaseAsset( + asset_id="eomt-installed-tree", + kind="runtime-artifact", + sha256=installed_asset_sha256, + byte_length=installed_asset_length, + repository_path=None, + ), + ] + if include_worker_image: + assets.append( + PortableLabV1ReleaseAsset( + asset_id=service_module.LAB_V1_WORKER_AGENT_IMAGE_ASSET_ID, + kind="container-image", + sha256=executor_image_sha256, + byte_length=None, + repository_path=None, + ) + ) + if include_unknown: + assets.append( + PortableLabV1ReleaseAsset( + asset_id="unknown-runtime-artifact", + kind="runtime-artifact", + sha256="9" * 64, + byte_length=None, + repository_path=None, + ) + ) + assets.sort(key=lambda asset: asset.asset_id) + return SimpleNamespace( + assets=tuple(assets), + candidate_sha256="a" * 64, + declared_blockers=(), + executor_image_sha256=executor_image_sha256, + repository_root=tmp_path, + ) + + +def test_release_admission_requires_exact_files_or_installation_evidence( + tmp_path: Path, +) -> None: + installed_payload = b"sealed EoMT tree identity" + installed_sha256 = _sha256(installed_payload) + receipt = _receipt(installed_sha256, len(installed_payload)) + release = _release( + tmp_path, + installed_asset_sha256=installed_sha256, + installed_asset_length=len(installed_payload), + ) + definition = SimpleNamespace(components=()) + + inspection = service_module._inspect_installed_lab_v1_release( # noqa: SLF001 + release=cast(PortableLabV1ReleaseCandidate, release), + receipt=cast(PortableLabV1WorkerInstallationReceipt, receipt), + definition=cast(PortableRunDefinition, definition), + running_worker_image_sha256="b" * 64, + ) + + assert inspection.ready + assert inspection.matched_assets == ( + "ddrnet-portable-config", + "eomt-installed-tree", + service_module.LAB_V1_WORKER_AGENT_IMAGE_ASSET_ID, + ) + + +def test_release_admission_rejects_uncovered_runtime_artifact(tmp_path: Path) -> None: + installed_payload = b"sealed EoMT tree identity" + installed_sha256 = _sha256(installed_payload) + receipt = _receipt(installed_sha256, len(installed_payload)) + release = _release( + tmp_path, + installed_asset_sha256=installed_sha256, + installed_asset_length=len(installed_payload), + include_unknown=True, + ) + + with pytest.raises( + service_module.M49WorkerCompositionError, + match="absent from its installation receipt", + ): + service_module._inspect_installed_lab_v1_release( # noqa: SLF001 + release=cast(PortableLabV1ReleaseCandidate, release), + receipt=cast(PortableLabV1WorkerInstallationReceipt, receipt), + definition=cast( + PortableRunDefinition, + SimpleNamespace(components=()), + ), + running_worker_image_sha256="b" * 64, + ) + + +@pytest.mark.parametrize("include_worker_image", [False, True]) +def test_release_admission_rejects_missing_or_changed_running_worker_image( + tmp_path: Path, + include_worker_image: bool, +) -> None: + receipt = _receipt("1" * 64, 1) + release = _release( + tmp_path, + installed_asset_sha256="1" * 64, + installed_asset_length=1, + include_worker_image=include_worker_image, + executor_image_sha256="b" * 64, + ) + + with pytest.raises( + service_module.M49WorkerCompositionError, + match="differs from the running Worker container", + ): + service_module._inspect_installed_lab_v1_release( # noqa: SLF001 + release=cast(PortableLabV1ReleaseCandidate, release), + receipt=cast(PortableLabV1WorkerInstallationReceipt, receipt), + definition=cast( + PortableRunDefinition, + SimpleNamespace(components=()), + ), + running_worker_image_sha256="c" * 64, + ) + + +def test_running_worker_image_uses_exact_docker_container_inspection() -> None: + container_id = "a" * 64 + image_sha256 = "b" * 64 + requested_paths: list[str] = [] + + def inspect(request: httpx.Request) -> httpx.Response: + requested_paths.append(request.url.path) + return httpx.Response( + 200, + json={ + "Id": container_id, + "Image": f"sha256:{image_sha256}", + "State": {"Running": True}, + }, + ) + + result = service_module._inspect_running_worker_container_image_sha256( # noqa: SLF001 + container_hostname=container_id[:12], + transport=httpx.MockTransport(inspect), + ) + + assert result == image_sha256 + assert requested_paths == [ + f"/v1.47/containers/{container_id[:12]}/json" + ] diff --git a/tests/test_observatory_m49_worker_entrypoint.py b/tests/test_observatory_m49_worker_entrypoint.py index ab73a84..ff6e22e 100644 --- a/tests/test_observatory_m49_worker_entrypoint.py +++ b/tests/test_observatory_m49_worker_entrypoint.py @@ -38,7 +38,10 @@ from k1link.observatory.portable_worker_runtime import ( PortableWorkerRuntimeRegistry, ) from k1link.observatory.worker_http_transport import ObservatoryWorkerHttpGateway -from k1link.observatory.worker_service import ObservatoryWorkerServiceConfiguration +from k1link.observatory.worker_service import ( + ObservatoryWorkerExecutorBuilderRegistration, + ObservatoryWorkerServiceConfiguration, +) REPOSITORY_ROOT = Path(__file__).resolve().parents[1] DEFINITIONS_PATH = REPOSITORY_ROOT / "config" / "observatory-portable-run-definitions.json" @@ -424,6 +427,62 @@ def test_fixed_m49_identity_allows_an_additional_ready_worker_profile( ) +def test_configured_lab_v1_builder_is_added_to_the_shared_worker_registry( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + configuration, _receipt = _fixture(tmp_path, monkeypatch) + configuration = replace( + configuration, + lab_v1_installation_receipt_file=( + service_module._FIXED_LAB_V1_INSTALLATION_RECEIPT_FILE # noqa: SLF001 + ), + lab_v1_release_candidate_file=( + service_module._FIXED_LAB_V1_RELEASE_CANDIDATE_FILE # noqa: SLF001 + ), + ) + lab_builder = ObservatoryWorkerExecutorBuilderRegistration( + setup_id="lab-v1-eomt-ddrnet-portable-v1", + builder=lambda _context: pytest.fail("builder must not be called here"), + ) + captured: dict[str, object] = {} + sentinel = object() + + def compose_lab_v1(**kwargs: object) -> ObservatoryWorkerExecutorBuilderRegistration: + captured["lab_inputs"] = kwargs + return lab_builder + + def compose_worker(**kwargs: object) -> object: + captured["worker_inputs"] = kwargs + return sentinel + + monkeypatch.setattr( + service_module, + "_compose_installed_lab_v1_builder", + compose_lab_v1, + ) + monkeypatch.setattr( + service_module, + "compose_installed_observatory_worker_service_from_builders", + compose_worker, + ) + + composed = service_module.compose_installed_m49_worker_service(configuration) + + assert composed is sentinel + lab_inputs = cast(dict[str, object], captured["lab_inputs"]) + assert lab_inputs["configuration"] is configuration + worker_inputs = cast(dict[str, object], captured["worker_inputs"]) + builders = cast( + tuple[ObservatoryWorkerExecutorBuilderRegistration, ...], + worker_inputs["builders"], + ) + assert tuple(builder.setup_id for builder in builders) == ( + service_module.M49_WORKER_SETUP_ID, + "lab-v1-eomt-ddrnet-portable-v1", + ) + + def test_fixed_m49_composition_rejects_receipt_asset_drift_before_gateway( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, @@ -467,12 +526,35 @@ def test_entrypoint_environment_requires_all_absolute_fixed_files(tmp_path: Path service_module.M49_WORKER_DEFINITIONS_FILE_ENV: str(tmp_path / "definitions.json"), service_module.M49_WORKER_RUNTIME_REGISTRY_FILE_ENV: str(tmp_path / "runtime.json"), service_module.M49_WORKER_INSTALLATION_RECEIPT_FILE_ENV: str(tmp_path / "receipt.json"), + service_module.LAB_V1_WORKER_INSTALLATION_RECEIPT_FILE_ENV: str( + service_module._FIXED_LAB_V1_INSTALLATION_RECEIPT_FILE # noqa: SLF001 + ), + service_module.LAB_V1_WORKER_RELEASE_CANDIDATE_FILE_ENV: str( + service_module._FIXED_LAB_V1_RELEASE_CANDIDATE_FILE # noqa: SLF001 + ), } configuration = service_module.M49WorkerEntrypointConfiguration.from_environment(environment) assert configuration.worker.base_url == "http://127.0.0.1:18080" assert configuration.installation_receipt_file == tmp_path / "receipt.json" + assert configuration.lab_v1_installation_receipt_file == ( + service_module._FIXED_LAB_V1_INSTALLATION_RECEIPT_FILE # noqa: SLF001 + ) + assert configuration.lab_v1_release_candidate_file == ( + service_module._FIXED_LAB_V1_RELEASE_CANDIDATE_FILE # noqa: SLF001 + ) + environment[service_module.LAB_V1_WORKER_RELEASE_CANDIDATE_FILE_ENV] = str( + tmp_path / "lab-v1-release.json" + ) + with pytest.raises( + service_module.M49WorkerCompositionError, + match="fixed /release files", + ): + service_module.M49WorkerEntrypointConfiguration.from_environment(environment) + environment[service_module.LAB_V1_WORKER_RELEASE_CANDIDATE_FILE_ENV] = str( + service_module._FIXED_LAB_V1_RELEASE_CANDIDATE_FILE # noqa: SLF001 + ) with pytest.raises(service_module.M49WorkerCompositionError, match="is required"): service_module.M49WorkerEntrypointConfiguration.from_environment( { diff --git a/tests/test_observatory_portable_lab_v1_promotion.py b/tests/test_observatory_portable_lab_v1_promotion.py new file mode 100644 index 0000000..70ed3f3 --- /dev/null +++ b/tests/test_observatory_portable_lab_v1_promotion.py @@ -0,0 +1,435 @@ +from __future__ import annotations + +import importlib.util +import json +import sys +from pathlib import Path +from typing import cast + +import pytest + +from k1link.observatory.portable_lab_v1_executor import ( + PortableLabV1ReleaseCandidate, +) +from k1link.observatory.portable_run_definitions import ( + PortableRunDefinitionRegistry, +) +from k1link.observatory.portable_worker_runtime import ( + PortableWorkerRuntimeRegistry, +) + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +DEFINITIONS = REPOSITORY_ROOT / "config" / "observatory-portable-run-definitions.json" +RUNTIME = REPOSITORY_ROOT / "config" / "observatory-worker-runtime-candidates.json" +PORTABLE_CONFIG = ( + REPOSITORY_ROOT / "config" / "perception" / "lab-v1-eomt-ddrnet-portable-v2.json" +) +PROMOTION_SCRIPT = ( + REPOSITORY_ROOT + / "experiments" + / "perception" + / "worker" + / "observatory_portable" + / "promote_portable_lab_v1_ready.py" +) +_SPEC = importlib.util.spec_from_file_location( + "observatory_portable_lab_v1_promotion_test", + PROMOTION_SCRIPT, +) +assert _SPEC is not None and _SPEC.loader is not None +promotion = importlib.util.module_from_spec(_SPEC) +sys.modules[_SPEC.name] = promotion +_SPEC.loader.exec_module(promotion) + + +def _asset( + asset_id: str, + container_path: str, + identity_sha256: str, + byte_length: int | None, + *, + tree: bool, +) -> dict[str, object]: + return { + "asset_id": asset_id, + "host_path": ( + "D:\\NDC_MISSIONCORE\\runtime\\assets\\observatory-portable\\" + + asset_id + ), + "container_path": container_path, + "kind": "tree" if tree else "file", + "verification": "identity-sha256" if tree else "sha256", + "identity_sha256": identity_sha256, + "byte_length": byte_length, + } + + +def _input_document( + work_root: Path, + *, + component_receipt: Path, + coordinator_receipt: Path, +) -> dict[str, object]: + eomt_assets = [ + _asset( + "eomt-environment", + "/environment", + "8c8f343a5368ff17edbb58defa1669f6eccfba767aab897a23693872070ab9e0", + 211_776_082, + tree=True, + ), + _asset( + "eomt-ffmpeg-runtime", + "/opt/ffmpeg", + "03651449fdcccec847a0f1241e1663a82cf374bd94e7470b4ddb0c0e46d88c69", + 256_208_352, + tree=True, + ), + _asset( + "eomt-model-cache", + "/cache", + "064870e58814b97027d6a7ccd553bf51f5b8e6a8ad82a1cc703584d2dca5690c", + 2_552_355_458, + tree=True, + ), + _asset( + "eomt-python-environment", + "/opt/env", + "b3f4efc53af491f174b1cff74b3ba03016e67c9c5c74257b49c6e7dd7d853f20", + 5_120_848_705, + tree=True, + ), + _asset( + "eomt-runner-bundle", + "/runner", + "3bcfb73db5079deffe51173198f7a02e9e4c49f5fc5439d7976757a430fe91d3", + 144_128, + tree=True, + ), + _asset( + "eomt-transformers-environment", + "/opt/transformers", + "f365de01426a33be51a310923c743655634d0868941bbf3f1aae1647fdeadfc9", + 225_272_284, + tree=True, + ), + _asset( + "k1-valid-fov-root", + "/valid-fov", + "f4fc2053e4e6213bb364c8773979b755d5682a81b3946c25ff86274bc5f0031e", + 6_019, + tree=True, + ), + ] + ddrnet_assets = [ + _asset( + "ddrnet-checkpoint", + "/opt/nodedc/assets/ddrnet-checkpoint", + "b99c2838051bcd7b092fd3970aa62a77d5c0bbb809c9b9afb2ff4b0ebdaa4ee6", + 259_419_077, + tree=False, + ), + _asset( + "ddrnet-goose-mapping", + "/opt/nodedc/assets/ddrnet-goose-mapping", + "88ae319ba5a3877dd3ae0773f693a6a5fdc283934140de9dfaff029108aefd7f", + 1_427, + tree=False, + ), + _asset( + "ddrnet-goose-runner", + "/opt/nodedc/assets/ddrnet-goose-runner", + "b18ad60f277eea69a240a28f290611b94627fb9707faf1bb3e6e22102dad67c1", + 32_877, + tree=False, + ), + _asset( + "vegetation-policy", + "/opt/nodedc/assets/vegetation-policy", + "b75c4ac841d7b4bcc57f7a9c8417ca2317d8ecfa499e72a9af8a8591a2ec0d35", + 3_022, + tree=False, + ), + _asset( + "vegetation-provider-map", + "/opt/nodedc/assets/vegetation-provider-map", + "f2b69046b6a740fd9532d2d88e7fabae7c20fb662f783c9502adc9026406f352", + 2_756, + tree=False, + ), + ] + return { + "schema_version": promotion.PROMOTION_INPUT_SCHEMA, + "source_revision": "1" * 40, + "component_source_revision": "2" * 40, + "coordinator_image_sha256": "c" * 64, + "work_root": { + "controller_root": str(work_root), + "engine_host_root": "D:\\NDC_MISSIONCORE\\runtime\\work", + }, + "components": { + "eomt": { + "base_image_sha256": ( + "58df7489c3f2276f9591d500a012dee03e23d35543ce3c390b4c001e6bf90794" + ), + "derived_image_sha256": "d" * 64, + "dockerfile_sha256": "2" * 64, + "installer_sha256": "3" * 64, + "shared_adapter_sha256": "4" * 64, + "component_adapter_sha256": "5" * 64, + "assets": eomt_assets, + }, + "ddrnet": { + "base_image_sha256": ( + "591cb382c099eeb05e7ec16e2371e0b2da54d2bb5c49ec0f4ac88dbf72b0f0cd" + ), + "derived_image_sha256": "e" * 64, + "dockerfile_sha256": "6" * 64, + "installer_sha256": "3" * 64, + "shared_adapter_sha256": "4" * 64, + "component_adapter_sha256": "7" * 64, + "assets": ddrnet_assets, + }, + }, + "installation_evidence": { + "component_image_installer_receipt_file": str(component_receipt), + "coordinator_image_installer_receipt_file": str(coordinator_receipt), + }, + } + + +def _write_installation_evidence(tmp_path: Path) -> tuple[Path, Path]: + source_revision = "1" * 40 + component_source_revision = "2" * 40 + component = tmp_path / "component-image-installation.json" + component.write_text( + json.dumps( + [ + { + "component": "eomt", + "status": "already-installed", + "tag": ( + "ndc/mission-core-lab-v1-eomt-adapter:" + f"{component_source_revision[:12]}" + ), + "base_image_sha256": ( + "58df7489c3f2276f9591d500a012dee03e23d35543ce3c390b4c001e6bf90794" + ), + "derived_image_sha256": "d" * 64, + "build_method": "docker-commit-exact-layer-v1", + }, + { + "component": "ddrnet", + "status": "already-installed", + "tag": ( + "ndc/mission-core-lab-v1-ddrnet-adapter:" + f"{component_source_revision[:12]}" + ), + "base_image_sha256": ( + "591cb382c099eeb05e7ec16e2371e0b2da54d2bb5c49ec0f4ac88dbf72b0f0cd" + ), + "derived_image_sha256": "e" * 64, + "build_method": "docker-commit-exact-layer-v1", + }, + ] + ), + encoding="utf-8", + ) + coordinator = tmp_path / "coordinator-image-installation.json" + coordinator.write_text( + json.dumps( + { + "schema_version": promotion.WORKER_AGENT_IMAGE_INSTALLATION_SCHEMA, + "status": "installed", + "worker_id": "worker-006", + "build_method": "docker-commit-exact-layer-v1", + "source_revision": source_revision, + "provenance": { + "git_archive_sha256": "8" * 64, + "git_archive_verification": "external-before-extract", + "staged_snapshot_sha256": "9" * 64, + "staged_snapshot_file_count": 42, + "staged_snapshot_byte_length": 4096, + "staged_snapshot_canonicalization": ( + "utf8-path-nul-length-nul-sha256-lf-v1" + ), + "embedded_snapshot_manifest_sha256": "a" * 64, + "embedded_snapshot_manifest_byte_length": 8192, + }, + "base_image_sha256": ( + "58df7489c3f2276f9591d500a012dee03e23d35543ce3c390b4c001e6bf90794" + ), + "derived_image_sha256": "c" * 64, + "image": { + "tag": ( + "ndc/mission-core-observatory-worker-agent:" + f"{source_revision[:12]}" + ), + "id": f"sha256:{'c' * 64}", + "size_bytes": 1_000_000, + "thin_layer_bytes": 10_000, + "maximum_thin_layer_bytes": 33_554_432, + "rootfs": { + "base_layer_count": 42, + "derived_layer_count": 1, + "derived_layer_diff_id": f"sha256:{'b' * 64}", + "pinned_base_is_exact_prefix": True, + }, + }, + "runtime_contract": { + "workdir": "/opt/nodedc/mission-core", + "entrypoint": [ + "python3", + "-m", + "k1link.observatory.m49_worker_container_main", + ], + "command": [], + "authority": "observation-only", + "models": "external", + "runtime_registries": "external-read-only", + }, + "smoke": { + "network": "none", + "read_only_rootfs": True, + "staged_source_bytes": "matched", + "embedded_context_bytes": "matched", + "embedded_snapshot_manifest": "matched", + "result": "passed", + }, + } + ), + encoding="utf-8", + ) + return component, coordinator + + +def _files(root: Path) -> dict[str, bytes]: + return { + path.relative_to(root).as_posix(): path.read_bytes() + for path in root.rglob("*") + if path.is_file() + } + + +def _row(document: dict[str, object], collection: str, setup_id: str) -> object: + rows = cast(list[dict[str, object]], document[collection]) + return next(row for row in rows if row["setup_id"] == setup_id) + + +def test_ready_promotion_is_deterministic_additive_and_round_trips( + tmp_path: Path, +) -> None: + work_root = tmp_path / "work" + work_root.mkdir() + component_receipt, coordinator_receipt = _write_installation_evidence(tmp_path) + input_path = tmp_path / "promotion-input.json" + input_path.write_text( + json.dumps( + _input_document( + work_root, + component_receipt=component_receipt, + coordinator_receipt=coordinator_receipt, + ) + ), + encoding="utf-8", + ) + inputs = promotion.load_promotion_input(input_path) + + first = promotion.generate_ready_lab_v1_artifacts( + promotion=inputs, + source_definition_registry=DEFINITIONS, + source_runtime_registry=RUNTIME, + ddrnet_portable_config=PORTABLE_CONFIG, + output_root=tmp_path / "ready-a", + ) + second = promotion.generate_ready_lab_v1_artifacts( + promotion=inputs, + source_definition_registry=DEFINITIONS, + source_runtime_registry=RUNTIME, + ddrnet_portable_config=PORTABLE_CONFIG, + output_root=tmp_path / "ready-b", + ) + + assert _files(first.root) == _files(second.root) + source_definitions = json.loads(DEFINITIONS.read_text(encoding="utf-8")) + ready_definitions_document = json.loads( + first.definition_registry_path.read_text(encoding="utf-8") + ) + assert _row( + ready_definitions_document, + "definitions", + "m49-tgs-portable-v2", + ) == _row(source_definitions, "definitions", "m49-tgs-portable-v2") + + definitions = PortableRunDefinitionRegistry.from_file( + first.definition_registry_path + ) + definition = definitions.resolve_setup(promotion.PORTABLE_LAB_V1_SETUP_ID) + runtime = PortableWorkerRuntimeRegistry.from_file( + first.runtime_registry_path, + definitions=definitions, + ).resolve(definition.setup_id, definition.definition_sha256) + release = PortableLabV1ReleaseCandidate.from_file( + first.release_candidate_path, + repository_root=first.root, + ) + release.bind_definition(definition) + + assert first.release_candidate_path.name == "lab-v1-executor-release.json" + assert definition.executor.ready + assert runtime.ready + assert [ + asset.asset_id for asset in release.assets if asset.kind == "repository-file" + ] == [promotion.DDRNET_PORTABLE_CONFIG_ASSET_ID] + assert promotion.PORTABLE_LAB_V1_WORKER_INSTALLATION_RECEIPT_ASSET_ID not in { + asset.asset_id for asset in release.assets + } + receipt_requirement = next( + asset + for asset in runtime.reusable_assets + if asset.asset_id + == promotion.PORTABLE_LAB_V1_WORKER_INSTALLATION_RECEIPT_ASSET_ID + ) + assert receipt_requirement.sha256 == first.installation_receipt_file_sha256 + assert first.release_candidate_sha256 == second.release_candidate_sha256 + assert first.release_sha256 == second.release_sha256 + assert first.definition_sha256 == second.definition_sha256 + assert first.runtime_candidate_sha256 == second.runtime_candidate_sha256 + + +def test_promotion_rejects_bare_or_mismatched_installer_claims( + tmp_path: Path, +) -> None: + work_root = tmp_path / "work" + work_root.mkdir() + component_receipt, coordinator_receipt = _write_installation_evidence(tmp_path) + bare = _input_document( + work_root, + component_receipt=component_receipt, + coordinator_receipt=coordinator_receipt, + ) + del bare["installation_evidence"] + bare_path = tmp_path / "bare.json" + bare_path.write_text(json.dumps(bare), encoding="utf-8") + with pytest.raises(promotion.PortableLabV1PromotionError, match="fields are invalid"): + promotion.load_promotion_input(bare_path) + + rows = json.loads(component_receipt.read_text(encoding="utf-8")) + rows[0]["derived_image_sha256"] = "f" * 64 + component_receipt.write_text(json.dumps(rows), encoding="utf-8") + mismatched = tmp_path / "mismatched.json" + mismatched.write_text( + json.dumps( + _input_document( + work_root, + component_receipt=component_receipt, + coordinator_receipt=coordinator_receipt, + ) + ), + encoding="utf-8", + ) + with pytest.raises( + promotion.PortableLabV1PromotionError, + match="does not bind the promotion", + ): + promotion.load_promotion_input(mismatched) diff --git a/tests/test_observatory_portable_lab_v1_runtime_anchor.py b/tests/test_observatory_portable_lab_v1_runtime_anchor.py new file mode 100644 index 0000000..6be8023 --- /dev/null +++ b/tests/test_observatory_portable_lab_v1_runtime_anchor.py @@ -0,0 +1,95 @@ +from __future__ import annotations + +from types import SimpleNamespace +from typing import cast + +import pytest + +from k1link.observatory import portable_lab_v1_worker as worker_module +from k1link.observatory.portable_lab_v1_worker import PortableLabV1WorkerError +from k1link.observatory.portable_run_definitions import PortableRunDefinition +from k1link.observatory.portable_worker_runtime import PortableWorkerRuntimeCandidate + + +def _candidate(*, asset_id: str, kind: str = "local-file") -> object: + return SimpleNamespace( + setup_id="lab-v1-eomt-ddrnet-portable-v1", + definition_id="lab-v1-eomt-ddrnet-portable", + definition_version=2, + definition_sha256="a" * 64, + result_contract_sha256="b" * 64, + phases=tuple( + SimpleNamespace(phase_id=phase_id) + for phase_id in worker_module.PORTABLE_LAB_V1_RUNTIME_PHASES + ), + executor=SimpleNamespace( + release_id="lab-v1-eomt-ddrnet-v1", + release_sha256="c" * 64, + image_sha256="d" * 64, + ), + reusable_assets=( + SimpleNamespace( + asset_id=asset_id, + kind=kind, + sha256="e" * 64, + byte_length=100, + ), + ), + ) + + +def _definition() -> object: + return SimpleNamespace( + definition_sha256="a" * 64, + executable_contract_sha256="f" * 64, + ) + + +def _installation() -> object: + seal = SimpleNamespace( + release_id="lab-v1-eomt-ddrnet-v1", + release_sha256="c" * 64, + executor_image_sha256="d" * 64, + ) + release = SimpleNamespace( + setup_id="lab-v1-eomt-ddrnet-portable-v1", + definition_id="lab-v1-eomt-ddrnet-portable", + definition_version=2, + definition_contract_sha256="f" * 64, + result_contract_sha256="b" * 64, + assets=(), + seal=lambda _inspection: seal, + ) + return SimpleNamespace(release=release, inspection=object()) + + +def test_runtime_only_installation_receipt_anchor_is_not_required_in_release() -> None: + worker_module._verify_candidate_release( # noqa: SLF001 + cast(PortableWorkerRuntimeCandidate, _candidate( + asset_id="lab-v1-worker-installation-receipt" + )), + cast(worker_module.PortableLabV1RunnerInstallation, _installation()), + cast(PortableRunDefinition, _definition()), + ) + + +@pytest.mark.parametrize( + ("asset_id", "kind"), + [ + ("unreviewed-runtime-anchor", "local-file"), + ("lab-v1-worker-installation-receipt", "container-image"), + ], +) +def test_no_other_runtime_asset_bypasses_release_coverage( + asset_id: str, + kind: str, +) -> None: + with pytest.raises(PortableLabV1WorkerError): + worker_module._verify_candidate_release( # noqa: SLF001 + cast( + PortableWorkerRuntimeCandidate, + _candidate(asset_id=asset_id, kind=kind), + ), + cast(worker_module.PortableLabV1RunnerInstallation, _installation()), + cast(PortableRunDefinition, _definition()), + ) diff --git a/tests/test_observatory_portable_lab_v1_worker_service.py b/tests/test_observatory_portable_lab_v1_worker_service.py index a64cea0..628b868 100644 --- a/tests/test_observatory_portable_lab_v1_worker_service.py +++ b/tests/test_observatory_portable_lab_v1_worker_service.py @@ -49,6 +49,7 @@ RELEASE_SHA256 = "7" * 64 IMAGE_SHA256 = "8" * 64 RELEASE_CANDIDATE_SHA256 = "9" * 64 SOURCE_REVISION = "a" * 40 +INSTALLER_SHA256 = hashlib.sha256(b"component-image-installer").hexdigest() BASE_IMAGE_SHA256S = { "eomt": "58df7489c3f2276f9591d500a012dee03e23d35543ce3c390b4c001e6bf90794", "ddrnet": "591cb382c099eeb05e7ec16e2371e0b2da54d2bb5c49ec0f4ac88dbf72b0f0cd", @@ -244,6 +245,10 @@ def _image_build( dockerfile_sha256=hashlib.sha256( f"{component}:dockerfile".encode() ).hexdigest(), + build_method=( + service_module.PORTABLE_LAB_V1_COMPONENT_IMAGE_BUILD_METHOD + ), + installer_sha256=INSTALLER_SHA256, shared_adapter_sha256=hashlib.sha256(b"shared-adapter").hexdigest(), component_adapter_sha256=hashlib.sha256( f"{component}:adapter".encode() @@ -293,6 +298,7 @@ def _write_receipt( "ddrnet", derived_image_sha256=runner.ddrnet.image_sha256, ), + installation_evidence_sha256="d" * 64, ) path = tmp_path / path_name path.write_bytes(canonical_json(document)) @@ -365,6 +371,7 @@ def _composition_inputs( ) for digest in ( build.dockerfile_sha256, + build.installer_sha256, build.shared_adapter_sha256, build.component_adapter_sha256, build.seal_sha256, @@ -402,6 +409,11 @@ def test_receipt_loader_round_trips_full_runner_identity(tmp_path: Path) -> None receipt.runner_installation.eomt.image_sha256 ) assert receipt.ddrnet_image_build.network == "none" + assert receipt.ddrnet_image_build.build_method == ( + "docker-commit-exact-layer-v1" + ) + assert receipt.ddrnet_image_build.installer_sha256 == INSTALLER_SHA256 + assert receipt.installation_evidence_sha256 == "d" * 64 assert receipt.runner_installation.definition_sha256 == ( definition.definition_sha256 ) @@ -411,6 +423,37 @@ def test_receipt_loader_round_trips_full_runner_identity(tmp_path: Path) -> None assert receipt.file_sha256 == hashlib.sha256(path.read_bytes()).hexdigest() +def test_component_image_build_seal_binds_actual_installer_method() -> None: + seal = _image_build("eomt", derived_image_sha256="b" * 64) + + with pytest.raises( + service_module.PortableLabV1WorkerCompositionError, + match="build provenance changed", + ): + service_module.PortableLabV1ComponentImageBuildSeal.seal( + component="eomt", + base_image_sha256=BASE_IMAGE_SHA256S["eomt"], + derived_image_sha256="b" * 64, + dockerfile_sha256=seal.dockerfile_sha256, + build_method="dockerfile-build-v1", + installer_sha256=INSTALLER_SHA256, + shared_adapter_sha256=seal.shared_adapter_sha256, + component_adapter_sha256=seal.component_adapter_sha256, + ) + + with pytest.raises( + service_module.PortableLabV1WorkerCompositionError, + match="image installer SHA-256 is invalid", + ): + replace(seal, installer_sha256="invalid") + + with pytest.raises( + service_module.PortableLabV1WorkerCompositionError, + match="build provenance changed", + ): + replace(seal, installer_sha256="f" * 64) + + def test_receipt_loader_rejects_noncanonical_links_and_oversized_files( tmp_path: Path, ) -> None: diff --git a/tests/test_observatory_worker_agent_image_artifact.py b/tests/test_observatory_worker_agent_image_artifact.py index 1dc5fc7..ccbcfb8 100644 --- a/tests/test_observatory_worker_agent_image_artifact.py +++ b/tests/test_observatory_worker_agent_image_artifact.py @@ -29,6 +29,7 @@ RUNTIME_REGISTRY_CONTAINER_PATH = ( "/run/nodedc/registries/observatory-worker-runtime-candidates.json" ) LAB_V1_RECEIPT_CONTAINER_PATH = "/release/lab-v1-worker-installation-receipt.json" +LAB_V1_RELEASE_CANDIDATE_CONTAINER_PATH = "/release/lab-v1-executor-release.json" def _document(path: Path) -> dict[str, object]: @@ -149,8 +150,13 @@ def test_install_plan_requires_offline_build_hardening_smoke_and_unfilled_receip assert document["state"] == "planned-not-built" build = cast(dict[str, object], document["build"]) assert build["source_revision"] is None - assert build["source_date_epoch"] is None - assert build["build_context_sha256"] is None + assert build["git_archive_sha256"] is None + assert build["staged_snapshot_sha256"] is None + assert build["installer"] == ( + "experiments/perception/worker/observatory_portable/" + "Install-Worker006AgentImage.ps1" + ) + assert "not executed" in cast(str, build["dockerfile_role"]) materialization = cast(dict[str, object], build["materialization"]) assert materialization == { "method": "git archive", @@ -158,25 +164,43 @@ def test_install_plan_requires_offline_build_hardening_smoke_and_unfilled_receip "reject_dirty_worktree": True, "archive_format": "tar", "archive_paths_source": "context manifest context_entries in declared order", - "build_context_sha256_subject": "exact git-archive tar bytes", + "git_archive_sha256_subject": "exact git-archive tar bytes", + "extraction_target": ( + "D:\\NDC_MISSIONCORE\\runtime\\staging\\" + "observatory-worker-agent-" + ), } + staged = cast(dict[str, object], build["staged_snapshot"]) + assert staged["canonicalization"] == "utf8-path-nul-length-nul-sha256-lf-v1" + assert staged["reject_reparse_points"] is True + assert staged["reject_unexpected_entries"] is True + assert staged["verify_timing"] == [ + "before temporary container creation", + "after source installation and before image commit", + ] + assert staged["embedded_manifest"] == ( + "/opt/nodedc/mission-core/release/worker-006-agent-staged-snapshot.json" + ) base = cast(dict[str, object], build["base_image"]) assert base["reference"] == BASE_REFERENCE assert base["sha256"] == BASE_SHA256 assert base["must_exist_locally"] is True assert base["pull_allowed"] is False - docker_build = cast(dict[str, object], build["docker_build"]) - assert docker_build == { + image_materialization = cast(dict[str, object], build["image_materialization"]) + assert image_materialization == { + "method": "docker-commit-exact-layer-v1", + "dockerfile_executed": False, "network": "none", "pull": False, - "no_cache": True, - "provenance": False, "platform": "linux/amd64", - "context_input": "exact git-archive tar bytes", - "required_build_args": [ - "NODEDC_SOURCE_REVISION", - "NODEDC_BUILD_CONTEXT_SHA256", - "SOURCE_DATE_EPOCH", + "source_input": "exact read-only staged snapshot", + "base_container": "docker create by exact pinned base image ID", + "commit": "docker commit --pause=true with fixed config changes", + "maximum_thin_layer_bytes": 33554432, + "embedded_payload": [ + "/opt/nodedc/mission-core/src/k1link", + "/opt/nodedc/mission-core/release/worker-006-agent-build-context.json", + "/opt/nodedc/mission-core/release/worker-006-agent-staged-snapshot.json", ], } assert build["required_preflight"] == [ @@ -185,6 +209,8 @@ def test_install_plan_requires_offline_build_hardening_smoke_and_unfilled_receip "selected revision equals HEAD", "base image inspect ID equals the pinned SHA-256", "context archive contains exactly the context manifest entries", + "git archive SHA-256 was verified externally before extraction", + "staged snapshot SHA-256 matches the exact canonical regular-file inventory", ] acceptance = cast(dict[str, object], document["build_acceptance"]) @@ -193,10 +219,31 @@ def test_install_plan_requires_offline_build_hardening_smoke_and_unfilled_receip assert labels["com.nodedc.base-image.sha256"] == BASE_SHA256 assert labels["com.nodedc.models"] == "external" assert labels["com.nodedc.runtime-registries"] == "external-read-only" + assert labels["com.nodedc.staged-snapshot.sha256"] == ( + "" + ) + assert labels["com.nodedc.embedded-snapshot-manifest.sha256"] == ( + "" + ) + assert labels["com.nodedc.build-method"] == "docker-commit-exact-layer-v1" + rootfs = cast(dict[str, object], acceptance["rootfs"]) + assert rootfs == { + "base_layer_chain": "exact prefix of the pinned base image RootFS.Layers", + "derived_layer_count": 1, + "derived_layer_diff_id_required": True, + "maximum_thin_layer_bytes": 33554432, + } + embedded = cast(dict[str, object], acceptance["embedded_identity"]) + assert "byte-for-byte" in cast(str, embedded["source_tree"]) + assert "byte-for-byte" in cast(str, embedded["context_manifest"]) + assert "every staged file" in cast(str, embedded["snapshot_manifest"]) smoke = cast(dict[str, object], acceptance["smoke"]) assert smoke["network"] == "none" assert smoke["read_only_rootfs"] is True assert smoke["platform"] == "linux/amd64" + assert smoke["staged_source_bytes"] == "matched" + assert smoke["embedded_context_bytes"] == "matched" + assert smoke["embedded_snapshot_manifest"] == "matched" assert smoke["expected_result"] == "exit-0" runtime = cast(dict[str, object], document["runtime"]) @@ -215,6 +262,9 @@ def test_install_plan_requires_offline_build_hardening_smoke_and_unfilled_receip "MISSIONCORE_OBSERVATORY_LAB_V1_INSTALLATION_RECEIPT_FILE": ( LAB_V1_RECEIPT_CONTAINER_PATH ), + "MISSIONCORE_OBSERVATORY_LAB_V1_RELEASE_CANDIDATE_FILE": ( + LAB_V1_RELEASE_CANDIDATE_CONTAINER_PATH + ), } registry_files = cast(dict[str, object], runtime["runtime_registry_files"]) assert registry_files["binding"] == "individual read-only bind files" @@ -339,19 +389,69 @@ def test_install_plan_requires_offline_build_hardening_smoke_and_unfilled_receip "state": "external-installed-receipt-required", "environment_variable": "MISSIONCORE_OBSERVATORY_LAB_V1_INSTALLATION_RECEIPT_FILE", "container_path": LAB_V1_RECEIPT_CONTAINER_PATH, + "release_candidate_environment_variable": ( + "MISSIONCORE_OBSERVATORY_LAB_V1_RELEASE_CANDIDATE_FILE" + ), + "release_candidate_container_path": ( + LAB_V1_RELEASE_CANDIDATE_CONTAINER_PATH + ), + "release_repository_root": "/release", "mode": "read-only", "owns_component_image_identities": True, "queued_jobs_may_override_component_images": False, } receipt = cast(dict[str, object], document["receipt_skeleton"]) - assert receipt["receipt_state"] == "not-built" + assert receipt["schema_version"] == ( + "missioncore.observatory-worker-agent-image-installation/v1" + ) + assert receipt["status"] is None + assert receipt["build_method"] == "docker-commit-exact-layer-v1" assert receipt["source_revision"] is None - assert receipt["source_date_epoch"] is None - assert receipt["build_context_sha256"] is None + provenance = cast(dict[str, object], receipt["provenance"]) + assert provenance == { + "git_archive_sha256": None, + "git_archive_verification": "external-before-extract", + "staged_snapshot_sha256": None, + "staged_snapshot_file_count": None, + "staged_snapshot_byte_length": None, + "staged_snapshot_canonicalization": ( + "utf8-path-nul-length-nul-sha256-lf-v1" + ), + "embedded_snapshot_manifest_sha256": None, + "embedded_snapshot_manifest_byte_length": None, + } assert receipt["base_image_sha256"] == BASE_SHA256 + assert receipt["derived_image_sha256"] is None image = cast(dict[str, object], receipt["image"]) - assert image == {"tag": None, "id": None, "size_bytes": None} - assert receipt["models_baked_into_image"] is False - assert receipt["runtime_registries_baked_into_image"] is False - assert receipt["authority"] == AUTHORITY + assert image == { + "tag": None, + "id": None, + "size_bytes": None, + "thin_layer_bytes": None, + "maximum_thin_layer_bytes": 33554432, + "rootfs": { + "base_layer_count": None, + "derived_layer_count": 1, + "derived_layer_diff_id": None, + "pinned_base_is_exact_prefix": True, + }, + } + runtime_contract = cast(dict[str, object], receipt["runtime_contract"]) + assert runtime_contract == { + "workdir": "/opt/nodedc/mission-core", + "entrypoint": ENTRYPOINT, + "command": [], + "authority": "observation-only", + "models": "external", + "runtime_registries": "external-read-only", + } + receipt_smoke = cast(dict[str, object], receipt["smoke"]) + assert receipt_smoke == { + "network": "none", + "read_only_rootfs": True, + "staged_source_bytes": None, + "embedded_context_bytes": None, + "embedded_snapshot_manifest": None, + "result": "not-run", + } diff --git a/tests/test_worker_006_agent_image_installer.py b/tests/test_worker_006_agent_image_installer.py new file mode 100644 index 0000000..a3d8ef7 --- /dev/null +++ b/tests/test_worker_006_agent_image_installer.py @@ -0,0 +1,163 @@ +from __future__ import annotations + +from pathlib import Path + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +INSTALLER = ( + REPOSITORY_ROOT + / "experiments/perception/worker/observatory_portable" + / "Install-Worker006AgentImage.ps1" +) +BASE_SHA256 = "58df7489c3f2276f9591d500a012dee03e23d35543ce3c390b4c001e6bf90794" + + +def _script() -> str: + return INSTALLER.read_text(encoding="utf-8") + + +def test_worker_agent_installer_is_local_offline_and_non_buildkit() -> None: + script = _script() + + assert '"docker-commit-exact-layer-v1"' in script + assert BASE_SHA256 in script + assert '"staging\\observatory-worker-agent-$SourceRevision"' in script + assert "docker create `" in script + assert "--network none" in script + assert "--cap-drop ALL" in script + assert "--security-opt no-new-privileges" in script + assert "target=/nodedc-build-source,readonly" in script + assert "docker commit --pause=true @changes $containerId $tag" in script + assert "docker rm -f $containerId" in script + assert "docker rm -f $containerName" not in script + + lowered = script.lower() + assert "docker build" not in lowered + assert "docker buildx" not in lowered + assert "docker pull" not in lowered + assert "--pull" not in lowered + assert "invoke-webrequest" not in lowered + assert "start-bitstransfer" not in lowered + assert "curl " not in lowered + assert "wget " not in lowered + assert "smb" not in lowered + + +def test_worker_agent_installer_separates_archive_and_staged_identities() -> None: + script = _script() + + assert "[string]$ExpectedGitArchiveSha256" in script + assert "[string]$ExpectedStagedSnapshotSha256" in script + assert script.count("Get-StagedSnapshotInspection $StagedSnapshotRoot") == 2 + assert '"com.nodedc.build-context.sha256" = $ExpectedGitArchiveSha256' in script + assert ( + '"com.nodedc.staged-snapshot.sha256" = $ExpectedStagedSnapshotSha256' + in script + ) + assert "LABEL com.nodedc.build-context.sha256=$ExpectedGitArchiveSha256" in script + assert ( + "LABEL com.nodedc.staged-snapshot.sha256=$ExpectedStagedSnapshotSha256" + in script + ) + assert 'git_archive_verification = "external-before-extract"' in script + assert 'canonicalization = "utf8-path-nul-length-nul-sha256-lf-v1"' in script + assert 'Assert-ExactDirectoryChildren $Root @("experiments", "src")' in script + assert 'Assert-ExactDirectoryChildren $srcRoot @("k1link")' in script + assert "Dockerfile.worker-006-agent" in script + assert "worker-006-agent-build-context.json" in script + assert ( + 'schema_version = "missioncore.observatory-worker-agent-embedded-snapshot/v1"' + in script + ) + assert "files = @($SnapshotInspection.files)" in script + assert "embedded_snapshot_manifest_sha256" in script + + +def test_worker_agent_installer_copies_only_runtime_source_and_contract() -> None: + script = _script() + + assert ( + "cp -a /nodedc-build-source/src/k1link " + "/opt/nodedc/mission-core/src/k1link" + ) in script + assert ( + '"cp /nodedc-build-source/$ContextManifestRelativePath " +' + in script + ) + assert "/opt/nodedc/mission-core/release/worker-006-agent-build-context.json" in script + assert ( + "/opt/nodedc/mission-core/release/worker-006-agent-staged-snapshot.json" + in script + ) + assert "cp -a /nodedc-build-source/experiments" not in script + assert "cp /nodedc-build-source/$DockerfileRelativePath" not in script + assert "test ! -e /opt/nodedc/mission-core/experiments" in script + assert "cmp -s" in script + assert "find /opt/nodedc/mission-core -type d -exec chmod 0555" in script + assert "find /opt/nodedc/mission-core -type f -exec chmod 0444" in script + assert "chmod 0555 /run/nodedc /run/nodedc/registries" in script + + +def test_worker_agent_installer_proves_base_chain_and_embedded_bytes() -> None: + script = _script() + + assert "$baseLayers = @($BaseImage.RootFS.Layers)" in script + assert "$imageLayers = @($Image.RootFS.Layers)" in script + assert "$imageLayers.Count -ne ($baseLayers.Count + 1)" in script + assert "RootFS does not extend the pinned base layer chain" in script + assert "derived_layer_diff_id = $derivedLayerDiffId" in script + assert "pinned_base_is_exact_prefix = $true" in script + assert "target=/nodedc-verify-source,readonly" in script + assert "target=/nodedc-verify-snapshot.json,readonly" in script + assert "cmp -s /nodedc-verify-snapshot.json" in script + assert ( + "/opt/nodedc/mission-core/release/worker-006-agent-staged-snapshot.json" + in script + ) + assert "find /nodedc-verify-source/src/k1link -type f" in script + assert "find /opt/nodedc/mission-core/src/k1link -type f" in script + assert "cmp -s /nodedc-verify-source/$ContextManifestRelativePath" in script + + +def test_worker_agent_installer_removes_failed_new_image_and_temporary_files() -> None: + script = _script() + + assert "$imageCommitted = $true" in script + assert ( + "Invoke-InstalledImageSmoke $tag $StagedSnapshotRoot $embeddedManifest" + in script + ) + assert "docker image rm --force $committedImageId" in script + assert "image verification failed and committed image cleanup failed" in script + assert "Remove-Item -LiteralPath $embeddedManifest.path -Force" in script + + +def test_worker_agent_installer_seals_runtime_contract_smoke_and_output() -> None: + script = _script() + + for value in ( + "PYTHONPATH=/opt/nodedc/mission-core/src", + "PYTHONNOUSERSITE=1", + "PYTHONDONTWRITEBYTECODE=1", + "PYTHONUNBUFFERED=1", + "WORKDIR $ImageWorkdir", + "USER 0:0", + "ENTRYPOINT $ImageEntrypoint", + "CMD $ImageCommand", + "com.nodedc.authority=observation-only", + "com.nodedc.models=external", + "com.nodedc.runtime-registries=external-read-only", + ): + assert value in script + assert "--read-only" in script + assert '"/tmp:rw,noexec,nosuid,size=16m"' in script + assert "import k1link.observatory.m49_worker_container_main" in script + assert "import k1link.observatory.m49_worker_service" in script + assert "compose_installed_m49_worker_service" in script + assert "$MaximumLayerBytes = [int64](32MB)" in script + assert "image is not within the thin-layer bound" in script + assert ( + 'schema_version = "missioncore.observatory-worker-agent-image-installation/v1"' + in script + ) + assert "derived_image_sha256 = ([string]$Image.Id).Substring(7)" in script + assert 'result = "passed"' in script