param( [string]$Instruction = "", [string]$InstructionBase64 = "", [string]$Mode = "", [switch]$ControlledApply, [switch]$RunNow, [string]$Source = "agent99-submit-request", [string]$RequestedBy = "", [string]$RequestedByBase64 = "", [string]$ExternalId = "", [string]$ReplyChatId = "", [string]$AgentRoot = "C:\Wooo\Agent99" ) $ErrorActionPreference = "Stop" function ConvertFrom-AgentBase64Argument { param([string]$Value, [string]$FieldName, [int]$MaximumBytes) try { $bytes = [Convert]::FromBase64String($Value) if ($bytes.Length -gt $MaximumBytes) { throw "${FieldName}_too_large" } $encoding = New-Object Text.UTF8Encoding($false, $true) $text = $encoding.GetString($bytes) if ($text.IndexOf([char]0) -ge 0) { throw "${FieldName}_nul_invalid" } $text } catch { throw "${FieldName}_base64_invalid" } } if ($Instruction -and $InstructionBase64) { throw "instruction_transport_ambiguous" } if ($RequestedBy -and $RequestedByBase64) { throw "requested_by_transport_ambiguous" } if ($InstructionBase64) { $Instruction = ConvertFrom-AgentBase64Argument $InstructionBase64 "instruction" 8192 } if ($RequestedByBase64) { $RequestedBy = ConvertFrom-AgentBase64Argument $RequestedByBase64 "requested_by" 1024 } if (-not $Instruction) { $Instruction = Read-Host "Agent99 request" } if (-not $Instruction) { throw "Instruction is required." } $allowedModes = @("Status", "Recover", "StartVMs", "PublicSmoke", "HarborRepair", "AwoooRepair", "Perf", "LoadShed", "SelfCheck", "BackupCheck") function Test-AgentContainsChar { param( [string]$Text, [int[]]$Codes ) foreach ($code in $Codes) { if ($Text.IndexOf([string][char]$code) -ge 0) { return $true } } 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 Write-AgentDurableJson { param( [object]$Value, [string]$Path, [string]$ExpectedId, [string]$ExpectedExternalId ) $temporaryPath = "$Path.tmp-$PID-$([guid]::NewGuid().ToString('N'))" $stream = $null $writer = $null try { $json = $Value | ConvertTo-Json -Depth 10 $encoding = New-Object Text.UTF8Encoding($false) $stream = [IO.File]::Open($temporaryPath, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None) $writer = New-Object IO.StreamWriter($stream, $encoding) $writer.Write($json) $writer.Flush() $stream.Flush($true) $writer.Dispose(); $writer = $null $stream.Dispose(); $stream = $null $temporaryHash = (Get-FileHash -LiteralPath $temporaryPath -Algorithm SHA256).Hash.ToLowerInvariant() Move-Item -LiteralPath $temporaryPath -Destination $Path -Force $readbackHash = (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant() if ($readbackHash -ne $temporaryHash) { throw "durable_json_hash_mismatch" } $readback = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json if ([string]$readback.id -ne $ExpectedId) { throw "durable_json_id_mismatch" } if ([string]$readback.externalId -ne $ExpectedExternalId) { throw "durable_json_external_id_mismatch" } [pscustomobject]@{ ok = $true path = $Path sha256 = $readbackHash id = [string]$readback.id externalId = [string]$readback.externalId } } finally { if ($writer) { $writer.Dispose() } if ($stream) { $stream.Dispose() } Remove-Item -LiteralPath $temporaryPath -Force -ErrorAction SilentlyContinue } } function Read-AgentDurableIngressArtifact { param( [string]$Path, [string]$ExpectedId, [string]$ExpectedExternalId, [string]$ExpectedSource, [string]$ExpectedInstruction, [string]$ExpectedMode, [bool]$ExpectedControlledApply ) if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { throw "ingress_artifact_missing" } if ((Get-Item -LiteralPath $Path).Length -gt 262144) { throw "ingress_artifact_oversized" } $hash = (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant() $readback = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json $modeValue = if ($readback.PSObject.Properties["mode"]) { [string]$readback.mode } elseif ($readback.PSObject.Properties["resolvedMode"]) { [string]$readback.resolvedMode } else { "" } if ( [string]$readback.id -ne $ExpectedId -or [string]$readback.externalId -ne $ExpectedExternalId -or [string]$readback.source -ne $ExpectedSource -or [string]$readback.instruction -ne $ExpectedInstruction -or $modeValue -ne $ExpectedMode -or -not $readback.PSObject.Properties["controlledApply"] -or $readback.controlledApply -isnot [bool] -or [bool]$readback.controlledApply -ne $ExpectedControlledApply ) { throw "ingress_artifact_identity_conflict" } [pscustomobject]@{ ok = $true existing = $true path = $Path sha256 = $hash id = [string]$readback.id externalId = [string]$readback.externalId } } function Get-AgentIngressLifecycleReceipt { param( [string]$QueueDirectory, [string]$RequestId, [string]$ExpectedExternalId, [string]$ExpectedSource, [string]$ExpectedInstruction, [string]$ExpectedMode, [bool]$ExpectedControlledApply ) $processedDirectory = Join-Path $QueueDirectory "processed" $failedDirectory = Join-Path $QueueDirectory "failed" $known = @( [pscustomobject]@{ state = "pending"; path = (Join-Path $QueueDirectory "$RequestId.json") }, [pscustomobject]@{ state = "running"; path = (Join-Path $QueueDirectory "running-$RequestId.json") }, [pscustomobject]@{ state = "processed"; path = (Join-Path $processedDirectory "processed-$RequestId.json") }, [pscustomobject]@{ state = "failed"; path = (Join-Path $failedDirectory "failed-$RequestId.json") } ) if (Test-Path -LiteralPath $failedDirectory -PathType Container) { foreach ($stale in @(Get-ChildItem -LiteralPath $failedDirectory -File -Filter "stale-*-*$RequestId.json" -ErrorAction SilentlyContinue)) { $known += [pscustomobject]@{ state = "stale_quarantined"; path = $stale.FullName } } } $present = @($known | Where-Object { Test-Path -LiteralPath $_.path -PathType Leaf } | Sort-Object path -Unique) if ($present.Count -gt 1) { throw "ingress_lifecycle_ambiguous" } if ($present.Count -eq 0) { return $null } $receipt = Read-AgentDurableIngressArtifact ` -Path $present[0].path ` -ExpectedId $RequestId ` -ExpectedExternalId $ExpectedExternalId ` -ExpectedSource $ExpectedSource ` -ExpectedInstruction $ExpectedInstruction ` -ExpectedMode $ExpectedMode ` -ExpectedControlledApply $ExpectedControlledApply $receipt | Add-Member -NotePropertyName lifecycleState -NotePropertyValue ([string]$present[0].state) -Force $receipt } 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, [string]$ExplicitMode ) if ($ExplicitMode) { if ($ExplicitMode -notin $allowedModes) { throw "Mode is not allowlisted: $ExplicitMode" } return $ExplicitMode } $lower = $Text.ToLowerInvariant() $mentionsPublic = ($lower -match "public|smoke|website|site|route|url|http|502" -or (Test-AgentContainsChar $Text @(0x7DB2, 0x7AD9))) $wantsRecovery = ($lower -match "recover|reboot|restore|restart|startup|repair|fix|502" -or (Test-AgentContainsChar $Text @(0x6062, 0x5FA9, 0x91CD, 0x555F, 0x4FEE))) if ($mentionsPublic -and $wantsRecovery) { return "Recover" } if ($mentionsPublic) { return "PublicSmoke" } if ($wantsRecovery) { return "Recover" } if ($lower -match "vm|vmware|startvm|start vm|start vms" -or (Test-AgentContainsChar $Text @(0x865B, 0x64EC, 0x4E3B, 0x6A5F))) { return "StartVMs" } if ($lower -match "cpu|load|memory|ram|disk|perf|performance|pressure" -or (Test-AgentContainsChar $Text @(0x6548, 0x80FD, 0x78C1, 0x789F, 0x8CA0, 0x8F09))) { return "Perf" } if ($lower -match "backup|snapshot|restore drill|cron" -or (Test-AgentContainsChar $Text @(0x5099, 0x4EFD))) { return "BackupCheck" } if ($lower -match "self|agent|health|selfcheck" -or (Test-AgentContainsChar $Text @(0x81EA, 0x6AA2, 0x5065, 0x5EB7))) { return "SelfCheck" } if ($lower -match "harbor|registry") { return "HarborRepair" } if ($lower -match "awoooi|k3s|pod|pods") { return "AwoooRepair" } return "Status" } $modeName = Resolve-AgentRequestMode $Instruction $Mode $queueDir = Join-Path $AgentRoot "queue" $requestDir = Join-Path $AgentRoot "requests" $evidenceDir = Join-Path $AgentRoot "evidence" New-Item -ItemType Directory -Force -Path $queueDir, $requestDir, $evidenceDir | Out-Null $deterministicIngress = [bool]$ExternalId $ingressDigest = Get-AgentSha256 (([string]$Source) + "`n" + ([string]$ExternalId)) $id = if ($deterministicIngress) { "request-" + $ingressDigest.Substring(0, 32) } else { "request-" + (Get-Date -Format "yyyyMMdd-HHmmss") + "-" + ([guid]::NewGuid().ToString("N").Substring(0, 8)) } $ingressMutex = $null $ingressMutexAcquired = $false try { $ingressMutex = New-Object Threading.Mutex($false, ("Global\Wooo-Agent99-Submit-" + $ingressDigest.Substring(0, 32))) try { $ingressMutexAcquired = [bool]$ingressMutex.WaitOne([TimeSpan]::FromSeconds(30)) } catch [Threading.AbandonedMutexException] { $ingressMutexAcquired = $true } if (-not $ingressMutexAcquired) { throw "ingress_single_writer_timeout" } $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 resolvedMode = $modeName controlledApply = [bool]$ControlledApply 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) } $requestPath = Join-Path $requestDir "$id.json" $queuePath = Join-Path $queueDir "$id.json" $requestReceipt = if (Test-Path -LiteralPath $requestPath -PathType Leaf) { Read-AgentDurableIngressArtifact $requestPath $id $ExternalId $Source $Instruction $modeName ([bool]$ControlledApply) } else { Write-AgentDurableJson $request $requestPath $id $ExternalId } $queueItem = [pscustomobject]@{ id = $id mode = $modeName controlledApply = [bool]$ControlledApply 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) } $queueReceipt = Get-AgentIngressLifecycleReceipt $queueDir $id $ExternalId $Source $Instruction $modeName ([bool]$ControlledApply) if (-not $queueReceipt) { $queueReceipt = Write-AgentDurableJson $queueItem $queuePath $id $ExternalId $queueReceipt | Add-Member -NotePropertyName existing -NotePropertyValue $false -Force $queueReceipt | Add-Member -NotePropertyName lifecycleState -NotePropertyValue "pending" -Force } $submitEvidence = Join-Path $evidenceDir "agent99-submit-$id.json" $result = [pscustomobject]@{ ok = $true id = $id instruction = $Instruction resolvedMode = $modeName controlledApply = [bool]$ControlledApply externalId = $ExternalId requestPath = $requestPath queuePath = $queuePath queueLifecyclePath = [string]$queueReceipt.path queueLifecycleState = [string]$queueReceipt.lifecycleState idempotentReplay = [bool]$queueReceipt.existing durableRequest = [bool]$requestReceipt.ok requestSha256 = [string]$requestReceipt.sha256 durableQueue = [bool]$queueReceipt.ok queueSha256 = [string]$queueReceipt.sha256 submitEvidencePath = $submitEvidence runNow = [bool]$RunNow automationRunId = $dispatchIdentity.automationRunId traceId = $dispatchIdentity.traceId workItemId = $dispatchIdentity.workItemId idempotencyKey = $dispatchIdentity.idempotencyKey canonicalDigest = $dispatchIdentity.canonicalDigest controlDispatchStatus = "not_requested" controlExitCode = $null } if ($RunNow) { try { Start-ScheduledTask -TaskPath "\" -TaskName "Wooo-Agent99-Control-Loop" -ErrorAction Stop $result.controlDispatchStatus = "scheduled_task_signaled" } catch { $result.ok = $false $result.controlDispatchStatus = "scheduled_task_signal_failed" $result.controlExitCode = -1 } } $submitReceipt = Write-AgentDurableJson $result $submitEvidence $id $ExternalId $result | Add-Member -NotePropertyName submitEvidenceSha256 -NotePropertyValue ([string]$submitReceipt.sha256) -Force $result | ConvertTo-Json -Depth 8 } finally { if ($ingressMutexAcquired -and $ingressMutex) { try { $ingressMutex.ReleaseMutex() } catch {} } if ($ingressMutex) { try { $ingressMutex.Dispose() } catch {} } }