diff --git a/experiments/perception/worker/observatory_portable/Install-LabV1FfmpegRuntime.ps1 b/experiments/perception/worker/observatory_portable/Install-LabV1FfmpegRuntime.ps1 new file mode 100644 index 0000000..4a77a6e --- /dev/null +++ b/experiments/perception/worker/observatory_portable/Install-LabV1FfmpegRuntime.ps1 @@ -0,0 +1,186 @@ +[CmdletBinding()] +param( + [string]$TargetRoot = ( + "D:\NDC_MISSIONCORE\runtime\assets\observatory-portable\" + + "eomt-ffmpeg-runtime-frigate-8a364092b03561b9-v1" + ) +) + +$ErrorActionPreference = "Stop" +$ProgressPreference = "SilentlyContinue" + +$SourceImageSha256 = "8a364092b03561b9c08ac00730206e363a53d07ea0304f7d543b403b65432b5e" +$SourceImage = "sha256:$SourceImageSha256" +$SourcePath = "/usr/lib/ffmpeg/7.0" +$AssetId = "eomt-ffmpeg-runtime" +$IdentityAlgorithm = "relative-path-tab-size-tab-file-sha256-lf/v1" + +function Assert-LastExitCode { + param([string]$Operation) + + if ($LASTEXITCODE -ne 0) { + throw "$Operation failed with exit code $LASTEXITCODE" + } +} + +function Get-Sha256Hex { + param([byte[]]$Payload) + + $algorithm = [Security.Cryptography.SHA256]::Create() + try { + return -join @($algorithm.ComputeHash($Payload) | ForEach-Object { + $_.ToString("x2", [Globalization.CultureInfo]::InvariantCulture) + }) + } + finally { + $algorithm.Dispose() + } +} + +$target = [IO.Path]::GetFullPath($TargetRoot) +$allowedParent = [IO.Path]::GetFullPath( + "D:\NDC_MISSIONCORE\runtime\assets\observatory-portable" +).TrimEnd("\") +if ( + [IO.Path]::GetPathRoot($target).TrimEnd("\") -ine "D:" -or + (Split-Path -Parent $target) -ine $allowedParent -or + (Split-Path -Leaf $target) -ne ( + "eomt-ffmpeg-runtime-frigate-8a364092b03561b9-v1" + ) +) { + throw "LAB V1 FFmpeg runtime target is outside its fixed local asset root" +} +if (Test-Path -LiteralPath $target) { + throw "LAB V1 FFmpeg runtime target already exists" +} + +$parent = Split-Path -Parent $target +$null = New-Item -ItemType Directory -Path $parent -Force +$staging = "$target.staging-$([Guid]::NewGuid().ToString('N'))" +$null = New-Item -ItemType Directory -Path $staging +$containerId = $null +$published = $false + +try { + $inspectedImage = (& docker image inspect --format "{{.Id}}" $SourceImage).Trim() + Assert-LastExitCode "Exact FFmpeg source image inspection" + if ($inspectedImage -ne $SourceImage) { + throw "Installed FFmpeg source image identity changed" + } + + $containerId = (& docker create ` + --network none ` + --read-only ` + --cap-drop ALL ` + --security-opt no-new-privileges ` + --entrypoint /bin/true ` + $SourceImage).Trim() + Assert-LastExitCode "FFmpeg source container creation" + if ($containerId -notmatch "^[a-f0-9]{64}$") { + throw "FFmpeg source container id is invalid" + } + + & docker cp "${containerId}:${SourcePath}/." $staging + Assert-LastExitCode "FFmpeg runtime extraction" + + $prefix = $staging.TrimEnd("\") + "\" + $records = @() + [int64]$totalBytes = 0 + foreach ($file in @(Get-ChildItem -LiteralPath $staging -Recurse -File -Force)) { + if ($file.Attributes -band [IO.FileAttributes]::ReparsePoint) { + throw "FFmpeg runtime contains a reparse point" + } + if (-not $file.FullName.StartsWith($prefix, [StringComparison]::OrdinalIgnoreCase)) { + throw "FFmpeg runtime member escaped staging" + } + $relative = $file.FullName.Substring($prefix.Length).Replace("\", "/") + if ( + -not $relative -or + $relative.StartsWith("/") -or + $relative.Split("/") -contains ".." + ) { + throw "FFmpeg runtime member path is invalid" + } + $digest = (Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256).Hash.ToLowerInvariant() + $records += [pscustomobject]@{ + relative_path = $relative + byte_length = [int64]$file.Length + sha256 = $digest + } + $totalBytes += [int64]$file.Length + } + $records = @($records | Sort-Object -CaseSensitive -Property relative_path) + if ($records.Count -lt 2) { + throw "FFmpeg runtime inventory is incomplete" + } + + $builder = New-Object Text.StringBuilder + foreach ($record in $records) { + $null = $builder.Append($record.relative_path) + $null = $builder.Append([char]9) + $null = $builder.Append($record.byte_length) + $null = $builder.Append([char]9) + $null = $builder.Append($record.sha256) + $null = $builder.Append([char]10) + } + $utf8 = New-Object Text.UTF8Encoding($false) + $manifestBytes = $utf8.GetBytes($builder.ToString()) + $identitySha256 = Get-Sha256Hex $manifestBytes + [IO.File]::WriteAllBytes( + (Join-Path $staging "tree-manifest.tsv"), + $manifestBytes + ) + + $ffmpeg = @($records | Where-Object relative_path -eq "bin/ffmpeg") + $ffprobe = @($records | Where-Object relative_path -eq "bin/ffprobe") + if ($ffmpeg.Count -ne 1 -or $ffprobe.Count -ne 1) { + throw "FFmpeg runtime binaries are absent or ambiguous" + } + $receipt = [ordered]@{ + schema_version = "missioncore.sealed-tree-runtime/v1" + asset_id = $AssetId + identity_algorithm = $IdentityAlgorithm + identity_sha256 = $identitySha256 + source_image_sha256 = $SourceImageSha256 + source_path = $SourcePath + file_count = $records.Count + byte_length = $totalBytes + manifest_relative_path = "tree-manifest.tsv" + binaries = [ordered]@{ + ffmpeg = [ordered]@{ + relative_path = "bin/ffmpeg" + byte_length = $ffmpeg[0].byte_length + sha256 = $ffmpeg[0].sha256 + } + ffprobe = [ordered]@{ + relative_path = "bin/ffprobe" + byte_length = $ffprobe[0].byte_length + sha256 = $ffprobe[0].sha256 + } + } + } + [IO.File]::WriteAllText( + (Join-Path $staging "tree-receipt.json"), + (($receipt | ConvertTo-Json -Depth 8) + [char]10), + $utf8 + ) + + Move-Item -LiteralPath $staging -Destination $target + $published = $true + [pscustomobject]@{ + path = $target + identity_sha256 = $identitySha256 + file_count = $records.Count + byte_length = $totalBytes + ffmpeg_sha256 = $ffmpeg[0].sha256 + ffprobe_sha256 = $ffprobe[0].sha256 + } | ConvertTo-Json -Compress +} +finally { + if ($containerId -match "^[a-f0-9]{64}$") { + & docker rm -f $containerId *> $null + } + if (-not $published -and (Test-Path -LiteralPath $staging)) { + Remove-Item -LiteralPath $staging -Recurse -Force + } +}