From 40794f13aabd43aafcf5b1733775e4e2f607c380 Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Wed, 5 Aug 2026 15:54:42 +0300 Subject: [PATCH] feat(perception): keep worker shadow local --- ...ission-core-worker-shadow-v1.template.json | 6 + ...E_4_OBJECT_CENTRIC_RECORDED_REALTIME_CV.md | 19 +- scripts/Invoke-M4DetectorShadow.ps1 | 385 ++++++++++++++++++ scripts/build_m4_worker_shadow_artifact.py | 8 +- tests/test_m4_worker_shadow_artifact.py | 10 +- 5 files changed, 421 insertions(+), 7 deletions(-) create mode 100644 scripts/Invoke-M4DetectorShadow.ps1 diff --git a/config/deployment/mission-core-worker-shadow-v1.template.json b/config/deployment/mission-core-worker-shadow-v1.template.json index 904dd38..cff73b7 100644 --- a/config/deployment/mission-core-worker-shadow-v1.template.json +++ b/config/deployment/mission-core-worker-shadow-v1.template.json @@ -6,6 +6,12 @@ "minimum_end_to_end_fps": 10.004 }, "artifact_type": "shadow-release", + "boundary": { + "external_deploy_registry": false, + "nodedc_platform_repository": false, + "repository": "NODEDC_MISSION_CORE", + "server_docker_runtime": false + }, "code_revision": "__CODE_REVISION__", "component": "mission-core-worker", "container": { diff --git a/docs/22_MILESTONE_4_OBJECT_CENTRIC_RECORDED_REALTIME_CV.md b/docs/22_MILESTONE_4_OBJECT_CENTRIC_RECORDED_REALTIME_CV.md index b1239e2..25087b6 100644 --- a/docs/22_MILESTONE_4_OBJECT_CENTRIC_RECORDED_REALTIME_CV.md +++ b/docs/22_MILESTONE_4_OBJECT_CENTRIC_RECORDED_REALTIME_CV.md @@ -2,7 +2,7 @@ Date: 2026-08-05 -Status: in progress; M4.0–M4.2 implemented, M4.3 execution seam ready/runtime gate open +Status: in progress; M4.0–M4.2 implemented, M4.3 local shadow artifact ready/runtime gate open Audit base: `1b3e0b3` on `feat/simulation-polygon-s1` @@ -727,9 +727,10 @@ runner: This increment does **not** claim a new 4,489-frame Triton execution. The existing E46J 47.840 FPS result remains the baseline evidence. A fresh provider execution -requires a digest-bound shadow package; ad-hoc executable staging on Worker 006 -is prohibited by the deployment canon. Therefore M4.3 runtime/capacity exit and -its final checker remain open, and M4.4 does not start yet. +requires a digest-bound Mission Core shadow package with its own reviewed local +runner. NODEDC Platform, its repository, its deploy registry and server Docker +runtime are explicitly outside this boundary. Therefore M4.3 runtime/capacity +exit and its final checker remain open, and M4.4 does not start yet. Validation after adding the execution seam: 42 focused-and-related tests and the complete Python suite (`1230 passed, 1 skipped`). Scoped Ruff and strict mypy pass @@ -739,6 +740,16 @@ Portable-worker hardening adds two architecture checks and leaves the complete suite at `1232 passed, 1 skipped`; scoped Ruff and strict mypy remain clean. This hardening changes no durable Worker 006 process and makes no fresh capacity claim. +The local shadow artifact now contains its own Mission Core PowerShell runner, +runtime wheel, baseline and descriptor. The runner verifies the artifact, all +eight pinned source/model files, dependency inventories, Worker/Triton identities +and free-space guard before creating a single one-shot candidate container. The +candidate reuses the existing Triton network namespace over loopback, exposes no +port, mounts source read-only, writes only immutable result evidence, and is +removed after success or failure. Durable E15 and Triton are re-verified after +the run. This is a Mission Core operator workflow, not a cross-repository deploy +workflow. + ## Implementation order The implementation sequence is intentionally strict: diff --git a/scripts/Invoke-M4DetectorShadow.ps1 b/scripts/Invoke-M4DetectorShadow.ps1 new file mode 100644 index 0000000..8545f59 --- /dev/null +++ b/scripts/Invoke-M4DetectorShadow.ps1 @@ -0,0 +1,385 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string]$ReleaseRoot, + [Parameter(Mandatory = $true)] + [string]$ArtifactPath, + [Parameter(Mandatory = $true)] + [ValidatePattern("^[a-f0-9]{64}$")] + [string]$ExpectedArtifactSha256, + [string]$OutputRoot = "D:\NDC_MISSIONCORE\runtime\results\m4-detector-replay", + [ValidateRange(1, 1000)] + [int]$FreeGiBFloor = 300, + [switch]$PreflightOnly +) + +$ErrorActionPreference = "Stop" +$ProgressPreference = "SilentlyContinue" + +function Assert-LastExitCode([string]$Operation) { + if ($LASTEXITCODE -ne 0) { + throw "$Operation failed with exit code $LASTEXITCODE" + } +} + +function Get-Sha256([string]$Path) { + return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant() +} + +function Assert-FileSha256([string]$Path, [string]$Expected, [string]$Label) { + $item = Get-Item -LiteralPath (Resolve-Path -LiteralPath $Path).Path -Force + if ($item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) { + throw "$Label must be a regular file" + } + $observed = Get-Sha256 $item.FullName + if ($observed -cne $Expected) { + throw "$Label SHA-256 changed: expected $Expected, observed $observed" + } + return $item.FullName +} + +function Resolve-DDirectory([string]$Path, [string]$Label, [bool]$Create) { + if ($Create -and -not (Test-Path -LiteralPath $Path)) { + $null = New-Item -ItemType Directory -Path $Path + } + $item = Get-Item -LiteralPath (Resolve-Path -LiteralPath $Path).Path -Force + $root = [IO.Path]::GetPathRoot($item.FullName).TrimEnd("\") + if ( + -not $item.PSIsContainer -or + ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -or + $root -ine "D:" + ) { + throw "$Label must be a real D: directory" + } + return $item.FullName +} + +function Convert-ToDockerPath([string]$Path) { + return $Path.Replace("\", "/") +} + +function Assert-FreeSpace([string]$Phase) { + $free = [int64](Get-PSDrive -Name D).Free + $floor = [int64]$FreeGiBFloor * 1GB + Write-Output ( + "DISK_GUARD PHASE={0} DRIVE=D FREE_BYTES={1} FREE_GIB={2} FLOOR_GIB={3}" -f + $Phase, $free, [math]::Round($free / 1GB, 3), $FreeGiBFloor + ) + if ($free -lt ($floor + 1GB)) { + throw "D: lacks the guarded M4 reserve during $Phase" + } + return $free +} + +function Get-ContainerIdentity([string]$Name) { + $json = & docker inspect $Name + Assert-LastExitCode "Docker inspection for $Name" + $rows = @($json | ConvertFrom-Json) + if ($rows.Count -ne 1) { + throw "Docker identity for $Name is not unique" + } + return $rows[0] +} + +function Assert-ContainerIdentity( + [string]$Name, + [string]$ExpectedId, + [string]$ExpectedImageId, + [bool]$RequireHealthy +) { + $container = Get-ContainerIdentity $Name + if ( + $container.Id -cne $ExpectedId -or + $container.Image -cne $ExpectedImageId -or + -not $container.State.Running + ) { + throw "$Name identity or running state changed" + } + if ($RequireHealthy -and $container.State.Health.Status -cne "healthy") { + throw "$Name is not healthy" + } + return $container +} + +function Get-RequiredText([object]$Value, [string]$Label) { + if ($Value -isnot [string] -or [string]::IsNullOrWhiteSpace($Value)) { + throw "$Label must be a non-empty string" + } + return [string]$Value +} + +function Write-Utf8NoBom([string]$Path, [string]$Value) { + $encoding = New-Object System.Text.UTF8Encoding($false) + [IO.File]::WriteAllText($Path, $Value, $encoding) +} + +function Get-DirectoryTreeSha256([string]$Root, [object[]]$Files) { + $rows = @( + foreach ($file in $Files) { + [pscustomobject]@{ + Relative = [IO.Path]::GetRelativePath($Root, $file.FullName).Replace("\", "/") + File = $file + } + } + ) + $rows = @($rows | Sort-Object -Property Relative -CaseSensitive) + $digest = [Security.Cryptography.IncrementalHash]::CreateHash( + [Security.Cryptography.HashAlgorithmName]::SHA256 + ) + $encoding = New-Object System.Text.UTF8Encoding($false) + try { + foreach ($row in $rows) { + $fileSha256 = Get-Sha256 $row.File.FullName + $record = "{0}`t{1}`t{2}`n" -f $row.Relative, $row.File.Length, $fileSha256 + $digest.AppendData($encoding.GetBytes($record)) + } + return ([BitConverter]::ToString($digest.GetHashAndReset())).Replace("-", "").ToLowerInvariant() + } finally { + $digest.Dispose() + } +} + +if ($env:COMPUTERNAME -cne "DESKTOP-OPJ8J04") { + throw "M4 shadow release is pinned to DESKTOP-OPJ8J04" +} + +$release = Resolve-DDirectory $ReleaseRoot "M4 release root" $false +$payload = Resolve-DDirectory (Join-Path $release "payload") "M4 payload" $false +$artifact = Assert-FileSha256 $ArtifactPath $ExpectedArtifactSha256 "M4 release artifact" +$descriptorPath = Join-Path $payload "mission-core-worker-shadow-v1.json" +$descriptor = Get-Content -LiteralPath $descriptorPath -Raw | ConvertFrom-Json + +if ( + $descriptor.schema_version -cne "nodedc.mission-core-worker.shadow-release/v1" -or + $descriptor.component -cne "mission-core-worker" -or + $descriptor.artifact_type -cne "shadow-release" -or + $descriptor.transition -cne "m4-detector-shadow-v1" -or + $descriptor.boundary.repository -cne "NODEDC_MISSION_CORE" -or + $descriptor.boundary.nodedc_platform_repository -ne $false -or + $descriptor.boundary.external_deploy_registry -ne $false -or + $descriptor.boundary.server_docker_runtime -ne $false -or + $descriptor.host.node -cne $env:COMPUTERNAME -or + $descriptor.host.worker_id -cne "worker-006" -or + $descriptor.acceptance.expected_frames -ne 4489 -or + [double]$descriptor.acceptance.minimum_end_to_end_fps -ne 10.004 -or + $descriptor.acceptance.failed_frames -ne 0 -or + $descriptor.acceptance.class_routing_used -ne $false +) { + throw "M4 shadow descriptor contract changed" +} + +$wheelName = Get-RequiredText $descriptor.release.wheel.name "Wheel name" +$baselineName = Get-RequiredText $descriptor.release.baseline.name "Baseline name" +$wheelPath = Assert-FileSha256 ( + Join-Path $payload $wheelName +) $descriptor.release.wheel.sha256 "M4 runtime wheel" +$baselinePath = Assert-FileSha256 ( + Join-Path $payload $baselineName +) $descriptor.release.baseline.sha256 "M4 baseline" + +foreach ($entry in $descriptor.inputs.PSObject.Properties) { + $input = $entry.Value + $null = Assert-FileSha256 $input.host_path $input.sha256 ("M4 input {0}" -f $entry.Name) +} +foreach ($dependency in $descriptor.dependencies) { + $root = Resolve-DDirectory $dependency.host_path ("M4 dependency {0}" -f $dependency.id) $false + $files = @() + foreach ($include in $dependency.includes) { + $candidate = if ($include -eq ".") { $root } else { Join-Path $root $include } + $item = Get-Item -LiteralPath (Resolve-Path -LiteralPath $candidate).Path -Force + if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { + throw "M4 dependency $($dependency.id) contains a reparse-point root" + } + if ($item.PSIsContainer) { + $files += @(Get-ChildItem -LiteralPath $item.FullName -File -Recurse -Force) + } else { + $files += @($item) + } + } + $uniqueFiles = @($files | Sort-Object -Property FullName -Unique) + $bytes = [int64]0 + foreach ($file in $uniqueFiles) { + if ($file.Attributes -band [IO.FileAttributes]::ReparsePoint) { + throw "M4 dependency $($dependency.id) contains a reparse-point file" + } + $bytes += [int64]$file.Length + } + $treeSha256 = Get-DirectoryTreeSha256 $root $uniqueFiles + if ( + $uniqueFiles.Count -ne [int]$dependency.file_count -or + $bytes -ne [int64]$dependency.bytes -or + $treeSha256 -cne [string]$dependency.tree_sha256 + ) { + throw ( + "M4 dependency {0} inventory changed: expected {1}/{2}/{3}, observed {4}/{5}/{6}" -f + $dependency.id, + $dependency.file_count, + $dependency.bytes, + $dependency.tree_sha256, + $uniqueFiles.Count, + $bytes, + $treeSha256 + ) + } +} + +$imageRef = Get-RequiredText $descriptor.container.image_ref "Container image reference" +& docker image inspect $imageRef *> $null +Assert-LastExitCode "Pinned M4 image inspection" +$predecessor = $descriptor.predecessor.durable_worker +$tritonExpected = $descriptor.predecessor.triton +$durable = Assert-ContainerIdentity ( + Get-RequiredText $predecessor.name "Durable worker name" +) $predecessor.container_id $predecessor.image_id $false +$triton = Assert-ContainerIdentity ( + Get-RequiredText $tritonExpected.name "Triton name" +) $tritonExpected.container_id $tritonExpected.image_id $true + +$output = Resolve-DDirectory $OutputRoot "M4 output root" $true +$freeBefore = Assert-FreeSpace "preflight" +$patchId = Get-RequiredText $descriptor.patch_id "Patch id" +if ($patchId -notmatch "^[A-Za-z0-9._-]{1,96}$") { + throw "M4 patch id is invalid" +} +if ($PreflightOnly) { + Write-Output ("PATCH_ID={0}" -f $patchId) + Write-Output ("ARTIFACT_SHA256={0}" -f $ExpectedArtifactSha256) + Write-Output ("DURABLE_WORKER_ID={0}" -f $durable.Id) + Write-Output ("TRITON_CONTAINER_ID={0}" -f $triton.Id) + Write-Output ("DISK_FREE_BYTES_BEFORE={0}" -f $freeBefore) + Write-Output "PREFLIGHT=accepted" + return +} +$candidateName = "ndc-mission-core-m4-detector-shadow" +if (& docker ps -a --format "{{.Names}}" --filter "name=^/$candidateName$") { + throw "M4 candidate container already exists" +} +$scratch = Join-Path $output (".runtime-{0}" -f $patchId) +if (Test-Path -LiteralPath $scratch) { + throw "M4 runtime scratch already exists" +} +$null = New-Item -ItemType Directory -Path $scratch +$scratch = Resolve-DDirectory $scratch "M4 runtime scratch" $false +$runtimeIdentityPath = Join-Path $scratch "runtime-identity.json" +Write-Utf8NoBom $runtimeIdentityPath "{}" + +$dockerPayload = Convert-ToDockerPath $payload +$dockerOutput = Convert-ToDockerPath $output +$dockerScratch = Convert-ToDockerPath $scratch +$dockerArguments = @( + "create", + "--name", $candidateName, + "--network", ("container:{0}" -f $tritonExpected.name), + "--read-only", + "--security-opt", "no-new-privileges:true", + "--cap-drop", "ALL", + "--pids-limit", "256", + "--tmpfs", "/tmp:rw,noexec,nosuid,size=2g", + "-e", "PYTHONDONTWRITEBYTECODE=1", + "-e", ("PYTHONPATH=/release/{0}:/opt/media:/opt/opencv:/opt/pillow" -f $wheelName), + "-v", ("{0}:/release:ro" -f $dockerPayload), + "-v", ("{0}:/output:rw" -f $dockerOutput), + "-v", ("{0}:/run/mission-core:ro" -f $dockerScratch) +) + +foreach ($entry in $descriptor.inputs.PSObject.Properties) { + $input = $entry.Value + if ($null -ne $input.container_path) { + $dockerArguments += @( + "-v", + ("{0}:{1}:ro" -f (Convert-ToDockerPath $input.host_path), $input.container_path) + ) + } +} +foreach ($dependency in $descriptor.dependencies) { + $dockerArguments += @( + "-v", + ("{0}:{1}:ro" -f (Convert-ToDockerPath $dependency.host_path), $dependency.container_path) + ) +} +$dockerArguments += @( + "--entrypoint", "python3", + $imageRef, + "-m", "k1link.perception.detector_replay_cli", + "--baseline", ("/release/{0}" -f $baselineName), + "--camera-summary", $descriptor.inputs.camera_summary.container_path, + "--camera-index", $descriptor.inputs.camera_index.container_path, + "--source-pack-manifest", $descriptor.inputs.source_pack_manifest.container_path, + "--source-pack", $descriptor.inputs.source_pack.container_path, + "--video", $descriptor.inputs.video.container_path, + "--valid-fov-mask", $descriptor.inputs.valid_fov_mask.container_path, + "--triton-origin", $descriptor.container.triton_origin, + "--runtime-identity", "/run/mission-core/runtime-identity.json", + "--output-root", "/output" +) + +$candidateCreated = $false +$runFailure = $null +try { + $candidateId = (& docker @dockerArguments).Trim() + Assert-LastExitCode "M4 candidate creation" + if ($candidateId -notmatch "^[a-f0-9]{64}$") { + throw "M4 candidate id is invalid" + } + $candidateCreated = $true + $candidate = Get-ContainerIdentity $candidateName + if ( + $candidate.Id -cne $candidateId -or + $candidate.Image -cne $descriptor.container.image_id -or + $candidate.HostConfig.NetworkMode -cne ("container:{0}" -f $triton.Id) -or + -not $candidate.HostConfig.ReadonlyRootfs + ) { + throw "M4 candidate isolation contract changed" + } + $runtimeIdentity = [ordered]@{ + schema_version = "missioncore.perception-runtime-identity/v1" + worker_id = "worker-006" + worker_node = $env:COMPUTERNAME + worker_container_id = $candidate.Id + worker_image_id = $candidate.Image + triton_container_id = $triton.Id + triton_image_id = $triton.Image + triton_model_sha256 = $descriptor.inputs.yolox_model.sha256 + triton_model_config_sha256 = $descriptor.inputs.yolox_config.sha256 + valid_fov_mask_sha256 = $descriptor.inputs.valid_fov_mask.sha256 + artifact_sha256 = $ExpectedArtifactSha256 + code_revision = $descriptor.code_revision + source_mount_read_only = $true + model_service_reused = $true + public_worker_port_added = $false + same_host_tensor_transport = $true + } + Write-Utf8NoBom $runtimeIdentityPath ($runtimeIdentity | ConvertTo-Json -Depth 4) + Write-Output ("PATCH_ID={0}" -f $patchId) + Write-Output ("ARTIFACT_SHA256={0}" -f $ExpectedArtifactSha256) + Write-Output ("CANDIDATE_CONTAINER_ID={0}" -f $candidate.Id) + Write-Output ("DURABLE_WORKER_ID={0}" -f $durable.Id) + Write-Output ("TRITON_CONTAINER_ID={0}" -f $triton.Id) + & docker start --attach $candidateName + Assert-LastExitCode "M4 detector shadow" +} catch { + $runFailure = $_ +} finally { + if ($candidateCreated) { + & docker rm --force $candidateName *> $null + if ($LASTEXITCODE -ne 0 -and $null -eq $runFailure) { + $runFailure = "M4 candidate cleanup failed" + } + } + Remove-Item -LiteralPath $scratch -Force -Recurse -ErrorAction SilentlyContinue +} + +$null = Assert-ContainerIdentity $predecessor.name $predecessor.container_id ( + $predecessor.image_id +) $false +$null = Assert-ContainerIdentity $tritonExpected.name $tritonExpected.container_id ( + $tritonExpected.image_id +) $true +$freeAfter = Assert-FreeSpace "completed" +Write-Output ("DISK_FREE_BYTES_BEFORE={0}" -f $freeBefore) +Write-Output ("DISK_FREE_BYTES_AFTER={0}" -f $freeAfter) +Write-Output "DURABLE_WORKER_ACTION=none" +Write-Output "TRITON_ACTION=none" +if ($null -ne $runFailure) { + throw $runFailure +} diff --git a/scripts/build_m4_worker_shadow_artifact.py b/scripts/build_m4_worker_shadow_artifact.py index 7a0eaa4..c69aa44 100644 --- a/scripts/build_m4_worker_shadow_artifact.py +++ b/scripts/build_m4_worker_shadow_artifact.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Build the deterministic, data-only Worker 006 M4 detector shadow artifact.""" +"""Build the deterministic, self-contained local Worker 006 M4 shadow artifact.""" from __future__ import annotations @@ -20,11 +20,14 @@ BASELINE = REPOSITORY_ROOT / "config/perception/m4-recorded-realtime-baseline-v1 DESCRIPTOR_TEMPLATE = ( REPOSITORY_ROOT / "config/deployment/mission-core-worker-shadow-v1.template.json" ) +RUNNER = REPOSITORY_ROOT / "scripts/Invoke-M4DetectorShadow.ps1" WHEEL_NAME = "nodedc_mission_core-0.1.0-py3-none-any.whl" +RUNNER_NAME = RUNNER.name PATCH_ID = re.compile(r"^[A-Za-z0-9._-]{1,96}$") EXPECTED_BASELINE_SHA256 = "ea10359339e6cce31b5780a2710299771cab7cc0c1c2a2b56a1621f786b31fa8" EXPECTED_WHEEL_SHA256 = "df756938f2c212fb6d1770c83569e5366c708d70d74e0434895a21be55a16102" PAYLOAD_FILES = ( + RUNNER_NAME, WHEEL_NAME, "m4-recorded-realtime-baseline-v1.json", "mission-core-worker-shadow-v1.json", @@ -140,6 +143,7 @@ def build_artifact(patch_id: str, output_directory: Path) -> dict[str, object]: payload.mkdir() wheel = build_wheel(stage / "wheel") (payload / WHEEL_NAME).write_bytes(wheel.read_bytes()) + (payload / RUNNER_NAME).write_bytes(RUNNER.read_bytes()) (payload / "m4-recorded-realtime-baseline-v1.json").write_bytes(BASELINE.read_bytes()) descriptor_bytes = render_descriptor(patch_id, revision) (payload / "mission-core-worker-shadow-v1.json").write_bytes(descriptor_bytes) @@ -171,7 +175,7 @@ def main() -> int: parser.add_argument( "--output-directory", type=Path, - default=REPOSITORY_ROOT / ".runtime/deploy-artifacts", + default=REPOSITORY_ROOT / ".runtime/worker-artifacts", ) arguments = parser.parse_args() try: diff --git a/tests/test_m4_worker_shadow_artifact.py b/tests/test_m4_worker_shadow_artifact.py index 82438aa..2cada0d 100644 --- a/tests/test_m4_worker_shadow_artifact.py +++ b/tests/test_m4_worker_shadow_artifact.py @@ -29,7 +29,7 @@ def _regular_files(archive: tarfile.TarFile) -> dict[str, bytes]: return result -def test_worker_shadow_artifact_is_deterministic_narrow_and_data_only( +def test_worker_shadow_artifact_is_deterministic_narrow_and_self_contained( tmp_path: Path, ) -> None: patch_id = "mission-core-m4-detector-shadow-unit-001" @@ -52,6 +52,7 @@ def test_worker_shadow_artifact_is_deterministic_narrow_and_data_only( "manifest.env", "files.txt", "payload", + "payload/Invoke-M4DetectorShadow.ps1", "payload/m4-recorded-realtime-baseline-v1.json", "payload/mission-core-worker-shadow-v1.json", f"payload/{BUILDER.WHEEL_NAME}", @@ -61,6 +62,7 @@ def test_worker_shadow_artifact_is_deterministic_narrow_and_data_only( == (f"id={patch_id}\ncomponent=mission-core-worker\ntype=shadow-release\n").encode() ) assert regular["files.txt"].decode().splitlines() == list(BUILDER.PAYLOAD_FILES) + assert regular[f"payload/{BUILDER.RUNNER_NAME}"] == BUILDER.RUNNER.read_bytes() assert _sha256(regular[f"payload/{BUILDER.WHEEL_NAME}"]) == (BUILDER.EXPECTED_WHEEL_SHA256) assert _sha256(regular["payload/m4-recorded-realtime-baseline-v1.json"]) == ( BUILDER.EXPECTED_BASELINE_SHA256 @@ -68,6 +70,12 @@ def test_worker_shadow_artifact_is_deterministic_narrow_and_data_only( descriptor = json.loads(regular["payload/mission-core-worker-shadow-v1.json"]) assert descriptor["patch_id"] == patch_id assert descriptor["code_revision"] == first["code_revision"] + assert descriptor["boundary"] == { + "external_deploy_registry": False, + "nodedc_platform_repository": False, + "repository": "NODEDC_MISSION_CORE", + "server_docker_runtime": False, + } assert descriptor["container"]["public_ports"] is False assert descriptor["rollback"] == { "durable_worker_action": "none",