feat(agent99): ship incident lifecycle cards
This commit is contained in:
158
scripts/reboot-recovery/agent99-recover-receipt-readback.ps1
Normal file
158
scripts/reboot-recovery/agent99-recover-receipt-readback.ps1
Normal file
@@ -0,0 +1,158 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$AgentRoot = "C:\Wooo\Agent99",
|
||||
[string]$NotBefore = ""
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$notBeforeTime = [datetime]::MinValue
|
||||
if ($NotBefore) {
|
||||
$notBeforeTime = [datetime]::Parse($NotBefore)
|
||||
}
|
||||
|
||||
$evidenceDir = Join-Path $AgentRoot "evidence"
|
||||
$file = Get-ChildItem -Path $evidenceDir -Filter "agent99-Recover-*.json" -File -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.LastWriteTime -ge $notBeforeTime } |
|
||||
Sort-Object LastWriteTime -Descending |
|
||||
Select-Object -First 1
|
||||
if (-not $file) {
|
||||
throw "new_recover_evidence_missing"
|
||||
}
|
||||
|
||||
$raw = Get-Content -Path $file.FullName -Raw | ConvertFrom-Json
|
||||
$phases = @($raw.recoverySlo.phases | ForEach-Object {
|
||||
[pscustomobject]@{
|
||||
name = [string]$_.name
|
||||
status = [string]$_.status
|
||||
elapsedSeconds = [double]$_.elapsedSeconds
|
||||
}
|
||||
})
|
||||
$telegram = @($raw.telegram | ForEach-Object {
|
||||
$visualPath = if ($_.PSObject.Properties["visualPath"]) { [string]$_.visualPath } else { "" }
|
||||
[pscustomobject]@{
|
||||
eventType = [string]$_.eventType
|
||||
severity = [string]$_.severity
|
||||
sent = [bool]$_.sent
|
||||
suppressed = if ($_.PSObject.Properties["suppressed"]) { [bool]$_.suppressed } else { $false }
|
||||
reason = if ($_.PSObject.Properties["reason"]) { [string]$_.reason } else { "" }
|
||||
messageFormat = if ($_.PSObject.Properties["messageFormat"]) { [string]$_.messageFormat } else { "" }
|
||||
incidentId = if ($_.PSObject.Properties["incidentId"]) { [string]$_.incidentId } else { "" }
|
||||
fingerprint = if ($_.PSObject.Properties["fingerprint"]) { [string]$_.fingerprint } else { "" }
|
||||
lifecycle = if ($_.PSObject.Properties["lifecycle"]) { [string]$_.lifecycle } else { "" }
|
||||
stateKey = if ($_.PSObject.Properties["stateKey"]) { [string]$_.stateKey } else { "" }
|
||||
visualSent = if ($_.PSObject.Properties["visualSent"]) { [bool]$_.visualSent } else { $false }
|
||||
messageIdPresent = [bool]($_.PSObject.Properties["messageId"] -and $_.messageId)
|
||||
replyThreaded = [bool]($_.PSObject.Properties["replyToMessageId"] -and $_.replyToMessageId)
|
||||
incidentStateWritten = [bool]($_.PSObject.Properties["incidentStateWritten"] -and $_.incidentStateWritten)
|
||||
visualFile = if ($visualPath) { Split-Path -Leaf $visualPath } else { "" }
|
||||
visualExists = [bool]($visualPath -and (Test-Path -LiteralPath $visualPath))
|
||||
}
|
||||
})
|
||||
|
||||
$phaseFailures = @($phases | Where-Object { $_.status -ne "ok" })
|
||||
$sentCount = @($telegram | Where-Object { $_.sent }).Count
|
||||
$suppressedCount = @($telegram | Where-Object { $_.suppressed }).Count
|
||||
$lifecycleReceipts = @($telegram | Where-Object { $_.eventType -eq "recovery_slo_result" -and ($_.sent -or $_.suppressed) })
|
||||
$sentLifecycleReceipts = @($lifecycleReceipts | Where-Object { $_.sent })
|
||||
$notificationReceipted = [bool]($lifecycleReceipts.Count -gt 0)
|
||||
$humanReadableCardReady = [bool](
|
||||
$lifecycleReceipts.Count -gt 0 -and
|
||||
@($lifecycleReceipts | Where-Object {
|
||||
$_.messageFormat -ne "incident_card_zh_tw_v5" -or
|
||||
$_.incidentId -notmatch "^INC-[A-F0-9]{8}$" -or
|
||||
$_.lifecycle -notin @("remediating", "recovered", "blocked")
|
||||
}).Count -eq 0
|
||||
)
|
||||
$visualLifecycleReady = [bool](@($sentLifecycleReceipts | Where-Object { -not $_.visualSent }).Count -eq 0)
|
||||
$incidentStateReady = [bool](@($sentLifecycleReceipts | Where-Object { -not $_.messageIdPresent -or -not $_.incidentStateWritten }).Count -eq 0)
|
||||
$recoveryVerified = [bool](
|
||||
$raw.mode -eq "Recover" -and
|
||||
$raw.recoverySlo.completed -and
|
||||
$raw.recoverySlo.coordinatorVerified -and
|
||||
$phases.Count -gt 0 -and
|
||||
$phaseFailures.Count -eq 0
|
||||
)
|
||||
$recentReceipts = @(Get-ChildItem -Path $evidenceDir -Filter "agent99-Recover-*.json" -File -ErrorAction SilentlyContinue |
|
||||
Sort-Object LastWriteTime -Descending |
|
||||
Select-Object -First 5 |
|
||||
ForEach-Object {
|
||||
$historyFile = $_
|
||||
try {
|
||||
$history = Get-Content -Path $historyFile.FullName -Raw | ConvertFrom-Json
|
||||
$historyTelegram = @($history.telegram)
|
||||
[pscustomobject]@{
|
||||
evidenceFile = $historyFile.Name
|
||||
evidenceLastWriteTime = $historyFile.LastWriteTime.ToString("o")
|
||||
runId = [string]$history.recoverySlo.runId
|
||||
completed = [bool]$history.recoverySlo.completed
|
||||
telegramAttemptCount = $historyTelegram.Count
|
||||
telegramSentCount = @($historyTelegram | Where-Object { $_.sent }).Count
|
||||
telegramSuppressedCount = @($historyTelegram | Where-Object { $_.PSObject.Properties["suppressed"] -and $_.suppressed }).Count
|
||||
visualSentCount = @($historyTelegram | Where-Object { $_.PSObject.Properties["visualSent"] -and $_.visualSent }).Count
|
||||
visualFiles = @($historyTelegram | Where-Object { $_.PSObject.Properties["visualPath"] -and $_.visualPath } | ForEach-Object { Split-Path -Leaf ([string]$_.visualPath) })
|
||||
}
|
||||
} catch {
|
||||
[pscustomobject]@{
|
||||
evidenceFile = $historyFile.Name
|
||||
evidenceLastWriteTime = $historyFile.LastWriteTime.ToString("o")
|
||||
parseError = $_.Exception.GetType().Name
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
$result = [pscustomobject]@{
|
||||
schemaVersion = "agent99_recover_receipt_readback_v1"
|
||||
observedAt = (Get-Date).ToString("o")
|
||||
evidenceFile = $file.Name
|
||||
evidenceLastWriteTime = $file.LastWriteTime.ToString("o")
|
||||
secretValueRead = $false
|
||||
remoteWritePerformed = $false
|
||||
mode = [string]$raw.mode
|
||||
controlledApply = [bool]$raw.controlledApply
|
||||
vmCount = @($raw.vmResults).Count
|
||||
vmOkCount = @($raw.vmResults | Where-Object { $_.ok -or $_.skipped }).Count
|
||||
hostCount = @($raw.hosts).Count
|
||||
hostReachableCount = @($raw.hosts | Where-Object { $_.ping }).Count
|
||||
publicCount = @($raw.public).Count
|
||||
publicOkCount = @($raw.public | Where-Object { $_.ok }).Count
|
||||
recovery = [pscustomobject]@{
|
||||
runId = [string]$raw.recoverySlo.runId
|
||||
scope = [string]$raw.recoverySlo.scope
|
||||
completed = [bool]$raw.recoverySlo.completed
|
||||
withinSlo = [bool]$raw.recoverySlo.withinSlo
|
||||
elapsedSeconds = [double]$raw.recoverySlo.elapsedSeconds
|
||||
coordinatorVerified = [bool]$raw.recoverySlo.coordinatorVerified
|
||||
rebootSloClaimed = [bool]$raw.recoverySlo.rebootSloClaimed
|
||||
phases = $phases
|
||||
}
|
||||
telegramAttemptCount = $telegram.Count
|
||||
telegramSentCount = $sentCount
|
||||
telegramSuppressedCount = $suppressedCount
|
||||
lifecycleReceiptCount = $lifecycleReceipts.Count
|
||||
notificationReceipted = $notificationReceipted
|
||||
humanReadableCardReady = $humanReadableCardReady
|
||||
visualLifecycleReady = $visualLifecycleReady
|
||||
incidentStateReady = $incidentStateReady
|
||||
telegram = $telegram
|
||||
recentReceipts = $recentReceipts
|
||||
recoveryVerified = $recoveryVerified
|
||||
ok = [bool]($recoveryVerified -and $notificationReceipted -and $humanReadableCardReady -and $visualLifecycleReady -and $incidentStateReady)
|
||||
}
|
||||
|
||||
Write-Output "RECOVER_RECEIPT_OK=$([int]$result.ok)"
|
||||
Write-Output "RECOVERY_VERIFIED=$([int]$result.recoveryVerified)"
|
||||
Write-Output "NOTIFICATION_RECEIPTED=$([int]$result.notificationReceipted)"
|
||||
Write-Output "HUMAN_READABLE_CARD_READY=$([int]$result.humanReadableCardReady)"
|
||||
Write-Output "VISUAL_LIFECYCLE_READY=$([int]$result.visualLifecycleReady)"
|
||||
Write-Output "INCIDENT_STATE_READY=$([int]$result.incidentStateReady)"
|
||||
Write-Output "TELEGRAM_ATTEMPTS=$($result.telegramAttemptCount)"
|
||||
Write-Output "TELEGRAM_SENT=$($result.telegramSentCount)"
|
||||
Write-Output "TELEGRAM_SUPPRESSED=$($result.telegramSuppressedCount)"
|
||||
Write-Output "SECRET_VALUE_READ=0"
|
||||
Write-Output "REMOTE_WRITE_PERFORMED=0"
|
||||
Write-Output "JSON_BEGIN"
|
||||
$result | ConvertTo-Json -Depth 10 -Compress
|
||||
Write-Output "JSON_END"
|
||||
|
||||
if ($result.ok) { exit 0 }
|
||||
exit 2
|
||||
@@ -147,3 +147,23 @@ def test_live_preflight_adapts_inbox_evidence_without_raw_message_readback() ->
|
||||
assert safe_field in preflight
|
||||
for forbidden in ("messageText", "rawUpdate", "botToken", "chatId"):
|
||||
assert forbidden not in preflight
|
||||
|
||||
|
||||
def test_recover_receipt_readback_requires_verifier_and_notification() -> None:
|
||||
readback = read("scripts/reboot-recovery/agent99-recover-receipt-readback.ps1")
|
||||
|
||||
assert 'schemaVersion = "agent99_recover_receipt_readback_v1"' in readback
|
||||
assert '$raw.recoverySlo.coordinatorVerified' in readback
|
||||
assert '$phaseFailures.Count -eq 0' in readback
|
||||
assert '$notificationReceipted' in readback
|
||||
assert '$humanReadableCardReady' in readback
|
||||
assert '$visualLifecycleReady' in readback
|
||||
assert '$incidentStateReady' in readback
|
||||
assert 'incident_card_zh_tw_v5' in readback
|
||||
assert '$recoveryVerified -and $notificationReceipted -and $humanReadableCardReady' in readback
|
||||
assert '$recentReceipts' in readback
|
||||
assert 'visualSentCount' in readback
|
||||
assert 'secretValueRead = $false' in readback
|
||||
assert 'remoteWritePerformed = $false' in readback
|
||||
for forbidden in ("botToken", "chatId", "messageText", "rawUpdate", ".env"):
|
||||
assert forbidden not in readback
|
||||
|
||||
Reference in New Issue
Block a user