fix(agent99): require remediation verifier success

This commit is contained in:
ogt
2026-07-10 22:17:15 +08:00
parent 1bba6c95c0
commit 8b44260469
4 changed files with 81 additions and 3 deletions

View File

@@ -1557,8 +1557,10 @@ function Invoke-LoadShedding {
}
$cooldownMinutes = if ($action.cooldownMinutes) { [int]$action.cooldownMinutes } else { 30 }
$failureBackoffMinutes = if ($action.PSObject.Properties["failureBackoffMinutes"]) { [int]$action.failureBackoffMinutes } else { 10 }
$safeName = ($action.name -replace "[^A-Za-z0-9_.-]", "_")
$markerPath = Join-Path $EvidenceDir "loadshedding-$safeName.marker"
$failureMarkerPath = Join-Path $EvidenceDir "loadshedding-$safeName.failure.marker"
if (Test-Path $markerPath) {
$ageMinutes = ((Get-Date) - (Get-Item $markerPath).LastWriteTime).TotalMinutes
if ($ageMinutes -lt $cooldownMinutes) {
@@ -1566,6 +1568,13 @@ function Invoke-LoadShedding {
continue
}
}
if (Test-Path $failureMarkerPath) {
$failureAgeMinutes = ((Get-Date) - (Get-Item $failureMarkerPath).LastWriteTime).TotalMinutes
if ($failureAgeMinutes -lt $failureBackoffMinutes) {
$results += [pscustomobject]@{ name = $action.name; host = $action.host; skipped = $true; reason = "failure_backoff"; ageMinutes = [math]::Round($failureAgeMinutes, 1) }
continue
}
}
Record-AgentEvent "load_shed_attempt" $hostSeverity "host=$($action.host) action=$($action.name) command=$($action.command)" $action -NoAlert
$timeoutSeconds = if ($action.PSObject.Properties["timeoutSeconds"]) { [int]$action.timeoutSeconds } else { 60 }
@@ -1597,14 +1606,21 @@ function Invoke-LoadShedding {
}
}
$effectiveOk = [bool]($run.ok -or $postVerifyOk)
Set-Content -Path $markerPath -Value (Get-Date -Format o) -Encoding UTF8
$postVerifyRequired = [bool]($action.PSObject.Properties["postVerifyCommand"] -and $action.postVerifyCommand)
$effectiveOk = if ($postVerifyRequired) { [bool]$postVerifyOk } else { [bool]$run.ok }
if ($effectiveOk) {
Set-Content -Path $markerPath -Value (Get-Date -Format o) -Encoding UTF8
Remove-Item -Force $failureMarkerPath -ErrorAction SilentlyContinue | Out-Null
} else {
Set-Content -Path $failureMarkerPath -Value (Get-Date -Format o) -Encoding UTF8
}
$result = [pscustomobject]@{
name = $action.name
host = $action.host
skipped = $false
ok = $effectiveOk
executionOk = [bool]$run.ok
postVerifyRequired = $postVerifyRequired
postVerifyOk = [bool]$postVerifyOk
postVerify = $postVerify
route = $run.route

View File

@@ -17,6 +17,7 @@ $StageDir = Join-Path $DeployRoot "staging-$stamp"
$BackupDir = Join-Path $DeployRoot "backup-$stamp"
$ManifestPath = Join-Path $StateDir "runtime-manifest.json"
$EvidencePath = Join-Path $EvidenceDir "agent99-deploy-$stamp.json"
$script:ConfigMigrations = @()
$runtimeFiles = @(
"agent99-bootstrap.ps1",
@@ -54,6 +55,15 @@ function Add-DefaultProperty {
}
}
function Set-AgentProperty {
param([object]$Object, [string]$Name, [object]$Value)
if ($Object.PSObject.Properties[$Name]) {
$Object.PSObject.Properties[$Name].Value = $Value
} else {
$Object | Add-Member -MemberType NoteProperty -Name $Name -Value $Value
}
}
function Write-DeployEvidence {
param([bool]$Ok, [string]$Status, [string]$ErrorText, [object[]]$Files, [object]$TaskResult)
New-Item -ItemType Directory -Force -Path $EvidenceDir | Out-Null
@@ -71,6 +81,7 @@ function Write-DeployEvidence {
validateOnly = [bool]$ValidateOnly
registerTasks = [bool]$RegisterTasks
error = $ErrorText
configMigrations = @($script:ConfigMigrations)
files = $Files
taskRegistration = $TaskResult
} | ConvertTo-Json -Depth 10 | Set-Content -Path $EvidencePath -Encoding UTF8
@@ -177,6 +188,25 @@ try {
Add-DefaultProperty $config "performance" ([pscustomobject]@{})
Add-DefaultProperty $config.performance "processCpuHostPercentWarning" 25
Add-DefaultProperty $config.performance "processCpuHostPercentCritical" 50
$policyConfig = Get-Content (Join-Path $StageDir "agent99.config.99.example.json") -Raw | ConvertFrom-Json
foreach ($sourceAction in @($policyConfig.performance.loadShedding.actions | Where-Object { $_.PSObject.Properties["policyVersion"] })) {
$liveAction = $config.performance.loadShedding.actions | Where-Object { $_.name -eq $sourceAction.name } | Select-Object -First 1
if (-not $liveAction) { continue }
$sourceVersion = [int]$sourceAction.policyVersion
$liveVersion = if ($liveAction.PSObject.Properties["policyVersion"]) { [int]$liveAction.policyVersion } else { 0 }
if ($sourceVersion -le $liveVersion) { continue }
foreach ($propertyName in @("policyVersion", "host", "command", "timeoutSeconds", "postVerifyCommand", "postVerifyMaxDiskUsedPercent", "postVerifyOkRegex", "rollback", "cooldownMinutes", "failureBackoffMinutes", "minSeverity", "reasonMatches", "enabled")) {
if ($sourceAction.PSObject.Properties[$propertyName]) {
Set-AgentProperty $liveAction $propertyName $sourceAction.PSObject.Properties[$propertyName].Value
}
}
$script:ConfigMigrations += [pscustomobject]@{
name = [string]$sourceAction.name
fromPolicyVersion = $liveVersion
toPolicyVersion = $sourceVersion
}
}
$config | ConvertTo-Json -Depth 20 | Set-Content -Path $ConfigPath -Encoding UTF8
$manifestRows = @()

View File

@@ -156,13 +156,15 @@
"actions": [
{
"name": "prune-188-docker-build-cache-disk-pressure",
"policyVersion": 2,
"host": "192.168.0.188",
"command": "echo BEFORE_DF=$(df -P / | awk 'NR==2{print $5}'); docker system df; docker builder prune -af --filter until=24h; echo AFTER_DF=$(df -P / | awk 'NR==2{print $5}'); docker system df",
"command": "echo BEFORE_DF=$(df -P / | awk 'NR==2{print $5}'); docker system df; docker builder prune -af; echo AFTER_DF=$(df -P / | awk 'NR==2{print $5}'); docker system df",
"timeoutSeconds": 240,
"postVerifyCommand": "df -P / | awk 'NR==2{gsub(\"%\",\"\",$5); print \"__DISK_USED__=\"$5}'",
"postVerifyMaxDiskUsedPercent": 89,
"rollback": "echo rollback_not_applicable_for_build_cache_prune",
"cooldownMinutes": 120,
"failureBackoffMinutes": 10,
"minSeverity": "warning",
"reasonMatches": [
"disk_used_warning",

View File

@@ -68,3 +68,33 @@ def test_agent99_deployer_verifies_promoted_runtime_and_writes_revision() -> Non
):
assert f'"{name}"' in deploy
assert f'"{name}"' in bootstrap
def test_load_shedding_requires_configured_post_verifier() -> None:
source = CONTROL.read_text(encoding="utf-8")
function = source[source.index("function Invoke-LoadShedding") :]
function = function[: function.index("function Get-AgentBooleanSetting")]
assert 'reason = "failure_backoff"' in function
assert 'postVerifyRequired = [bool]' in function
assert "$effectiveOk = if ($postVerifyRequired)" in function
assert "Set-Content -Path $failureMarkerPath" in function
assert "$run.ok -or $postVerifyOk" not in function
def test_disk_pressure_policy_is_versioned_and_migrated_by_deployer() -> None:
config = json.loads((ROOT / "agent99.config.99.example.json").read_text())
deploy = DEPLOY.read_text(encoding="utf-8")
action = next(
item
for item in config["performance"]["loadShedding"]["actions"]
if item["name"] == "prune-188-docker-build-cache-disk-pressure"
)
assert action["policyVersion"] == 2
assert "docker builder prune -af;" in action["command"]
assert "until=24h" not in action["command"]
assert action["failureBackoffMinutes"] == 10
assert "$script:ConfigMigrations" in deploy
assert '"policyVersion"' in deploy
assert "toPolicyVersion = $sourceVersion" in deploy