Some checks failed
CD Pipeline / select-latest-carrier (push) Successful in 2m2s
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 6m29s
CD Pipeline / revalidate-deploy-carrier (push) Successful in 29s
CD Pipeline / build-and-deploy (push) Failing after 42s
CD Pipeline / revalidate-post-deploy-carrier (push) Has been skipped
CD Pipeline / post-deploy-checks (push) Has been skipped
284 lines
12 KiB
PowerShell
284 lines
12 KiB
PowerShell
[CmdletBinding()]
|
|
param(
|
|
[ValidateSet("Check", "Apply")]
|
|
[string]$Mode = "Check",
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateRange(1, 2147483647)]
|
|
[int]$ExpectedControlPid,
|
|
[Parameter(Mandatory = $true)]
|
|
[datetime]$ExpectedControlCreated,
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateRange(1, 2147483647)]
|
|
[int]$ExpectedRecoverPid,
|
|
[Parameter(Mandatory = $true)]
|
|
[datetime]$ExpectedRecoverCreated,
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateRange(1, 2147483647)]
|
|
[int]$ExpectedRecoverParentPid,
|
|
[string]$AgentRoot = "C:\Wooo\Agent99"
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
$taskName = "Wooo-Agent99-Control-Loop"
|
|
$runtimeWritePerformed = $false
|
|
|
|
function Get-ExactProcess {
|
|
param([int]$ProcessId)
|
|
Get-CimInstance Win32_Process -ErrorAction Stop |
|
|
Where-Object { [int]$_.ProcessId -eq $ProcessId } |
|
|
Select-Object -First 1
|
|
}
|
|
|
|
function Test-ExactCreationTime {
|
|
param([object]$Process, [datetime]$ExpectedCreated)
|
|
if (-not $Process -or -not $Process.CreationDate) { return $false }
|
|
$actual = ([datetime]$Process.CreationDate).ToUniversalTime()
|
|
$expected = $ExpectedCreated.ToUniversalTime()
|
|
[bool]($actual.Ticks -eq $expected.Ticks)
|
|
}
|
|
|
|
function Test-ControlIdentity {
|
|
param([object]$Process)
|
|
[bool](
|
|
$Process -and
|
|
[string]$Process.Name -eq "powershell.exe" -and
|
|
(Test-ExactCreationTime $Process $ExpectedControlCreated) -and
|
|
([string]$Process.CommandLine) -match '(?i)agent99-run\.ps1"?\s+-Mode\s+ControlTick(\s|$)'
|
|
)
|
|
}
|
|
|
|
function Test-RecoverIdentity {
|
|
param([object]$Process)
|
|
[bool](
|
|
$Process -and
|
|
[string]$Process.Name -eq "powershell.exe" -and
|
|
(Test-ExactCreationTime $Process $ExpectedRecoverCreated) -and
|
|
[int]$Process.ParentProcessId -eq $ExpectedRecoverParentPid -and
|
|
([string]$Process.CommandLine) -match '(?i)agent99-run\.ps1"?\s+-Mode\s+Recover\s+-ControlledApply'
|
|
)
|
|
}
|
|
|
|
function Wait-ProcessAbsent {
|
|
param([int]$ProcessId, [int]$TimeoutSeconds)
|
|
$stopwatch = [Diagnostics.Stopwatch]::StartNew()
|
|
while ($stopwatch.Elapsed.TotalSeconds -lt $TimeoutSeconds) {
|
|
if (-not (Get-Process -Id $ProcessId -ErrorAction SilentlyContinue)) { return $true }
|
|
Start-Sleep -Milliseconds 200
|
|
}
|
|
[bool](-not (Get-Process -Id $ProcessId -ErrorAction SilentlyContinue))
|
|
}
|
|
|
|
function Test-RecoveryMutexActive {
|
|
$mutex = $null
|
|
$acquired = $false
|
|
try {
|
|
$mutex = [Threading.Mutex]::new($false, "Global\WoooAgent99RecoveryTransportV1")
|
|
try { $acquired = [bool]$mutex.WaitOne(0) } catch [Threading.AbandonedMutexException] { $acquired = $true }
|
|
[bool](-not $acquired)
|
|
} finally {
|
|
if ($acquired -and $mutex) { try { $mutex.ReleaseMutex() } catch {} }
|
|
if ($mutex) { $mutex.Dispose() }
|
|
}
|
|
}
|
|
|
|
function Write-DurableReceipt {
|
|
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 8))
|
|
$writer.Flush()
|
|
$stream.Flush($true)
|
|
$writer.Dispose(); $writer = $null
|
|
$stream.Dispose(); $stream = $null
|
|
$beforeHash = (Get-FileHash -LiteralPath $temp -Algorithm SHA256).Hash.ToLowerInvariant()
|
|
Move-Item -LiteralPath $temp -Destination $Path -Force
|
|
$afterHash = (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
|
|
if ($beforeHash -ne $afterHash) { throw "receipt_readback_mismatch" }
|
|
} finally {
|
|
if ($writer) { $writer.Dispose() }
|
|
if ($stream) { $stream.Dispose() }
|
|
Remove-Item -LiteralPath $temp -Force -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
|
|
$maintenanceMutex = [Threading.Mutex]::new($false, "Global\WoooAgent99ControlLoopMaintenanceV1")
|
|
$maintenanceMutexAcquired = $false
|
|
try {
|
|
try {
|
|
$maintenanceMutexAcquired = [bool]$maintenanceMutex.WaitOne(0)
|
|
} catch [Threading.AbandonedMutexException] {
|
|
$maintenanceMutexAcquired = $true
|
|
}
|
|
if (-not $maintenanceMutexAcquired) {
|
|
[pscustomobject]@{
|
|
schemaVersion = "agent99_control_loop_maintenance_v2"
|
|
timestamp = (Get-Date).ToString("o")
|
|
mode = $Mode
|
|
precheckPassed = $false
|
|
blockers = @("maintenance_writer_active")
|
|
terminal = "blocked_single_writer_active"
|
|
runtimeWritePerformed = $false
|
|
secretValueRead = $false
|
|
} | ConvertTo-Json -Depth 5 -Compress
|
|
exit 75
|
|
}
|
|
|
|
$blockers = New-Object System.Collections.Generic.List[string]
|
|
if ($env:COMPUTERNAME -ne "WOOO-SUPER") { $blockers.Add("computer_identity_mismatch") }
|
|
$task = Get-ScheduledTask -TaskName $taskName -TaskPath "\" -ErrorAction SilentlyContinue
|
|
if (-not $task) {
|
|
$blockers.Add("control_task_missing")
|
|
} else {
|
|
$actions = @($task.Actions)
|
|
if ($actions.Count -ne 1) { $blockers.Add("control_task_action_count_mismatch") }
|
|
if ($actions.Count -eq 1) {
|
|
if ([string]$actions[0].Execute -notmatch '(?i)\\WindowsPowerShell\\v1\.0\\powershell\.exe$') { $blockers.Add("control_task_executable_mismatch") }
|
|
if ([string]$actions[0].Arguments -notmatch '(?i)agent99-run\.ps1"?\s+-Mode\s+ControlTick(\s|$)') { $blockers.Add("control_task_arguments_mismatch") }
|
|
}
|
|
}
|
|
|
|
$control = Get-ExactProcess $ExpectedControlPid
|
|
$recover = Get-ExactProcess $ExpectedRecoverPid
|
|
if (-not $control) {
|
|
$blockers.Add("control_process_missing")
|
|
} else {
|
|
if (-not (Test-ControlIdentity $control)) { $blockers.Add("control_process_identity_mismatch") }
|
|
}
|
|
if (-not $recover) {
|
|
$blockers.Add("recover_process_missing")
|
|
} else {
|
|
if (-not (Test-RecoverIdentity $recover)) { $blockers.Add("recover_process_identity_mismatch") }
|
|
}
|
|
|
|
$precheckPassed = [bool]($blockers.Count -eq 0)
|
|
$controlStopped = $false
|
|
$recoverStopped = $false
|
|
$leaseMetadataRemoved = $false
|
|
$mutexReleased = $false
|
|
$taskAfter = $task
|
|
$operationIntentRef = ""
|
|
$deviceMutationAttempted = $false
|
|
$operationFailureType = ""
|
|
$terminal = if ($precheckPassed) { "maintenance_freeze_check_verified" } else { "blocked_identity_mismatch" }
|
|
|
|
if ($Mode -eq "Apply" -and $precheckPassed) {
|
|
$intentStamp = Get-Date -Format "yyyyMMdd-HHmmss-fff"
|
|
$operationIntentRef = "agent99-control-loop-maintenance-intent-$intentStamp-$([guid]::NewGuid().ToString('N').Substring(0, 8)).json"
|
|
$operationIntentPath = Join-Path $AgentRoot "evidence\$operationIntentRef"
|
|
$operationIntent = [pscustomobject]@{
|
|
schemaVersion = "agent99_control_loop_maintenance_intent_v1"
|
|
timestamp = (Get-Date).ToString("o")
|
|
taskName = $taskName
|
|
taskEnabledBefore = [bool]$task.Settings.Enabled
|
|
expectedControlPid = $ExpectedControlPid
|
|
expectedControlCreated = $ExpectedControlCreated.ToUniversalTime().ToString("o")
|
|
expectedRecoverPid = $ExpectedRecoverPid
|
|
expectedRecoverCreated = $ExpectedRecoverCreated.ToUniversalTime().ToString("o")
|
|
expectedRecoverParentPid = $ExpectedRecoverParentPid
|
|
plannedActions = @("disable_control_task", "stop_exact_control_tree", "stop_exact_recover_tree", "remove_exact_recovery_lease")
|
|
rollback = "keep task frozen on partial mutation; restore only after patched runtime verifier"
|
|
vmPowerChangePerformed = $false
|
|
vmwareVmxProcessTerminated = $false
|
|
secretValueRead = $false
|
|
}
|
|
Write-DurableReceipt $operationIntent $operationIntentPath
|
|
$runtimeWritePerformed = $true
|
|
|
|
try {
|
|
$deviceMutationAttempted = $true
|
|
Disable-ScheduledTask -TaskName $taskName -TaskPath "\" | Out-Null
|
|
Stop-ScheduledTask -TaskName $taskName -TaskPath "\" -ErrorAction SilentlyContinue
|
|
$controlStopped = Wait-ProcessAbsent $ExpectedControlPid 15
|
|
if (-not $controlStopped) {
|
|
$controlRecheck = Get-ExactProcess $ExpectedControlPid
|
|
if (-not (Test-ControlIdentity $controlRecheck)) { throw "control_recheck_identity_mismatch" }
|
|
& "$env:SystemRoot\System32\taskkill.exe" /PID $ExpectedControlPid /T /F 2>&1 | Out-Null
|
|
$controlStopped = Wait-ProcessAbsent $ExpectedControlPid 15
|
|
}
|
|
if (-not (Wait-ProcessAbsent $ExpectedRecoverPid 5)) {
|
|
$recoverRecheck = Get-ExactProcess $ExpectedRecoverPid
|
|
if (-not (Test-RecoverIdentity $recoverRecheck)) { throw "recover_recheck_identity_mismatch" }
|
|
& "$env:SystemRoot\System32\taskkill.exe" /PID $ExpectedRecoverPid /T /F 2>&1 | Out-Null
|
|
}
|
|
$recoverStopped = Wait-ProcessAbsent $ExpectedRecoverPid 15
|
|
$controlStopped = [bool]($controlStopped -or (Wait-ProcessAbsent $ExpectedControlPid 5))
|
|
|
|
$leasePath = Join-Path $AgentRoot "state\recovery-transport-lease.json"
|
|
$mutexReleased = [bool](-not (Test-RecoveryMutexActive))
|
|
if ($recoverStopped -and $mutexReleased -and (Test-Path -LiteralPath $leasePath)) {
|
|
$lease = Get-Content -LiteralPath $leasePath -Raw | ConvertFrom-Json
|
|
if (
|
|
[int]$lease.processId -eq $ExpectedRecoverPid -and
|
|
[string]$lease.runId -match "^(?:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|agent99-recovery-[0-9]{8}-[0-9]{6}-[0-9a-f]{8})$"
|
|
) {
|
|
Remove-Item -LiteralPath $leasePath -Force
|
|
}
|
|
}
|
|
$leaseMetadataRemoved = [bool](-not (Test-Path -LiteralPath $leasePath))
|
|
$taskAfter = Get-ScheduledTask -TaskName $taskName -TaskPath "\" -ErrorAction Stop
|
|
$verified = [bool](-not $taskAfter.Settings.Enabled -and $controlStopped -and $recoverStopped -and $mutexReleased -and $leaseMetadataRemoved)
|
|
$terminal = if ($verified) { "control_loop_maintenance_frozen" } else { "maintenance_freeze_unverified" }
|
|
} catch {
|
|
$operationFailureType = $_.Exception.GetType().Name
|
|
$taskAfter = Get-ScheduledTask -TaskName $taskName -TaskPath "\" -ErrorAction SilentlyContinue
|
|
$controlStopped = Wait-ProcessAbsent $ExpectedControlPid 1
|
|
$recoverStopped = Wait-ProcessAbsent $ExpectedRecoverPid 1
|
|
$mutexReleased = [bool](-not (Test-RecoveryMutexActive))
|
|
$leasePath = Join-Path $AgentRoot "state\recovery-transport-lease.json"
|
|
$leaseMetadataRemoved = [bool](-not (Test-Path -LiteralPath $leasePath))
|
|
$terminal = if ($deviceMutationAttempted) { "maintenance_partial_frozen_manual_recovery" } else { "maintenance_apply_failed_no_mutation" }
|
|
}
|
|
}
|
|
|
|
$result = [pscustomobject]@{
|
|
schemaVersion = "agent99_control_loop_maintenance_v2"
|
|
timestamp = (Get-Date).ToString("o")
|
|
mode = $Mode
|
|
taskName = $taskName
|
|
expectedControlPid = $ExpectedControlPid
|
|
expectedControlCreated = $ExpectedControlCreated.ToUniversalTime().ToString("o")
|
|
expectedRecoverPid = $ExpectedRecoverPid
|
|
expectedRecoverCreated = $ExpectedRecoverCreated.ToUniversalTime().ToString("o")
|
|
expectedRecoverParentPid = $ExpectedRecoverParentPid
|
|
precheckPassed = $precheckPassed
|
|
blockers = @($blockers)
|
|
taskEnabledBefore = if ($task) { [bool]$task.Settings.Enabled } else { $null }
|
|
taskEnabledAfter = if ($taskAfter) { [bool]$taskAfter.Settings.Enabled } else { $null }
|
|
operationIntentRef = $operationIntentRef
|
|
deviceMutationAttempted = $deviceMutationAttempted
|
|
operationFailureType = $operationFailureType
|
|
rollbackStatus = if ($deviceMutationAttempted -and $terminal -ne "control_loop_maintenance_frozen") { "task_kept_frozen_manual_recovery_required" } else { "not_required" }
|
|
controlStopped = $controlStopped
|
|
recoverStopped = $recoverStopped
|
|
leaseMetadataRemoved = $leaseMetadataRemoved
|
|
terminal = $terminal
|
|
runtimeWritePerformed = $runtimeWritePerformed
|
|
vmPowerChangePerformed = $false
|
|
vmwareVmxProcessTerminated = $false
|
|
vmxLockDeleted = $false
|
|
hostRebootPerformed = $false
|
|
secretValueRead = $false
|
|
}
|
|
|
|
if ($Mode -eq "Apply") {
|
|
$path = Join-Path $AgentRoot ("evidence\agent99-control-loop-maintenance-" + (Get-Date -Format "yyyyMMdd-HHmmss-fff") + "-" + ([guid]::NewGuid().ToString('N').Substring(0, 8)) + ".json")
|
|
Write-DurableReceipt $result $path
|
|
$result | Add-Member -NotePropertyName receiptRef -NotePropertyValue (Split-Path -Leaf $path) -Force
|
|
}
|
|
$result | ConvertTo-Json -Depth 8 -Compress
|
|
if (-not $precheckPassed) { exit 64 }
|
|
if ($Mode -eq "Check") { exit 0 }
|
|
if ($terminal -eq "control_loop_maintenance_frozen") { exit 0 }
|
|
exit 70
|
|
} finally {
|
|
if ($maintenanceMutexAcquired -and $maintenanceMutex) {
|
|
try { $maintenanceMutex.ReleaseMutex() } catch {}
|
|
}
|
|
if ($maintenanceMutex) { $maintenanceMutex.Dispose() }
|
|
}
|