diff --git a/agent99-control-plane.ps1 b/agent99-control-plane.ps1 index fe2163d56..b7176a386 100644 --- a/agent99-control-plane.ps1 +++ b/agent99-control-plane.ps1 @@ -5011,6 +5011,26 @@ function Update-AgentProblemKnowledge { } } +function Get-AgentCompletionCallbackIncidentId { + param( + [object]$Result, + [string]$FallbackAlertId = "" + ) + + $identity = Get-AgentObjectValue $Result "identity" $null + $canonicalIncidentId = [string](Get-AgentObjectValue $identity "incident_id" "") + foreach ($candidate in @($canonicalIncidentId, $FallbackAlertId)) { + if ($candidate -and $candidate.Length -le 30) { + return $candidate + } + } + + $surrogateSource = if ($canonicalIncidentId) { $canonicalIncidentId } else { $FallbackAlertId } + if (-not $surrogateSource) { return $null } + $digest = Get-AgentSha256Hex $surrogateSource + return "INC-AG99-$($digest.Substring(0, 21).ToUpperInvariant())" +} + function Get-AgentCompletionCallbackSettings { if (-not $Config.PSObject.Properties["completionCallback"] -or -not $Config.completionCallback) { return [pscustomobject]@{ enabled = $false } @@ -5072,6 +5092,7 @@ function Invoke-AgentCompletionCallback { $outcomeState = [string](Get-AgentObjectValue $outcome "state" (Get-AgentObjectValue $Result "outcomeState" "failed")) $resultId = [string](Get-AgentObjectValue $Result "id" "unknown") $alertId = [string](Get-AgentObjectValue $Result "alertId" "") + $callbackIncidentId = Get-AgentCompletionCallbackIncidentId $Result $alertId $correlationKey = [string](Get-AgentObjectValue $Result "correlationKey" "") $callbackId = Convert-AgentSafeKey "agent99:$resultId`:$outcomeState" $evidencePath = [string](Get-AgentObjectValue $Result "evidence" "") @@ -5084,7 +5105,7 @@ function Invoke-AgentCompletionCallback { run_id = $resultId trace_id = if ($correlationKey) { $correlationKey } else { $resultId } work_item_id = if ($alertId) { "agent99-incident:$alertId" } else { "agent99-command:$resultId" } - alert_id = if ($alertId) { $alertId } else { $null } + alert_id = $callbackIncidentId correlation_key = if ($correlationKey) { $correlationKey } else { $null } source = [string](Get-AgentObjectValue $Result "source" "agent99") mode = [string](Get-AgentObjectValue $Result "mode" "Status") diff --git a/agent99-synthetic-tests.ps1 b/agent99-synthetic-tests.ps1 index a9c243a21..7f479f933 100644 --- a/agent99-synthetic-tests.ps1 +++ b/agent99-synthetic-tests.ps1 @@ -42,6 +42,7 @@ foreach ($functionName in @( "Get-AgentIncidentCardModel", "Format-AgentTelegramText", "Format-AgentTelegramCaption", + "Get-AgentCompletionCallbackIncidentId", "Convert-AgentRecoveryReadback", "Get-HostSshUser", "Get-AgentSshTransportConfig", @@ -60,6 +61,18 @@ foreach ($functionName in @( $formatFunctions[$functionName] = $true } } + +if ($formatFunctions.ContainsKey("Get-AgentCompletionCallbackIncidentId")) { + $canonicalIncident = Get-AgentCompletionCallbackIncidentId ([pscustomobject]@{ + identity = [pscustomobject]@{ incident_id = "INC-20260714-A1B2C3" } + }) "awoooi-agent99-ae36a454-c9eb-5f2d-9e35-be852b3eac9b" + $surrogateIncident = Get-AgentCompletionCallbackIncidentId $null "awoooi-agent99-ae36a454-c9eb-5f2d-9e35-be852b3eac9b" + Add-SyntheticCheck "callback:canonical_incident_identity" ($canonicalIncident -eq "INC-20260714-A1B2C3") "canonical dispatch identity wins over external alert id" + Add-SyntheticCheck "callback:bounded_incident_surrogate" ($surrogateIncident -match '^INC-AG99-[A-F0-9]{21}$' -and $surrogateIncident.Length -eq 30) "long external identity is deterministic and schema-bounded" +} else { + Add-SyntheticCheck "callback:canonical_incident_parser" $false "Get-AgentCompletionCallbackIncidentId not found" +} + if ($formatFunctions.ContainsKey("Format-AgentTelegramText")) { $perfCard = Format-AgentTelegramText "warning" "performance_warning" "test" ([pscustomobject]@{ host = "192.168.0.120" diff --git a/apps/api/tests/test_agent99_control_plane_outcome_contract.py b/apps/api/tests/test_agent99_control_plane_outcome_contract.py index b5136d97c..fabffea4b 100644 --- a/apps/api/tests/test_agent99_control_plane_outcome_contract.py +++ b/apps/api/tests/test_agent99_control_plane_outcome_contract.py @@ -102,6 +102,17 @@ def test_agent99_posts_fenced_outcome_to_production_ingestion() -> None: assert field in source +def test_agent99_completion_callback_uses_bounded_canonical_incident_id() -> None: + source = _CONTROL_PLANE.read_text(encoding="utf-8") + + assert "function Get-AgentCompletionCallbackIncidentId" in source + assert 'Get-AgentObjectValue $identity "incident_id" ""' in source + assert 'return "INC-AG99-' in source + assert "$callbackIncidentId = Get-AgentCompletionCallbackIncidentId" in source + assert "alert_id = $callbackIncidentId" in source + assert "alert_id = if ($alertId)" not in source + + def test_agent99_backupcheck_reads_offsite_and_escrow_without_mutation() -> None: source = _CONTROL_PLANE.read_text(encoding="utf-8") readback = source[