diff --git a/agent99-contract-check.ps1 b/agent99-contract-check.ps1 index 25f8be84e..9e7c80fd5 100644 --- a/agent99-contract-check.ps1 +++ b/agent99-contract-check.ps1 @@ -141,7 +141,8 @@ Add-Check "transport:dedicated_identity" ($control.Contains("sshIdentityFile") - Add-Check "transport:jump_first" ($control.Contains("preferJumpHost") -and $control.Contains("ssh_jump_fallback")) "preferred jump with bounded fallback is present" Add-Check "transport:stale_process_guard" ($control.Contains("Invoke-AgentStaleSshProcessGuard") -and $control.Contains("taskkill.exe /PID") -and $control.Contains("ssh_stale_process_guard")) "timed-out and stale SSH clients are bounded" Add-Check "transport:cross_process_serialization" ($control.Contains("Enter-AgentSshTransportLock") -and $control.Contains("[IO.FileShare]::None") -and $control.Contains("ssh_transport_lock_failed")) "cross-process SSH sessions are serialized" -Add-Check "windows99:input_independent_control" ($windowsControlBaseline.Contains('$FallbackInputTip = "0409:00000409"') -and $windowsControlBaseline.Contains('$PreservedChineseLanguage = "zh-Hant-TW"') -and $windowsControlBaseline.Contains('encodedCommandCompatible = $true') -and $windowsControlBaseline.Contains('inputMethodIndependent = $true') -and $windowsControlBaseline.Contains('Invoke-IndependentSnapshot') -and $windowsControlBaseline.Contains('Restore-WindowsControlBaseline')) "Windows99 keeps Traditional Chinese/Bopomofo while enforcing a US-keyboard and UTF-8 fallback with an independent verifier and rollback" +Add-Check "windows99:input_independent_control" ($windowsControlBaseline.Contains('$FallbackInputTip = "0409:00000409"') -and $windowsControlBaseline.Contains('$PreservedChineseLanguage = "zh-Hant-TW"') -and $windowsControlBaseline.Contains('encodedCommandCompatible = $true') -and $windowsControlBaseline.Contains('inputMethodIndependent = $true') -and $windowsControlBaseline.Contains('Invoke-IndependentSnapshot') -and $windowsControlBaseline.Contains('Restore-WindowsControlBaseline') -and $windowsControlBaseline.Contains('$Mode -eq "Check" -and $runtimeVerified')) "Windows99 keeps Traditional Chinese/Bopomofo while enforcing a US-keyboard and UTF-8 fallback with an independent verifier, fail-closed drift detection, and rollback" +Add-Check "windows99:selfcheck_control_baseline" ($controlPlane.Contains('function Test-AgentWindowsControlBaseline') -and $controlPlane.Contains('$windowsControlBaseline = Test-AgentWindowsControlBaseline $runtimeManifest') -and $controlPlane.Contains('windows_control_baseline_verified_no_write') -and $controlPlane.Contains('windowsControlBaseline = $windowsControlBaseline')) "Agent99 SelfCheck continuously verifies the Windows99 keyboard, UTF-8 console, and SSH control baseline without applying writes" $dbExecutorIdentityContract = [bool]( $dbBoundedExecutor.Contains('$CommandId = "telegram_receipt_index_v2_apply_v1"') -and $dbBoundedExecutor.Contains('$CanonicalAsset = "public.awooop_outbound_message"') -and diff --git a/agent99-control-plane.ps1 b/agent99-control-plane.ps1 index fb342cb5a..e79c610f8 100644 --- a/agent99-control-plane.ps1 +++ b/agent99-control-plane.ps1 @@ -4092,12 +4092,123 @@ function Test-AgentRuntimeManifest { } } +function Test-AgentWindowsControlBaseline { + param([object]$RuntimeManifest) + + $baselineScript = Join-Path $PSScriptRoot "agent99-windows-control-baseline.ps1" + $sourceRevision = if ($RuntimeManifest -and $RuntimeManifest.sourceRevision) { + ([string]$RuntimeManifest.sourceRevision).Trim().ToLowerInvariant() + } else { + "" + } + if (-not (Test-Path -LiteralPath $baselineScript -PathType Leaf)) { + return [pscustomobject]@{ + ok = $false + severity = "critical" + reason = "windows_control_baseline_script_missing" + runtimeWritePerformed = $false + evidenceRef = $null + evidenceSha256 = $null + } + } + if ($sourceRevision -notmatch "^[0-9a-f]{40}$") { + return [pscustomobject]@{ + ok = $false + severity = "critical" + reason = "windows_control_baseline_source_revision_invalid" + runtimeWritePerformed = $false + evidenceRef = $null + evidenceSha256 = $null + } + } + + $parsedRunId = [guid]::Empty + $runId = if ( + $AutomationRunId -and + [guid]::TryParse([string]$AutomationRunId, [ref]$parsedRunId) + ) { + [string]$AutomationRunId + } else { + [guid]::NewGuid().ToString() + } + $traceId = if ($TraceId -match "^[A-Za-z0-9][A-Za-z0-9._:@+\-]{0,127}$") { + [string]$TraceId + } else { + "agent99-selfcheck-$($runId.Substring(0, 12))" + } + $workItemId = if ($WorkItemId -match "^[A-Za-z0-9][A-Za-z0-9._:@+\-]{0,127}$") { + [string]$WorkItemId + } else { + "AG99-SELF-CONTROL-BASELINE" + } + $evidencePath = Join-Path $EvidenceDir "agent99-WindowsControlBaseline-$runId.json" + + try { + & $baselineScript ` + -Mode Check ` + -TraceId $traceId ` + -RunId $runId ` + -WorkItemId $workItemId ` + -SourceRevision $sourceRevision ` + -AgentRoot $Config.agentRoot | Out-Null + if (-not (Test-Path -LiteralPath $evidencePath -PathType Leaf)) { + throw "windows_control_baseline_evidence_missing" + } + $receipt = Get-Content -LiteralPath $evidencePath -Raw | ConvertFrom-Json + $identityMatched = [bool]( + [string]$receipt.runId -eq $runId -and + [string]$receipt.traceId -eq $traceId -and + [string]$receipt.workItemId -eq $workItemId -and + [string]$receipt.sourceRevision -eq $sourceRevision + ) + $verified = [bool]( + $identityMatched -and + $receipt.mode -eq "Check" -and + $receipt.execution.performed -eq $false -and + $receipt.verifier.passed -eq $true -and + $receipt.verifier.exactAgreement -eq $true -and + $receipt.transport.inputMethodIndependent -eq $true -and + $receipt.transport.encodedCommandCompatible -eq $true -and + $receipt.terminal -eq "verified_check_no_write" + ) + return [pscustomobject]@{ + ok = $verified + severity = if ($verified) { "ok" } else { "critical" } + reason = if ($verified) { "windows_control_baseline_verified_no_write" } else { "windows_control_baseline_drift_or_verifier_failed" } + runId = $runId + traceId = $traceId + workItemId = $workItemId + sourceRevision = $sourceRevision + runtimeWritePerformed = $false + identityMatched = $identityMatched + terminal = [string]$receipt.terminal + evidenceRef = Split-Path -Leaf $evidencePath + evidenceSha256 = (Get-FileHash -LiteralPath $evidencePath -Algorithm SHA256).Hash.ToLowerInvariant() + } + } catch { + return [pscustomobject]@{ + ok = $false + severity = "critical" + reason = "windows_control_baseline_readback_failed" + errorCode = if ([string]$_.Exception.Message -match "^[A-Za-z0-9_:-]{1,96}$") { [string]$_.Exception.Message } else { "windows_control_baseline_readback_failed" } + runId = $runId + traceId = $traceId + workItemId = $workItemId + sourceRevision = $sourceRevision + runtimeWritePerformed = $false + evidenceRef = if (Test-Path -LiteralPath $evidencePath -PathType Leaf) { Split-Path -Leaf $evidencePath } else { $null } + evidenceSha256 = if (Test-Path -LiteralPath $evidencePath -PathType Leaf) { (Get-FileHash -LiteralPath $evidencePath -Algorithm SHA256).Hash.ToLowerInvariant() } else { $null } + } + } +} + function Test-AgentSelfHealth { $selfConfig = Get-AgentSelfHealthConfig $tasks = Test-AgentScheduledTaskHealth $selfConfig.scheduledTasks $evidence = Test-AgentEvidenceFreshness $selfConfig.evidenceFreshness $tasks $latestPerf = Test-AgentLatestPerformanceEvidence $selfConfig.requiredHosts $runtimeManifest = Test-AgentRuntimeManifest + $windowsControlBaseline = Test-AgentWindowsControlBaseline $runtimeManifest $telegram = if ($selfConfig.requireRecentTelegramDelivery) { Test-AgentRecentTelegramDelivery $selfConfig.evidenceFreshness } else { @@ -4111,11 +4222,12 @@ function Test-AgentSelfHealth { if ($latestPerf.severity -eq "critical") { $critical += 1 } if ($telegram.severity -eq "critical") { $critical += 1 } if ($runtimeManifest.severity -eq "critical") { $critical += 1 } + if ($windowsControlBaseline.severity -eq "critical") { $critical += 1 } $warning = $evidenceWarning if ($latestPerf.severity -eq "warning") { $warning += 1 } $severity = if ($critical -gt 0) { "critical" } elseif ($warning -gt 0) { "warning" } else { "ok" } - $message = "selfHealth severity=$severity taskFailures=$taskFailures evidenceCritical=$evidenceCritical evidenceWarning=$evidenceWarning perf=$($latestPerf.severity) telegram=$($telegram.severity) runtimeManifest=$($runtimeManifest.severity)" + $message = "selfHealth severity=$severity taskFailures=$taskFailures evidenceCritical=$evidenceCritical evidenceWarning=$evidenceWarning perf=$($latestPerf.severity) telegram=$($telegram.severity) runtimeManifest=$($runtimeManifest.severity) windowsControlBaseline=$($windowsControlBaseline.severity)" Record-AgentEvent "agent_selfcheck" $(if ($severity -eq "ok") { "info" } else { $severity }) $message $null -Alert [pscustomobject]@{ @@ -4126,6 +4238,7 @@ function Test-AgentSelfHealth { latestPerformance = $latestPerf telegramDelivery = $telegram runtimeManifest = $runtimeManifest + windowsControlBaseline = $windowsControlBaseline summary = [pscustomobject]@{ taskFailures = $taskFailures evidenceCritical = $evidenceCritical @@ -4135,6 +4248,9 @@ function Test-AgentSelfHealth { runtimeManifestSeverity = $runtimeManifest.severity runtimeManifestSourceRevision = $runtimeManifest.sourceRevision runtimeManifestDriftCount = $runtimeManifest.driftCount + windowsControlBaselineSeverity = $windowsControlBaseline.severity + windowsControlBaselineReason = $windowsControlBaseline.reason + windowsControlBaselineEvidenceRef = $windowsControlBaseline.evidenceRef } } } @@ -5045,8 +5161,21 @@ function Get-AgentOutcomeContract { } "SelfCheck" { $selfOk = [bool]($data.selfHealth -and $data.selfHealth.severity -eq "ok") + $windowsBaseline = if ($data.selfHealth -and $data.selfHealth.PSObject.Properties["windowsControlBaseline"]) { $data.selfHealth.windowsControlBaseline } else { $null } + $windowsBaselineOk = [bool]( + $windowsBaseline -and + $windowsBaseline.ok -eq $true -and + $windowsBaseline.runtimeWritePerformed -eq $false -and + $windowsBaseline.reason -eq "windows_control_baseline_verified_no_write" -and + $windowsBaseline.evidenceRef -and + $windowsBaseline.evidenceSha256 -match "^[0-9a-f]{64}$" + ) $checks += New-AgentOutcomeCheck "agent_self_health_ok" $selfOk - $modeVerifierPassed = $selfOk + $checks += New-AgentOutcomeCheck "windows99_control_baseline_no_write" $windowsBaselineOk + if ($windowsBaselineOk) { + $modeEvidenceRefs["windows_control_baseline_evidence_ref"] = [string]$windowsBaseline.evidenceRef + } + $modeVerifierPassed = [bool]($selfOk -and $windowsBaselineOk) } "BackupCheck" { $backup = $data.backupHealth diff --git a/agent99-windows-control-baseline.ps1 b/agent99-windows-control-baseline.ps1 index b83beda78..9015cf110 100644 --- a/agent99-windows-control-baseline.ps1 +++ b/agent99-windows-control-baseline.ps1 @@ -340,7 +340,7 @@ if ($Mode -eq "Apply" -and $writeAttempted -and -not $runtimeVerified) { } $finishedAt = Get-Date -$terminal = if ($Mode -eq "Check" -and $independentAgreement -and -not $errorCode) { +$terminal = if ($Mode -eq "Check" -and $runtimeVerified) { "verified_check_no_write" } elseif ($runtimeVerified -and $writeAttempted) { "runtime_verified_controlled_apply" @@ -352,7 +352,7 @@ $terminal = if ($Mode -eq "Check" -and $independentAgreement -and -not $errorCod "degraded_with_safe_next_action" } $passed = [bool]( - ($Mode -eq "Check" -and $independentAgreement -and -not $errorCode) -or + ($Mode -eq "Check" -and $runtimeVerified) -or ($Mode -eq "Apply" -and $runtimeVerified) ) $receipt = [ordered]@{ diff --git a/apps/api/tests/test_agent99_alert_dispatch_order_contract.py b/apps/api/tests/test_agent99_alert_dispatch_order_contract.py index 6d008d522..6d1916e87 100644 --- a/apps/api/tests/test_agent99_alert_dispatch_order_contract.py +++ b/apps/api/tests/test_agent99_alert_dispatch_order_contract.py @@ -4,6 +4,7 @@ ROOT = Path(__file__).resolve().parents[3] SRE_INBOX = ROOT / "agent99-sre-alert-inbox.ps1" TELEGRAM_INBOX = ROOT / "agent99-telegram-inbox.ps1" CONTROL_PLANE = ROOT / "agent99-control-plane.ps1" +WINDOWS_CONTROL_BASELINE = ROOT / "agent99-windows-control-baseline.ps1" SRE_RELAY = ROOT / "agent99-sre-alert-relay.ps1" @@ -215,6 +216,43 @@ def test_agent99_control_plane_health_is_exact_selfcheck_no_write() -> None: assert scope_field in processing +def test_agent99_selfcheck_verifies_windows99_control_baseline_no_write() -> None: + source = CONTROL_PLANE.read_text(encoding="utf-8") + verifier = source[ + source.index("function Test-AgentWindowsControlBaseline") : source.index( + "function Test-AgentSelfHealth" + ) + ] + selfcheck = source[ + source.index("function Test-AgentSelfHealth") : source.index( + "function Get-AgentUnixAgeMinutes" + ) + ] + + assert '"agent99-windows-control-baseline.ps1"' in verifier + assert "-Mode Check" in verifier + assert "-Mode Apply" not in verifier + assert 'runtimeWritePerformed = $false' in verifier + assert 'receipt.verifier.passed -eq $true' in verifier + assert 'receipt.transport.inputMethodIndependent -eq $true' in verifier + assert 'receipt.transport.encodedCommandCompatible -eq $true' in verifier + assert 'receipt.terminal -eq "verified_check_no_write"' in verifier + assert "$windowsControlBaseline = Test-AgentWindowsControlBaseline" in selfcheck + assert 'windowsControlBaseline = $windowsControlBaseline' in selfcheck + assert 'windowsControlBaselineSeverity = $windowsControlBaseline.severity' in selfcheck + assert 'New-AgentOutcomeCheck "windows99_control_baseline_no_write"' in source + assert '"windows_control_baseline_evidence_ref"' in source + + +def test_windows99_control_baseline_check_fails_closed_on_desired_state_drift() -> None: + source = WINDOWS_CONTROL_BASELINE.read_text(encoding="utf-8") + terminal = source[source.index("$terminal = if") : source.index("$receipt =")] + + assert '$Mode -eq "Check" -and $runtimeVerified' in terminal + assert '$Mode -eq "Check" -and $independentAgreement' not in terminal + assert '$Mode -eq "Check" -and $runtimeVerified' in source[source.index("$passed =") :] + + def test_agent99_typed_guard_rejects_before_dedupe_or_queue_write() -> None: source = SRE_INBOX.read_text(encoding="utf-8") processing = source[source.index("foreach ($file in $files)") :]