fix(agent99): harden control loop recovery
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

This commit is contained in:
Your Name
2026-07-19 13:32:44 +08:00
parent c0afb10861
commit 3a81d9bedc
13 changed files with 3170 additions and 75 deletions

View File

@@ -0,0 +1,283 @@
[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() }
}

View File

@@ -0,0 +1,516 @@
[CmdletBinding()]
param(
[ValidateSet("Check", "Apply")]
[string]$Mode = "Check",
[Parameter(Mandatory = $true)]
[ValidatePattern("^(?:[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})$")]
[string]$ExpectedRunId,
[Parameter(Mandatory = $true)]
[ValidateRange(1, 2147483647)]
[int]$ExpectedParentPid,
[Parameter(Mandatory = $true)]
[datetime]$ExpectedParentCreated,
[Parameter(Mandatory = $true)]
[ValidateRange(1, 2147483647)]
[int]$ExpectedChildPid,
[Parameter(Mandatory = $true)]
[datetime]$ExpectedChildCreated,
[Parameter(Mandatory = $true)]
[string]$ExpectedVmxPath,
[ValidateRange(20, 1440)]
[int]$MinimumAgeMinutes = 20,
[ValidatePattern("^$|^[A-Za-z0-9._-]{1,180}\.json$")]
[string]$PriorReceiptRef = "",
[string]$AgentRoot = "C:\Wooo\Agent99"
)
$ErrorActionPreference = "Stop"
$script:RuntimeWritePerformed = $false
$script:ReceiptPath = ""
$postFailureReadbackErrors = @()
function Get-Sha256Text {
param([string]$Text)
$sha = [Security.Cryptography.SHA256]::Create()
try {
([BitConverter]::ToString($sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($Text)))).Replace("-", "").ToLowerInvariant()
} finally {
$sha.Dispose()
}
}
function Write-AtomicJson {
param([object]$Value, [string]$Path)
$directory = Split-Path -Parent $Path
if (-not (Test-Path -LiteralPath $directory)) {
New-Item -ItemType Directory -Path $directory -Force | Out-Null
}
$temp = "$Path.tmp-$PID-$([guid]::NewGuid().ToString('N'))"
$stream = $null
$writer = $null
try {
$json = $Value | ConvertTo-Json -Depth 8
$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($json)
$writer.Flush()
$stream.Flush($true)
$writer.Dispose()
$writer = $null
$stream.Dispose()
$stream = $null
$tempHash = (Get-FileHash -LiteralPath $temp -Algorithm SHA256).Hash.ToLowerInvariant()
Move-Item -LiteralPath $temp -Destination $Path -Force
if (-not (Test-Path -LiteralPath $Path)) { throw "receipt_publish_failed" }
$readbackHash = (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
if ($readbackHash -ne $tempHash) { throw "receipt_readback_mismatch" }
} finally {
if ($writer) { $writer.Dispose() }
if ($stream) { $stream.Dispose() }
Remove-Item -LiteralPath $temp -Force -ErrorAction SilentlyContinue
}
}
function Get-ExactProcess {
param([int]$ProcessId)
Get-CimInstance Win32_Process -ErrorAction Stop | Where-Object { [int]$_.ProcessId -eq $ProcessId } | Select-Object -First 1
}
function Test-ExactProcessIdentity {
param(
[object]$Process,
[int]$ExpectedPid,
[datetime]$ExpectedCreated,
[string]$ExpectedName,
[int]$ExpectedParentPid = -1
)
if (-not $Process -or -not $Process.CreationDate) { return $false }
[bool](
[int]$Process.ProcessId -eq $ExpectedPid -and
[string]$Process.Name -eq $ExpectedName -and
($ExpectedParentPid -lt 0 -or [int]$Process.ParentProcessId -eq $ExpectedParentPid) -and
([datetime]$Process.CreationDate).ToUniversalTime().Ticks -eq $ExpectedCreated.ToUniversalTime().Ticks
)
}
function Wait-ExactProcessIdentityAbsent {
param(
[int]$ProcessId,
[datetime]$ExpectedCreated,
[int]$TimeoutSeconds = 10
)
$stopwatch = [Diagnostics.Stopwatch]::StartNew()
while ($stopwatch.Elapsed.TotalSeconds -lt $TimeoutSeconds) {
$current = Get-ExactProcess $ProcessId
if (-not $current -or ([datetime]$current.CreationDate).ToUniversalTime().Ticks -ne $ExpectedCreated.ToUniversalTime().Ticks) { return $true }
Start-Sleep -Milliseconds 200
}
$current = Get-ExactProcess $ProcessId
[bool](-not $current -or ([datetime]$current.CreationDate).ToUniversalTime().Ticks -ne $ExpectedCreated.ToUniversalTime().Ticks)
}
function Get-FailedMutationReadback {
param(
[int]$ParentProcessId,
[datetime]$ParentCreated,
[int]$ChildProcessId,
[datetime]$ChildCreated,
[string]$LeasePath
)
$errors = @()
try {
$parentAbsent = Wait-ExactProcessIdentityAbsent $ParentProcessId $ParentCreated 1
} catch {
$parentAbsent = $false
$errors += "parent_identity_unknown"
}
try {
$childAbsent = Wait-ExactProcessIdentityAbsent $ChildProcessId $ChildCreated 1
} catch {
$childAbsent = $false
$errors += "child_identity_unknown"
}
try {
$recoveryMutexReleased = [bool](-not (Test-RecoveryMutexActive))
} catch {
$recoveryMutexReleased = $false
$errors += "recovery_mutex_unknown"
}
try {
$leaseRemoved = [bool](-not (Test-Path -LiteralPath $LeasePath))
} catch {
$leaseRemoved = $false
$errors += "lease_metadata_unknown"
}
[pscustomobject]@{
parentAbsent = [bool]$parentAbsent
childAbsent = [bool]$childAbsent
recoveryMutexReleased = [bool]$recoveryMutexReleased
leaseRemoved = [bool]$leaseRemoved
complete = [bool]($errors.Count -eq 0)
errors = @($errors)
}
}
function Wait-ExactProcessAbsent {
param(
[int]$ProcessId,
[int]$TimeoutSeconds = 10
)
$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 Get-DescendantProcesses {
param([int]$ParentProcessId)
$all = @(Get-CimInstance Win32_Process -ErrorAction Stop)
$known = New-Object 'System.Collections.Generic.HashSet[int]'
[void]$known.Add($ParentProcessId)
$rows = @()
do {
$added = $false
foreach ($processInfo in $all) {
if (
$known.Contains([int]$processInfo.ParentProcessId) -and
-not $known.Contains([int]$processInfo.ProcessId)
) {
[void]$known.Add([int]$processInfo.ProcessId)
$rows += $processInfo
$added = $true
}
}
} while ($added)
@($rows)
}
function Invoke-ExactTaskkill {
param([int]$ProcessId)
$output = @(& "$env:SystemRoot\System32\taskkill.exe" /PID $ProcessId /T /F 2>&1)
[pscustomobject]@{
exitCode = [int]$LASTEXITCODE
outputPresent = [bool]($output.Count -gt 0)
rawOutputStored = $false
}
}
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() }
}
}
$reconcilerMutex = [Threading.Mutex]::new($false, "Global\WoooAgent99StaleRecoveryReconcilerV1")
$reconcilerMutexAcquired = $false
try { $reconcilerMutexAcquired = [bool]$reconcilerMutex.WaitOne(0) } catch [Threading.AbandonedMutexException] { $reconcilerMutexAcquired = $true }
if (-not $reconcilerMutexAcquired) {
[pscustomobject]@{
schemaVersion = "agent99_stale_recovery_breakglass_v3"
mode = $Mode
precheckPassed = $false
blockers = @("stale_recovery_reconciler_active")
terminal = "blocked_single_writer_active"
runtimeWritePerformed = $false
secretValueRead = $false
} | ConvertTo-Json -Compress
$reconcilerMutex.Dispose()
exit 75
}
try {
$leasePath = Join-Path $AgentRoot "state\recovery-transport-lease.json"
$preconditions = New-Object System.Collections.Generic.List[string]
$lease = $null
if (-not (Test-Path -LiteralPath $leasePath)) {
$preconditions.Add("lease_missing")
} else {
try { $lease = Get-Content -LiteralPath $leasePath -Raw | ConvertFrom-Json } catch { $preconditions.Add("lease_parse_failed") }
}
$parent = Get-ExactProcess $ExpectedParentPid
$child = Get-ExactProcess $ExpectedChildPid
$canonicalVmx = [IO.Path]::GetFullPath($ExpectedVmxPath).TrimEnd("\")
$vmxIdentityDigest = Get-Sha256Text $canonicalVmx.ToLowerInvariant()
$priorReceipt = $null
$priorReceiptVerified = $false
$now = Get-Date
$parentAgeMinutes = -1
if ($parent) {
try { $parentAgeMinutes = [math]::Round(($now - (Get-Process -Id $ExpectedParentPid -ErrorAction Stop).StartTime).TotalMinutes, 2) } catch {}
}
if (-not $lease) {
$preconditions.Add("lease_unavailable")
} else {
if ([string]$lease.schemaVersion -ne "agent99_recovery_transport_lease_v1") { $preconditions.Add("lease_schema_mismatch") }
if ([string]$lease.runId -ne $ExpectedRunId) { $preconditions.Add("lease_run_id_mismatch") }
if ([int]$lease.processId -ne $ExpectedParentPid) { $preconditions.Add("lease_process_id_mismatch") }
if ([bool]$lease.storesSecret) { $preconditions.Add("lease_secret_contract_invalid") }
}
if (-not $parent) {
$preconditions.Add("parent_missing")
} else {
$parentCommand = [string]$parent.CommandLine
if (-not (Test-ExactProcessIdentity $parent $ExpectedParentPid $ExpectedParentCreated "powershell.exe")) { $preconditions.Add("parent_identity_mismatch") }
if ($parentCommand -notmatch '(?i)agent99-run\.ps1"?\s+-Mode\s+Recover\s+-ControlledApply') { $preconditions.Add("parent_command_mismatch") }
$runIdBoundToCommand = if ($ExpectedRunId -match '^agent99-recovery-') {
$parentCommand -notmatch "(?i)-AutomationRunId\s+"
} else {
$parentCommand.IndexOf($ExpectedRunId, [StringComparison]::OrdinalIgnoreCase) -ge 0
}
if (-not $runIdBoundToCommand) { $preconditions.Add("parent_run_id_mismatch") }
if ($lease -and $lease.acquiredAt) {
$leaseAcquiredUtc = ([datetime]$lease.acquiredAt).ToUniversalTime()
$parentCreatedUtc = $ExpectedParentCreated.ToUniversalTime()
if ($leaseAcquiredUtc -lt $parentCreatedUtc -or $leaseAcquiredUtc -gt $parentCreatedUtc.AddMinutes(5)) { $preconditions.Add("lease_parent_creation_mismatch") }
} else {
$preconditions.Add("lease_acquired_at_missing")
}
if ($parentAgeMinutes -lt $MinimumAgeMinutes) { $preconditions.Add("parent_not_stale") }
}
if (-not $child) {
if (-not $PriorReceiptRef) {
$preconditions.Add("child_missing_without_prior_receipt")
} else {
$priorPath = Join-Path (Join-Path $AgentRoot "evidence") $PriorReceiptRef
try {
$priorReceipt = Get-Content -LiteralPath $priorPath -Raw | ConvertFrom-Json
$priorSchema = [string]$priorReceipt.schemaVersion
$priorCreationIdentityValid = if ($priorSchema -eq "agent99_stale_recovery_breakglass_v3") {
[bool](
$priorReceipt.PSObject.Properties["expectedParentCreated"] -and
$priorReceipt.PSObject.Properties["expectedChildCreated"] -and
([datetime]$priorReceipt.expectedParentCreated).ToUniversalTime().Ticks -eq $ExpectedParentCreated.ToUniversalTime().Ticks -and
([datetime]$priorReceipt.expectedChildCreated).ToUniversalTime().Ticks -eq $ExpectedChildCreated.ToUniversalTime().Ticks
)
} else {
[bool]($priorSchema -eq "agent99_stale_recovery_breakglass_v2" -and $priorReceipt.childTerminated -is [bool] -and $priorReceipt.childTerminated)
}
$priorReceiptVerified = [bool](
$priorSchema -in @("agent99_stale_recovery_breakglass_v2", "agent99_stale_recovery_breakglass_v3") -and
$priorCreationIdentityValid -and
[string]$priorReceipt.mode -eq "Apply" -and
[string]$priorReceipt.expectedRunId -eq $ExpectedRunId -and
[int]$priorReceipt.expectedParentPid -eq $ExpectedParentPid -and
[int]$priorReceipt.expectedChildPid -eq $ExpectedChildPid -and
[string]$priorReceipt.vmxIdentityDigest -eq $vmxIdentityDigest -and
$priorReceipt.precheckPassed -is [bool] -and
$priorReceipt.precheckPassed -eq $true -and
$priorReceipt.runtimeWritePerformed -is [bool] -and
$priorReceipt.runtimeWritePerformed -eq $true -and
[string]$priorReceipt.terminal -in @("child_termination_failed", "child_terminated_parent_draining")
)
if (-not $priorReceiptVerified) { $preconditions.Add("prior_receipt_identity_mismatch") }
} catch {
$preconditions.Add("prior_receipt_readback_failed")
}
}
} else {
$childCommand = [string]$child.CommandLine
if (-not (Test-ExactProcessIdentity $child $ExpectedChildPid $ExpectedChildCreated "vmrun.exe" $ExpectedParentPid)) { $preconditions.Add("child_identity_mismatch") }
if ($childCommand -notmatch "(?i)vmrun\.exe.*\sstart\s") { $preconditions.Add("child_command_mismatch") }
if ($childCommand.IndexOf($canonicalVmx, [StringComparison]::OrdinalIgnoreCase) -lt 0) { $preconditions.Add("child_vmx_mismatch") }
if ($childCommand -notmatch "(?i)\snogui(\s|$)") { $preconditions.Add("child_nogui_mismatch") }
}
if (-not (Test-RecoveryMutexActive)) { $preconditions.Add("recovery_mutex_not_active") }
$precheckPassed = [bool]($preconditions.Count -eq 0)
$terminal = if ($precheckPassed) { "check_verified" } else { "blocked_identity_mismatch" }
$childTerminated = $false
$childAlreadyAbsent = [bool](-not $child -and $priorReceiptVerified)
$parentDrained = $false
$parentTerminated = $false
$mutexReleased = $false
$leaseMetadataRemoved = $false
$childTaskkill = $null
$parentTaskkill = $null
$operationIntentRef = ""
$parentCleanupIntentRef = ""
$mutationAttempted = $false
$operationFailureType = ""
if ($Mode -eq "Apply" -and $precheckPassed) {
try {
$intentStamp = Get-Date -Format "yyyyMMdd-HHmmss-fff"
$operationIntentRef = "agent99-stale-recovery-breakglass-intent-$intentStamp-$([guid]::NewGuid().ToString('N').Substring(0, 8)).json"
$operationIntentPath = Join-Path (Join-Path $AgentRoot "evidence") $operationIntentRef
$operationIntent = [pscustomobject]@{
schemaVersion = "agent99_stale_recovery_breakglass_intent_v1"
timestamp = (Get-Date).ToString("o")
expectedRunId = $ExpectedRunId
expectedParentPid = $ExpectedParentPid
expectedParentCreated = $ExpectedParentCreated.ToUniversalTime().ToString("o")
expectedChildPid = $ExpectedChildPid
expectedChildCreated = $ExpectedChildCreated.ToUniversalTime().ToString("o")
vmxIdentityDigest = $vmxIdentityDigest
childAlreadyAbsent = $childAlreadyAbsent
precheckPassed = $true
plannedActions = @("terminate_exact_vmrun_child_if_present", "wait_or_terminate_exact_recover_parent", "remove_exact_lease_metadata")
rollback = "process_termination_not_reversible; preserve intent and terminal receipt"
vmPowerChangePerformed = $false
vmwareVmxProcessTerminated = $false
secretValueRead = $false
}
Write-AtomicJson $operationIntent $operationIntentPath
$script:RuntimeWritePerformed = $true
if ($childAlreadyAbsent) {
$childTerminated = $true
} else {
$childRecheck = Get-ExactProcess $ExpectedChildPid
if (
-not (Test-ExactProcessIdentity $childRecheck $ExpectedChildPid $ExpectedChildCreated "vmrun.exe" $ExpectedParentPid) -or
([string]$childRecheck.CommandLine) -ne $childCommand
) {
$terminal = "child_recheck_identity_changed"
} else {
$mutationAttempted = $true
$childTaskkill = Invoke-ExactTaskkill $ExpectedChildPid
$script:RuntimeWritePerformed = $true
$childTerminated = Wait-ExactProcessIdentityAbsent $ExpectedChildPid $ExpectedChildCreated 15
}
}
if (-not $childTerminated) {
if ($terminal -ne "child_recheck_identity_changed") { $terminal = "child_termination_failed" }
} else {
$terminal = "child_terminated_parent_draining"
$parentDrained = Wait-ExactProcessIdentityAbsent $ExpectedParentPid $ExpectedParentCreated 30
if (-not $parentDrained) {
$parentRecheck = Get-ExactProcess $ExpectedParentPid
$descendants = @(Get-DescendantProcesses $ExpectedParentPid)
$parentRecheckValid = [bool](
(Test-ExactProcessIdentity $parentRecheck $ExpectedParentPid $ExpectedParentCreated "powershell.exe") -and
([string]$parentRecheck.CommandLine) -eq $parentCommand -and
$descendants.Count -eq 0
)
if ($parentRecheckValid) {
$parentIntentStamp = Get-Date -Format "yyyyMMdd-HHmmss-fff"
$parentCleanupIntentRef = "agent99-stale-recovery-parent-cleanup-intent-$parentIntentStamp.json"
$intentPath = Join-Path (Join-Path $AgentRoot "evidence") $parentCleanupIntentRef
$intent = [pscustomobject]@{
schemaVersion = "agent99_stale_recovery_parent_cleanup_intent_v1"
timestamp = (Get-Date).ToString("o")
expectedRunId = $ExpectedRunId
expectedParentPid = $ExpectedParentPid
expectedParentCreated = $ExpectedParentCreated.ToUniversalTime().ToString("o")
expectedChildPid = $ExpectedChildPid
expectedChildCreated = $ExpectedChildCreated.ToUniversalTime().ToString("o")
vmxIdentityDigest = $vmxIdentityDigest
childAbsent = $true
descendantCount = 0
leaseIdentityVerified = $true
mutexActive = $true
nextAction = "terminate_exact_stale_recover_parent"
secretValueRead = $false
}
Write-AtomicJson $intent $intentPath
$script:RuntimeWritePerformed = $true
$mutationAttempted = $true
$parentTaskkill = Invoke-ExactTaskkill $ExpectedParentPid
$parentTerminated = Wait-ExactProcessIdentityAbsent $ExpectedParentPid $ExpectedParentCreated 15
$parentDrained = $parentTerminated
} else {
$terminal = "parent_cleanup_identity_changed"
}
}
$mutexReleased = [bool](-not (Test-RecoveryMutexActive))
if ($parentDrained -and $mutexReleased) {
$leaseRecheck = $null
try { $leaseRecheck = Get-Content -LiteralPath $leasePath -Raw | ConvertFrom-Json } catch {}
if (
$leaseRecheck -and
[string]$leaseRecheck.runId -eq $ExpectedRunId -and
[int]$leaseRecheck.processId -eq $ExpectedParentPid -and
-not (Get-ExactProcess $ExpectedParentPid)
) {
$mutationAttempted = $true
Remove-Item -LiteralPath $leasePath -Force -ErrorAction Stop
$script:RuntimeWritePerformed = $true
}
$leaseMetadataRemoved = [bool](-not (Test-Path -LiteralPath $leasePath))
if ($leaseMetadataRemoved) { $terminal = "stale_recovery_released" } else { $terminal = "lease_metadata_cleanup_failed" }
}
}
} catch {
$operationFailureType = $_.Exception.GetType().Name
$terminal = if ($mutationAttempted) { "partial_write_failed_closed" } else { "apply_intent_or_precheck_failed_no_mutation" }
$failureReadback = Get-FailedMutationReadback $ExpectedParentPid $ExpectedParentCreated $ExpectedChildPid $ExpectedChildCreated $leasePath
$parentDrained = [bool]$failureReadback.parentAbsent
$childTerminated = [bool]$failureReadback.childAbsent
$mutexReleased = [bool]$failureReadback.recoveryMutexReleased
$leaseMetadataRemoved = [bool]$failureReadback.leaseRemoved
$postFailureReadbackErrors = @($failureReadback.errors)
}
}
$result = [pscustomobject]@{
schemaVersion = "agent99_stale_recovery_breakglass_v3"
timestamp = (Get-Date).ToString("o")
mode = $Mode
host = $env:COMPUTERNAME
expectedRunId = $ExpectedRunId
expectedParentPid = $ExpectedParentPid
expectedParentCreated = $ExpectedParentCreated.ToUniversalTime().ToString("o")
expectedChildPid = $ExpectedChildPid
expectedChildCreated = $ExpectedChildCreated.ToUniversalTime().ToString("o")
vmxIdentityDigest = $vmxIdentityDigest
minimumAgeMinutes = $MinimumAgeMinutes
observedParentAgeMinutes = $parentAgeMinutes
precheckPassed = $precheckPassed
blockers = @($preconditions)
priorReceiptRef = $PriorReceiptRef
priorReceiptVerified = $priorReceiptVerified
childAlreadyAbsent = $childAlreadyAbsent
childTerminated = $childTerminated
childTaskkillExitCode = if ($childTaskkill) { [int]$childTaskkill.exitCode } else { $null }
parentDrained = $parentDrained
parentTerminated = $parentTerminated
parentTaskkillExitCode = if ($parentTaskkill) { [int]$parentTaskkill.exitCode } else { $null }
operationIntentRef = $operationIntentRef
parentCleanupIntentRef = $parentCleanupIntentRef
mutationAttempted = $mutationAttempted
operationFailureType = $operationFailureType
postFailureReadbackComplete = [bool]($postFailureReadbackErrors.Count -eq 0)
postFailureReadbackErrors = @($postFailureReadbackErrors)
rollbackStatus = if ($mutationAttempted) { "not_reversible_process_termination_manual_verification_required" } else { "not_required_no_mutation" }
mutexReleased = $mutexReleased
leaseMetadataRemoved = $leaseMetadataRemoved
terminal = $terminal
runtimeWritePerformed = $script:RuntimeWritePerformed
vmPowerChangePerformed = $false
vmwareVmxProcessTerminated = $false
vmxLockDeleted = $false
hostRebootPerformed = $false
secretValueRead = $false
}
if ($Mode -eq "Apply") {
$stamp = Get-Date -Format "yyyyMMdd-HHmmss-fff"
$script:ReceiptPath = Join-Path $AgentRoot "evidence\agent99-stale-recovery-breakglass-$stamp-$([guid]::NewGuid().ToString('N').Substring(0, 8)).json"
Write-AtomicJson $result $script:ReceiptPath
$result | Add-Member -NotePropertyName receiptRef -NotePropertyValue (Split-Path -Leaf $script:ReceiptPath) -Force
}
} finally {
if ($reconcilerMutexAcquired -and $reconcilerMutex) {
try { $reconcilerMutex.ReleaseMutex() } catch {}
}
if ($reconcilerMutex) { $reconcilerMutex.Dispose() }
}
$result | ConvertTo-Json -Depth 8 -Compress
if (-not $precheckPassed) { exit 64 }
if ($Mode -eq "Check") { exit 0 }
if ($terminal -eq "stale_recovery_released") { exit 0 }
if ($terminal -eq "child_terminated_parent_draining") { exit 2 }
exit 70

View File

@@ -0,0 +1,345 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$ControlPlaneSource
)
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
function Get-ExactFunctionSource {
param([object]$Ast, [string]$Name)
$matches = @($Ast.FindAll({
param($node)
$node -is [Management.Automation.Language.FunctionDefinitionAst] -and $node.Name -eq $Name
}, $true))
if ($matches.Count -ne 1) { throw "function_identity_mismatch:$Name" }
[string]$matches[0].Extent.Text
}
function Assert-Replay {
param([bool]$Condition, [string]$Reason)
if (-not $Condition) { throw $Reason }
}
$tokens = $null
$errors = $null
$ast = [Management.Automation.Language.Parser]::ParseInput(
$ControlPlaneSource,
[ref]$tokens,
[ref]$errors
)
if ($errors.Count -ne 0) { throw "control_plane_parse_failed" }
foreach ($name in @(
"Get-AgentProcessDescendants",
"ConvertTo-AgentNativeProcessArgument",
"ConvertTo-AgentProcessIdentitySnapshot",
"Get-AgentCreationBoundedDescendants",
"Test-AgentProcessSnapshotMatch",
"Select-AgentProcessTreeIdentitySnapshot",
"Get-AgentProcessTreeIdentitySnapshot",
"Get-AgentProcessIdentityReadback",
"Test-AgentProcessIdentityActive",
"Wait-AgentProcessTreeIdentityAbsent",
"Stop-AgentBoundedProcessSnapshot",
"Read-AgentBoundedTextFile",
"Invoke-AgentBoundedProcess"
)) {
. ([ScriptBlock]::Create((Get-ExactFunctionSource $ast $name)))
}
$root = Join-Path $env:TEMP ("agent99-bounded-process-replay-" + [guid]::NewGuid().ToString("N"))
$normal = $null
$timeout = $null
$flood = $null
$rootOnly = $null
$rootOnlyChildIdentity = $null
$orphanRace = $null
$orphanRaceChildIdentity = $null
$pidReuseSelection = $null
$creationBoundSelection = $null
$stopReadbackFailure = $null
$stopReadbackFailureChildIdentity = $null
$cimFailure = $null
$cimFailureChildIdentity = $null
$cleanupVerified = $false
try {
New-Item -ItemType Directory -Path $root -Force | Out-Null
$powershell = Join-Path $env:SystemRoot "System32\WindowsPowerShell\v1.0\powershell.exe"
$cmd = Join-Path $env:SystemRoot "System32\cmd.exe"
$normal = Invoke-AgentBoundedProcess `
-FilePath $cmd `
-Arguments @("/d", "/c", "echo", "AG99_BOUNDED_OK") `
-TimeoutSeconds 5 `
-MaximumOutputBytes 4096
Assert-Replay ($normal.ok -and $normal.completed -and $normal.stdout -match "AG99_BOUNDED_OK") "normal_process_failed"
$childPath = Join-Path $root "child.ps1"
$parentPath = Join-Path $root "parent.ps1"
[IO.File]::WriteAllText($childPath, 'Start-Sleep -Seconds 30', (New-Object Text.UTF8Encoding($false)))
[IO.File]::WriteAllText($parentPath, @'
param([string]$ChildPath)
$powershell = Join-Path $env:SystemRoot "System32\WindowsPowerShell\v1.0\powershell.exe"
$arguments = "-NoProfile -NonInteractive -File `"$ChildPath`""
Start-Process -FilePath $powershell -ArgumentList $arguments -WindowStyle Hidden | Out-Null
Start-Sleep -Seconds 30
'@, (New-Object Text.UTF8Encoding($false)))
$timeout = Invoke-AgentBoundedProcess `
-FilePath $powershell `
-Arguments @("-NoProfile", "-NonInteractive", "-File", $parentPath, "-ChildPath", $childPath) `
-TimeoutSeconds 2 `
-MaximumOutputBytes 4096
Assert-Replay ($timeout.timedOut -and -not $timeout.completed) "timeout_not_detected"
Assert-Replay ($timeout.processTreeStopped -and -not $timeout.treeReadbackFailed) "timeout_tree_not_stopped"
Assert-Replay ([int]$timeout.killedIdentityCount -ge 2) "timeout_descendant_not_observed"
$floodPath = Join-Path $root "flood.ps1"
[IO.File]::WriteAllText($floodPath, @'
for ($index = 0; $index -lt 1000; $index++) {
[Console]::Out.Write(('X' * 1024))
}
Start-Sleep -Seconds 5
'@, (New-Object Text.UTF8Encoding($false)))
$flood = Invoke-AgentBoundedProcess `
-FilePath $powershell `
-Arguments @("-NoProfile", "-NonInteractive", "-File", $floodPath) `
-TimeoutSeconds 10 `
-MaximumOutputBytes 4096
Assert-Replay ($flood.outputLimitExceeded -and -not $flood.completed) "streaming_output_limit_not_detected"
Assert-Replay ([long]$flood.capturedBytes -le [long]$flood.maximumOutputBytes) "captured_output_exceeded_contract"
Assert-Replay ($flood.processTreeStopped -and -not $flood.treeReadbackFailed) "flood_tree_not_stopped"
$rootOnlyChildPidPath = Join-Path $root "root-only-child.pid"
$rootOnlyChildPath = Join-Path $root "root-only-child.ps1"
$rootOnlyParentPath = Join-Path $root "root-only-parent.ps1"
[IO.File]::WriteAllText($rootOnlyChildPath, @'
param([string]$PidPath)
[IO.File]::WriteAllText($PidPath, [string]$PID, (New-Object Text.UTF8Encoding($false)))
Start-Sleep -Seconds 30
'@, (New-Object Text.UTF8Encoding($false)))
[IO.File]::WriteAllText($rootOnlyParentPath, @'
param([string]$ChildPath, [string]$ChildPidPath)
$powershell = Join-Path $env:SystemRoot "System32\WindowsPowerShell\v1.0\powershell.exe"
$arguments = "-NoProfile -NonInteractive -File `"$ChildPath`" -PidPath `"$ChildPidPath`""
Start-Process -FilePath $powershell -ArgumentList $arguments -WindowStyle Hidden | Out-Null
Start-Sleep -Seconds 30
'@, (New-Object Text.UTF8Encoding($false)))
$rootOnly = Invoke-AgentBoundedProcess `
-FilePath $powershell `
-Arguments @("-NoProfile", "-NonInteractive", "-File", $rootOnlyParentPath, "-ChildPath", $rootOnlyChildPath, "-ChildPidPath", $rootOnlyChildPidPath) `
-TimeoutSeconds 2 `
-MaximumOutputBytes 4096 `
-TerminationScope Root
Assert-Replay ($rootOnly.timedOut -and $rootOnly.forcedTerminationPerformed) "root_only_timeout_not_detected"
Assert-Replay ($rootOnly.processScopeStopped -and -not $rootOnly.processTreeStopped) "root_only_scope_not_preserved"
Assert-Replay ([int]$rootOnly.preservedDescendantCount -ge 1) "root_only_descendant_not_preserved"
Assert-Replay (Test-Path -LiteralPath $rootOnlyChildPidPath -PathType Leaf) "root_only_child_pid_missing"
$rootOnlyChildPid = [int]([IO.File]::ReadAllText($rootOnlyChildPidPath))
$rootOnlyChildProcess = Get-CimInstance Win32_Process -Filter "ProcessId = $rootOnlyChildPid" -ErrorAction Stop
$rootOnlyChildIdentity = [pscustomobject]@{
processId = [int]$rootOnlyChildProcess.ProcessId
parentProcessId = [int]$rootOnlyChildProcess.ParentProcessId
creationUtc = ([datetime]$rootOnlyChildProcess.CreationDate).ToUniversalTime().ToString("o")
name = [string]$rootOnlyChildProcess.Name
}
Assert-Replay (Test-AgentProcessIdentityActive $rootOnlyChildIdentity) "root_only_child_not_active_after_parent_stop"
$orphanRaceChildPidPath = Join-Path $root "orphan-race-child.pid"
$orphanRaceParentPath = Join-Path $root "orphan-race-parent.ps1"
[IO.File]::WriteAllText($orphanRaceParentPath, @'
param([string]$ChildPath, [string]$ChildPidPath)
$powershell = Join-Path $env:SystemRoot "System32\WindowsPowerShell\v1.0\powershell.exe"
$arguments = "-NoProfile -NonInteractive -File `"$ChildPath`" -PidPath `"$ChildPidPath`""
Start-Process -FilePath $powershell -ArgumentList $arguments -WindowStyle Hidden | Out-Null
$stopwatch = [Diagnostics.Stopwatch]::StartNew()
while (-not (Test-Path -LiteralPath $ChildPidPath) -and $stopwatch.Elapsed.TotalSeconds -lt 5) {
Start-Sleep -Milliseconds 50
}
Start-Sleep -Seconds 1
'@, (New-Object Text.UTF8Encoding($false)))
$orphanStartInfo = New-Object Diagnostics.ProcessStartInfo
$orphanStartInfo.FileName = $powershell
$orphanStartInfo.Arguments = "-NoProfile -NonInteractive -File `"$orphanRaceParentPath`" -ChildPath `"$rootOnlyChildPath`" -ChildPidPath `"$orphanRaceChildPidPath`""
$orphanStartInfo.UseShellExecute = $false
$orphanStartInfo.CreateNoWindow = $true
$orphanRoot = New-Object Diagnostics.Process
$orphanRoot.StartInfo = $orphanStartInfo
Assert-Replay $orphanRoot.Start() "orphan_race_parent_start_failed"
$orphanRootInfo = Get-CimInstance Win32_Process -Filter "ProcessId = $([int]$orphanRoot.Id)" -ErrorAction Stop | Select-Object -First 1
$orphanRootIdentity = ConvertTo-AgentProcessIdentitySnapshot $orphanRootInfo
Assert-Replay ([bool]$orphanRootIdentity) "orphan_race_root_identity_capture_failed"
Assert-Replay $orphanRoot.WaitForExit(8000) "orphan_race_parent_did_not_exit"
Assert-Replay (Test-Path -LiteralPath $orphanRaceChildPidPath -PathType Leaf) "orphan_race_child_pid_missing"
$orphanIdentities = @(Get-AgentProcessTreeIdentitySnapshot $orphanRootIdentity $orphanRoot.ExitTime.ToUniversalTime())
Assert-Replay ($orphanIdentities.Count -ge 1) "orphan_race_descendant_snapshot_missing"
Assert-Replay (@($orphanIdentities | Where-Object { [int]$_.processId -eq [int]$orphanRoot.Id }).Count -eq 0) "orphan_race_root_unexpectedly_present"
$orphanRaceChildIdentity = @($orphanIdentities | Select-Object -First 1)[0]
$orphanRace = Stop-AgentBoundedProcessSnapshot $orphanRoot $orphanIdentities "Tree"
Assert-Replay ($orphanRace.processTreeStopped -and $orphanRace.processScopeStopped) "orphan_race_tree_false_green"
Assert-Replay (-not (Test-AgentProcessIdentityActive $orphanRaceChildIdentity)) "orphan_race_child_still_active"
$pidReuseRootCreated = [datetime]::UtcNow.AddMinutes(-5)
$pidReuseRootExited = $pidReuseRootCreated.AddSeconds(2)
$pidReuseExpectedRoot = [pscustomobject]@{
processId = 9001
parentProcessId = 7000
creationUtc = $pidReuseRootCreated.ToString("o")
name = "powershell.exe"
}
$pidReuseProcesses = @(
[pscustomobject]@{ ProcessId = 9001; ParentProcessId = 8000; CreationDate = $pidReuseRootExited.AddSeconds(1); Name = "powershell.exe" },
[pscustomobject]@{ ProcessId = 9002; ParentProcessId = 9001; CreationDate = $pidReuseRootCreated.AddSeconds(1); Name = "cmd.exe" },
[pscustomobject]@{ ProcessId = 9003; ParentProcessId = 9002; CreationDate = $pidReuseRootExited.AddSeconds(1); Name = "conhost.exe" },
[pscustomobject]@{ ProcessId = 9004; ParentProcessId = 9001; CreationDate = $pidReuseRootExited.AddSeconds(2); Name = "not-agent99.exe" },
[pscustomobject]@{ ProcessId = 9005; ParentProcessId = 9002; CreationDate = $pidReuseRootCreated.AddMilliseconds(500); Name = "older-than-parent.exe" }
)
$pidReuseSelection = @(Select-AgentProcessTreeIdentitySnapshot $pidReuseProcesses $pidReuseExpectedRoot $pidReuseRootExited)
$pidReuseSelectedIds = @($pidReuseSelection | ForEach-Object { [int]$_.processId } | Sort-Object)
Assert-Replay (($pidReuseSelectedIds -join ",") -eq "9002,9003") "pid_reuse_selection_not_identity_bounded"
$liveRootCreated = [datetime]::UtcNow.AddMinutes(-1)
$liveRootIdentity = [pscustomobject]@{ processId = 9101; parentProcessId = 7000; creationUtc = $liveRootCreated.ToString("o"); name = "powershell.exe" }
$liveRootProcesses = @(
[pscustomobject]@{ ProcessId = 9101; ParentProcessId = 7000; CreationDate = $liveRootCreated; Name = "powershell.exe" },
[pscustomobject]@{ ProcessId = 9102; ParentProcessId = 9101; CreationDate = $liveRootCreated.AddSeconds(1); Name = "valid-child.exe" },
[pscustomobject]@{ ProcessId = 9103; ParentProcessId = 9101; CreationDate = $liveRootCreated.AddSeconds(-1); Name = "older-unrelated.exe" }
)
$creationBoundSelection = @(Select-AgentProcessTreeIdentitySnapshot $liveRootProcesses $liveRootIdentity ([datetime]::MinValue))
$creationBoundIds = @($creationBoundSelection | ForEach-Object { [int]$_.processId } | Sort-Object)
Assert-Replay (($creationBoundIds -join ",") -eq "9101,9102") "live_root_child_creation_bound_failed"
$stopReadbackChildPidPath = Join-Path $root "stop-readback-failure-child.pid"
function Get-AgentProcessIdentityReadback { [pscustomobject]@{ known = $false; active = $false; reason = "synthetic_stop_readback_failure" } }
$stopReadbackFailure = Invoke-AgentBoundedProcess `
-FilePath $powershell `
-Arguments @("-NoProfile", "-NonInteractive", "-File", $rootOnlyParentPath, "-ChildPath", $rootOnlyChildPath, "-ChildPidPath", $stopReadbackChildPidPath) `
-TimeoutSeconds 2 `
-MaximumOutputBytes 4096
. ([ScriptBlock]::Create((Get-ExactFunctionSource $ast "Get-AgentProcessIdentityReadback")))
Assert-Replay ($stopReadbackFailure.timedOut -and $stopReadbackFailure.identityReadbackFailed) "stop_readback_failure_not_detected"
Assert-Replay ($stopReadbackFailure.treeReadbackFailed -and $stopReadbackFailure.rootFallbackTerminationUsed) "stop_readback_failure_not_fail_closed"
Assert-Replay ($stopReadbackFailure.processScopeStopped -and -not $stopReadbackFailure.processTreeStopped) "stop_readback_failure_tree_false_green"
Assert-Replay (Test-Path -LiteralPath $stopReadbackChildPidPath -PathType Leaf) "stop_readback_failure_child_pid_missing"
$stopReadbackChildPid = [int]([IO.File]::ReadAllText($stopReadbackChildPidPath))
$stopReadbackChildProcess = Get-CimInstance Win32_Process -Filter "ProcessId = $stopReadbackChildPid" -ErrorAction Stop
$stopReadbackFailureChildIdentity = ConvertTo-AgentProcessIdentitySnapshot $stopReadbackChildProcess
Assert-Replay (Test-AgentProcessIdentityActive $stopReadbackFailureChildIdentity) "stop_readback_failure_child_unexpectedly_stopped"
$cimFailureChildPidPath = Join-Path $root "cim-failure-child.pid"
function Get-AgentProcessTreeIdentitySnapshot { throw "synthetic_cim_failure" }
$cimFailure = Invoke-AgentBoundedProcess `
-FilePath $powershell `
-Arguments @("-NoProfile", "-NonInteractive", "-File", $rootOnlyParentPath, "-ChildPath", $rootOnlyChildPath, "-ChildPidPath", $cimFailureChildPidPath) `
-TimeoutSeconds 2 `
-MaximumOutputBytes 4096
. ([ScriptBlock]::Create((Get-ExactFunctionSource $ast "Get-AgentProcessTreeIdentitySnapshot")))
. ([ScriptBlock]::Create((Get-ExactFunctionSource $ast "Get-AgentProcessIdentityReadback")))
Assert-Replay ($cimFailure.timedOut -and $cimFailure.treeReadbackFailed) "cim_failure_not_detected"
Assert-Replay ($cimFailure.rootFallbackTerminationUsed -and $cimFailure.processScopeStopped) "cim_failure_root_not_stopped"
Assert-Replay (-not $cimFailure.processTreeStopped) "cim_failure_tree_false_green"
Assert-Replay (Test-Path -LiteralPath $cimFailureChildPidPath -PathType Leaf) "cim_failure_child_pid_missing"
$cimFailureChildPid = [int]([IO.File]::ReadAllText($cimFailureChildPidPath))
$cimFailureChildProcess = Get-CimInstance Win32_Process -Filter "ProcessId = $cimFailureChildPid" -ErrorAction Stop
$cimFailureChildIdentity = [pscustomobject]@{
processId = [int]$cimFailureChildProcess.ProcessId
parentProcessId = [int]$cimFailureChildProcess.ParentProcessId
creationUtc = ([datetime]$cimFailureChildProcess.CreationDate).ToUniversalTime().ToString("o")
name = [string]$cimFailureChildProcess.Name
}
Assert-Replay (Test-AgentProcessIdentityActive $cimFailureChildIdentity) "cim_failure_child_unexpectedly_stopped"
} finally {
. ([ScriptBlock]::Create((Get-ExactFunctionSource $ast "Get-AgentProcessTreeIdentitySnapshot")))
if ($rootOnlyChildIdentity -and (Test-AgentProcessIdentityActive $rootOnlyChildIdentity)) {
& "$env:SystemRoot\System32\taskkill.exe" /PID ([int]$rootOnlyChildIdentity.processId) /T /F 2>&1 | Out-Null
[void](Wait-AgentProcessTreeIdentityAbsent @($rootOnlyChildIdentity) 8)
}
if ($cimFailureChildIdentity -and (Test-AgentProcessIdentityActive $cimFailureChildIdentity)) {
& "$env:SystemRoot\System32\taskkill.exe" /PID ([int]$cimFailureChildIdentity.processId) /T /F 2>&1 | Out-Null
[void](Wait-AgentProcessTreeIdentityAbsent @($cimFailureChildIdentity) 8)
}
if ($orphanRaceChildIdentity -and (Test-AgentProcessIdentityActive $orphanRaceChildIdentity)) {
& "$env:SystemRoot\System32\taskkill.exe" /PID ([int]$orphanRaceChildIdentity.processId) /T /F 2>&1 | Out-Null
[void](Wait-AgentProcessTreeIdentityAbsent @($orphanRaceChildIdentity) 8)
}
if ($stopReadbackFailureChildIdentity -and (Test-AgentProcessIdentityActive $stopReadbackFailureChildIdentity)) {
& "$env:SystemRoot\System32\taskkill.exe" /PID ([int]$stopReadbackFailureChildIdentity.processId) /T /F 2>&1 | Out-Null
[void](Wait-AgentProcessTreeIdentityAbsent @($stopReadbackFailureChildIdentity) 8)
}
Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue
$cleanupVerified = [bool](
-not (Test-Path -LiteralPath $root) -and
(-not $rootOnlyChildIdentity -or -not (Test-AgentProcessIdentityActive $rootOnlyChildIdentity)) -and
(-not $orphanRaceChildIdentity -or -not (Test-AgentProcessIdentityActive $orphanRaceChildIdentity)) -and
(-not $stopReadbackFailureChildIdentity -or -not (Test-AgentProcessIdentityActive $stopReadbackFailureChildIdentity)) -and
(-not $cimFailureChildIdentity -or -not (Test-AgentProcessIdentityActive $cimFailureChildIdentity))
)
}
[pscustomobject]@{
schemaVersion = "agent99_bounded_process_replay_v2"
ok = [bool]($normal.ok -and $timeout.timedOut -and $timeout.processTreeStopped -and $flood.outputLimitExceeded -and $flood.processTreeStopped -and $rootOnly.timedOut -and $rootOnly.processScopeStopped -and -not $rootOnly.processTreeStopped -and $orphanRace.processTreeStopped -and $pidReuseSelection.Count -eq 2 -and $creationBoundSelection.Count -eq 2 -and $stopReadbackFailure.identityReadbackFailed -and -not $stopReadbackFailure.processTreeStopped -and $cimFailure.timedOut -and $cimFailure.treeReadbackFailed -and $cimFailure.rootFallbackTerminationUsed -and $cimFailure.processScopeStopped -and -not $cimFailure.processTreeStopped -and $cleanupVerified)
normal = [pscustomobject]@{
exitCode = [int]$normal.exitCode
completed = [bool]$normal.completed
}
timeout = [pscustomobject]@{
exitCode = [int]$timeout.exitCode
timedOut = [bool]$timeout.timedOut
processTreeStopped = [bool]$timeout.processTreeStopped
killedIdentityCount = [int]$timeout.killedIdentityCount
}
flood = [pscustomobject]@{
exitCode = [int]$flood.exitCode
outputLimitExceeded = [bool]$flood.outputLimitExceeded
capturedBytes = [long]$flood.capturedBytes
maximumOutputBytes = [long]$flood.maximumOutputBytes
processTreeStopped = [bool]$flood.processTreeStopped
}
rootOnly = [pscustomobject]@{
exitCode = [int]$rootOnly.exitCode
timedOut = [bool]$rootOnly.timedOut
terminationScope = [string]$rootOnly.terminationScope
processScopeStopped = [bool]$rootOnly.processScopeStopped
processTreeStopped = [bool]$rootOnly.processTreeStopped
preservedDescendantCount = [int]$rootOnly.preservedDescendantCount
}
orphanRace = [pscustomobject]@{
rootExitedBeforeSnapshot = $true
descendantSnapshotCount = [int]$orphanIdentities.Count
processScopeStopped = [bool]$orphanRace.processScopeStopped
processTreeStopped = [bool]$orphanRace.processTreeStopped
}
pidReuseSelection = [pscustomobject]@{
reusedRootExcluded = [bool](@($pidReuseSelection | Where-Object { [int]$_.processId -eq 9001 }).Count -eq 0)
originalChildIncluded = [bool](@($pidReuseSelection | Where-Object { [int]$_.processId -eq 9002 }).Count -eq 1)
originalGrandchildIncluded = [bool](@($pidReuseSelection | Where-Object { [int]$_.processId -eq 9003 }).Count -eq 1)
reusedRootChildExcluded = [bool](@($pidReuseSelection | Where-Object { [int]$_.processId -eq 9004 }).Count -eq 0)
intermediateOlderProcessExcluded = [bool](@($pidReuseSelection | Where-Object { [int]$_.processId -eq 9005 }).Count -eq 0)
}
creationBounds = [pscustomobject]@{
liveRootIncluded = [bool](@($creationBoundSelection | Where-Object { [int]$_.processId -eq 9101 }).Count -eq 1)
validChildIncluded = [bool](@($creationBoundSelection | Where-Object { [int]$_.processId -eq 9102 }).Count -eq 1)
olderUnrelatedChildExcluded = [bool](@($creationBoundSelection | Where-Object { [int]$_.processId -eq 9103 }).Count -eq 0)
}
stopReadbackFailure = [pscustomobject]@{
identityReadbackFailed = [bool]$stopReadbackFailure.identityReadbackFailed
treeReadbackFailed = [bool]$stopReadbackFailure.treeReadbackFailed
rootFallbackTerminationUsed = [bool]$stopReadbackFailure.rootFallbackTerminationUsed
processScopeStopped = [bool]$stopReadbackFailure.processScopeStopped
processTreeStopped = [bool]$stopReadbackFailure.processTreeStopped
}
cimFailure = [pscustomobject]@{
exitCode = [int]$cimFailure.exitCode
timedOut = [bool]$cimFailure.timedOut
treeReadbackFailed = [bool]$cimFailure.treeReadbackFailed
rootFallbackTerminationUsed = [bool]$cimFailure.rootFallbackTerminationUsed
processScopeStopped = [bool]$cimFailure.processScopeStopped
processTreeStopped = [bool]$cimFailure.processTreeStopped
}
cleanupVerified = $cleanupVerified
productionPathWritePerformed = $false
vmPowerChangePerformed = $false
hostRebootPerformed = $false
secretValuesRead = $false
} | ConvertTo-Json -Compress -Depth 5

View File

@@ -0,0 +1,55 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$BreakglassSource
)
$ErrorActionPreference = "Stop"
$tokens = $null
$errors = $null
$ast = [Management.Automation.Language.Parser]::ParseInput(
$BreakglassSource,
[ref]$tokens,
[ref]$errors
)
if ($errors.Count -ne 0) { throw "breakglass_parse_failed" }
$matches = @($ast.FindAll({
param($node)
$node -is [Management.Automation.Language.FunctionDefinitionAst] -and
$node.Name -eq "Get-FailedMutationReadback"
}, $true))
if ($matches.Count -ne 1) { throw "failure_readback_function_identity_mismatch" }
. ([ScriptBlock]::Create([string]$matches[0].Extent.Text))
function Wait-ExactProcessIdentityAbsent { throw "synthetic_cim_failure" }
function Test-RecoveryMutexActive { throw "synthetic_mutex_readback_failure" }
function Test-Path { param([string]$LiteralPath); throw "synthetic_filesystem_readback_failure" }
$readback = Get-FailedMutationReadback `
1001 ([datetime]::UtcNow.AddHours(-1)) `
1002 ([datetime]::UtcNow.AddHours(-1)) `
"C:\synthetic\recovery-transport-lease.json"
$expected = @(
"parent_identity_unknown",
"child_identity_unknown",
"recovery_mutex_unknown",
"lease_metadata_unknown"
)
if ($readback.complete) { throw "failure_readback_false_green" }
if ($readback.parentAbsent -or $readback.childAbsent -or $readback.recoveryMutexReleased -or $readback.leaseRemoved) {
throw "failure_readback_unknown_projected_true"
}
if ((Compare-Object $expected @($readback.errors)).Count -ne 0) { throw "failure_readback_errors_incomplete" }
[pscustomobject]@{
schemaVersion = "agent99_breakglass_failure_readback_replay_v1"
ok = $true
complete = [bool]$readback.complete
errors = @($readback.errors)
terminalReceiptConstructionCanContinue = $true
runtimeWritePerformed = $false
processTerminationPerformed = $false
vmPowerChangePerformed = $false
hostRebootPerformed = $false
secretValuesRead = $false
} | ConvertTo-Json -Compress -Depth 4

View File

@@ -0,0 +1,160 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$TelegramInboxSource,
[Parameter(Mandatory = $true)]
[string]$SubmitRequestSource
)
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
function Assert-Replay {
param([bool]$Condition, [string]$Reason)
if (-not $Condition) { throw $Reason }
}
function Invoke-SyntheticCase {
param(
[string]$Name,
[string]$Text
)
$root = Join-Path $env:TEMP ("agent99-telegram-ingress-replay-" + $Name + "-" + [guid]::NewGuid().ToString("N"))
$bin = Join-Path $root "bin"
$config = Join-Path $root "config"
$telegramPath = Join-Path $bin "agent99-telegram-inbox.ps1"
$submitPath = Join-Path $bin "agent99-submit-request.ps1"
$stdoutPath = Join-Path $root "telegram.out"
$stderrPath = Join-Path $root "telegram.err"
try {
New-Item -ItemType Directory -Path $bin, $config -Force | Out-Null
$encoding = New-Object Text.UTF8Encoding($true)
[IO.File]::WriteAllText($telegramPath, $TelegramInboxSource, $encoding)
[IO.File]::WriteAllText($submitPath, $SubmitRequestSource, $encoding)
[IO.File]::WriteAllText(
(Join-Path $config "agent99.config.json"),
'{"telegram":{"autoIngestMonitoringAlerts":true}}',
(New-Object Text.UTF8Encoding($false))
)
$powershell = Join-Path $env:SystemRoot "System32\WindowsPowerShell\v1.0\powershell.exe"
$output = @(& $powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File $telegramPath -AgentRoot $root -SyntheticUpdateText $Text -SyntheticChatId "synthetic-chat" 2>$stderrPath)
$exitCode = [int]$LASTEXITCODE
[IO.File]::WriteAllLines($stdoutPath, @($output | ForEach-Object { [string]$_ }), (New-Object Text.UTF8Encoding($false)))
$payload = (($output | ForEach-Object { [string]$_ }) -join "`n") | ConvertFrom-Json
$queueFiles = @(Get-ChildItem -LiteralPath (Join-Path $root "queue") -File -Filter "*.json" -ErrorAction SilentlyContinue)
$evidenceFiles = @(Get-ChildItem -LiteralPath (Join-Path $root "evidence") -File -Filter "agent99-TelegramInbox-*.json" -ErrorAction SilentlyContinue)
$submitOutFile = @(Get-ChildItem -LiteralPath (Join-Path $root "evidence") -File -Filter "agent99-telegram-submit-*.out" -ErrorAction SilentlyContinue | Select-Object -First 1)
$submitErrFile = @(Get-ChildItem -LiteralPath (Join-Path $root "evidence") -File -Filter "agent99-telegram-submit-*.err" -ErrorAction SilentlyContinue | Select-Object -First 1)
[pscustomobject]@{
name = $Name
exitCode = $exitCode
ok = [bool]$payload.ok
reason = [string]$payload.reason
processedCount = [int]$payload.processedCount
failedUpdateCount = [int]$payload.failedUpdateCount
durableQueued = [bool](@($payload.processed | Where-Object { $_.durableQueued }).Count -gt 0)
submitExitCode = if (@($payload.processed).Count -gt 0) { [int]$payload.processed[0].submitExitCode } else { $null }
failureReason = if (@($payload.processed).Count -gt 0) { [string]$payload.processed[0].failureReason } else { "" }
submitStdoutBytes = if ($submitOutFile.Count -eq 1) { [long]$submitOutFile[0].Length } else { 0 }
submitStderrBytes = if ($submitErrFile.Count -eq 1) { [long]$submitErrFile[0].Length } else { 0 }
offsetBefore = [int64]$payload.offsetBefore
offsetAfter = [int64]$payload.offsetAfter
queueCount = $queueFiles.Count
evidenceCount = $evidenceFiles.Count
offsetFilePresent = Test-Path -LiteralPath (Join-Path $root "state\telegram-inbox-offset.json")
stderrPresent = [bool]((Test-Path -LiteralPath $stderrPath) -and (Get-Item -LiteralPath $stderrPath).Length -gt 0)
}
} finally {
Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue
if (Test-Path -LiteralPath $root) { throw "synthetic_case_cleanup_failed:$Name" }
}
}
function Invoke-QueueCommitReplayCase {
$root = Join-Path $env:TEMP ("agent99-telegram-ingress-replay-reentry-" + [guid]::NewGuid().ToString("N"))
$bin = Join-Path $root "bin"
$config = Join-Path $root "config"
$telegramPath = Join-Path $bin "agent99-telegram-inbox.ps1"
$submitPath = Join-Path $bin "agent99-submit-request.ps1"
$fixedUpdateId = [int64]1784464999
$instruction = "/agent99 status readback"
$needle = '$submitReceipt = Write-AgentDurableJson $result $submitEvidence $id $ExternalId'
try {
New-Item -ItemType Directory -Path $bin, $config -Force | Out-Null
$encoding = New-Object Text.UTF8Encoding($true)
[IO.File]::WriteAllText($telegramPath, $TelegramInboxSource, $encoding)
[IO.File]::WriteAllText(
(Join-Path $config "agent99.config.json"),
'{"telegram":{"autoIngestMonitoringAlerts":true}}',
(New-Object Text.UTF8Encoding($false))
)
Assert-Replay $SubmitRequestSource.Contains($needle) "queue_commit_fault_marker_missing"
$faultedSubmit = $SubmitRequestSource.Replace($needle, ('throw "synthetic_after_queue_commit"' + "`r`n" + $needle))
[IO.File]::WriteAllText($submitPath, $faultedSubmit, $encoding)
$powershell = Join-Path $env:SystemRoot "System32\WindowsPowerShell\v1.0\powershell.exe"
$firstOutput = @(& $powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File $telegramPath -AgentRoot $root -SyntheticUpdateText $instruction -SyntheticUpdateId $fixedUpdateId -SyntheticChatId "synthetic-chat" 2>$null)
$firstExit = [int]$LASTEXITCODE
$firstPayload = (($firstOutput | ForEach-Object { [string]$_ }) -join "`n") | ConvertFrom-Json
$firstQueue = @(Get-ChildItem -LiteralPath (Join-Path $root "queue") -File -Filter "*.json" -ErrorAction SilentlyContinue)
$firstRequests = @(Get-ChildItem -LiteralPath (Join-Path $root "requests") -File -Filter "*.json" -ErrorAction SilentlyContinue)
Assert-Replay ($firstExit -ne 0 -and -not [bool]$firstPayload.ok) "queue_commit_fault_false_green"
Assert-Replay ($firstQueue.Count -eq 1 -and $firstRequests.Count -eq 1) "queue_commit_fault_artifact_count_invalid"
$firstQueuePayload = Get-Content -LiteralPath $firstQueue[0].FullName -Raw | ConvertFrom-Json
[IO.File]::WriteAllText($submitPath, $SubmitRequestSource, $encoding)
$secondOutput = @(& $powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File $telegramPath -AgentRoot $root -SyntheticUpdateText $instruction -SyntheticUpdateId $fixedUpdateId -SyntheticChatId "synthetic-chat" 2>$null)
$secondExit = [int]$LASTEXITCODE
$secondPayload = (($secondOutput | ForEach-Object { [string]$_ }) -join "`n") | ConvertFrom-Json
$secondQueue = @(Get-ChildItem -LiteralPath (Join-Path $root "queue") -File -Filter "*.json" -ErrorAction SilentlyContinue)
$secondRequests = @(Get-ChildItem -LiteralPath (Join-Path $root "requests") -File -Filter "*.json" -ErrorAction SilentlyContinue)
$processed = @($secondPayload.processed)
Assert-Replay ($secondExit -eq 0 -and [bool]$secondPayload.ok) "queue_commit_retry_failed"
Assert-Replay ($secondQueue.Count -eq 1 -and $secondRequests.Count -eq 1) "queue_commit_retry_duplicated_artifact"
Assert-Replay ($processed.Count -eq 1 -and [bool]$processed[0].durableQueued -and [bool]$processed[0].idempotentReplay) "queue_commit_retry_not_idempotent"
Assert-Replay ([string]$processed[0].queueId -eq [string]$firstQueuePayload.id) "queue_commit_retry_identity_changed"
[pscustomobject]@{
firstExitCode = $firstExit
firstReason = [string]$firstPayload.reason
secondExitCode = $secondExit
secondReason = [string]$secondPayload.reason
requestId = [string]$firstQueuePayload.id
queueCountAfterRetry = $secondQueue.Count
requestCountAfterRetry = $secondRequests.Count
idempotentReplay = [bool]$processed[0].idempotentReplay
offsetBefore = [int64]$secondPayload.offsetBefore
offsetAfter = [int64]$secondPayload.offsetAfter
}
} finally {
Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue
if (Test-Path -LiteralPath $root) { throw "queue_commit_replay_cleanup_failed" }
}
}
$success = Invoke-SyntheticCase "success" "/agent99 status readback"
$failure = Invoke-SyntheticCase "enqueue-failure" ("/agent99 " + ("A" * 9000))
$queueCommitReplay = Invoke-QueueCommitReplayCase
Assert-Replay ($success.exitCode -eq 0 -and $success.ok) ("success_case_failed:" + ($success | ConvertTo-Json -Compress))
Assert-Replay ($success.processedCount -eq 1 -and $success.durableQueued -and $success.queueCount -eq 1) "success_queue_not_durable"
Assert-Replay ($success.evidenceCount -eq 1 -and -not $success.offsetFilePresent) "success_evidence_or_synthetic_offset_invalid"
Assert-Replay ($failure.exitCode -ne 0 -and -not $failure.ok) ("failure_case_false_green:" + ($failure | ConvertTo-Json -Compress))
Assert-Replay ($failure.reason -eq "update_processing_failed_no_checkpoint_past_failure") "failure_reason_mismatch"
Assert-Replay ($failure.failedUpdateCount -eq 1 -and $failure.queueCount -eq 0) "failure_queue_or_count_invalid"
Assert-Replay ($failure.offsetBefore -eq $failure.offsetAfter -and -not $failure.offsetFilePresent) "failure_offset_advanced"
Assert-Replay ($failure.evidenceCount -eq 1) "failure_evidence_missing"
[pscustomobject]@{
schemaVersion = "agent99_telegram_durable_ingress_replay_v1"
ok = $true
success = $success
enqueueFailure = $failure
queueCommitReplay = $queueCommitReplay
cleanupVerified = $true
productionPathWritePerformed = $false
scheduledTaskStarted = $false
telegramApiCalled = $false
secretValuesRead = $false
} | ConvertTo-Json -Compress -Depth 6

