From a661824f33bc336dadb79b4d8d96f08a59591041 Mon Sep 17 00:00:00 2001 From: ogt Date: Tue, 14 Jul 2026 14:22:14 +0800 Subject: [PATCH] fix(agent99): scope deploy runtime verifier --- .../agent99-remote-atomic-deploy-receiver.ps1 | 45 ++++++++++++++++++- ...remote_atomic_deploy_transport_contract.py | 19 ++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/scripts/reboot-recovery/agent99-remote-atomic-deploy-receiver.ps1 b/scripts/reboot-recovery/agent99-remote-atomic-deploy-receiver.ps1 index 9902af325..3f9803aec 100644 --- a/scripts/reboot-recovery/agent99-remote-atomic-deploy-receiver.ps1 +++ b/scripts/reboot-recovery/agent99-remote-atomic-deploy-receiver.ps1 @@ -259,15 +259,49 @@ function Invoke-AgentLivePreflight { try { $payload = $match.Groups["json"].Value | ConvertFrom-Json } catch { $payload = $null } } $summary = if ($payload -and $payload.PSObject.Properties["summary"]) { $payload.summary } else { $null } + [string[]]$blockingReasons = if ($processResult.timedOut) { + @("preflight_process_timeout") + } elseif ($summary) { + @($summary.blockingReasons | ForEach-Object { [string]$_ }) + } else { + @("preflight_output_unparseable") + } + [string[]]$externalBlockingReasons = @($blockingReasons | Where-Object { $_ -eq "self_health_not_ok" }) + [string[]]$runtimePlaneBlockingReasons = @($blockingReasons | Where-Object { $_ -ne "self_health_not_ok" }) + $acceptedExitCode = [bool]($processResult.exitCode -in @(0, 2)) + $runtimePlaneReady = [bool]( + $acceptedExitCode -and + -not $processResult.timedOut -and + $payload -and + $payload.manifest -and + [bool]$payload.manifest.runtimeMatched -and + $summary -and + [int]$summary.requiredTaskCount -gt 0 -and + [int]$summary.healthyTaskCount -eq [int]$summary.requiredTaskCount -and + [int]$summary.taskFailureCount -eq 0 -and + [int]$summary.requiredEvidenceFailureCount -eq 0 -and + [int]$summary.requiredEvidenceParseFailureCount -eq 0 -and + [int]$summary.requiredEvidenceContentFailureCount -eq 0 -and + [int]$summary.relayListenerCount -gt 0 -and + [int]$summary.alertIncomingCount -eq 0 -and + [int]$summary.alertRunningCount -eq 0 -and + [int]$summary.pendingCommandCount -eq 0 -and + $runtimePlaneBlockingReasons.Count -eq 0 + ) return [pscustomobject]@{ - ok = [bool]($processResult.exitCode -eq 0 -and -not $processResult.timedOut -and $summary -and $summary.preflightGreen) + ok = $runtimePlaneReady exitCode = [int]$processResult.exitCode + acceptedExitCode = $acceptedExitCode timedOut = [bool]$processResult.timedOut parsed = [bool]($null -ne $payload) sourceRevision = if ($payload -and $payload.manifest) { [string]$payload.manifest.sourceRevision } else { "" } runtimeMatched = [bool]($payload -and $payload.manifest -and $payload.manifest.runtimeMatched) healthyTaskCount = if ($summary) { [int]$summary.healthyTaskCount } else { 0 } requiredTaskCount = if ($summary) { [int]$summary.requiredTaskCount } else { 0 } + taskFailureCount = if ($summary) { [int]$summary.taskFailureCount } else { -1 } + requiredEvidenceFailureCount = if ($summary) { [int]$summary.requiredEvidenceFailureCount } else { -1 } + requiredEvidenceParseFailureCount = if ($summary) { [int]$summary.requiredEvidenceParseFailureCount } else { -1 } + requiredEvidenceContentFailureCount = if ($summary) { [int]$summary.requiredEvidenceContentFailureCount } else { -1 } relayListenerCount = if ($summary) { [int]$summary.relayListenerCount } else { 0 } relayListenerGenerations = if ($payload) { @($payload.relayListeners | ForEach-Object { @@ -281,9 +315,16 @@ function Invoke-AgentLivePreflight { } } | Where-Object { $_ } | Sort-Object -Unique) } else { @() } + alertIncomingCount = if ($summary) { [int]$summary.alertIncomingCount } else { -1 } + alertRunningCount = if ($summary) { [int]$summary.alertRunningCount } else { -1 } pendingCommandCount = if ($summary) { [int]$summary.pendingCommandCount } else { -1 } preflightGreen = [bool]($summary -and $summary.preflightGreen) - blockingReasons = if ($processResult.timedOut) { @("preflight_process_timeout") } elseif ($summary) { @($summary.blockingReasons | ForEach-Object { [string]$_ }) } else { @("preflight_output_unparseable") } + overallPreflightGreen = [bool]($summary -and $summary.preflightGreen) + runtimePlaneReady = $runtimePlaneReady + externalDegraded = [bool]($runtimePlaneReady -and $externalBlockingReasons.Count -gt 0) + blockingReasons = $blockingReasons + runtimePlaneBlockingReasons = $runtimePlaneBlockingReasons + externalBlockingReasons = $externalBlockingReasons } } diff --git a/scripts/reboot-recovery/tests/test_agent99_remote_atomic_deploy_transport_contract.py b/scripts/reboot-recovery/tests/test_agent99_remote_atomic_deploy_transport_contract.py index a25484ee7..81a229433 100644 --- a/scripts/reboot-recovery/tests/test_agent99_remote_atomic_deploy_transport_contract.py +++ b/scripts/reboot-recovery/tests/test_agent99_remote_atomic_deploy_transport_contract.py @@ -486,6 +486,25 @@ def test_receiver_does_not_treat_http_sys_pid4_as_the_userspace_generation() -> assert "$after.listenerGenerations" not in restart +def test_receiver_scopes_deploy_health_separately_from_external_asset_health() -> None: + source = RECEIVER.read_text(encoding="utf-8") + preflight = source[source.index("function Invoke-AgentLivePreflight") :] + preflight = preflight[: preflight.index("function Get-AgentRelayRuntime")] + + assert '$processResult.exitCode -in @(0, 2)' in preflight + assert '$externalBlockingReasons' in preflight + assert '$runtimePlaneBlockingReasons' in preflight + assert '$_ -eq "self_health_not_ok"' in preflight + assert '$_ -ne "self_health_not_ok"' in preflight + assert '[int]$summary.healthyTaskCount -eq [int]$summary.requiredTaskCount' in preflight + assert '[int]$summary.requiredEvidenceFailureCount -eq 0' in preflight + assert '[int]$summary.alertIncomingCount -eq 0' in preflight + assert '[int]$summary.alertRunningCount -eq 0' in preflight + assert '[int]$summary.pendingCommandCount -eq 0' in preflight + assert '$runtimePlaneBlockingReasons.Count -eq 0' in preflight + assert 'externalDegraded = [bool](' in preflight + + def test_transport_only_restarts_existing_task_without_mutating_its_definition() -> None: source = "\n".join( (