param( [string]$AgentRoot = "C:\Wooo\Agent99", [switch]$RunNow, [string]$SyntheticUpdateText = "", [string]$SyntheticChatTitle = "AwoooI SRE", [string]$SyntheticChatId = "" ) $ErrorActionPreference = "Stop" $ConfigPath = Join-Path $AgentRoot "config\agent99.config.json" $EvidenceDir = Join-Path $AgentRoot "evidence" $StateDir = Join-Path $AgentRoot "state" $BinDir = Join-Path $AgentRoot "bin" $SubmitScript = Join-Path $BinDir "agent99-submit-request.ps1" $SreAlertInboxScript = Join-Path $BinDir "agent99-sre-alert-inbox.ps1" $AlertIncomingDir = Join-Path $AgentRoot "alerts\incoming" New-Item -ItemType Directory -Force -Path $EvidenceDir, $StateDir, $AlertIncomingDir | Out-Null function Load-AgentConfig { if (-not (Test-Path $ConfigPath)) { throw "Config not found: $ConfigPath" } Get-Content $ConfigPath -Raw | ConvertFrom-Json } function Invoke-AgentSshText { param( [string]$TargetHost, [string]$Command, [int]$TimeoutSeconds = 30 ) $id = [guid]::NewGuid().ToString("N") $stdoutPath = Join-Path $env:TEMP "agent99-tg-ssh-$id.out" $stderrPath = Join-Path $env:TEMP "agent99-tg-ssh-$id.err" $args = @( "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=accept-new", "-o", "NumberOfPasswordPrompts=0", "-o", "ConnectTimeout=10", "-l", "wooo", $TargetHost, $Command ) $process = Start-Process -FilePath "ssh.exe" -ArgumentList $args -NoNewWindow -RedirectStandardOutput $stdoutPath -RedirectStandardError $stderrPath -PassThru $completed = $process.WaitForExit($TimeoutSeconds * 1000) if (-not $completed) { $process.Kill() $process.WaitForExit(2000) | Out-Null } $process.Refresh() $stdout = if (Test-Path $stdoutPath) { Get-Content $stdoutPath -Raw } else { "" } $stderr = if (Test-Path $stderrPath) { Get-Content $stderrPath -Raw } else { "" } Remove-Item $stdoutPath, $stderrPath -Force -ErrorAction SilentlyContinue | Out-Null [pscustomobject]@{ ok = [bool]($completed -and (($null -eq $process.ExitCode) -or $process.ExitCode -eq 0)) exitCode = if ($completed -and $null -ne $process.ExitCode) { $process.ExitCode } elseif ($completed) { 0 } else { -2 } output = (@($stdout, $stderr) | Where-Object { $_ }) -join "`n" } } function Convert-AgentBase64 { param([string]$Text) [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($Text)) } function Get-AgentTelegramUpdates { param( [object]$Config, [int64]$Offset ) $relay = $Config.telegram.relay if (-not ($relay -and $relay.enabled)) { return [pscustomobject]@{ ok = $false; error = "relay_disabled"; updates = @(); raw = $null } } $relayHost = if ($relay.host) { [string]$relay.host } else { "192.168.0.110" } $container = if ($relay.container) { [string]$relay.container } else { "stockplatform-v2-api-1" } $allowedTitlePattern = if ($Config.telegram.allowedCommandChatTitlePattern) { [string]$Config.telegram.allowedCommandChatTitlePattern } else { "AwoooI SRE|AWOOOI SRE|SRE" } if ($container -notmatch "^[A-Za-z0-9_.-]+$") { return [pscustomobject]@{ ok = $false; error = "invalid_relay_container"; updates = @(); raw = $null } } $python = @' import base64, json, os, re, sys, urllib.parse, urllib.request, urllib.error token = os.environ.get("TELEGRAM_BOT_TOKEN") or "" chat = os.environ.get("TELEGRAM_CHAT_ID") or "" def redact(value): text = str(value or "") for secret in (token, chat): if secret: text = text.replace(secret, "") return text[-1200:] def emit(payload): print(json.dumps(payload, ensure_ascii=False)) try: offset = int(sys.argv[2]) if len(sys.argv) > 2 else 0 allowed_title_pattern = base64.b64decode(sys.argv[3]).decode() if len(sys.argv) > 3 and sys.argv[3] else "" allowed_title_re = re.compile(allowed_title_pattern, re.I) if allowed_title_pattern else None if not token or not chat: emit({"ok": False, "error": "telegram_env_missing", "updates": []}) sys.exit(0) params = { "timeout": "0", "allowed_updates": json.dumps(["message"]) } if offset > 0: params["offset"] = str(offset) url = "https://api.telegram.org/bot" + token + "/getUpdates?" + urllib.parse.urlencode(params) try: raw = urllib.request.urlopen(url, timeout=20).read().decode() except urllib.error.HTTPError as exc: body = exc.read().decode(errors="replace") emit({"ok": False, "error": "telegram_http_error", "status": exc.code, "description": redact(body), "updates": []}) sys.exit(0) except Exception as exc: emit({"ok": False, "error": "telegram_request_failed", "errorType": type(exc).__name__, "description": redact(exc), "updates": []}) sys.exit(0) try: data = json.loads(raw) except Exception as exc: emit({"ok": False, "error": "telegram_json_parse_failed", "errorType": type(exc).__name__, "description": redact(exc), "updates": []}) sys.exit(0) if not data.get("ok", False): emit({"ok": False, "error": "telegram_api_not_ok", "description": redact(data.get("description", "")), "errorCode": data.get("error_code"), "updates": []}) sys.exit(0) items = [] for item in data.get("result", []): msg = item.get("message") or {} chat_obj = msg.get("chat") or {} chat_id = str(chat_obj.get("id") or "") chat_title = chat_obj.get("title") or chat_obj.get("username") or chat_obj.get("first_name") or "" chat_matches_config = chat_id == str(chat) chat_matches_title = bool(allowed_title_re and allowed_title_re.search(chat_title)) if not (chat_matches_config or chat_matches_title): continue text = msg.get("text") or "" if not text: continue sender = msg.get("from") or {} items.append({ "update_id": item.get("update_id"), "message_id": msg.get("message_id"), "date": msg.get("date"), "text": text, "chat_id": chat_id, "chat_title": chat_title, "chat_type": chat_obj.get("type"), "chat_match": "configured_chat_id" if chat_matches_config else "allowed_title", "from": sender.get("username") or sender.get("first_name") or str(sender.get("id") or "") }) emit({"ok": True, "updates": items}) except Exception as exc: emit({"ok": False, "error": "telegram_inbox_exception", "errorType": type(exc).__name__, "description": redact(exc), "updates": []}) '@ $pythonB64 = Convert-AgentBase64 $python $allowedTitleB64 = Convert-AgentBase64 $allowedTitlePattern $bootstrap = 'import base64,sys;exec(base64.b64decode(sys.argv[1]))' $remoteCommand = "docker exec $container python3 -c '$bootstrap' $pythonB64 $Offset $allowedTitleB64" $run = Invoke-AgentSshText $relayHost $remoteCommand 35 if (-not $run.ok) { return [pscustomobject]@{ ok = $false; error = $run.output; updates = @(); raw = $null } } try { $parsed = $run.output | ConvertFrom-Json $errorText = if ($parsed.ok) { $null } elseif ($parsed.description) { "$($parsed.error): $($parsed.description)" } else { [string]$parsed.error } return [pscustomobject]@{ ok = [bool]$parsed.ok; error = $errorText; updates = @($parsed.updates); raw = $parsed } } catch { return [pscustomobject]@{ ok = $false; error = $_.Exception.Message; updates = @(); raw = $run.output } } } function Resolve-AgentTelegramInstruction { param([string]$Text) $trimmed = $Text.Trim() $patterns = @( "^/agent99\s+(.+)$", "^!agent99\s+(.+)$", "^#agent99\s+(.+)$", "^agent99\s+(.+)$", "^@agent99\s+(.+)$" ) foreach ($pattern in $patterns) { $match = [regex]::Match($trimmed, $pattern, [Text.RegularExpressions.RegexOptions]::IgnoreCase) if ($match.Success) { return $match.Groups[1].Value.Trim() } } return $null } function Test-AgentMonitoringAlertText { param([string]$Text) $trimmed = $Text.Trim() if (-not $trimmed) { return $false } $lower = $trimmed.ToLowerInvariant() if ($lower -match "^agent99 alert:|\[agent99\]|event:\s+operator command|operator_command_result") { return $false } if ($lower -match "alertmanager|\bfiring\b|\bcritical\b|\bwarning\b|\bdegraded\b|\bdown\b|\b502\b|provider_freshness_signal|source_provider_freshness_triage|raw_signal_normalized|controlled_runtime_candidate|freshness|last_seen|last seen|ingestion|imagepullbackoff|errimagepull|crashloopbackoff|harbor|registry|k3s|backup|snapshot|cpu|load|memory|disk|hostdown|host_down|vm_not_running") { return $true } foreach ($code in @(0x544A, 0x8B66, 0x7570, 0x5E38, 0x56B4, 0x91CD, 0x8CA0, 0x8F09, 0x78C1, 0x789F, 0x5099, 0x4EFD)) { if ($trimmed.IndexOf([string][char]$code) -ge 0) { return $true } } return $false } function Test-AgentTextContainsCode { param( [string]$Text, [int[]]$Codes ) foreach ($code in $Codes) { if ($Text.IndexOf([string][char]$code) -ge 0) { return $true } } return $false } function Get-AgentTelegramAlertSeverity { param([string]$Text) $lower = $Text.ToLowerInvariant() if ($lower -match "resolved|recovered" -or (Test-AgentTextContainsCode $Text @(0x6062, 0x89E3))) { return "resolved" } if ($lower -match "critical|p0|emergency|fatal|down|failed" -or (Test-AgentTextContainsCode $Text @(0x56B4, 0x91CD, 0x7DCA, 0x6025))) { return "critical" } if ($lower -match "warning|warn|degraded" -or (Test-AgentTextContainsCode $Text @(0x544A, 0x8B66, 0x7570, 0x5E38))) { return "warning" } return "warning" } function Get-AgentTelegramAlertKind { param([string]$Text) $lower = $Text.ToLowerInvariant() if ($lower -match "provider_freshness_signal|source_provider_freshness_triage|raw_signal_normalized|controlled_runtime_candidate|freshness|last_seen|last seen|ingestion") { return "provider_freshness_signal" } if ($lower -match "502|site_502|public_route|website|route") { return "public_route_502" } if ($lower -match "cpu|load" -or (Test-AgentTextContainsCode $Text @(0x8CA0, 0x8F09))) { return "performance_cpu" } if ($lower -match "disk" -or (Test-AgentTextContainsCode $Text @(0x78C1, 0x789F))) { return "performance_disk" } if ($lower -match "memory|mem_|ram" -or (Test-AgentTextContainsCode $Text @(0x8A18, 0x61B6, 0x9AD4))) { return "performance_memory" } if ($lower -match "harbor|registry") { return "harbor_registry" } if ($lower -match "awoooi|k3s|imagepullbackoff|errimagepull|crashloopbackoff|pod|deployment") { return "awoooi_k3s" } if ($lower -match "backup|snapshot|cron" -or (Test-AgentTextContainsCode $Text @(0x5099, 0x4EFD))) { return "backup_health" } if ($lower -match "hostdown|host_down|vm_not_running|reboot|restart|down" -or (Test-AgentTextContainsCode $Text @(0x91CD, 0x555F, 0x95DC, 0x6A5F))) { return "host_recovery" } return "monitoring_alert" } function Get-AgentTelegramAlertService { param([string]$Text) $lower = $Text.ToLowerInvariant() if ($lower -match "stock") { return "stockplatform" } if ($lower -match "2026fifa|fifa") { return "2026fifa" } if ($lower -match "gitea") { return "gitea" } if ($lower -match "harbor") { return "harbor" } if ($lower -match "agent99") { return "agent99" } if ($lower -match "awoooi|k3s|502|site|route|website") { return "awoooi" } return "unknown" } function Get-AgentTelegramAlertHost { param([string]$Text) $match = [regex]::Match($Text, "(192\\.168\\.0\\.(110|112|120|121|188|99))") if ($match.Success) { return $match.Groups[1].Value } foreach ($hostSuffix in @("110", "112", "120", "121", "188", "99")) { if ($Text -match "(^|\D)$hostSuffix(\D|$)") { return "192.168.0.$hostSuffix" } } return "" } function New-AgentMonitoringAlertFromTelegram { param([object]$Update) $text = [string]$Update.text $severity = Get-AgentTelegramAlertSeverity $text if ($severity -eq "resolved") { return [pscustomobject]@{ ok = $false skipped = $true reason = "resolved_alert_no_remediation" } } $alertId = "telegram-monitoring-" + [string]$Update.update_id $alertPath = Join-Path $AlertIncomingDir ($alertId + ".json") $message = if ($text.Length -gt 2000) { $text.Substring(0, 2000) + "`n...(truncated)" } else { $text } $alert = [pscustomobject]@{ id = $alertId source = "telegram-sre-war-room-monitoring" severity = $severity kind = Get-AgentTelegramAlertKind $text service = Get-AgentTelegramAlertService $text host = Get-AgentTelegramAlertHost $text title = "AwoooI SRE Telegram monitoring alert" message = $message labels = if ((Get-AgentTelegramAlertKind $text) -eq "provider_freshness_signal") { @("telegram", "sre-war-room", "monitoring", "provider-freshness", "no-false-green") } else { @("telegram", "sre-war-room", "monitoring") } telegramUpdateId = $Update.update_id telegramMessageId = $Update.message_id telegramChatTitle = $Update.chat_title telegramChatType = $Update.chat_type replyChatId = [string]$Update.chat_id force = $true controlledApply = $true createdAt = (Get-Date -Format o) } $alert | ConvertTo-Json -Depth 8 | Set-Content -Path $alertPath -Encoding UTF8 $sreExitCode = $null if ($RunNow -and (Test-Path $SreAlertInboxScript)) { $process = Start-Process -FilePath "powershell.exe" -ArgumentList @("-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $SreAlertInboxScript, "-AgentRoot", $AgentRoot, "-RunNow") -Wait -PassThru -WindowStyle Hidden $process.Refresh() $sreExitCode = $process.ExitCode } [pscustomobject]@{ ok = $true skipped = $false alertId = $alertId alertPath = $alertPath severity = $alert.severity kind = $alert.kind service = $alert.service host = $alert.host sreInboxExitCode = $sreExitCode } } $stamp = Get-Date -Format "yyyyMMdd-HHmmss" $evidencePath = Join-Path $EvidenceDir "agent99-TelegramInbox-$stamp.json" $offsetPath = Join-Path $StateDir "telegram-inbox-offset.json" $offset = 0 if ((-not $SyntheticUpdateText) -and (Test-Path $offsetPath)) { try { $offsetState = Get-Content $offsetPath -Raw | ConvertFrom-Json if ($offsetState.nextOffset) { $offset = [int64]$offsetState.nextOffset } } catch { $offset = 0 } } $config = Load-AgentConfig $autoIngestMonitoringAlerts = $true if ($config.telegram -and $config.telegram.PSObject.Properties["autoIngestMonitoringAlerts"]) { $autoIngestMonitoringAlerts = [bool]$config.telegram.autoIngestMonitoringAlerts } if ($SyntheticUpdateText) { $updatesResult = [pscustomobject]@{ ok = $true error = $null updates = @([pscustomobject]@{ update_id = [int64](Get-Date -UFormat %s) message_id = 1 date = [int64](Get-Date -UFormat %s) text = $SyntheticUpdateText chat_id = $SyntheticChatId chat_title = $SyntheticChatTitle chat_type = "supergroup" chat_match = "synthetic" from = "synthetic-sre-monitor" }) raw = $null } } else { $updatesResult = Get-AgentTelegramUpdates $config $offset } $processed = @() $alerted = @() $ignored = @() $maxUpdateId = if ($offset -gt 0) { $offset - 1 } else { 0 } foreach ($update in @($updatesResult.updates)) { if ($update.update_id -gt $maxUpdateId) { $maxUpdateId = [int64]$update.update_id } $instruction = Resolve-AgentTelegramInstruction ([string]$update.text) if (-not $instruction) { if ($autoIngestMonitoringAlerts -and (Test-AgentMonitoringAlertText ([string]$update.text))) { $alertResult = New-AgentMonitoringAlertFromTelegram $update if ($alertResult.ok) { $alerted += [pscustomobject]@{ updateId = $update.update_id messageId = $update.message_id from = $update.from chatTitle = $update.chat_title chatType = $update.chat_type chatMatch = $update.chat_match alertId = $alertResult.alertId severity = $alertResult.severity kind = $alertResult.kind service = $alertResult.service host = $alertResult.host alertPath = $alertResult.alertPath sreInboxExitCode = $alertResult.sreInboxExitCode } } else { $ignored += [pscustomobject]@{ updateId = $update.update_id reason = $alertResult.reason } } continue } $ignored += [pscustomobject]@{ updateId = $update.update_id reason = "no_agent99_prefix" } continue } $submitArgs = @( "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $SubmitScript, "-Instruction", $instruction, "-ControlledApply", "-Source", "telegram-sre-war-room", "-RequestedBy", ([string]$update.from), "-ExternalId", ("telegram-update-" + [string]$update.update_id), "-ReplyChatId", ([string]$update.chat_id) ) if ($RunNow) { $submitArgs += "-RunNow" } $submit = Start-Process -FilePath "powershell.exe" -ArgumentList $submitArgs -Wait -PassThru -NoNewWindow -RedirectStandardOutput (Join-Path $EvidenceDir "agent99-telegram-submit-$($update.update_id).out") -RedirectStandardError (Join-Path $EvidenceDir "agent99-telegram-submit-$($update.update_id).err") $submit.Refresh() $processed += [pscustomobject]@{ updateId = $update.update_id messageId = $update.message_id from = $update.from chatTitle = $update.chat_title chatType = $update.chat_type chatMatch = $update.chat_match instruction = $instruction submitExitCode = if ($null -ne $submit.ExitCode) { $submit.ExitCode } else { 0 } } } if ((-not $SyntheticUpdateText) -and $updatesResult.ok -and $maxUpdateId -ge 0) { [pscustomobject]@{ nextOffset = $maxUpdateId + 1 updatedAt = (Get-Date -Format o) } | ConvertTo-Json -Depth 4 | Set-Content -Path $offsetPath -Encoding UTF8 } $result = [pscustomobject]@{ timestamp = (Get-Date -Format o) ok = [bool]$updatesResult.ok error = $updatesResult.error offsetBefore = $offset offsetAfter = if ($SyntheticUpdateText) { $offset } elseif ($updatesResult.ok) { $maxUpdateId + 1 } else { $offset } synthetic = [bool]$SyntheticUpdateText updatesSeen = @($updatesResult.updates).Count processedCount = @($processed).Count alertedCount = @($alerted).Count ignoredCount = @($ignored).Count processed = $processed alerted = $alerted ignored = $ignored offsetPath = $offsetPath } $result | ConvertTo-Json -Depth 10 | Set-Content -Path $evidencePath -Encoding UTF8 $result | ConvertTo-Json -Depth 10