View File

@@ -0,0 +1,276 @@
from pathlib import Path
ROOT = Path(__file__).resolve().parents[3]
CONTROL = ROOT / "agent99-control-plane.ps1"
BREAKGLASS = ROOT / "scripts" / "reboot-recovery" / "agent99-stale-recovery-breakglass.ps1"
MAINTENANCE = ROOT / "scripts" / "reboot-recovery" / "agent99-control-loop-maintenance.ps1"
REGISTER_TASKS = ROOT / "agent99-register-tasks.ps1"
SRE_INBOX = ROOT / "agent99-sre-alert-inbox.ps1"
TELEGRAM_INBOX = ROOT / "agent99-telegram-inbox.ps1"
SUBMIT_REQUEST = ROOT / "agent99-submit-request.ps1"
BOUNDED_REPLAY = ROOT / "scripts" / "reboot-recovery" / "tests" / "agent99-bounded-process-replay.ps1"
TELEGRAM_REPLAY = ROOT / "scripts" / "reboot-recovery" / "tests" / "agent99-telegram-durable-ingress-replay.ps1"
BREAKGLASS_REPLAY = ROOT / "scripts" / "reboot-recovery" / "tests" / "agent99-breakglass-failure-readback-replay.ps1"
def test_vmrun_start_is_process_aware_and_bounded() -> None:
source = CONTROL.read_text(encoding="utf-8")
assert "function Get-AgentVmwareVmxProcessReadback" in source
assert "vmx_process_already_running" in source
assert "vmx_process_readback_failed" in source
assert "function Invoke-AgentBoundedVmrunStart" in source
assert "Get-AgentVmwareVmxProcessReadback ([string]$vm.vmx)" in source
assert "Invoke-AgentBoundedVmrunStart" in source
assert '& $vmrun start $vm.vmx nogui' not in source
def test_bounded_runner_kills_trees_but_vmrun_preserves_started_vm_descendants() -> None:
source = CONTROL.read_text(encoding="utf-8")
function = source.split("function Invoke-AgentBoundedProcess", 1)[1].split(
"function Test-PublicRoutes", 1
)[0]
assert "$stopwatch.Elapsed.TotalSeconds -ge $TimeoutSeconds" in function
assert "StandardOutput.ReadAsync" in function
assert "StandardError.ReadAsync" in function
assert "($capturedBytes + $chunkBytes) -gt $MaximumOutputBytes" in function
assert function.index("($capturedBytes + $chunkBytes) -gt $MaximumOutputBytes") < function.index("$process.WaitForExit()")
assert '[ValidateSet("Tree", "Root")]' in function
assert "processScopeStopped" in function
assert "preservedDescendantCount" in function
assert "Get-AgentProcessTreeIdentitySnapshot $rootProcessIdentity $rootExitedUtc" in function
assert "ConvertTo-AgentProcessIdentitySnapshot $rootProcessInfo" in function
assert "rootIdentityCaptured" in function
assert "Stop-AgentBoundedProcessSnapshot $process $treeIdentities $TerminationScope" in function
assert "processTreeStopped" in function
assert "Read-AgentBoundedTextFile $stdout" in function
assert "Remove-Item -LiteralPath $stdout, $stderr" in function
stopper = source.split("function Stop-AgentBoundedProcessSnapshot", 1)[1].split(
"function Test-AgentProcessIdentityActive", 1
)[0]
assert 'taskkill.exe" /PID ([string]$rootId) /T /F' in stopper
assert 'taskkill.exe" /PID ([string]$rootId) /F' in stopper
assert 'taskkill.exe" /PID ([string]([int]$identity.processId)) /T /F' in stopper
snapshot = source.split("function Select-AgentProcessTreeIdentitySnapshot", 1)[1].split(
"function Get-AgentProcessTreeIdentitySnapshot", 1
)[0]
assert "Test-AgentProcessSnapshotMatch $RootIdentity $currentRoot" in snapshot
assert 'throw "bounded_process_root_identity_changed_before_exit_readback"' in snapshot
assert "-le $RootExitedUtc.ToUniversalTime()" in snapshot
assert "Get-AgentCreationBoundedDescendants $Processes @($currentRoot)" in snapshot
descendants = source.split("function Get-AgentCreationBoundedDescendants", 1)[1].split(
"function Test-AgentProcessSnapshotMatch", 1
)[0]
assert "-ge $parentCreated" in descendants
assert "System.Collections.Generic.HashSet[int]" in descendants
assert "function Get-AgentProcessIdentityReadback" in source
assert 'reason = "identity_readback_failed"' in source
assert 'throw "process_tree_identity_readback_failed"' in source
assert "identityReadbackFailed" in stopper
vmrun = source.split("function Invoke-AgentBoundedVmrunStart", 1)[1].split(
"function Start-ConfiguredVMs", 1
)[0]
assert "Invoke-AgentBoundedProcess" in vmrun
assert "-TerminationScope Root" in vmrun
assert "processScopeStopped" in vmrun
def test_perf_guard_can_release_an_exact_stale_recovery_before_deferral() -> None:
source = CONTROL.read_text(encoding="utf-8")
assert "function Invoke-AgentStaleRecoveryTransportGuard" in source
assert 'if ($Mode -in @("Perf", "LoadShed") -and $ControlledApply)' in source
invocation = source.index("$staleRecoveryTransportGuard = Invoke-AgentStaleRecoveryTransportGuard")
lease_probe = source.index("$recoveryLeaseObservation = Test-AgentRecoveryTransportLease", invocation)
assert invocation < lease_probe
assert "agent99_stale_recovery_guard_intent_v1" in source
assert "agent99_stale_recovery_guard_receipt_v1" in source
assert "$stream.Flush($true)" in source
assert "stale_recovery_receipt_readback_mismatch" in source
assert "stale_recovery_descendant_identity_unapproved" in source
assert "Get-AgentProcessDescendants $recheckAll ([int]$lease.processId)" in source
assert "Global\\WoooAgent99StaleRecoveryReconcilerV1" in source
assert "Wait-AgentCimProcessIdentityAbsent $parent 15" in source
assert "Test-AgentCimProcessIdentityMatch $child $childBeforeKill" in source
assert "$leaseAcquiredUtc.Ticks" in source
assert "vmPowerChangePerformed = $false" in source
assert "vmwareVmxProcessTerminated = $false" in source
def test_breakglass_targets_only_exact_stale_vmrun_processes() -> None:
source = BREAKGLASS.read_text(encoding="utf-8")
assert "lease_run_id_mismatch" in source
assert "lease_process_id_mismatch" in source
assert "parent_run_id_mismatch" in source
assert "child_identity_mismatch" in source
assert "child_vmx_mismatch" in source
assert "recovery_mutex_not_active" in source
assert 'taskkill.exe" /PID $ProcessId /T /F' in source
assert "Invoke-ExactTaskkill $ExpectedChildPid" in source
assert "prior_receipt_identity_mismatch" in source
assert '"agent99_stale_recovery_breakglass_v2", "agent99_stale_recovery_breakglass_v3"' in source
assert "Global\\WoooAgent99StaleRecoveryReconcilerV1" in source
assert "Wait-ExactProcessIdentityAbsent" in source
assert "parent_cleanup_identity_changed" in source
assert "Get-DescendantProcesses $ExpectedParentPid" in source
assert "descendantCount = 0" in source
assert "agent99_stale_recovery_parent_cleanup_intent_v1" in source
assert "Write-AtomicJson $intent $intentPath" in source
assert "Invoke-ExactTaskkill $ExpectedParentPid" in source
assert "vmwareVmxProcessTerminated = $false" in source
assert "vmxLockDeleted = $false" in source
assert "hostRebootPerformed = $false" in source
assert "Remove-Item" in source
assert ".lck" not in source
def test_breakglass_apply_is_receipted_and_check_is_no_write() -> None:
source = BREAKGLASS.read_text(encoding="utf-8")
check_pos = source.index('if ($Mode -eq "Apply" -and $precheckPassed)')
operation_intent_pos = source.index("Write-AtomicJson $operationIntent $operationIntentPath")
child_kill_pos = source.index("Invoke-ExactTaskkill $ExpectedChildPid")
intent_write_pos = source.index("Write-AtomicJson $intent $intentPath")
parent_kill_pos = source.index("Invoke-ExactTaskkill $ExpectedParentPid")
receipt_pos = source.index('if ($Mode -eq "Apply") {', parent_kill_pos)
assert check_pos < operation_intent_pos < child_kill_pos < intent_write_pos < parent_kill_pos < receipt_pos
assert "agent99_stale_recovery_breakglass_intent_v1" in source
assert '"partial_write_failed_closed"' in source
assert "operationFailureType" in source
assert "Write-AtomicJson $result $script:ReceiptPath" in source
assert "function Get-FailedMutationReadback" in source
assert '"parent_identity_unknown"' in source
assert '"child_identity_unknown"' in source
assert '"recovery_mutex_unknown"' in source
assert '"lease_metadata_unknown"' in source
assert "postFailureReadbackComplete" in source
assert "$stream.Flush($true)" in source
assert "receipt_readback_mismatch" in source
assert 'runtimeWritePerformed = $script:RuntimeWritePerformed' in source
assert 'if ($Mode -eq "Check") { exit 0 }' in source
def test_control_loop_maintenance_freeze_is_exact_and_receipted() -> None:
source = MAINTENANCE.read_text(encoding="utf-8")
assert 'if ($env:COMPUTERNAME -ne "WOOO-SUPER")' in source
assert 'agent99-run\\.ps1"?\\s+-Mode\\s+ControlTick' in source
assert 'agent99-run\\.ps1"?\\s+-Mode\\s+Recover\\s+-ControlledApply' in source
assert "Global\\WoooAgent99ControlLoopMaintenanceV1" in source
assert "blocked_single_writer_active" in source
assert "Test-ExactCreationTime $Process $ExpectedControlCreated" in source
assert "Test-ExactCreationTime $Process $ExpectedRecoverCreated" in source
assert "[int]$Process.ParentProcessId -eq $ExpectedRecoverParentPid" in source
assert 'Disable-ScheduledTask -TaskName $taskName -TaskPath "\\"' in source
assert 'Stop-ScheduledTask -TaskName $taskName -TaskPath "\\"' in source
assert "control_recheck_identity_mismatch" in source
assert 'taskkill.exe" /PID $ExpectedRecoverPid /T /F' in source
assert "$stream.Flush($true)" in source
assert "control_loop_maintenance_frozen" in source
assert "agent99_control_loop_maintenance_intent_v1" in source
assert source.index("Write-DurableReceipt $operationIntent $operationIntentPath") < source.index(
'Disable-ScheduledTask -TaskName $taskName'
)
assert "maintenance_partial_frozen_manual_recovery" in source
assert "taskEnabledBefore" in source
assert "taskEnabledAfter" in source
assert "operationFailureType" in source
assert "vmwareVmxProcessTerminated = $false" in source
assert "hostRebootPerformed = $false" in source
def test_control_loop_deadline_and_queue_batch_are_compatible() -> None:
control = CONTROL.read_text(encoding="utf-8")
tasks = REGISTER_TASKS.read_text(encoding="utf-8")
assert "$script:Agent99QueueOuterDeadlineSeconds = 1680" in control
assert "-ExecutionTimeLimit (New-TimeSpan -Minutes 35)" in tasks
control_registration = tasks.split('$controlArgs = ', 1)[1].split('$telegramInboxScript = ', 1)[0]
assert "-Settings $controlSettings" in control_registration
queue_selection = control.split("function Invoke-AgentQueuedCommands", 1)[1].split("$results = @()", 1)[0]
assert "Select-Object -First 1" in queue_selection
def test_ingress_signals_tasks_without_waiting_for_control_execution() -> None:
sre = SRE_INBOX.read_text(encoding="utf-8")
telegram = TELEGRAM_INBOX.read_text(encoding="utf-8")
submit = SUBMIT_REQUEST.read_text(encoding="utf-8")
assert 'Start-ScheduledTask -TaskPath "\\" -TaskName "Wooo-Agent99-Control-Loop"' in sre
assert 'Start-ScheduledTask -TaskPath "\\" -TaskName "Wooo-Agent99-Control-Loop"' in submit
assert 'Start-ScheduledTask -TaskPath "\\" -TaskName "Wooo-Agent99-SRE-Alert-Inbox"' in telegram
assert '"-InstructionBase64", (Convert-AgentBase64 $instruction)' in telegram
assert '"-RequestedByBase64", (Convert-AgentBase64 ([string]$update.from))' in telegram
assert '"-AgentRoot", $AgentRoot' in telegram
assert "$null = $submit.Handle" in telegram
assert "instruction_transport_ambiguous" in submit
assert "scheduled_task_signaled" in sre
assert "scheduled_task_signaled" in submit
assert "function Write-AgentDurableJson" in submit
assert '"request-" + $ingressDigest.Substring(0, 32)' in submit
assert "Global\\Wooo-Agent99-Submit-" in submit
assert "function Get-AgentIngressLifecycleReceipt" in submit
assert 'throw "ingress_lifecycle_ambiguous"' in submit
assert "idempotentReplay = [bool]$queueReceipt.existing" in submit
assert "durableQueue = [bool]$queueReceipt.ok" in submit
assert "submitEvidenceSha256" in submit
assert "function Get-AgentDurableSubmitReadback" in telegram
assert "Sort-Object update_id" in telegram
assert "lastDurablyHandledUpdateId" in telegram
assert "update_processing_failed_no_checkpoint_past_failure" in telegram
assert "offset_checkpoint_failed_safe_replay_required" in telegram
assert "Write-AgentDurableJson $result $evidencePath" in telegram
assert "Write-AgentDurableJson $offsetState $offsetPath" in telegram
assert "$maxUpdateId + 1" not in telegram
def test_windows_native_bounded_process_replay_covers_timeout_tree_and_stream_cap() -> None:
source = BOUNDED_REPLAY.read_text(encoding="utf-8")
assert "timeout_descendant_not_observed" in source
assert "streaming_output_limit_not_detected" in source
assert "root_only_descendant_not_preserved" in source
assert "-TerminationScope Root" in source
assert "cim_failure_root_not_stopped" in source
assert "cim_failure_tree_false_green" in source
assert "orphan_race_descendant_snapshot_missing" in source
assert "orphan_race_tree_false_green" in source
assert "pid_reuse_selection_not_identity_bounded" in source
assert "reusedRootExcluded" in source
assert "reusedRootChildExcluded" in source
assert "live_root_child_creation_bound_failed" in source
assert "intermediateOlderProcessExcluded" in source
assert "stop_readback_failure_not_detected" in source
assert "stop_readback_failure_not_fail_closed" in source
assert "stop_readback_failure_tree_false_green" in source
assert "captured_output_exceeded_contract" in source
assert "cleanupVerified" in source
assert "productionPathWritePerformed = $false" in source
def test_windows_native_telegram_replay_covers_durable_success_and_enqueue_failure() -> None:
source = TELEGRAM_REPLAY.read_text(encoding="utf-8")
assert "success_queue_not_durable" in source
assert "failure_case_false_green" in source
assert "failure_offset_advanced" in source
assert "synthetic_after_queue_commit" in source
assert "queue_commit_retry_duplicated_artifact" in source
assert "queue_commit_retry_not_idempotent" in source
assert '"A" * 9000' in source
assert "telegramApiCalled = $false" in source
assert "productionPathWritePerformed = $false" in source
def test_windows_native_breakglass_failure_readback_replay_is_fail_closed() -> None:
source = BREAKGLASS_REPLAY.read_text(encoding="utf-8")
assert "synthetic_cim_failure" in source
assert "synthetic_mutex_readback_failure" in source
assert "synthetic_filesystem_readback_failure" in source
assert "failure_readback_false_green" in source
assert "terminalReceiptConstructionCanContinue = $true" in source
assert "runtimeWritePerformed = $false" in source

