param( [string]$Instruction = "", [string]$Mode = "", [switch]$ControlledApply, [switch]$RunNow, [string]$Source = "agent99-submit-request", [string]$RequestedBy = "", [string]$ExternalId = "", [string]$ReplyChatId = "", [string]$AgentRoot = "C:\Wooo\Agent99" ) $ErrorActionPreference = "Stop" 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 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 $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 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" $request | ConvertTo-Json -Depth 8 | Set-Content -Path $requestPath -Encoding UTF8 [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) } | ConvertTo-Json -Depth 8 | Set-Content -Path $queuePath -Encoding UTF8 $result = [pscustomobject]@{ ok = $true id = $id instruction = $Instruction resolvedMode = $modeName controlledApply = [bool]$ControlledApply requestPath = $requestPath queuePath = $queuePath runNow = [bool]$RunNow automationRunId = $dispatchIdentity.automationRunId traceId = $dispatchIdentity.traceId workItemId = $dispatchIdentity.workItemId idempotencyKey = $dispatchIdentity.idempotencyKey canonicalDigest = $dispatchIdentity.canonicalDigest controlExitCode = $null } if ($RunNow) { $launcher = Join-Path $AgentRoot "agent99-run.ps1" $process = Start-Process -FilePath "powershell.exe" -ArgumentList @("-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $launcher, "-Mode", "ControlTick") -Wait -PassThru -WindowStyle Hidden $process.Refresh() $result.controlExitCode = $process.ExitCode } $submitEvidence = Join-Path $evidenceDir "agent99-submit-$id.json" $result | ConvertTo-Json -Depth 8 | Set-Content -Path $submitEvidence -Encoding UTF8 $result | ConvertTo-Json -Depth 8