fix(agent99): unblock bounded maintenance deployment
Some checks failed
CD Pipeline / select-latest-carrier (push) Successful in 1m9s
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Failing after 10m26s
CD Pipeline / revalidate-deploy-carrier (push) Has been skipped
CD Pipeline / build-and-deploy (push) Has been skipped
CD Pipeline / revalidate-post-deploy-carrier (push) Has been skipped
CD Pipeline / post-deploy-checks (push) Has been skipped
MCP External Protocol Replay / replay-and-verify (push) Successful in 44s
MCP External Artifact Mirror / mirror-and-verify (push) Successful in 33s
E2E Health Check / e2e-health (push) Failing after 48s
AI 技術雷達監控 / ai-technology-watch (push) Successful in 34s
Agent Version Lifecycle Watch / version-lifecycle-watch (push) Successful in 7s
Agent Market Watch / market-watch (push) Failing after 26s
AWOOOI Harbor 110 Local Repair / workflow-shape (push) Successful in 1s
AWOOOI Harbor 110 Local Repair / harbor-110-local-repair (push) Successful in 23s
Some checks failed
CD Pipeline / select-latest-carrier (push) Successful in 1m9s
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Failing after 10m26s
CD Pipeline / revalidate-deploy-carrier (push) Has been skipped
CD Pipeline / build-and-deploy (push) Has been skipped
CD Pipeline / revalidate-post-deploy-carrier (push) Has been skipped
CD Pipeline / post-deploy-checks (push) Has been skipped
MCP External Protocol Replay / replay-and-verify (push) Successful in 44s
MCP External Artifact Mirror / mirror-and-verify (push) Successful in 33s
E2E Health Check / e2e-health (push) Failing after 48s
AI 技術雷達監控 / ai-technology-watch (push) Successful in 34s
Agent Version Lifecycle Watch / version-lifecycle-watch (push) Successful in 7s
Agent Market Watch / market-watch (push) Failing after 26s
AWOOOI Harbor 110 Local Repair / workflow-shape (push) Successful in 1s
AWOOOI Harbor 110 Local Repair / harbor-110-local-repair (push) Successful in 23s
This commit is contained in:
347
scripts/reboot-recovery/agent99-stale-queue-quarantine.ps1
Normal file
347
scripts/reboot-recovery/agent99-stale-queue-quarantine.ps1
Normal file
@@ -0,0 +1,347 @@
|
||||
[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("^[A-Za-z0-9][A-Za-z0-9._:@+-]{0,127}$")]
|
||||
[string]$RunId,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidatePattern("^[A-Za-z0-9][A-Za-z0-9._:@+-]{0,127}$")]
|
||||
[string]$WorkItemId,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidatePattern("^[A-Za-z0-9][A-Za-z0-9._-]{0,180}\.json$")]
|
||||
[string]$QueueFileName,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidatePattern("^[a-fA-F0-9]{64}$")]
|
||||
[string]$ExpectedQueueSha256,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidatePattern("^[A-Za-z0-9][A-Za-z0-9._:@+-]{0,127}$")]
|
||||
[string]$ExpectedCommandId,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidatePattern("^agent99-control-loop-maintenance-2[0-9]{7}-[0-9]{6}\.json$")]
|
||||
[string]$MaintenanceReceiptName,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidatePattern("^[a-fA-F0-9]{64}$")]
|
||||
[string]$ExpectedMaintenanceSha256,
|
||||
[ValidateRange(30, 1440)]
|
||||
[int]$MinimumAgeMinutes = 30,
|
||||
[string]$AgentRoot = "C:\Wooo\Agent99"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ExpectedQueueSha256 = $ExpectedQueueSha256.ToLowerInvariant()
|
||||
$ExpectedMaintenanceSha256 = $ExpectedMaintenanceSha256.ToLowerInvariant()
|
||||
$queueRoot = Join-Path $AgentRoot "queue"
|
||||
$failedRoot = Join-Path $queueRoot "failed"
|
||||
$evidenceRoot = Join-Path $AgentRoot "evidence"
|
||||
$sourcePath = Join-Path $queueRoot $QueueFileName
|
||||
$destinationName = "stale-maintenance-$QueueFileName"
|
||||
$destinationPath = Join-Path $failedRoot $destinationName
|
||||
$maintenancePath = Join-Path $evidenceRoot $MaintenanceReceiptName
|
||||
$runtimeWritePerformed = $false
|
||||
|
||||
function Get-AgentSha256 {
|
||||
param([string]$Path)
|
||||
(Get-FileHash -Algorithm SHA256 -LiteralPath $Path).Hash.ToLowerInvariant()
|
||||
}
|
||||
|
||||
function Write-AgentDurableJson {
|
||||
param([object]$Value, [string]$Path)
|
||||
|
||||
$temp = "$Path.tmp-$PID-$([guid]::NewGuid().ToString('N'))"
|
||||
$stream = $null
|
||||
$writer = $null
|
||||
try {
|
||||
$encoding = New-Object Text.UTF8Encoding($false)
|
||||
$stream = [IO.File]::Open(
|
||||
$temp,
|
||||
[IO.FileMode]::CreateNew,
|
||||
[IO.FileAccess]::Write,
|
||||
[IO.FileShare]::None
|
||||
)
|
||||
$writer = New-Object IO.StreamWriter($stream, $encoding)
|
||||
$writer.Write(($Value | ConvertTo-Json -Depth 10))
|
||||
$writer.Flush()
|
||||
$stream.Flush($true)
|
||||
$writer.Dispose(); $writer = $null
|
||||
$stream.Dispose(); $stream = $null
|
||||
[IO.File]::Move($temp, $Path)
|
||||
$hash = Get-AgentSha256 $Path
|
||||
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
|
||||
throw "durable_receipt_readback_missing"
|
||||
}
|
||||
return $hash
|
||||
} finally {
|
||||
if ($writer) { $writer.Dispose() }
|
||||
if ($stream) { $stream.Dispose() }
|
||||
Remove-Item -LiteralPath $temp -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
function Test-AgentRecoveryMutexActive {
|
||||
$mutex = $null
|
||||
$acquired = $false
|
||||
try {
|
||||
$mutex = [Threading.Mutex]::new(
|
||||
$false,
|
||||
"Global\WoooAgent99RecoveryTransportV1"
|
||||
)
|
||||
try {
|
||||
$acquired = [bool]$mutex.WaitOne(0)
|
||||
} catch [Threading.AbandonedMutexException] {
|
||||
$acquired = $true
|
||||
}
|
||||
return [bool](-not $acquired)
|
||||
} finally {
|
||||
if ($acquired -and $mutex) { try { $mutex.ReleaseMutex() } catch {} }
|
||||
if ($mutex) { $mutex.Dispose() }
|
||||
}
|
||||
}
|
||||
|
||||
function Get-AgentActiveControlProcesses {
|
||||
@(
|
||||
Get-CimInstance Win32_Process -Filter "Name='powershell.exe'" -ErrorAction Stop |
|
||||
Where-Object {
|
||||
([string]$_.CommandLine) -match '(?i)agent99-run\.ps1"?\s+-Mode\s+ControlTick(\s|$)' -or
|
||||
([string]$_.CommandLine) -match '(?i)agent99-run\.ps1"?\s+-Mode\s+Recover\s+-ControlledApply(\s|$)'
|
||||
} |
|
||||
ForEach-Object {
|
||||
[pscustomobject]@{
|
||||
processId = [int]$_.ProcessId
|
||||
creationDate = ([datetime]$_.CreationDate).ToUniversalTime().ToString("o")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function Get-AgentQuarantineGate {
|
||||
if (-not (Test-Path -LiteralPath $sourcePath -PathType Leaf)) {
|
||||
throw "exact_queue_source_missing"
|
||||
}
|
||||
if (Test-Path -LiteralPath $destinationPath) {
|
||||
throw "quarantine_destination_already_exists"
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $maintenancePath -PathType Leaf)) {
|
||||
throw "maintenance_receipt_missing"
|
||||
}
|
||||
$queueHash = Get-AgentSha256 $sourcePath
|
||||
$maintenanceHash = Get-AgentSha256 $maintenancePath
|
||||
if ($queueHash -ne $ExpectedQueueSha256) { throw "queue_hash_mismatch" }
|
||||
if ($maintenanceHash -ne $ExpectedMaintenanceSha256) {
|
||||
throw "maintenance_hash_mismatch"
|
||||
}
|
||||
|
||||
$task = Get-ScheduledTask `
|
||||
-TaskName "Wooo-Agent99-Control-Loop" `
|
||||
-TaskPath "\" `
|
||||
-ErrorAction Stop
|
||||
if ($task.Settings.Enabled -or [string]$task.State -ne "Disabled") {
|
||||
throw "control_loop_not_frozen"
|
||||
}
|
||||
$activeProcesses = Get-AgentActiveControlProcesses
|
||||
if ($activeProcesses.Count -ne 0) { throw "active_control_or_recover_process" }
|
||||
$leasePresent = [bool](Test-Path -LiteralPath (
|
||||
Join-Path $AgentRoot "state\recovery-transport-lease.json"
|
||||
) -PathType Leaf)
|
||||
if ($leasePresent) { throw "recovery_lease_present" }
|
||||
if (Test-AgentRecoveryMutexActive) { throw "recovery_mutex_active" }
|
||||
|
||||
$maintenance = Get-Content -LiteralPath $maintenancePath -Raw | ConvertFrom-Json
|
||||
if (
|
||||
[string]$maintenance.schemaVersion -ne "agent99_control_loop_maintenance_v2" -or
|
||||
[string]$maintenance.mode -ne "Apply" -or
|
||||
[string]$maintenance.taskName -ne "Wooo-Agent99-Control-Loop" -or
|
||||
[string]$maintenance.terminal -ne "control_loop_maintenance_frozen" -or
|
||||
$maintenance.precheckPassed -isnot [bool] -or -not $maintenance.precheckPassed -or
|
||||
$maintenance.controlStopped -isnot [bool] -or -not $maintenance.controlStopped -or
|
||||
$maintenance.recoverStopped -isnot [bool] -or -not $maintenance.recoverStopped -or
|
||||
$maintenance.leaseMetadataRemoved -isnot [bool] -or -not $maintenance.leaseMetadataRemoved -or
|
||||
$maintenance.runtimeWritePerformed -isnot [bool] -or -not $maintenance.runtimeWritePerformed -or
|
||||
$maintenance.vmPowerChangePerformed -isnot [bool] -or $maintenance.vmPowerChangePerformed -or
|
||||
$maintenance.vmwareVmxProcessTerminated -isnot [bool] -or $maintenance.vmwareVmxProcessTerminated -or
|
||||
$maintenance.vmxLockDeleted -isnot [bool] -or $maintenance.vmxLockDeleted -or
|
||||
$maintenance.hostRebootPerformed -isnot [bool] -or $maintenance.hostRebootPerformed -or
|
||||
$maintenance.secretValueRead -isnot [bool] -or $maintenance.secretValueRead
|
||||
) {
|
||||
throw "maintenance_receipt_contract_invalid"
|
||||
}
|
||||
|
||||
$command = Get-Content -LiteralPath $sourcePath -Raw | ConvertFrom-Json
|
||||
if (
|
||||
[string]$command.id -ne $ExpectedCommandId -or
|
||||
[string]$command.mode -ne "Recover" -or
|
||||
[string]$command.source -ne "agent99-recovery-observer" -or
|
||||
$command.controlledApply -isnot [bool] -or
|
||||
-not $command.controlledApply -or
|
||||
[string]$command.traceId -notmatch '^00-[0-9a-f]{32}-[0-9a-f]{16}-0[01]$' -or
|
||||
[string]$command.workItemId -notmatch '^agent99-auto:[A-Za-z0-9._:@+-]{1,160}$'
|
||||
) {
|
||||
throw "queue_identity_contract_invalid"
|
||||
}
|
||||
$sourceItem = Get-Item -LiteralPath $sourcePath
|
||||
$ageMinutes = [math]::Round(((Get-Date) - $sourceItem.LastWriteTime).TotalMinutes, 1)
|
||||
if ($ageMinutes -lt $MinimumAgeMinutes) { throw "queue_item_not_stale" }
|
||||
|
||||
[pscustomobject]@{
|
||||
queueHash = $queueHash
|
||||
maintenanceHash = $maintenanceHash
|
||||
ageMinutes = $ageMinutes
|
||||
commandId = [string]$command.id
|
||||
commandMode = [string]$command.mode
|
||||
commandSource = [string]$command.source
|
||||
controlTaskEnabled = [bool]$task.Settings.Enabled
|
||||
controlTaskState = [string]$task.State
|
||||
activeControlProcessCount = [int]$activeProcesses.Count
|
||||
recoveryLeasePresent = $leasePresent
|
||||
recoveryMutexActive = $false
|
||||
}
|
||||
}
|
||||
|
||||
$gate = Get-AgentQuarantineGate
|
||||
$base = [ordered]@{
|
||||
schemaVersion = "agent99_stale_queue_quarantine_v1"
|
||||
timestamp = (Get-Date).ToString("o")
|
||||
mode = $Mode
|
||||
traceId = $TraceId
|
||||
runId = $RunId
|
||||
workItemId = $WorkItemId
|
||||
commandId = $gate.commandId
|
||||
commandMode = $gate.commandMode
|
||||
commandSource = $gate.commandSource
|
||||
sourceFile = $QueueFileName
|
||||
sourceSha256 = $gate.queueHash
|
||||
destinationFile = $destinationName
|
||||
ageMinutes = $gate.ageMinutes
|
||||
minimumAgeMinutes = $MinimumAgeMinutes
|
||||
maintenanceReceipt = $MaintenanceReceiptName
|
||||
maintenanceReceiptSha256 = $gate.maintenanceHash
|
||||
activeControlProcessCount = $gate.activeControlProcessCount
|
||||
recoveryLeasePresent = $gate.recoveryLeasePresent
|
||||
recoveryMutexActive = $gate.recoveryMutexActive
|
||||
commandExecuted = $false
|
||||
deletePerformed = $false
|
||||
otherQueueMutationPerformed = $false
|
||||
rawInstructionStored = $false
|
||||
secretValueRead = $false
|
||||
runtimeWritePerformed = $false
|
||||
}
|
||||
|
||||
if ($Mode -eq "Check") {
|
||||
$base["terminal"] = "check_passed_apply_allowed"
|
||||
$base | ConvertTo-Json -Depth 8 -Compress
|
||||
exit 0
|
||||
}
|
||||
|
||||
$writerMutex = [Threading.Mutex]::new(
|
||||
$false,
|
||||
"Global\WoooAgent99StaleQueueMaintenanceV1"
|
||||
)
|
||||
$writerAcquired = $false
|
||||
$intentPath = ""
|
||||
$intentHash = ""
|
||||
$resultPath = ""
|
||||
$moved = $false
|
||||
$rollbackAttempted = $false
|
||||
$rollbackVerified = $false
|
||||
try {
|
||||
try {
|
||||
$writerAcquired = [bool]$writerMutex.WaitOne(0)
|
||||
} catch [Threading.AbandonedMutexException] {
|
||||
$writerAcquired = $true
|
||||
}
|
||||
if (-not $writerAcquired) { throw "queue_maintenance_writer_active" }
|
||||
|
||||
$gate = Get-AgentQuarantineGate
|
||||
$safeRunId = $RunId -replace "[^A-Za-z0-9._-]", "_"
|
||||
$stamp = Get-Date -Format "yyyyMMdd-HHmmss-fff"
|
||||
$intentPath = Join-Path $evidenceRoot (
|
||||
"agent99-stale-queue-quarantine-$stamp-$safeRunId-intent.json"
|
||||
)
|
||||
$resultPath = Join-Path $evidenceRoot (
|
||||
"agent99-stale-queue-quarantine-$stamp-$safeRunId-result.json"
|
||||
)
|
||||
$intent = [ordered]@{} + $base
|
||||
$intent["schemaVersion"] = "agent99_stale_queue_quarantine_intent_v1"
|
||||
$intent["mode"] = "Apply"
|
||||
$intent["plannedAction"] = "atomic_move_exact_queue_item_to_failed"
|
||||
$intent["rollback"] = "atomic_move_back_if_post_move_hash_verifier_fails"
|
||||
$intent["terminal"] = "intent_durable_apply_pending"
|
||||
$intentHash = Write-AgentDurableJson $intent $intentPath
|
||||
$runtimeWritePerformed = $true
|
||||
|
||||
$gate = Get-AgentQuarantineGate
|
||||
[IO.File]::Move($sourcePath, $destinationPath)
|
||||
$moved = $true
|
||||
$sourceAbsent = -not (Test-Path -LiteralPath $sourcePath)
|
||||
$destinationPresent = Test-Path -LiteralPath $destinationPath -PathType Leaf
|
||||
$destinationHash = if ($destinationPresent) {
|
||||
Get-AgentSha256 $destinationPath
|
||||
} else { "" }
|
||||
if (-not ($sourceAbsent -and $destinationPresent -and $destinationHash -eq $ExpectedQueueSha256)) {
|
||||
throw "post_move_verifier_failed"
|
||||
}
|
||||
|
||||
$result = [ordered]@{} + $base
|
||||
$result["schemaVersion"] = "agent99_stale_queue_quarantine_result_v1"
|
||||
$result["mode"] = "Apply"
|
||||
$result["intentFile"] = Split-Path -Leaf $intentPath
|
||||
$result["intentSha256"] = $intentHash
|
||||
$result["sourceAbsent"] = $sourceAbsent
|
||||
$result["destinationPresent"] = $destinationPresent
|
||||
$result["destinationSha256"] = $destinationHash
|
||||
$result["runtimeWritePerformed"] = $true
|
||||
$result["terminal"] = "verified_quarantined"
|
||||
$resultHash = Write-AgentDurableJson $result $resultPath
|
||||
$result["resultFile"] = Split-Path -Leaf $resultPath
|
||||
$result["resultSha256"] = $resultHash
|
||||
$result | ConvertTo-Json -Depth 8 -Compress
|
||||
exit 0
|
||||
} catch {
|
||||
$failureType = $_.Exception.GetType().Name
|
||||
if (
|
||||
$moved -and
|
||||
-not (Test-Path -LiteralPath $sourcePath) -and
|
||||
(Test-Path -LiteralPath $destinationPath -PathType Leaf)
|
||||
) {
|
||||
$rollbackAttempted = $true
|
||||
if ((Get-AgentSha256 $destinationPath) -eq $ExpectedQueueSha256) {
|
||||
[IO.File]::Move($destinationPath, $sourcePath)
|
||||
$rollbackVerified = [bool](
|
||||
(Test-Path -LiteralPath $sourcePath -PathType Leaf) -and
|
||||
-not (Test-Path -LiteralPath $destinationPath) -and
|
||||
(Get-AgentSha256 $sourcePath) -eq $ExpectedQueueSha256
|
||||
)
|
||||
}
|
||||
}
|
||||
$failure = [ordered]@{} + $base
|
||||
$failure["schemaVersion"] = "agent99_stale_queue_quarantine_result_v1"
|
||||
$failure["mode"] = "Apply"
|
||||
$failure["intentFile"] = if ($intentPath) { Split-Path -Leaf $intentPath } else { "" }
|
||||
$failure["intentSha256"] = $intentHash
|
||||
$failure["failureType"] = $failureType
|
||||
$failure["rollbackAttempted"] = $rollbackAttempted
|
||||
$failure["rollbackVerified"] = $rollbackVerified
|
||||
$failure["runtimeWritePerformed"] = $runtimeWritePerformed
|
||||
$failure["terminal"] = if ($rollbackAttempted -and $rollbackVerified) {
|
||||
"failed_rolled_back"
|
||||
} elseif ($rollbackAttempted) {
|
||||
"rollback_unverified"
|
||||
} else {
|
||||
"failed_no_queue_move"
|
||||
}
|
||||
if ($resultPath -and -not (Test-Path -LiteralPath $resultPath)) {
|
||||
try {
|
||||
$failureHash = Write-AgentDurableJson $failure $resultPath
|
||||
$failure["resultFile"] = Split-Path -Leaf $resultPath
|
||||
$failure["resultSha256"] = $failureHash
|
||||
} catch {}
|
||||
}
|
||||
$failure | ConvertTo-Json -Depth 8 -Compress
|
||||
exit 2
|
||||
} finally {
|
||||
if ($writerAcquired) { try { $writerMutex.ReleaseMutex() } catch {} }
|
||||
$writerMutex.Dispose()
|
||||
}
|
||||
Reference in New Issue
Block a user