View File

@@ -248,7 +248,12 @@ def test_phase_budget_and_systemd_outer_timeout_are_aligned() -> None:
assert "executorReserveSeconds -ge 60" in control
assert "queueReserveSeconds -ge 300" in control
assert "clientReserveSeconds -ge 120" in control
assert '$process.WaitForExit($script:Agent99QueueOuterDeadlineSeconds * 1000)' in control
queue_runner = control[
control.index("$execution = Invoke-AgentBoundedProcess") :
]
assert "-TimeoutSeconds $script:Agent99QueueOuterDeadlineSeconds" in queue_runner
assert "-MaximumOutputBytes 65536" in queue_runner
assert '$process.WaitForExit($script:Agent99QueueOuterDeadlineSeconds * 1000)' not in control
def test_receipts_and_readbacks_are_immutable_boot_bound_and_replay_safe() -> None:
@@ -516,7 +521,9 @@ def test_agent99_propagates_identity_and_requires_independent_receipt() -> None:
for parameter in ("AutomationRunId", "TraceId", "WorkItemId"):
assert f"[string]${parameter} = \"\"" in control
assert f'[string]`${parameter} = ""' in bootstrap
assert 'reason = "controlled_dispatch_identity_unsafe"' in control
assert '[string]$dispatchIdentityValidation.reason -eq "identity_control_value_unsafe"' in control
assert '"controlled_dispatch_identity_unsafe"' in control
assert "reason = $identityRejectionReason" in control
assert 'applyBlockedReason = "control_identity_incomplete_or_unsafe"' in function
assert '$dryRun.managerPreflightStatus -eq "ready"' in function
assert "-not $dryRun.artifactTransactionBlocked" in function