diff --git a/experiments/perception/worker/observatory_portable/Install-Worker006AgentImage.ps1 b/experiments/perception/worker/observatory_portable/Install-Worker006AgentImage.ps1 index 4ae6084..321ab9a 100644 --- a/experiments/perception/worker/observatory_portable/Install-Worker006AgentImage.ps1 +++ b/experiments/perception/worker/observatory_portable/Install-Worker006AgentImage.ps1 @@ -446,6 +446,63 @@ function New-DependencyBundleManifest { } } +function New-TemporaryShellScript { + param( + [ValidateSet("build", "smoke")] + [string]$Role, + [string]$Content + ) + + if ($Content.IndexOf([char]0) -ge 0) { + throw "Worker 006 temporary shell script contains a NUL byte" + } + $normalizedContent = $Content.Replace("`r`n", "`n").Replace("`r", "`n") + if (-not $normalizedContent.EndsWith("`n", [StringComparison]::Ordinal)) { + $normalizedContent += "`n" + } + $stagingRoot = Split-Path -Parent $StagedSnapshotRoot + $scriptPath = Join-Path ( + $stagingRoot + ) ("observatory-worker-agent-{0}-{1}.sh" -f $Role, [Guid]::NewGuid().ToString("N")) + $utf8 = New-Object Text.UTF8Encoding($false) + try { + [IO.File]::WriteAllText($scriptPath, $normalizedContent, $utf8) + $item = Get-Item -LiteralPath $scriptPath -Force + $bytes = [IO.File]::ReadAllBytes($item.FullName) + if ( + $bytes.Length -eq 0 -or + $bytes[$bytes.Length - 1] -ne 10 -or + $bytes -contains 13 + ) { + throw "Worker 006 temporary shell script is not UTF-8 LF text" + } + return [pscustomobject]@{ + role = $Role + path = $item.FullName + sha256 = Get-FileSha256 $item.FullName + byte_length = [int64]$item.Length + } + } + catch { + if (Test-Path -LiteralPath $scriptPath -PathType Leaf) { + Remove-Item -LiteralPath $scriptPath -Force + } + throw + } +} + +function Assert-TemporaryShellScriptIdentity { + param([object]$Script) + + $item = Get-Item -LiteralPath $Script.path -Force + if ( + [int64]$item.Length -ne [int64]$Script.byte_length -or + (Get-FileSha256 $item.FullName) -cne [string]$Script.sha256 + ) { + throw "Worker 006 temporary $($Script.role) shell script identity changed" + } +} + function Get-ImageInspection { param([string]$Reference) @@ -569,7 +626,7 @@ function Invoke-InstalledImageSmoke { [object]$DependencyManifest ) - $smokeScript = ( + $smokeScriptContent = ( "set -eu; " + "test ! -e /opt/nodedc/mission-core/experiments; " + "test -z `"`$(find /opt/nodedc/mission-core -perm /022 -print -quit)`"; " + @@ -629,38 +686,58 @@ function Invoke-InstalledImageSmoke { "cd /opt/nodedc/mission-core; " + "python3 -B -c '$PythonSmokeProgram'" ) - $sourceMount = ( - "type=bind,source=$SnapshotRoot," + - "target=/nodedc-verify-source,readonly" - ) - $manifestMount = ( - "type=bind,source=$($EmbeddedManifest.path)," + - "target=/nodedc-verify-snapshot.json,readonly" - ) - $dependencyMount = ( - "type=bind,source=$DependencyRoot," + - "target=/nodedc-verify-deps,readonly" - ) - $dependencyManifestMount = ( - "type=bind,source=$($DependencyManifest.path)," + - "target=/nodedc-verify-deps-manifest.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 ` - --mount $dependencyMount ` - --mount $dependencyManifestMount ` - --entrypoint /bin/sh ` - $Tag ` - -c $smokeScript - if ($LASTEXITCODE -ne 0) { - throw "Installed Worker 006 image smoke failed" + $smokeScriptFile = $null + try { + $smokeScriptFile = New-TemporaryShellScript "smoke" $smokeScriptContent + Assert-TemporaryShellScriptIdentity $smokeScriptFile + $sourceMount = ( + "type=bind,source=$SnapshotRoot," + + "target=/nodedc-verify-source,readonly" + ) + $manifestMount = ( + "type=bind,source=$($EmbeddedManifest.path)," + + "target=/nodedc-verify-snapshot.json,readonly" + ) + $dependencyMount = ( + "type=bind,source=$DependencyRoot," + + "target=/nodedc-verify-deps,readonly" + ) + $dependencyManifestMount = ( + "type=bind,source=$($DependencyManifest.path)," + + "target=/nodedc-verify-deps-manifest.json,readonly" + ) + $smokeScriptMount = ( + "type=bind,source=$($smokeScriptFile.path)," + + "target=/nodedc-smoke-script.sh,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 ` + --mount $dependencyMount ` + --mount $dependencyManifestMount ` + --mount $smokeScriptMount ` + --entrypoint /bin/sh ` + $Tag ` + /nodedc-smoke-script.sh + $smokeExitCode = $LASTEXITCODE + Assert-TemporaryShellScriptIdentity $smokeScriptFile + if ($smokeExitCode -ne 0) { + throw "Installed Worker 006 image smoke failed" + } + } + finally { + if ( + $null -ne $smokeScriptFile -and + (Test-Path -LiteralPath $smokeScriptFile.path -PathType Leaf) + ) { + Remove-Item -LiteralPath $smokeScriptFile.path -Force + } } } @@ -765,6 +842,7 @@ if ([string]$dependencyBefore.sha256 -cne $ExpectedDependencyBundleSha256) { } $embeddedManifest = $null $dependencyManifest = $null +$buildScriptFile = $null try { $embeddedManifest = New-StagedSnapshotManifest $snapshotBefore $dependencyManifest = New-DependencyBundleManifest $dependencyBefore @@ -828,7 +906,7 @@ try { throw "Worker 006 temporary build container name is already occupied" } - $copyScript = ( + $copyScriptContent = ( "set -eu; " + "test ! -e /opt/nodedc/mission-core; " + "mkdir -p /opt/nodedc/mission-core/src /opt/nodedc/mission-core/release " + @@ -880,6 +958,8 @@ try { "PYTHONNOUSERSITE=1 PYTHONDONTWRITEBYTECODE=1 " + "python3 -B -c '$PythonSmokeProgram'" ) + $buildScriptFile = New-TemporaryShellScript "build" $copyScriptContent + Assert-TemporaryShellScriptIdentity $buildScriptFile $sourceMount = ( "type=bind,source=$StagedSnapshotRoot," + "target=/nodedc-build-source,readonly" @@ -896,6 +976,10 @@ try { "type=bind,source=$($dependencyManifest.path)," + "target=/nodedc-build-deps-manifest.json,readonly" ) + $buildScriptMount = ( + "type=bind,source=$($buildScriptFile.path)," + + "target=/nodedc-build-script.sh,readonly" + ) $containerId = $null $imageCommitted = $false $committedImageId = $null @@ -911,9 +995,10 @@ try { --mount $manifestMount ` --mount $dependencyMount ` --mount $dependencyManifestMount ` + --mount $buildScriptMount ` --entrypoint /bin/sh ` $BaseImageReference ` - -c $copyScript + /nodedc-build-script.sh ) $createdIds = @( $createOutput | Where-Object { $_ -match "^[a-f0-9]{64}$" } @@ -925,7 +1010,9 @@ try { throw "Worker 006 temporary build container creation failed" } docker start --attach $containerId - if ($LASTEXITCODE -ne 0) { + $buildExitCode = $LASTEXITCODE + Assert-TemporaryShellScriptIdentity $buildScriptFile + if ($buildExitCode -ne 0) { throw "Worker 006 source installation command failed" } $snapshotAfter = Get-StagedSnapshotInspection $StagedSnapshotRoot @@ -1092,4 +1179,10 @@ finally { ) { Remove-Item -LiteralPath $dependencyManifest.path -Force } + if ( + $null -ne $buildScriptFile -and + (Test-Path -LiteralPath $buildScriptFile.path -PathType Leaf) + ) { + Remove-Item -LiteralPath $buildScriptFile.path -Force + } } 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 efe3d53..2592fc4 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 @@ -53,6 +53,29 @@ "runtime_bind_allowed": false, "network_install_allowed": false }, + "temporary_shell_scripts": { + "encoding": "utf-8", + "byte_order_mark": false, + "line_endings": "lf", + "identity_verification": "SHA-256 and byte length before and after container use", + "host_cleanup": "always", + "build": { + "container_path": "/nodedc-build-script.sh", + "mount": "read-only", + "invocation": [ + "/bin/sh", + "/nodedc-build-script.sh" + ] + }, + "installed_image_smoke": { + "container_path": "/nodedc-smoke-script.sh", + "mount": "read-only", + "invocation": [ + "/bin/sh", + "/nodedc-smoke-script.sh" + ] + } + }, "base_image": { "reference": "nvcr.io/nvidia/tritonserver:26.06-py3@sha256:58df7489c3f2276f9591d500a012dee03e23d35543ce3c390b4c001e6bf90794", "sha256": "58df7489c3f2276f9591d500a012dee03e23d35543ce3c390b4c001e6bf90794", @@ -130,9 +153,8 @@ "read_only_rootfs": true, "platform": "linux/amd64", "entrypoint": [ - "python3", - "-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)" + "/bin/sh", + "/nodedc-smoke-script.sh" ], "staged_source_bytes": "matched", "embedded_context_bytes": "matched", diff --git a/tests/test_observatory_worker_agent_image_artifact.py b/tests/test_observatory_worker_agent_image_artifact.py index 3bb343b..dd29d67 100644 --- a/tests/test_observatory_worker_agent_image_artifact.py +++ b/tests/test_observatory_worker_agent_image_artifact.py @@ -272,6 +272,26 @@ def test_install_plan_requires_offline_build_hardening_smoke_and_unfilled_receip "runtime_bind_allowed": False, "network_install_allowed": False, } + shell_scripts = cast(dict[str, object], build["temporary_shell_scripts"]) + assert shell_scripts == { + "encoding": "utf-8", + "byte_order_mark": False, + "line_endings": "lf", + "identity_verification": ( + "SHA-256 and byte length before and after container use" + ), + "host_cleanup": "always", + "build": { + "container_path": "/nodedc-build-script.sh", + "mount": "read-only", + "invocation": ["/bin/sh", "/nodedc-build-script.sh"], + }, + "installed_image_smoke": { + "container_path": "/nodedc-smoke-script.sh", + "mount": "read-only", + "invocation": ["/bin/sh", "/nodedc-smoke-script.sh"], + }, + } base = cast(dict[str, object], build["base_image"]) assert base["reference"] == BASE_REFERENCE assert base["sha256"] == BASE_SHA256 @@ -358,6 +378,7 @@ def test_install_plan_requires_offline_build_hardening_smoke_and_unfilled_receip assert smoke["network"] == "none" assert smoke["read_only_rootfs"] is True assert smoke["platform"] == "linux/amd64" + assert smoke["entrypoint"] == ["/bin/sh", "/nodedc-smoke-script.sh"] assert smoke["staged_source_bytes"] == "matched" assert smoke["embedded_context_bytes"] == "matched" assert smoke["embedded_snapshot_manifest"] == "matched" diff --git a/tests/test_worker_006_agent_image_installer.py b/tests/test_worker_006_agent_image_installer.py index 1fcc04c..fddf229 100644 --- a/tests/test_worker_006_agent_image_installer.py +++ b/tests/test_worker_006_agent_image_installer.py @@ -42,6 +42,26 @@ def test_worker_agent_installer_is_local_offline_and_non_buildkit() -> None: assert "smb" not in lowered +def test_worker_agent_installer_mounts_utf8_lf_shell_scripts_by_file() -> None: + script = _script() + + assert "function New-TemporaryShellScript" in script + assert '$Content.Replace("`r`n", "`n").Replace("`r", "`n")' in script + assert "New-Object Text.UTF8Encoding($false)" in script + assert "[IO.File]::ReadAllBytes($item.FullName)" in script + assert "$bytes[$bytes.Length - 1] -ne 10" in script + assert "$bytes -contains 13" in script + assert script.count("Assert-TemporaryShellScriptIdentity") == 5 + assert 'New-TemporaryShellScript "build" $copyScriptContent' in script + assert 'New-TemporaryShellScript "smoke" $smokeScriptContent' in script + assert "target=/nodedc-build-script.sh,readonly" in script + assert "target=/nodedc-smoke-script.sh,readonly" in script + assert "$BaseImageReference `\n /nodedc-build-script.sh" in script + assert "$Tag `\n /nodedc-smoke-script.sh" in script + assert "-c $copyScript" not in script + assert "-c $smokeScript" not in script + + def test_worker_agent_installer_separates_archive_and_staged_identities() -> None: script = _script() @@ -222,6 +242,8 @@ def test_worker_agent_installer_removes_failed_new_image_and_temporary_files() - assert "image verification failed and committed image cleanup failed" in script assert "Remove-Item -LiteralPath $embeddedManifest.path -Force" in script assert "Remove-Item -LiteralPath $dependencyManifest.path -Force" in script + assert "Remove-Item -LiteralPath $buildScriptFile.path -Force" in script + assert "Remove-Item -LiteralPath $smokeScriptFile.path -Force" in script def test_worker_agent_installer_seals_runtime_contract_smoke_and_output() -> None: