fix(agent99): bind local controlled dispatch identity
This commit is contained in:
@@ -93,6 +93,7 @@ $telegram = [IO.File]::ReadAllText((Join-Path $SourceRoot "agent99-telegram-inbo
|
||||
$bootstrap = [IO.File]::ReadAllText((Join-Path $SourceRoot "agent99-bootstrap.ps1"), [Text.Encoding]::UTF8)
|
||||
$deployer = [IO.File]::ReadAllText((Join-Path $SourceRoot "agent99-deploy.ps1"), [Text.Encoding]::UTF8)
|
||||
$taskRegistration = [IO.File]::ReadAllText((Join-Path $SourceRoot "agent99-register-tasks.ps1"), [Text.Encoding]::UTF8)
|
||||
$submitRequest = [IO.File]::ReadAllText((Join-Path $SourceRoot "agent99-submit-request.ps1"), [Text.Encoding]::UTF8)
|
||||
|
||||
Add-Check "transport:dedicated_identity" ($control.Contains("sshIdentityFile") -and $control.Contains("IdentitiesOnly=yes")) "dedicated SSH identity is enforced"
|
||||
Add-Check "transport:jump_first" ($control.Contains("preferJumpHost") -and $control.Contains("ssh_jump_fallback")) "preferred jump with bounded fallback is present"
|
||||
@@ -105,6 +106,7 @@ Add-Check "recovery:slo" ($control.Contains("recovery_slo_result") -and $control
|
||||
Add-Check "recovery:full_sop_coordinator" ($control.Contains("Invoke-AgentRecoveryCoordinatorReadback") -and $control.Contains("full_sop_scorecard") -and $control.Contains("SCORECARD_FRESH_REBOOT_WINDOW") -and $control.Contains('scope = $recoveryScope')) "Agent99 cannot close reboot recovery without the 110 full-SOP scorecard"
|
||||
Add-Check "recovery:host111_wol_executor" ($control.Contains("Invoke-AgentExternalHostRecovery") -and $control.Contains("Send-AgentWakeOnLan") -and $control.Contains('Get-AgentObjectValue $externalHost "recoveryAction" "wake_on_lan"') -and $control.Contains('New-AgentRecoveryPhase "external_host_recovery"') -and $control.Contains('New-AgentOutcomeCheck "external_hosts_recovered"')) "Host111 has an allowlisted WOL executor with independent ping/TCP verification"
|
||||
Add-Check "recovery:startup_queue_closure" ($taskRegistration.Contains('$submitRequest') -and $taskRegistration.Contains('-Mode Recover -ControlledApply -RunNow') -and $taskRegistration.Contains('-Source agent99-startup-recovery')) "startup recovery runs through queue, outcome, callback, Telegram lifecycle, and KM/RAG"
|
||||
Add-Check "recovery:startup_controlled_identity" ($submitRequest.Contains("New-AgentControlledDispatchIdentity") -and $submitRequest.Contains("New-AgentUuidV5") -and $submitRequest.Contains("agent99_controlled_dispatch_identity_v1") -and $submitRequest.Contains("canonicalDigest") -and $submitRequest.Contains("singleFlightKey") -and $submitRequest.Contains("approvalId")) "local and startup controlled requests carry the canonical AWOOOI dispatch identity"
|
||||
Add-Check "recovery:host112_immutable_apply_closure" ($control.Contains("managerApplyVerified") -and $control.Contains("durableReceiptVerified") -and $control.Contains('durableReceiptTerminal -eq "verified_healthy"') -and $control.Contains('durableReadbackReceiptLinkMatch') -and $control.Contains('durableReadbackTerminalMatch') -and $control.Contains('artifactTransactionStatus -eq "committed_verified_pair"') -and $control.Contains('artifactTransactionPriorPairVerified') -and $control.Contains('managerAttemptCount -eq "1"') -and $control.Contains('managerStartExit -eq "0"') -and $control.Contains('afterVerifierRole = "independent_second_verifier_only"')) "Host112 apply closure binds transport, fixed WAL, attributed start, immutable receipt/readback and independent verifier"
|
||||
Add-Check "recovery:host112_direct_transport" ($control.Contains("Invoke-AgentHost112SshText") -and $control.Contains('expectedUser = "kali"') -and $control.Contains('route = "direct_host112_kali"') -and $deployer.Contains('name = "host112_canonical_direct_route"') -and $bootstrap.Contains("Ensure-AgentHost112CanonicalSshConfig")) "Host112 recovery uses the canonical 99 direct key route"
|
||||
Add-Check "recovery:host112_canonical_vmx" ($deployer.Contains('name = "host112_canonical_vmx_path"') -and $deployer.Contains('Get-AgentHost112CanonicalVm $policyConfigPath') -and $deployer.Contains('Test-Path -LiteralPath $canonicalHost112Vmx -PathType Leaf') -and $bootstrap.Contains('Get-AgentHost112CanonicalVm $PolicyPath') -and $bootstrap.Contains('Set-AgentBootstrapProperty $config "vms" @(@($nonHost112Vms) + @($canonicalHost112Vm))')) "Host112 VM identity is normalized from one staged policy row and fails closed when absent"
|
||||
|
||||
@@ -35,6 +35,124 @@ function Test-AgentContainsChar {
|
||||
return $false
|
||||
}
|
||||
|
||||
function ConvertTo-AgentStableJson {
|
||||
param([System.Collections.IDictionary]$Value)
|
||||
return (ConvertTo-Json -InputObject $Value -Depth 8 -Compress)
|
||||
}
|
||||
|
||||
function Get-AgentSha256 {
|
||||
param([string]$Text)
|
||||
$sha = [System.Security.Cryptography.SHA256]::Create()
|
||||
try {
|
||||
$bytes = [Text.Encoding]::UTF8.GetBytes($Text)
|
||||
return ([BitConverter]::ToString($sha.ComputeHash($bytes))).Replace("-", "").ToLowerInvariant()
|
||||
} finally {
|
||||
$sha.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
function Convert-AgentGuidToNetworkBytes {
|
||||
param([guid]$Guid)
|
||||
[byte[]]$bytes = $Guid.ToByteArray()
|
||||
return [byte[]]@(
|
||||
$bytes[3], $bytes[2], $bytes[1], $bytes[0],
|
||||
$bytes[5], $bytes[4], $bytes[7], $bytes[6],
|
||||
$bytes[8], $bytes[9], $bytes[10], $bytes[11],
|
||||
$bytes[12], $bytes[13], $bytes[14], $bytes[15]
|
||||
)
|
||||
}
|
||||
|
||||
function New-AgentUuidV5 {
|
||||
param(
|
||||
[guid]$Namespace,
|
||||
[string]$Name
|
||||
)
|
||||
[byte[]]$namespaceBytes = Convert-AgentGuidToNetworkBytes $Namespace
|
||||
[byte[]]$nameBytes = [Text.Encoding]::UTF8.GetBytes($Name)
|
||||
[byte[]]$inputBytes = $namespaceBytes + $nameBytes
|
||||
$sha1 = [System.Security.Cryptography.SHA1]::Create()
|
||||
try {
|
||||
[byte[]]$hash = $sha1.ComputeHash($inputBytes)
|
||||
} finally {
|
||||
$sha1.Dispose()
|
||||
}
|
||||
$hash[6] = [byte](($hash[6] -band 0x0f) -bor 0x50)
|
||||
$hash[8] = [byte](($hash[8] -band 0x3f) -bor 0x80)
|
||||
$hex = -join @($hash[0..15] | ForEach-Object { $_.ToString("x2") })
|
||||
$uuidText = "{0}-{1}-{2}-{3}-{4}" -f $hex.Substring(0, 8), $hex.Substring(8, 4), $hex.Substring(12, 4), $hex.Substring(16, 4), $hex.Substring(20, 12)
|
||||
return [guid]::Parse($uuidText)
|
||||
}
|
||||
|
||||
function New-AgentControlledDispatchIdentity {
|
||||
param(
|
||||
[string]$RequestId,
|
||||
[string]$RequestSource,
|
||||
[string]$RequestExternalId
|
||||
)
|
||||
|
||||
$projectId = "awoooi"
|
||||
$incidentId = "agent99-local-$RequestId"
|
||||
$sourceFingerprint = Get-AgentSha256 "$RequestSource|$RequestExternalId|$RequestId"
|
||||
$routeId = "agent99_local_submit"
|
||||
$executionGeneration = "1"
|
||||
$approvalId = ""
|
||||
$workItemId = "agent99-local:$RequestId"
|
||||
$normalized = [ordered]@{
|
||||
approval_id = $approvalId
|
||||
execution_generation = $executionGeneration
|
||||
incident_id = $incidentId
|
||||
project_id = $projectId
|
||||
route_id = $routeId
|
||||
source_fingerprint = $sourceFingerprint
|
||||
work_item_id = $workItemId
|
||||
}
|
||||
$canonical = ConvertTo-AgentStableJson $normalized
|
||||
$digest = Get-AgentSha256 $canonical
|
||||
$singleFlightCanonical = ConvertTo-AgentStableJson ([ordered]@{
|
||||
incident_id = $incidentId
|
||||
project_id = $projectId
|
||||
route_id = $routeId
|
||||
source_fingerprint = $sourceFingerprint
|
||||
})
|
||||
$singleFlightDigest = Get-AgentSha256 $singleFlightCanonical
|
||||
$runId = (New-AgentUuidV5 -Namespace ([guid]::Parse("6ba7b811-9dad-11d1-80b4-00c04fd430c8")) -Name "agent99-controlled-dispatch:$canonical").ToString()
|
||||
$traceId = "00-$($digest.Substring(0, 32))-$($digest.Substring(32, 16))-01"
|
||||
$idempotencyKey = "agent99-controlled:$digest"
|
||||
$singleFlightKey = "agent99-controlled-flight:$singleFlightDigest"
|
||||
$public = [ordered]@{
|
||||
approval_id = $approvalId
|
||||
execution_generation = $executionGeneration
|
||||
idempotency_key = $idempotencyKey
|
||||
incident_id = $incidentId
|
||||
lock_owner = $runId
|
||||
project_id = $projectId
|
||||
route_id = $routeId
|
||||
run_id = $runId
|
||||
schema_version = "agent99_controlled_dispatch_identity_v1"
|
||||
single_flight_key = $singleFlightKey
|
||||
source_fingerprint = $sourceFingerprint
|
||||
trace_id = $traceId
|
||||
work_item_id = $workItemId
|
||||
}
|
||||
|
||||
return [pscustomobject]@{
|
||||
schemaVersion = [string]$public.schema_version
|
||||
projectId = $projectId
|
||||
incidentId = $incidentId
|
||||
sourceFingerprint = $sourceFingerprint
|
||||
dispatchRouteId = $routeId
|
||||
executionGeneration = $executionGeneration
|
||||
approvalId = $approvalId
|
||||
automationRunId = $runId
|
||||
traceId = $traceId
|
||||
workItemId = $workItemId
|
||||
idempotencyKey = $idempotencyKey
|
||||
singleFlightKey = $singleFlightKey
|
||||
lockOwner = $runId
|
||||
canonicalDigest = Get-AgentSha256 (ConvertTo-AgentStableJson $public)
|
||||
}
|
||||
}
|
||||
|
||||
function Resolve-AgentRequestMode {
|
||||
param(
|
||||
[string]$Text,
|
||||
@@ -98,6 +216,25 @@ $evidenceDir = Join-Path $AgentRoot "evidence"
|
||||
New-Item -ItemType Directory -Force -Path $queueDir, $requestDir, $evidenceDir | Out-Null
|
||||
|
||||
$id = "request-" + (Get-Date -Format "yyyyMMdd-HHmmss") + "-" + ([guid]::NewGuid().ToString("N").Substring(0, 8))
|
||||
$dispatchIdentity = [pscustomobject]@{
|
||||
schemaVersion = ""
|
||||
projectId = ""
|
||||
incidentId = ""
|
||||
sourceFingerprint = ""
|
||||
dispatchRouteId = ""
|
||||
executionGeneration = ""
|
||||
approvalId = ""
|
||||
automationRunId = ""
|
||||
traceId = ""
|
||||
workItemId = ""
|
||||
idempotencyKey = ""
|
||||
singleFlightKey = ""
|
||||
lockOwner = ""
|
||||
canonicalDigest = ""
|
||||
}
|
||||
if ($ControlledApply) {
|
||||
$dispatchIdentity = New-AgentControlledDispatchIdentity -RequestId $id -RequestSource $Source -RequestExternalId $ExternalId
|
||||
}
|
||||
$request = [pscustomobject]@{
|
||||
id = $id
|
||||
instruction = $Instruction
|
||||
@@ -106,6 +243,20 @@ $request = [pscustomobject]@{
|
||||
requestedBy = if ($RequestedBy) { $RequestedBy } else { $env:USERNAME }
|
||||
source = $Source
|
||||
externalId = $ExternalId
|
||||
dispatchIdentitySchemaVersion = $dispatchIdentity.schemaVersion
|
||||
automationRunId = $dispatchIdentity.automationRunId
|
||||
traceId = $dispatchIdentity.traceId
|
||||
workItemId = $dispatchIdentity.workItemId
|
||||
idempotencyKey = $dispatchIdentity.idempotencyKey
|
||||
executionGeneration = $dispatchIdentity.executionGeneration
|
||||
incidentId = $dispatchIdentity.incidentId
|
||||
approvalId = $dispatchIdentity.approvalId
|
||||
projectId = $dispatchIdentity.projectId
|
||||
sourceFingerprint = $dispatchIdentity.sourceFingerprint
|
||||
dispatchRouteId = $dispatchIdentity.dispatchRouteId
|
||||
singleFlightKey = $dispatchIdentity.singleFlightKey
|
||||
lockOwner = $dispatchIdentity.lockOwner
|
||||
canonicalDigest = $dispatchIdentity.canonicalDigest
|
||||
createdAt = (Get-Date -Format o)
|
||||
}
|
||||
|
||||
@@ -119,10 +270,25 @@ $request | ConvertTo-Json -Depth 8 | Set-Content -Path $requestPath -Encoding UT
|
||||
requestedBy = if ($RequestedBy) { $RequestedBy } else { $env:USERNAME }
|
||||
source = $Source
|
||||
externalId = $ExternalId
|
||||
correlationKey = $dispatchIdentity.idempotencyKey
|
||||
reason = "operator_instruction"
|
||||
instruction = $Instruction
|
||||
requestPath = $requestPath
|
||||
replyChatId = $ReplyChatId
|
||||
dispatchIdentitySchemaVersion = $dispatchIdentity.schemaVersion
|
||||
automationRunId = $dispatchIdentity.automationRunId
|
||||
traceId = $dispatchIdentity.traceId
|
||||
workItemId = $dispatchIdentity.workItemId
|
||||
idempotencyKey = $dispatchIdentity.idempotencyKey
|
||||
executionGeneration = $dispatchIdentity.executionGeneration
|
||||
incidentId = $dispatchIdentity.incidentId
|
||||
approvalId = $dispatchIdentity.approvalId
|
||||
projectId = $dispatchIdentity.projectId
|
||||
sourceFingerprint = $dispatchIdentity.sourceFingerprint
|
||||
dispatchRouteId = $dispatchIdentity.dispatchRouteId
|
||||
singleFlightKey = $dispatchIdentity.singleFlightKey
|
||||
lockOwner = $dispatchIdentity.lockOwner
|
||||
canonicalDigest = $dispatchIdentity.canonicalDigest
|
||||
createdAt = (Get-Date -Format o)
|
||||
} | ConvertTo-Json -Depth 8 | Set-Content -Path $queuePath -Encoding UTF8
|
||||
|
||||
@@ -135,6 +301,11 @@ $result = [pscustomobject]@{
|
||||
requestPath = $requestPath
|
||||
queuePath = $queuePath
|
||||
runNow = [bool]$RunNow
|
||||
automationRunId = $dispatchIdentity.automationRunId
|
||||
traceId = $dispatchIdentity.traceId
|
||||
workItemId = $dispatchIdentity.workItemId
|
||||
idempotencyKey = $dispatchIdentity.idempotencyKey
|
||||
canonicalDigest = $dispatchIdentity.canonicalDigest
|
||||
controlExitCode = $null
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,32 @@ def test_host111_is_in_recovery_executor_and_all_host_readiness_scope() -> None:
|
||||
assert '-Source agent99-startup-recovery' in tasks
|
||||
|
||||
|
||||
def test_startup_and_desktop_controlled_requests_bind_canonical_dispatch_identity() -> None:
|
||||
submit = read("agent99-submit-request.ps1")
|
||||
|
||||
assert "function New-AgentControlledDispatchIdentity" in submit
|
||||
assert "function New-AgentUuidV5" in submit
|
||||
assert '6ba7b811-9dad-11d1-80b4-00c04fd430c8' in submit
|
||||
assert 'agent99-controlled-dispatch:$canonical' in submit
|
||||
assert 'schema_version = "agent99_controlled_dispatch_identity_v1"' in submit
|
||||
for field in (
|
||||
"automationRunId",
|
||||
"traceId",
|
||||
"workItemId",
|
||||
"idempotencyKey",
|
||||
"executionGeneration",
|
||||
"incidentId",
|
||||
"approvalId",
|
||||
"projectId",
|
||||
"sourceFingerprint",
|
||||
"dispatchRouteId",
|
||||
"singleFlightKey",
|
||||
"lockOwner",
|
||||
"canonicalDigest",
|
||||
):
|
||||
assert f"{field} = $dispatchIdentity.{field}" in submit
|
||||
|
||||
|
||||
def test_coordinator_covers_all_hosts_and_full_recovery_evidence() -> None:
|
||||
control = read("agent99-control-plane.ps1")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user