Files
awoooi/ops/agent99-truth-chain-index-manager.ps1
Your Name 9098b91757
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
feat(agent99): add digest-bound truth-chain index executor
2026-07-17 06:17:32 +08:00

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