From 5cd865c805ac91d12c1120f3a022431d0efd4004 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 22 Jul 2026 17:30:21 +0800 Subject: [PATCH] fix(agent99): bind extended maintenance restore --- ...ent99-control-loop-maintenance-restore.ps1 | 35 +++++++++++++++++-- .../tests/test_agent99_vmrun_timeout_guard.py | 12 +++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/scripts/reboot-recovery/agent99-control-loop-maintenance-restore.ps1 b/scripts/reboot-recovery/agent99-control-loop-maintenance-restore.ps1 index b3998d582..437a90301 100644 --- a/scripts/reboot-recovery/agent99-control-loop-maintenance-restore.ps1 +++ b/scripts/reboot-recovery/agent99-control-loop-maintenance-restore.ps1 @@ -5,8 +5,10 @@ param( [Parameter(Mandatory = $true)] [ValidatePattern("^agent99-control-loop-maintenance-2[0-9]{7}-[0-9]{6}-[0-9]{3}-[0-9a-f]{8}\.json$")] [string]$MaintenanceReceiptRef, - [ValidateRange(1, 60)] + [ValidateRange(1, 180)] [int]$MaxReceiptAgeMinutes = 30, + [ValidatePattern("^(?:|[0-9a-f]{64})$")] + [string]$ExpectedMaintenanceReceiptSha256 = "", [string]$AgentRoot = "C:\Wooo\Agent99" ) @@ -155,13 +157,33 @@ try { $blockers.Add("maintenance_receipt_missing") } else { try { - $maintenance = Get-Content -LiteralPath $maintenancePath -Raw | ConvertFrom-Json - $maintenanceSha256 = (Get-FileHash -LiteralPath $maintenancePath -Algorithm SHA256).Hash.ToLowerInvariant() + $maintenanceBytes = [IO.File]::ReadAllBytes($maintenancePath) + $sha256 = [Security.Cryptography.SHA256]::Create() + try { + $maintenanceSha256 = ([BitConverter]::ToString($sha256.ComputeHash($maintenanceBytes))).Replace("-", "").ToLowerInvariant() + } finally { + $sha256.Dispose() + } + $strictUtf8 = New-Object Text.UTF8Encoding($false, $true) + $maintenanceJson = $strictUtf8.GetString($maintenanceBytes) + $maintenance = $maintenanceJson | ConvertFrom-Json + if ($null -eq $maintenance -or $maintenance -isnot [pscustomobject]) { + throw "maintenance_receipt_shape_invalid" + } $maintenanceAgeMinutes = ((Get-Date) - (Get-Item -LiteralPath $maintenancePath).LastWriteTime).TotalMinutes } catch { $blockers.Add("maintenance_receipt_invalid") } } + if ($MaxReceiptAgeMinutes -gt 60 -and [string]::IsNullOrWhiteSpace($ExpectedMaintenanceReceiptSha256)) { + $blockers.Add("extended_receipt_window_requires_exact_sha256") + } + if ( + -not [string]::IsNullOrWhiteSpace($ExpectedMaintenanceReceiptSha256) -and + -not [string]::Equals($maintenanceSha256, $ExpectedMaintenanceReceiptSha256, [StringComparison]::Ordinal) + ) { + $blockers.Add("maintenance_receipt_sha256_mismatch") + } if ($maintenance) { if ( [string]$maintenance.schemaVersion -ne "agent99_control_loop_maintenance_v2" -or @@ -191,6 +213,9 @@ try { $intentPath = Join-Path $evidenceDir $intentRef try { $maintenanceIntent = Get-Content -LiteralPath $intentPath -Raw -ErrorAction Stop | ConvertFrom-Json + if ($null -eq $maintenanceIntent -or $maintenanceIntent -isnot [pscustomobject]) { + throw "maintenance_intent_shape_invalid" + } } catch { $blockers.Add("maintenance_intent_invalid") } @@ -257,6 +282,8 @@ try { taskName = $taskName maintenanceReceiptRef = $MaintenanceReceiptRef maintenanceReceiptSha256 = $maintenanceSha256 + maxReceiptAgeMinutes = $MaxReceiptAgeMinutes + extendedReceiptWindow = [bool]($MaxReceiptAgeMinutes -gt 60) plannedActions = @("enable_exact_control_task", "start_exact_control_task", "verify_control_task_started") rollback = "stop and disable the exact control task if postcheck fails" vmPowerChangePerformed = $false @@ -310,6 +337,8 @@ try { maintenanceReceiptRef = $MaintenanceReceiptRef maintenanceReceiptSha256 = $maintenanceSha256 maintenanceAgeMinutes = if ($null -ne $maintenanceAgeMinutes) { [math]::Round($maintenanceAgeMinutes, 2) } else { $null } + maxReceiptAgeMinutes = $MaxReceiptAgeMinutes + extendedReceiptWindow = [bool]($MaxReceiptAgeMinutes -gt 60) precheckPassed = $precheckPassed blockers = @($blockers) operationIntentRef = $operationIntentRef diff --git a/scripts/reboot-recovery/tests/test_agent99_vmrun_timeout_guard.py b/scripts/reboot-recovery/tests/test_agent99_vmrun_timeout_guard.py index b317e26f0..452e9d060 100644 --- a/scripts/reboot-recovery/tests/test_agent99_vmrun_timeout_guard.py +++ b/scripts/reboot-recovery/tests/test_agent99_vmrun_timeout_guard.py @@ -191,6 +191,18 @@ def test_control_loop_maintenance_restore_is_bounded_and_receipted() -> None: assert 'if ($env:COMPUTERNAME -ne "WOOO-SUPER")' in source assert "Global\\WoooAgent99ControlLoopMaintenanceV1" in source assert 'ValidateSet("Check", "Apply")' in source + assert "ValidateRange(1, 180)" in source + assert "ExpectedMaintenanceReceiptSha256" in source + assert "extended_receipt_window_requires_exact_sha256" in source + assert "maintenance_receipt_sha256_mismatch" in source + assert "[IO.File]::ReadAllBytes($maintenancePath)" in source + assert "ComputeHash($maintenanceBytes)" in source + assert "maintenance_receipt_shape_invalid" in source + assert "maintenance_intent_shape_invalid" in source + assert "Get-Content -LiteralPath $maintenancePath" not in source + extended_guard = source.index("extended_receipt_window_requires_exact_sha256") + maintenance_contract = source.index('schemaVersion -ne "agent99_control_loop_maintenance_v2"') + assert extended_guard < maintenance_contract assert "maintenanceReceiptSha256" in source assert 'schemaVersion -ne "agent99_control_loop_maintenance_v2"' in source assert 'terminal -ne "control_loop_maintenance_frozen"' in source