All checks were successful
CD Pipeline / workflow-shape (push) Successful in 1s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 2m54s
CD Pipeline / build-and-deploy (push) Successful in 15m28s
CD Pipeline / post-deploy-checks (push) Successful in 4m16s
316 lines
14 KiB
PowerShell
316 lines
14 KiB
PowerShell
[CmdletBinding()]
|
|
param(
|
|
[ValidateSet("Check", "Apply")][string]$Mode = "Check",
|
|
[Parameter(Mandatory = $true)][ValidatePattern("^[A-Za-z0-9][A-Za-z0-9._:@+-]{0,127}$")][string]$TraceId,
|
|
[Parameter(Mandatory = $true)][ValidatePattern("^[0-9a-f-]{36}$")][string]$RunId,
|
|
[Parameter(Mandatory = $true)][ValidatePattern("^[A-Za-z0-9][A-Za-z0-9._:@+-]{0,127}$")][string]$WorkItemId,
|
|
[Parameter(Mandatory = $true)][ValidatePattern("^[0-9a-f]{40}$")][string]$SourceRevision,
|
|
[Parameter(Mandatory = $true)][ValidatePattern("^[0-9a-f]{64}$")][string]$MigrationSha256,
|
|
[Parameter(Mandatory = $true)][ValidatePattern("^[0-9a-f]{64}$")][string]$ExpectedOverlaySha256,
|
|
[string]$StagedOverlayPath = "C:\Windows\Temp\Agent99\truth-chain-index-executor-overlay.py",
|
|
[Parameter(Mandatory = $true)][string]$ResultPath,
|
|
[string]$AgentRoot = "C:\Wooo\Agent99"
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = "Stop"
|
|
$ProgressPreference = "SilentlyContinue"
|
|
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
|
|
|
|
$CommandId = "truth_chain_structured_indexes_v1"
|
|
$CanonicalAsset = "database:awoooi:prod/truth-chain-structured-indexes"
|
|
$JumpHost = "192.168.0.110"
|
|
$ControlPlaneHost = "192.168.0.120"
|
|
$RemoteUser = "wooo"
|
|
$Kubectl = "/usr/local/bin/kubectl"
|
|
$Namespace = "awoooi-prod"
|
|
$PodSelector = "app=awoooi-api,environment=prod,system=awoooi"
|
|
$Container = "api"
|
|
$IdentityFile = Join-Path $AgentRoot "keys\agent99_ed25519"
|
|
$ApiPodPattern = "^awoooi-api-[a-z0-9][a-z0-9-]{0,126}$"
|
|
$EvidenceRoot = [System.IO.Path]::GetFullPath((Join-Path $AgentRoot "evidence\"))
|
|
$StagingRoot = [System.IO.Path]::GetFullPath("C:\Windows\Temp\Agent99\")
|
|
$ResultFullPath = [System.IO.Path]::GetFullPath($ResultPath)
|
|
$OverlayFullPath = [System.IO.Path]::GetFullPath($StagedOverlayPath)
|
|
$startedAt = Get-Date
|
|
$checkReceipt = $null
|
|
$applyReceipt = $null
|
|
$verifierReceipt = $null
|
|
$executorPod = $null
|
|
$verifierPod = $null
|
|
$errorCode = $null
|
|
$transportAnomalies = @()
|
|
$transientCleanupPassed = $true
|
|
$stagedOverlayDeleted = $false
|
|
|
|
if (-not $ResultFullPath.StartsWith($EvidenceRoot, [System.StringComparison]::OrdinalIgnoreCase)) { throw "result_path_outside_evidence" }
|
|
if (-not $OverlayFullPath.StartsWith($StagingRoot, [System.StringComparison]::OrdinalIgnoreCase)) { throw "overlay_path_outside_staging" }
|
|
|
|
function Get-Sha256 {
|
|
param([string]$Path)
|
|
(Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
|
|
}
|
|
|
|
function Write-AtomicJson {
|
|
param([string]$Path, [object]$Value)
|
|
[System.IO.Directory]::CreateDirectory((Split-Path -Parent $Path)) | Out-Null
|
|
$temporaryPath = "$Path.tmp"
|
|
[System.IO.File]::WriteAllText($temporaryPath, ($Value | ConvertTo-Json -Depth 20), [System.Text.UTF8Encoding]::new($false))
|
|
Move-Item -LiteralPath $temporaryPath -Destination $Path -Force
|
|
}
|
|
|
|
function Get-ReadyApiPods {
|
|
$remoteCommand = (
|
|
"ssh -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes " +
|
|
"-o NumberOfPasswordPrompts=0 -o ConnectTimeout=8 -l $RemoteUser " +
|
|
"$ControlPlaneHost sudo -n $Kubectl get pods " +
|
|
"--namespace $Namespace --selector '$PodSelector' " +
|
|
"--field-selector 'status.phase=Running' --output name"
|
|
)
|
|
$arguments = @(
|
|
"-i", $IdentityFile,
|
|
"-o", "BatchMode=yes",
|
|
"-o", "IdentitiesOnly=yes",
|
|
"-o", "StrictHostKeyChecking=yes",
|
|
"-o", "NumberOfPasswordPrompts=0",
|
|
"-o", "ConnectTimeout=8",
|
|
"-o", "ServerAliveInterval=20",
|
|
"-o", "ServerAliveCountMax=3",
|
|
"-l", $RemoteUser,
|
|
$JumpHost,
|
|
$remoteCommand
|
|
)
|
|
$token = [Guid]::NewGuid().ToString("N")
|
|
$stdoutPath = Join-Path $env:TEMP "agent99-index-pods-$token.out"
|
|
$stderrPath = Join-Path $env:TEMP "agent99-index-pods-$token.err"
|
|
try {
|
|
$process = Start-Process -FilePath "ssh.exe" -ArgumentList $arguments -NoNewWindow `
|
|
-RedirectStandardOutput $stdoutPath -RedirectStandardError $stderrPath -PassThru
|
|
if (-not $process.WaitForExit(120 * 1000)) {
|
|
try { & taskkill.exe /PID $process.Id /T /F 2>&1 | Out-Null } catch {}
|
|
throw "pod_inventory_timeout"
|
|
}
|
|
$process.WaitForExit() | Out-Null
|
|
$process.Refresh()
|
|
$exitCode = if ($null -ne $process.ExitCode) { [int]$process.ExitCode } else { 255 }
|
|
$stdout = if (Test-Path -LiteralPath $stdoutPath -PathType Leaf) { [string](Get-Content -LiteralPath $stdoutPath -Raw) } else { "" }
|
|
$pods = @()
|
|
foreach ($line in @($stdout -split "`r?`n" | Where-Object { $_.Trim() })) {
|
|
if ($line.Trim() -notmatch "^pod/(.+)$") { throw "pod_inventory_shape_invalid" }
|
|
$podName = [string]$Matches[1]
|
|
if ($podName -notmatch $ApiPodPattern) { throw "pod_inventory_shape_invalid" }
|
|
$pods += $podName
|
|
}
|
|
$pods = @($pods | Sort-Object -Unique)
|
|
if ($pods.Count -lt 2) { throw "two_ready_api_pods_required" }
|
|
if ($exitCode -notin @(0, 255)) { throw "pod_inventory_transport_failed" }
|
|
if ($exitCode -eq 255) {
|
|
$script:transportAnomalies += [ordered]@{ stage = "pod_inventory"; exitCode = 255; acceptedBecause = "strict_pod_inventory_contract" }
|
|
}
|
|
return $pods
|
|
} finally {
|
|
Remove-Item -LiteralPath $stdoutPath, $stderrPath -Force -ErrorAction SilentlyContinue
|
|
if ((Test-Path -LiteralPath $stdoutPath) -or (Test-Path -LiteralPath $stderrPath)) { $script:transientCleanupPassed = $false }
|
|
}
|
|
}
|
|
|
|
function Test-StageReceipt {
|
|
param([object]$Receipt, [string]$ExpectedMode, [string]$ExpectedPod)
|
|
if (-not $Receipt) { return $false }
|
|
$allowedTerminal = switch ($ExpectedMode) {
|
|
"check" { [string]$Receipt.terminal -eq "check_ready" }
|
|
"apply" { [string]$Receipt.terminal -eq "apply_verified_local" }
|
|
"verify" { [string]$Receipt.terminal -eq "verified_healthy" }
|
|
default { $false }
|
|
}
|
|
return [bool](
|
|
[string]$Receipt.schema_version -eq "agent99_truth_chain_index_executor_v1" -and
|
|
[string]$Receipt.command_id -eq $CommandId -and
|
|
[string]$Receipt.canonical_asset -eq $CanonicalAsset -and
|
|
[string]$Receipt.mode -eq $ExpectedMode -and
|
|
[string]$Receipt.trace_id -eq $TraceId -and
|
|
[string]$Receipt.run_id -eq $RunId -and
|
|
[string]$Receipt.work_item_id -eq $WorkItemId -and
|
|
[string]$Receipt.source_revision -eq $SourceRevision -and
|
|
[string]$Receipt.migration_sha256 -eq $MigrationSha256 -and
|
|
[string]$Receipt.pod_name -eq $ExpectedPod -and
|
|
[string]$Receipt.process_identity_sha256 -match "^[0-9a-f]{64}$" -and
|
|
[string]$Receipt.operation_digest_sha256 -match "^[0-9a-f]{64}$" -and
|
|
$Receipt.raw_sql_accepted -eq $false -and
|
|
$Receipt.raw_sql_emitted -eq $false -and
|
|
$Receipt.query_parameters_emitted -eq $false -and
|
|
$Receipt.secret_value_read -eq $false -and
|
|
$Receipt.arbitrary_command_allowed -eq $false -and
|
|
$Receipt.cross_domain_fallback_allowed -eq $false -and
|
|
$Receipt.verifier.passed -eq $true -and
|
|
$allowedTerminal
|
|
)
|
|
}
|
|
|
|
function Get-CatalogSignature {
|
|
param([object]$Catalog)
|
|
if (-not $Catalog) { return "" }
|
|
[ordered]@{
|
|
expectedExtensionCount = [int]$Catalog.expected_extension_count
|
|
extensionCount = [int]$Catalog.extension_count
|
|
missingExtensions = @($Catalog.missing_extensions)
|
|
expectedIndexCount = [int]$Catalog.expected_index_count
|
|
presentIndexCount = [int]$Catalog.present_index_count
|
|
validReadyIndexCount = [int]$Catalog.valid_ready_index_count
|
|
missingIndexes = @($Catalog.missing_indexes)
|
|
invalidOrUnreadyIndexes = @($Catalog.invalid_or_unready_indexes)
|
|
passed = [bool]$Catalog.passed
|
|
} | ConvertTo-Json -Depth 6 -Compress
|
|
}
|
|
|
|
function Invoke-FixedStage {
|
|
param([ValidateSet("check", "apply", "verify")][string]$Stage, [string]$PodName)
|
|
if ($PodName -notmatch $ApiPodPattern) { throw "target_pod_contract_failed" }
|
|
$remoteCommand = (
|
|
"ssh -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes " +
|
|
"-o NumberOfPasswordPrompts=0 -o ConnectTimeout=8 -l $RemoteUser " +
|
|
"$ControlPlaneHost sudo -n $Kubectl exec --stdin " +
|
|
"--namespace $Namespace --container $Container $PodName -- " +
|
|
"python -B - --mode $Stage --trace-id $TraceId --run-id $RunId " +
|
|
"--work-item-id $WorkItemId --source-revision $SourceRevision " +
|
|
"--migration-sha256 $MigrationSha256"
|
|
)
|
|
$arguments = @(
|
|
"-i", $IdentityFile,
|
|
"-o", "BatchMode=yes",
|
|
"-o", "IdentitiesOnly=yes",
|
|
"-o", "StrictHostKeyChecking=yes",
|
|
"-o", "NumberOfPasswordPrompts=0",
|
|
"-o", "ConnectTimeout=8",
|
|
"-o", "ServerAliveInterval=20",
|
|
"-o", "ServerAliveCountMax=3",
|
|
"-l", $RemoteUser,
|
|
$JumpHost,
|
|
$remoteCommand
|
|
)
|
|
$token = [Guid]::NewGuid().ToString("N")
|
|
$stdoutPath = Join-Path $env:TEMP "agent99-index-$Stage-$token.out"
|
|
$stderrPath = Join-Path $env:TEMP "agent99-index-$Stage-$token.err"
|
|
try {
|
|
$process = Start-Process -FilePath "ssh.exe" -ArgumentList $arguments -NoNewWindow `
|
|
-RedirectStandardInput $OverlayFullPath -RedirectStandardOutput $stdoutPath `
|
|
-RedirectStandardError $stderrPath -PassThru
|
|
$timeoutSeconds = if ($Stage -eq "apply") { 1800 } else { 180 }
|
|
if (-not $process.WaitForExit($timeoutSeconds * 1000)) {
|
|
try { & taskkill.exe /PID $process.Id /T /F 2>&1 | Out-Null } catch {}
|
|
throw "stage_timeout:$Stage"
|
|
}
|
|
$process.WaitForExit() | Out-Null
|
|
$process.Refresh()
|
|
$exitCode = if ($null -ne $process.ExitCode) { [int]$process.ExitCode } else { 255 }
|
|
try {
|
|
$receipt = Get-Content -LiteralPath $stdoutPath -Raw | ConvertFrom-Json
|
|
} catch {
|
|
throw "stage_receipt_parse_failed:$Stage"
|
|
}
|
|
if (-not (Test-StageReceipt -Receipt $receipt -ExpectedMode $Stage -ExpectedPod $PodName)) { throw "stage_receipt_contract_failed:$Stage" }
|
|
if ($exitCode -notin @(0, 255)) { throw "stage_transport_failed:$Stage" }
|
|
if ($exitCode -eq 255) {
|
|
$script:transportAnomalies += [ordered]@{ stage = $Stage; pod = $PodName; exitCode = 255; acceptedBecause = "strict_stage_receipt_contract" }
|
|
}
|
|
return $receipt
|
|
} finally {
|
|
Remove-Item -LiteralPath $stdoutPath, $stderrPath -Force -ErrorAction SilentlyContinue
|
|
if ((Test-Path -LiteralPath $stdoutPath) -or (Test-Path -LiteralPath $stderrPath)) { $script:transientCleanupPassed = $false }
|
|
}
|
|
}
|
|
|
|
try {
|
|
foreach ($path in @($OverlayFullPath, $IdentityFile)) {
|
|
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { throw "required_asset_missing:$([System.IO.Path]::GetFileName($path))" }
|
|
}
|
|
if ((Get-Sha256 $OverlayFullPath) -ne $ExpectedOverlaySha256) { throw "overlay_hash_mismatch" }
|
|
$pods = Get-ReadyApiPods
|
|
$executorPod = [string]$pods[0]
|
|
$verifierPod = [string]$pods[1]
|
|
$checkReceipt = Invoke-FixedStage -Stage "check" -PodName $executorPod
|
|
if ($Mode -eq "Apply" -and -not $checkReceipt.catalog_after.passed) {
|
|
$applyReceipt = Invoke-FixedStage -Stage "apply" -PodName $executorPod
|
|
}
|
|
$verifierStage = if ($Mode -eq "Apply") { "verify" } else { "check" }
|
|
$verifierReceipt = Invoke-FixedStage -Stage $verifierStage -PodName $verifierPod
|
|
} catch {
|
|
$errorCode = [string]$_.Exception.Message
|
|
} finally {
|
|
Remove-Item -LiteralPath $OverlayFullPath -Force -ErrorAction SilentlyContinue
|
|
$stagedOverlayDeleted = -not (Test-Path -LiteralPath $OverlayFullPath)
|
|
}
|
|
|
|
$independentVerifier = [bool](
|
|
$checkReceipt -and $verifierReceipt -and
|
|
[string]$checkReceipt.pod_name -ne [string]$verifierReceipt.pod_name -and
|
|
[string]$checkReceipt.process_identity_sha256 -ne [string]$verifierReceipt.process_identity_sha256
|
|
)
|
|
$writesExpected = [bool]($Mode -eq "Apply" -and $checkReceipt -and -not $checkReceipt.catalog_after.passed)
|
|
$applyContractPassed = [bool](
|
|
-not $writesExpected -or
|
|
($applyReceipt -and $applyReceipt.write_attempted -eq $true -and $applyReceipt.verifier.passed -eq $true)
|
|
)
|
|
$catalogAgreement = [bool](
|
|
$checkReceipt -and $verifierReceipt -and
|
|
(Get-CatalogSignature $checkReceipt.catalog_after) -eq
|
|
(Get-CatalogSignature $verifierReceipt.catalog_after)
|
|
)
|
|
$targetReady = [bool]($verifierReceipt -and $verifierReceipt.catalog_after.passed -eq $true)
|
|
$modeContractPassed = [bool](
|
|
($Mode -eq "Check" -and $catalogAgreement) -or
|
|
($Mode -eq "Apply" -and $targetReady)
|
|
)
|
|
$passed = [bool](
|
|
-not $errorCode -and $checkReceipt -and $verifierReceipt -and
|
|
$independentVerifier -and $applyContractPassed -and $modeContractPassed -and
|
|
$transientCleanupPassed -and $stagedOverlayDeleted
|
|
)
|
|
$finishedAt = Get-Date
|
|
$terminal = if ($passed) {
|
|
if ($Mode -eq "Check") { "verified_check_no_write" }
|
|
elseif ($writesExpected) { "runtime_verified_controlled_apply" }
|
|
else { "runtime_verified_healthy_no_write" }
|
|
} else {
|
|
"degraded_with_safe_next_action"
|
|
}
|
|
$receipt = [ordered]@{
|
|
schemaVersion = "agent99_truth_chain_index_manager_v1"
|
|
traceId = $TraceId
|
|
runId = $RunId
|
|
workItemId = $WorkItemId
|
|
generatedAt = $finishedAt.ToString("o")
|
|
executor = "host:192.168.0.99"
|
|
target = $CanonicalAsset
|
|
sourceRevision = $SourceRevision
|
|
migrationSha256 = $MigrationSha256
|
|
dispatchPath = "windows99_digest_bound_stdin_to_host110_to_host120_to_two_api_pods"
|
|
risk = [ordered]@{ level = if ($Mode -eq "Apply") { "medium" } else { "low" }; rawSqlAccepted = $false; arbitraryCommandAllowed = $false; secretValueRead = $false }
|
|
check = [ordered]@{ overlaySha256 = $ExpectedOverlaySha256; receipt = $checkReceipt }
|
|
execution = [ordered]@{ mode = $Mode; performed = $writesExpected; receipt = $applyReceipt; errorCode = $errorCode; elapsedMs = [math]::Round(($finishedAt - $startedAt).TotalMilliseconds) }
|
|
verifier = [ordered]@{ receipt = $verifierReceipt; independent = $independentVerifier; catalogAgreement = $catalogAgreement; targetReady = $targetReady; passed = $passed }
|
|
cleanup = [ordered]@{ stagedOverlayDeleted = $stagedOverlayDeleted; transientOutputDeleted = $transientCleanupPassed }
|
|
rollback = [ordered]@{ automatic = $false; sourceAsset = "awooop_truth_chain_structured_reference_indexes_2026-07-17_down.sql" }
|
|
transportAnomalies = @($transportAnomalies)
|
|
telegramNotificationSent = $false
|
|
kmRagWritebackPerformed = $false
|
|
terminal = $terminal
|
|
}
|
|
Write-AtomicJson -Path $ResultFullPath -Value $receipt
|
|
|
|
[ordered]@{
|
|
resultPath = $ResultFullPath
|
|
resultSha256 = Get-Sha256 $ResultFullPath
|
|
terminal = $terminal
|
|
writesExpected = $writesExpected
|
|
writeCount = if ($applyReceipt) { @($applyReceipt.operations).Count } else { 0 }
|
|
validReadyIndexCount = if ($verifierReceipt) { [int]$verifierReceipt.catalog_after.valid_ready_index_count } else { 0 }
|
|
targetReady = $targetReady
|
|
independentVerifier = $independentVerifier
|
|
stagedOverlayDeleted = $stagedOverlayDeleted
|
|
errorCode = $errorCode
|
|
} | ConvertTo-Json -Depth 8 -Compress
|
|
|
|
if (-not $passed) { exit 3 }
|
|
exit 0
|