Files
awoooi/agent99-sre-alert-relay.ps1
ogt 6cf8429d17
All checks were successful
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 2m32s
CD Pipeline / build-and-deploy (push) Successful in 8m2s
CD Pipeline / post-deploy-checks (push) Successful in 2m22s
fix(automation): enforce durable alert closure
Restore D037 durable incident readback without weakening database failure semantics.

Fence Agent99 dispatch and terminal learning to canonical identities, durable leases, verifier receipts, and reconciler-owned checkpoints.

Drain backup and restore legacy receipts in bounded cohorts with strict transport, claim, projection, and no-false-green controls.

Keep SSH service refusal visible while separating it from broker network-policy reachability.
2026-07-14 09:40:53 +08:00

351 lines
13 KiB
PowerShell

param(
[string]$AgentRoot = "C:\Wooo\Agent99",
[string]$Prefix = "http://+:8787/agent99/sre-alert/",
[string]$TokenEnv = "AGENT99_SRE_RELAY_TOKEN",
[string[]]$AllowedRemotePrefixes = @("127.", "::1", "192.168.0."),
[int]$MaxBodyBytes = 262144,
[switch]$RunOnce
)
$ErrorActionPreference = "Stop"
$EvidenceDir = Join-Path $AgentRoot "evidence"
$AlertRoot = Join-Path $AgentRoot "alerts"
$IncomingDir = Join-Path $AlertRoot "incoming"
$LogDir = Join-Path $AgentRoot "logs"
$BinDir = Join-Path $AgentRoot "bin"
$ProcessedDir = Join-Path (Join-Path $AgentRoot "queue") "processed"
$SreAlertInboxScript = Join-Path $BinDir "agent99-sre-alert-inbox.ps1"
New-Item -ItemType Directory -Force -Path $EvidenceDir, $IncomingDir, $LogDir, $ProcessedDir | Out-Null
function Convert-AgentSafeFileToken {
param([string]$Value)
$safe = [regex]::Replace($Value, "[^A-Za-z0-9_.-]", "_")
if (-not $safe) { return "relay-alert" }
if ($safe.Length -gt 96) { return $safe.Substring(0, 96) }
return $safe
}
function Get-AgentField {
param(
[object]$Object,
[string]$Name,
[object]$Default = $null
)
if ($Object -and $Object.PSObject.Properties[$Name]) {
return $Object.PSObject.Properties[$Name].Value
}
return $Default
}
function Get-AgentEnvValue {
param([string]$Name)
foreach ($target in @("Process", "User", "Machine")) {
$value = [Environment]::GetEnvironmentVariable($Name, $target)
if ($value) { return $value }
}
return ""
}
function Test-AgentPublicReceiptRef {
param([string]$Value)
if (-not $Value -or $Value.Length -gt 256) { return $false }
if ($Value -match "(?i)(authorization|bearer|password|passwd|token|secret|api[_-]?key|private[_-]?key|cookie|session)(\s|:|=)|-----BEGIN.*PRIVATE KEY-----|(^|[/\.])\.env($|[/\.])") { return $false }
if ($Value -match "\.\.|://|[\\?&#=\s]") { return $false }
return [bool]($Value -match "^[A-Za-z0-9][A-Za-z0-9._:/@+%-]{0,255}$")
}
function Get-AgentCanonicalOutcomeIdentity {
param([object]$Result, [object]$Outcome)
$outer = Get-AgentField $Result "identity" $null
$nested = Get-AgentField $Outcome "identity" $null
$required = @(
"schema_version", "project_id", "incident_id", "source_fingerprint",
"route_id", "execution_generation", "approval_id", "run_id", "trace_id",
"work_item_id", "idempotency_key", "single_flight_key", "lock_owner",
"canonical_digest"
)
if (-not $outer -or -not $nested) { return $null }
foreach ($name in $required) {
$outerValue = [string](Get-AgentField $outer $name "")
$nestedValue = [string](Get-AgentField $nested $name "")
if ($name -ne "approval_id" -and (-not $outerValue -or -not $nestedValue)) {
return $null
}
if ($outerValue -ne $nestedValue) { return $null }
}
return $outer
}
function Get-AgentOutcomeReadback {
param([string]$RunId)
$parsedRunId = [guid]::Empty
if (-not [guid]::TryParse($RunId, [ref]$parsedRunId)) {
return @{ ok = $false; found = $false; error = "invalid_run_id" }
}
foreach ($file in @(Get-ChildItem -Path $ProcessedDir -Filter "processed-*.json" -File -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 256)) {
try {
$result = Get-Content $file.FullName -Raw | ConvertFrom-Json
if ([string](Get-AgentField $result "automationRunId" "") -ne $RunId) { continue }
$outcome = Get-AgentField $result "outcome" $null
$canonicalIdentity = Get-AgentCanonicalOutcomeIdentity $result $outcome
if (-not $canonicalIdentity) { continue }
$safeChecks = @()
foreach ($check in @((Get-AgentField $outcome "checks" @()) | Select-Object -First 32)) {
$safeChecks += @{
name = [string](Get-AgentField $check "name" "")
required = [bool](Get-AgentField $check "required" $false)
passed = [bool](Get-AgentField $check "passed" $false)
}
}
$safeEvidenceRefs = @{}
$outcomeEvidenceRefs = Get-AgentField $outcome "evidenceRefs" $null
foreach ($name in @(
"backup_status_evidence_ref",
"freshness_evidence_ref",
"offsite_verify_evidence_ref",
"escrow_evidence_ref",
"restore_drill_evidence_ref",
"source_resolution_receipt_ref"
)) {
$value = [string](Get-AgentField $outcomeEvidenceRefs $name "")
if (Test-AgentPublicReceiptRef $value) { $safeEvidenceRefs[$name] = $value }
}
return @{
ok = $true
found = $true
schemaVersion = "agent99_outcome_readback_v1"
automationRunId = $RunId
traceId = [string](Get-AgentField $result "traceId" "")
workItemId = [string](Get-AgentField $result "workItemId" "")
idempotencyKey = [string](Get-AgentField $result "idempotencyKey" "")
executionGeneration = [string](Get-AgentField $result "executionGeneration" "1")
incidentId = [string](Get-AgentField $result "incidentId" "")
approvalId = [string](Get-AgentField $result "approvalId" "")
identity = $canonicalIdentity
controlledApply = [bool](Get-AgentField $result "controlledApply" $false)
mode = [string](Get-AgentField $result "mode" "")
outcome = @{
schemaVersion = [string](Get-AgentField $outcome "schemaVersion" "")
state = [string](Get-AgentField $outcome "state" "unknown")
resolved = [bool](Get-AgentField $outcome "resolved" $false)
transportOk = [bool](Get-AgentField $outcome "transportOk" $false)
verifierName = [string](Get-AgentField $outcome "verifierName" "")
verifierPassed = [bool](Get-AgentField $outcome "verifierPassed" $false)
sourceEventResolved = [bool](Get-AgentField $outcome "sourceEventResolved" $false)
identity = $canonicalIdentity
failedChecks = @((Get-AgentField $outcome "failedChecks" @()) | Select-Object -First 32)
checks = $safeChecks
evidenceRefs = $safeEvidenceRefs
verifiedAt = [string](Get-AgentField $outcome "verifiedAt" "")
}
storesRawEvidence = $false
}
} catch {
continue
}
}
return @{
ok = $true
found = $false
schemaVersion = "agent99_outcome_readback_v1"
automationRunId = $RunId
storesRawEvidence = $false
}
}
function Write-AgentRelayEvent {
param([hashtable]$Event)
$path = Join-Path $LogDir "agent99-sre-alert-relay.jsonl"
$Event["timestamp"] = (Get-Date).ToString("o")
($Event | ConvertTo-Json -Compress -Depth 8) | Add-Content -Encoding UTF8 $path
}
function Send-AgentRelayResponse {
param(
[System.Net.HttpListenerContext]$Context,
[int]$StatusCode,
[hashtable]$Body
)
$json = $Body | ConvertTo-Json -Compress -Depth 8
$bytes = [System.Text.Encoding]::UTF8.GetBytes($json)
$Context.Response.StatusCode = $StatusCode
$Context.Response.ContentType = "application/json; charset=utf-8"
$Context.Response.ContentLength64 = $bytes.Length
$Context.Response.OutputStream.Write($bytes, 0, $bytes.Length)
$Context.Response.Close()
}
function Test-AgentRelayRemoteAllowed {
param([System.Net.HttpListenerContext]$Context)
$remote = [string]$Context.Request.RemoteEndPoint.Address
foreach ($prefix in $AllowedRemotePrefixes) {
if ($remote.StartsWith($prefix)) { return $true }
}
return $false
}
function Start-AgentSreAlertInbox {
if (-not (Test-Path $SreAlertInboxScript)) {
return [pscustomobject]@{ started = $false; reason = "sre_alert_inbox_script_missing" }
}
$ps = Join-Path $env:SystemRoot "System32\WindowsPowerShell\v1.0\powershell.exe"
$stamp = Get-Date -Format "yyyyMMdd-HHmmss"
$stdout = Join-Path $LogDir "agent99-sre-alert-relay-trigger-$stamp.out"
$stderr = Join-Path $LogDir "agent99-sre-alert-relay-trigger-$stamp.err"
$args = @(
"-NoProfile",
"-ExecutionPolicy", "Bypass",
"-File", $SreAlertInboxScript,
"-AgentRoot", $AgentRoot,
"-RunNow"
)
$proc = Start-Process -FilePath $ps -ArgumentList $args -PassThru -WindowStyle Hidden -RedirectStandardOutput $stdout -RedirectStandardError $stderr
return [pscustomobject]@{
started = $true
processId = $proc.Id
stdout = $stdout
stderr = $stderr
}
}
if (-not $Prefix.EndsWith("/")) {
$Prefix = "$Prefix/"
}
$listener = [System.Net.HttpListener]::new()
$listener.Prefixes.Add($Prefix)
$startupStamp = Get-Date -Format "yyyyMMdd-HHmmss"
$startupEvidence = Join-Path $EvidenceDir "agent99-SreAlertRelay-start-$startupStamp.json"
@{
ok = $true
prefix = $Prefix
tokenEnv = $TokenEnv
tokenConfigured = [bool](Get-AgentEnvValue $TokenEnv)
allowedRemotePrefixes = $AllowedRemotePrefixes
maxBodyBytes = $MaxBodyBytes
runOnce = [bool]$RunOnce
startedAt = (Get-Date).ToString("o")
} | ConvertTo-Json -Depth 6 | Set-Content -Encoding UTF8 $startupEvidence
try {
$listener.Start()
Write-AgentRelayEvent @{
event = "relay_started"
ok = $true
prefix = $Prefix
tokenConfigured = [bool](Get-AgentEnvValue $TokenEnv)
evidence = $startupEvidence
}
do {
$context = $listener.GetContext()
$remote = [string]$context.Request.RemoteEndPoint.Address
try {
if (-not (Test-AgentRelayRemoteAllowed $context)) {
Send-AgentRelayResponse $context 403 @{ ok = $false; error = "remote_not_allowed" }
Write-AgentRelayEvent @{ event = "relay_rejected"; ok = $false; remote = $remote; reason = "remote_not_allowed" }
continue
}
$expectedToken = Get-AgentEnvValue $TokenEnv
if ($expectedToken) {
$actualToken = [string]$context.Request.Headers["X-Agent99-Relay-Token"]
if ($actualToken -ne $expectedToken) {
Send-AgentRelayResponse $context 401 @{ ok = $false; error = "invalid_relay_token" }
Write-AgentRelayEvent @{ event = "relay_rejected"; ok = $false; remote = $remote; reason = "invalid_relay_token" }
continue
}
}
if ($context.Request.HttpMethod -eq "GET" -and -not $expectedToken) {
Send-AgentRelayResponse $context 503 @{ ok = $false; found = $false; error = "outcome_readback_auth_not_configured" }
Write-AgentRelayEvent @{ event = "outcome_readback_rejected"; ok = $false; remote = $remote; reason = "auth_not_configured" }
continue
}
if ($context.Request.HttpMethod -eq "GET") {
$runId = [string]$context.Request.QueryString["run_id"]
$readback = Get-AgentOutcomeReadback $runId
$statusCode = if ($readback.ok) { 200 } else { 400 }
Send-AgentRelayResponse $context $statusCode $readback
Write-AgentRelayEvent @{
event = "outcome_readback"
ok = [bool]$readback.ok
found = [bool]$readback.found
remote = $remote
runId = if ($readback.ok) { $runId } else { "invalid" }
storesRawEvidence = $false
}
continue
}
if ($context.Request.HttpMethod -ne "POST") {
Send-AgentRelayResponse $context 405 @{ ok = $false; error = "method_not_allowed" }
Write-AgentRelayEvent @{ event = "relay_rejected"; ok = $false; remote = $remote; reason = "method_not_allowed" }
continue
}
if ($context.Request.ContentLength64 -gt $MaxBodyBytes) {
Send-AgentRelayResponse $context 413 @{ ok = $false; error = "payload_too_large" }
Write-AgentRelayEvent @{ event = "relay_rejected"; ok = $false; remote = $remote; reason = "payload_too_large" }
continue
}
$reader = [System.IO.StreamReader]::new($context.Request.InputStream, $context.Request.ContentEncoding)
$body = $reader.ReadToEnd()
if ($body.Length -gt $MaxBodyBytes) {
Send-AgentRelayResponse $context 413 @{ ok = $false; error = "payload_too_large" }
Write-AgentRelayEvent @{ event = "relay_rejected"; ok = $false; remote = $remote; reason = "payload_too_large_readback" }
continue
}
$alert = $body | ConvertFrom-Json
$alertId = [string](Get-AgentField $alert "id" ("relay-" + [guid]::NewGuid().ToString("N")))
$safeId = Convert-AgentSafeFileToken $alertId
$fileStamp = Get-Date -Format "yyyyMMdd-HHmmss"
$tmpPath = Join-Path $IncomingDir ".$safeId-$fileStamp-$([guid]::NewGuid().ToString("N")).tmp"
$finalPath = Join-Path $IncomingDir "$safeId-$fileStamp.json"
$alert | ConvertTo-Json -Depth 20 | Set-Content -Encoding UTF8 $tmpPath
Move-Item -Force $tmpPath $finalPath
$trigger = Start-AgentSreAlertInbox
Send-AgentRelayResponse $context 202 @{
ok = $true
alertId = $alertId
incomingFile = $finalPath
inboxTriggered = [bool]$trigger.started
inboxProcessId = $trigger.processId
}
Write-AgentRelayEvent @{
event = "relay_accepted"
ok = $true
remote = $remote
alertId = $alertId
incomingFile = $finalPath
inboxTriggered = [bool]$trigger.started
inboxProcessId = $trigger.processId
}
} catch {
try {
Send-AgentRelayResponse $context 500 @{ ok = $false; error = "relay_exception"; errorType = $_.Exception.GetType().Name }
} catch {
}
Write-AgentRelayEvent @{
event = "relay_exception"
ok = $false
remote = $remote
errorType = $_.Exception.GetType().Name
error = $_.Exception.Message
}
}
} while (-not $RunOnce)
} finally {
if ($listener.IsListening) { $listener.Stop() }
$listener.Close()
}