From 6dbdabcc8307bbc17606d3af12e57763e250cafe Mon Sep 17 00:00:00 2001 From: ogt Date: Fri, 17 Jul 2026 11:11:52 +0800 Subject: [PATCH] fix(agent99): hash baseline evidence identity --- agent99-contract-check.ps1 | 1 + agent99-control-plane.ps1 | 8 ++++-- agent99-windows-control-baseline.ps1 | 17 ++++++++++-- ...t_agent99_alert_dispatch_order_contract.py | 9 +++++++ .../agent99-remote-atomic-deploy-receiver.ps1 | 12 ++++++++- ...remote_atomic_deploy_transport_contract.py | 7 +++++ .../test_agent99_windows_control_baseline.py | 27 +++++++++++++++++++ 7 files changed, 76 insertions(+), 5 deletions(-) diff --git a/agent99-contract-check.ps1 b/agent99-contract-check.ps1 index d9404fad3..46f850946 100644 --- a/agent99-contract-check.ps1 +++ b/agent99-contract-check.ps1 @@ -145,6 +145,7 @@ Add-Check "transport:stale_process_guard" ($control.Contains("Invoke-AgentStaleS 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') -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" ($control.Contains('function Test-AgentWindowsControlBaseline') -and $control.Contains('$windowsControlBaseline = Test-AgentWindowsControlBaseline $runtimeManifest') -and $control.Contains('windows_control_baseline_verified_no_write') -and $control.Contains('windowsControlBaseline = $windowsControlBaseline')) "Agent99 SelfCheck continuously verifies the Windows99 keyboard, UTF-8 console, and SSH control baseline without applying writes" +Add-Check "windows99:baseline_evidence_identity" ($windowsControlBaseline.Contains('$EvidenceRunToken = Get-AgentRunEvidenceToken -Value $RunId') -and $windowsControlBaseline.Contains('agent99-WindowsControlBaseline-$EvidenceRunToken.json') -and $windowsControlBaseline.Contains('algorithm = "sha256_utf8"') -and $windowsControlBaseline.Contains('rawIdentityInFileName = $false') -and $control.Contains('$evidenceRunToken = Get-AgentSha256Hex $runId') -and $control.Contains('agent99-WindowsControlBaseline-$evidenceRunToken.json') -and $control.Contains('$receipt.evidenceIdentity.runToken -eq $evidenceRunToken') -and -not $windowsControlBaseline.Contains('agent99-WindowsControlBaseline-$RunId.json') -and -not $control.Contains('agent99-WindowsControlBaseline-$runId.json')) "Windows99 baseline producer and SelfCheck consumer bind the same SHA-256 run token without placing raw controlled-apply identities in filenames" Add-Check "queue:stale_running_claim_recovery" ($control.Contains('function Get-AgentRunningQueueHealth') -and $control.Contains('function Invoke-AgentStaleRunningQueueRecovery') -and $control.Contains('move_stale_running_claims_to_failed_quarantine') -and $control.Contains('quarantine_preserves_original_json_and_sha256') -and $control.Contains('$file.Name -in $staleRunningNames') -and $control.Contains('runningQueueStaleCount = $runningQueue.staleCount')) "Agent99 quarantines expired running claims without replay or deletion, verifies preserved SHA256, exposes stale counts in SelfCheck, and does not let stale Recover claims suppress new recovery" $dbExecutorIdentityContract = [bool]( $dbBoundedExecutor.Contains('$CommandId = "telegram_receipt_index_v2_apply_v1"') -and diff --git a/agent99-control-plane.ps1 b/agent99-control-plane.ps1 index c9836f63d..b8ebc6a6d 100644 --- a/agent99-control-plane.ps1 +++ b/agent99-control-plane.ps1 @@ -4300,7 +4300,8 @@ function Test-AgentWindowsControlBaseline { } else { "AG99-SELF-CONTROL-BASELINE" } - $evidencePath = Join-Path $EvidenceDir "agent99-WindowsControlBaseline-$runId.json" + $evidenceRunToken = Get-AgentSha256Hex $runId + $evidencePath = Join-Path $EvidenceDir "agent99-WindowsControlBaseline-$evidenceRunToken.json" try { & $baselineScript ` @@ -4318,7 +4319,10 @@ function Test-AgentWindowsControlBaseline { [string]$receipt.runId -eq $runId -and [string]$receipt.traceId -eq $traceId -and [string]$receipt.workItemId -eq $workItemId -and - [string]$receipt.sourceRevision -eq $sourceRevision + [string]$receipt.sourceRevision -eq $sourceRevision -and + [string]$receipt.evidenceIdentity.algorithm -eq "sha256_utf8" -and + [string]$receipt.evidenceIdentity.runToken -eq $evidenceRunToken -and + $receipt.evidenceIdentity.rawIdentityInFileName -eq $false ) $verified = [bool]( $identityMatched -and diff --git a/agent99-windows-control-baseline.ps1 b/agent99-windows-control-baseline.ps1 index 9015cf110..92e544364 100644 --- a/agent99-windows-control-baseline.ps1 +++ b/agent99-windows-control-baseline.ps1 @@ -2,7 +2,7 @@ param( [ValidateSet("Check", "Apply")][string]$Mode = "Check", [Parameter(Mandatory = $true)][ValidatePattern("^[A-Za-z0-9][A-Za-z0-9._:@+-]{0,127}$")][string]$TraceId, - [Parameter(Mandatory = $true)][ValidatePattern("^[0-9a-f-]{36}$")][string]$RunId, + [Parameter(Mandatory = $true)][ValidatePattern("^[A-Za-z0-9][A-Za-z0-9._:@+-]{0,127}$")][string]$RunId, [Parameter(Mandatory = $true)][ValidatePattern("^[A-Za-z0-9][A-Za-z0-9._:@+-]{0,127}$")][string]$WorkItemId, [Parameter(Mandatory = $true)][ValidatePattern("^[0-9a-f]{40}$")][string]$SourceRevision, [string]$AgentRoot = "C:\Wooo\Agent99" @@ -15,6 +15,17 @@ $ProgressPreference = "SilentlyContinue" [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false) $global:OutputEncoding = [System.Text.UTF8Encoding]::new($false) +function Get-AgentRunEvidenceToken { + param([Parameter(Mandatory = $true)][string]$Value) + + $sha = [Security.Cryptography.SHA256]::Create() + try { + return (($sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($Value)) | ForEach-Object { $_.ToString("x2") }) -join "") + } finally { + $sha.Dispose() + } +} + $SchemaVersion = "agent99_windows_control_baseline_v1" $CanonicalAsset = "host:192.168.0.99/windows-control-baseline" $ExpectedComputerName = "WOOO-SUPER" @@ -25,7 +36,8 @@ $FallbackLanguage = "en-US" $FallbackInputTip = "0409:00000409" $ConsoleRegistryPath = "HKCU:\Console" $EvidenceRoot = [System.IO.Path]::GetFullPath((Join-Path $AgentRoot "evidence\")) -$EvidencePath = [System.IO.Path]::GetFullPath((Join-Path $EvidenceRoot "agent99-WindowsControlBaseline-$RunId.json")) +$EvidenceRunToken = Get-AgentRunEvidenceToken -Value $RunId +$EvidencePath = [System.IO.Path]::GetFullPath((Join-Path $EvidenceRoot "agent99-WindowsControlBaseline-$EvidenceRunToken.json")) $startedAt = Get-Date $actions = @() $errorCode = $null @@ -360,6 +372,7 @@ $receipt = [ordered]@{ traceId = $TraceId runId = $RunId workItemId = $WorkItemId + evidenceIdentity = [ordered]@{ algorithm = "sha256_utf8"; runToken = $EvidenceRunToken; rawIdentityInFileName = $false } generatedAt = $finishedAt.ToString("o") sourceRevision = $SourceRevision executor = "host:192.168.0.99" 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 d05a4585f..73abb7632 100644 --- a/apps/api/tests/test_agent99_alert_dispatch_order_contract.py +++ b/apps/api/tests/test_agent99_alert_dispatch_order_contract.py @@ -237,6 +237,11 @@ def test_agent99_selfcheck_verifies_windows99_control_baseline_no_write() -> Non 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 "Get-AgentSha256Hex" in verifier + assert "$evidenceRunToken = Get-AgentSha256Hex $runId" in verifier + assert '"agent99-WindowsControlBaseline-$evidenceRunToken.json"' in verifier + assert '"agent99-WindowsControlBaseline-$runId.json"' not in verifier + assert 'receipt.evidenceIdentity.runToken -eq $evidenceRunToken' in verifier assert "$windowsControlBaseline = Test-AgentWindowsControlBaseline" in selfcheck assert 'windowsControlBaseline = $windowsControlBaseline' in selfcheck assert 'windowsControlBaselineSeverity = $windowsControlBaseline.severity' in selfcheck @@ -263,6 +268,10 @@ def test_windows99_contract_check_reads_selfcheck_from_defined_control_source() assert "$control.Contains" in contract assert "$controlPlane.Contains" not in contract + assert 'Add-Check "windows99:baseline_evidence_identity"' in contract + assert "$windowsControlBaseline.Contains" in contract + assert "agent99-WindowsControlBaseline-$EvidenceRunToken.json" in contract + assert "agent99-WindowsControlBaseline-$evidenceRunToken.json" in contract def test_agent99_quarantines_stale_running_claims_without_replay() -> None: diff --git a/scripts/reboot-recovery/agent99-remote-atomic-deploy-receiver.ps1 b/scripts/reboot-recovery/agent99-remote-atomic-deploy-receiver.ps1 index 61bbe3058..af83a6372 100644 --- a/scripts/reboot-recovery/agent99-remote-atomic-deploy-receiver.ps1 +++ b/scripts/reboot-recovery/agent99-remote-atomic-deploy-receiver.ps1 @@ -86,6 +86,12 @@ function Get-AgentSha256File { return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant() } +function Get-AgentRunEvidenceToken { + param([string]$Value) + + return Get-AgentSha256Bytes ([Text.Encoding]::UTF8.GetBytes($Value)) +} + function Get-AgentLauncherContract { $requiredMarkers = @( '[switch]$ReconcileOnly', @@ -318,7 +324,8 @@ function Invoke-AgentWindowsControlBaseline { ) $scriptPath = Join-Path $BinDir "agent99-windows-control-baseline.ps1" - $evidencePath = Join-Path $EvidenceDir "agent99-WindowsControlBaseline-$RunId.json" + $evidenceRunToken = Get-AgentRunEvidenceToken -Value $RunId + $evidencePath = Join-Path $EvidenceDir "agent99-WindowsControlBaseline-$evidenceRunToken.json" $arguments = @( "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", $scriptPath, @@ -345,6 +352,9 @@ function Invoke-AgentWindowsControlBaseline { [string]$evidence.runId -eq $RunId -and [string]$evidence.workItemId -eq $WorkItemId -and [string]$evidence.sourceRevision -eq $SourceRevision -and + [string]$evidence.evidenceIdentity.algorithm -eq "sha256_utf8" -and + [string]$evidence.evidenceIdentity.runToken -eq $evidenceRunToken -and + $evidence.evidenceIdentity.rawIdentityInFileName -eq $false -and [string]$evidence.terminal -in @("runtime_verified_controlled_apply", "runtime_verified_healthy_no_write") -and $evidence.verifier.passed -eq $true -and $evidence.transport.inputMethodIndependent -eq $true -and 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 f4544e578..c00631130 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 @@ -265,11 +265,18 @@ def test_receiver_rolls_back_failed_deploy_or_failed_independent_post_verifier() assert "$runtimeManifest.sourceRevision -eq $sourceRevision" in source assert '$runtimeManifest.fileCount -eq 18' in source assert "function Invoke-AgentWindowsControlBaseline" in source + assert "function Get-AgentRunEvidenceToken" in source + assert "$evidenceRunToken = Get-AgentRunEvidenceToken -Value $RunId" in source + assert '"agent99-WindowsControlBaseline-$evidenceRunToken.json"' in source + assert '"agent99-WindowsControlBaseline-$RunId.json"' not in source assert '"agent99-windows-control-baseline.ps1"' in source assert '"-Mode", "Apply"' in source assert '"agent99_windows_control_baseline_v1"' in source assert '"runtime_verified_controlled_apply"' in source assert '"runtime_verified_healthy_no_write"' in source + assert '[string]$evidence.evidenceIdentity.algorithm -eq "sha256_utf8"' in source + assert '[string]$evidence.evidenceIdentity.runToken -eq $evidenceRunToken' in source + assert "$evidence.evidenceIdentity.rawIdentityInFileName -eq $false" in source assert "$evidence.transport.inputMethodIndependent -eq $true" in source assert "$evidence.risk.hostReboot -eq $false" in source assert "$evidence.risk.userLogoff -eq $false" in source diff --git a/scripts/reboot-recovery/tests/test_agent99_windows_control_baseline.py b/scripts/reboot-recovery/tests/test_agent99_windows_control_baseline.py index 0fc6e7bc5..3009e3d5d 100644 --- a/scripts/reboot-recovery/tests/test_agent99_windows_control_baseline.py +++ b/scripts/reboot-recovery/tests/test_agent99_windows_control_baseline.py @@ -3,6 +3,7 @@ from pathlib import Path ROOT = Path(__file__).resolve().parents[3] BASELINE = ROOT / "agent99-windows-control-baseline.ps1" +CONTROL_PLANE = ROOT / "agent99-control-plane.ps1" def _source() -> str: @@ -41,6 +42,32 @@ def test_baseline_supports_check_apply_independent_verify_and_rollback() -> None assert '"degraded_with_safe_next_action"' in source +def test_baseline_accepts_transport_run_ids_but_hashes_the_evidence_filename() -> None: + source = _source() + + safe_identity_pattern = "^[A-Za-z0-9][A-Za-z0-9._:@+-]{0,127}$" + assert source.count(f'[ValidatePattern("{safe_identity_pattern}")]') == 3 + assert "function Get-AgentRunEvidenceToken" in source + assert "$EvidenceRunToken = Get-AgentRunEvidenceToken -Value $RunId" in source + assert '"agent99-WindowsControlBaseline-$EvidenceRunToken.json"' in source + assert '"agent99-WindowsControlBaseline-$RunId.json"' not in source + assert 'algorithm = "sha256_utf8"' in source + assert "runToken = $EvidenceRunToken" in source + assert "rawIdentityInFileName = $false" in source + + +def test_control_plane_reads_the_same_hashed_baseline_receipt() -> None: + source = CONTROL_PLANE.read_text(encoding="utf-8") + + assert "function Get-AgentSha256Hex" in source + assert "$evidenceRunToken = Get-AgentSha256Hex $runId" in source + assert '"agent99-WindowsControlBaseline-$evidenceRunToken.json"' in source + assert '"agent99-WindowsControlBaseline-$runId.json"' not in source + assert '[string]$receipt.evidenceIdentity.algorithm -eq "sha256_utf8"' in source + assert '[string]$receipt.evidenceIdentity.runToken -eq $evidenceRunToken' in source + assert "$receipt.evidenceIdentity.rawIdentityInFileName -eq $false" in source + + def test_baseline_sets_only_the_bounded_control_settings() -> None: source = _source()