feat(agent99): bridge sre alerts to windows relay

This commit is contained in:
ogt
2026-07-10 00:26:52 +08:00
parent e150a85a6d
commit 8a766db5e2
19 changed files with 6673 additions and 0 deletions

View File

@@ -0,0 +1,691 @@
# Agent99 AI Workstation SOP
Last updated: 2026-07-09
## Objective
192.168.0.99 is the AI Agent workstation and local operations control node.
It must act as:
- Secretary: reminders, status summaries, evidence packets, operator updates.
- Assistant: routine checks, file organization, browser/readback checks, alert routing.
- Engineer: VM control, service recovery, deploy/readback verification, runbook execution.
- Incident operator: reboot detection, recovery orchestration, Telegram alerting, SLA countdown.
The machine must be able to operate VMware, SSH to service hosts, run public HTTP readbacks, and produce evidence without depending on old chat context.
## Agent99 Operating Modes
Observe mode:
- Read host, VM, service, and website state.
- Produce evidence only.
- Default mode for scheduled checks.
Controlled apply mode:
- Execute approved low-blast-radius actions.
- Must write evidence before and after each action.
- Requires explicit operator approval or scheduled startup recovery policy.
Break-glass mode:
- Reserved for destructive, irreversible, secret-bearing, cost-bearing, or firewall/provider cutover actions.
- Must not be hidden inside automatic tasks.
## Required Local Capabilities On 99
Base runtime:
- Windows PowerShell 5+ or PowerShell 7.
- Windows OpenSSH client.
- `curl.exe`.
- VMware Workstation.
- `vmrun.exe`.
- Windows Task Scheduler.
Agent filesystem:
- `C:\Wooo\Agent99\bin`
- `C:\Wooo\Agent99\config`
- `C:\Wooo\Agent99\evidence`
- `C:\Wooo\Agent99\logs`
- `C:\Wooo\Agent99\queue`
- `C:\Wooo\Agent99\requests`
- `C:\Wooo\Agent99\alerts\incoming`
- `C:\Wooo\Agent99\playbooks`
Agent scripts:
- `agent99-bootstrap.ps1`
- `agent99-control-plane.ps1`
- `agent99-submit-request.ps1`
- `agent99-telegram-inbox.ps1`
- `agent99-sre-alert-inbox.ps1`
- `host-reboot-scorecard.ps1`
- `agent99.config.json`
## Command Ingress Without Codex Quota
Codex is not the runtime command plane. Agent99 must accept operator work through local 99 channels even when Codex is unavailable.
Supported command channels:
- Local desktop: `Agent99 Submit Request.lnk`.
- Local queue: write allowlisted command JSON to `C:\Wooo\Agent99\queue`.
- AwoooI SRE Telegram group: send `/agent99 ...`, `!agent99 ...`, `#agent99 ...`, `agent99 ...`, or `@agent99 ...`.
- SRE war room alerts: write alert JSON to `C:\Wooo\Agent99\alerts\incoming`.
- AwoooI SRE Telegram monitoring alerts: non-command messages that look like Alertmanager/FIRING/critical/warning/502/CPU/disk/Harbor/K3s/backup/host-down/provider-freshness alerts are auto-ingested.
Command processing requirements:
- Every command resolves to an allowlisted mode only.
- Website plus 502 plus recover/repair intent resolves to `Recover`, not passive smoke-only checking.
- Alert-driven remediation uses controlled apply only for allowlisted modes such as `Recover`, `HarborRepair`, `AwoooRepair`, `Perf`, and `LoadShed`.
- Every accepted command writes a request record, queue record, execution result, and evidence pointer.
- Telegram messages must be human-readable: source, instruction, mode, result, and evidence path. Raw Python, raw stack trace, token, chat id, cookies, sessions, or `.env` must never be sent.
- Telegram SRE group commands must be recorded with `source=telegram-sre-war-room`.
- Telegram SRE monitoring alerts must be recorded with `source=telegram-sre-war-room-monitoring`.
- Telegram SRE group matching uses `allowedCommandChatTitlePattern=AwoooI SRE|AWOOOI SRE|SRE`, so the command path is not limited to the default private chat id.
- When a command originates from a matched Telegram group, completion replies are routed back to the same Telegram origin while evidence stores only `telegram-origin`, not the raw chat id.
- After automated recovery completes, Agent99 must report the result back to Telegram so the project AI Agents and human operators know whether the abnormality is resolved.
- Every queue execution must update problem counts, write a Playbook/KM record, and append a RAG-import JSONL line.
- Operator command and remediation result Telegram reports are not deduped; each completed repair emits a completion report. Noisy warning/critical signals still use dedupe.
SRE Telegram command examples:
- `/agent99 檢查所有網站是否正常若502就自動修復`
- `/agent99 檢查110和188 CPU負載必要時執行允許的降載`
- `/agent99 檢查AWOOOI K3s與Harbor若ImagePullBackOff就修復`
- `/agent99 檢查備份健康狀態`
- `/agent99 建立 provider freshness candidate要求 provider window、last seen、direct ref 與 verifier readback`
Monitoring alert auto-ingest examples:
- `Alertmanager FIRING critical public_route_502 service=awoooi host=192.168.0.120 HTTP 502`
- `HostHighCpuLoad warning host=192.168.0.110`
- `HarborRegistryDown critical host=192.168.0.110`
- `KubePodCrashLooping critical namespace=awoooi-prod`
- `provider_freshness_signal target=provider_freshness_signal freshness last_seen missing direct_ref required`
Problem memory outputs:
- Problem counters: `C:\Wooo\Agent99\state\problem-counts.json`.
- Full event ledger: `C:\Wooo\Agent99\playbooks\agent99-incident-records.jsonl`.
- Playbook/KM summary: `C:\Wooo\Agent99\playbooks\agent99-incident-km.md`.
- RAG import feed: `C:\Wooo\Agent99\playbooks\rag-import\agent99-incidents.jsonl`.
- Dashboard: `C:\Wooo\Agent99\dashboard\index.html`, section `Problem Counts`.
## SRE War Room Alert Contract
AWOOOI SRE monitors and other AI Agent work windows can hand alerts to Agent99 by writing JSON files to:
`C:\Wooo\Agent99\alerts\incoming`
Minimum fields:
```json
{
"id": "alert-unique-id",
"source": "awoooi-sre-war-room",
"severity": "critical",
"kind": "public_route_502",
"service": "awoooi",
"host": "192.168.0.120",
"title": "AWOOOI public route returned 502",
"message": "The public health route is returning 502.",
"createdAt": "2026-07-09T22:50:00+08:00"
}
```
Routing rules:
- `502`, `site_502`, `public_route_502`, `route_down`, `host_down`, VM down, or reboot detected -> `Recover`.
- CPU, memory, disk, performance pressure -> `Perf` with controlled apply.
- Harbor or registry -> `HarborRepair`.
- AWOOOI, K3s, image pull, pod, deployment -> `AwoooRepair`.
- Backup, snapshot, restore drill, or cron -> `BackupCheck`.
- Unknown actionable alert -> `Status` evidence first.
Alert processing evidence:
- Inbox evidence: `agent99-SreAlertInbox-*.json`.
- Queued request: `C:\Wooo\Agent99\requests`.
- Execution evidence: `agent99-Recover-*.json`, `agent99-Perf-*.json`, or the resolved mode evidence file.
## VM Control Requirements
Agent99 must be able to operate these VM hosts:
- 110
- 112
- 120
- 121
- 188
Required actions:
- Detect VM powered-off state.
- Start missing VMs through `vmrun.exe`.
- Detect VMware config lock symptoms.
- Avoid deleting VM lock files unless VMware process state proves the VM is not running.
- Prefer guest shutdown/restart first; use reset only when the guest is stuck and SLA requires recovery.
## Engineering Recovery Requirements
Agent99 must support these engineering actions:
- SSH readback to 110 and 120.
- Harbor core / Redis / registry checks on 110.
- K3s AWOOOI deployment and pod checks on 120.
- Public route readback for user-visible websites.
- Evidence capture after every action.
- Telegram alert hooks.
## Main Scheduled Automations
Startup recovery:
- Trigger: 99 boot or operator logon.
- Frequency: once at startup, then repeat during first recovery window.
- Mode: controlled apply allowed for approved actions.
- Purpose: start VMs, restore Harbor, restore K3s pods, verify public routes.
Heartbeat:
- Trigger: every 5 minutes.
- Mode: observe by default.
- Purpose: detect host down, 502, service degraded, backup job missing, or stale freshness.
Daily evidence summary:
- Trigger: daily.
- Mode: observe.
- Purpose: produce a short host/service/site/backup status packet.
## Telegram Alert Classes
The agent must send alerts for:
- `host_down`
- `host_reboot_detected`
- `vm_not_running`
- `vm_start_attempted`
- `service_recovering`
- `harbor_unhealthy`
- `k3s_image_pull_failure`
- `site_502`
- `site_recovered`
- `service_degraded`
- `backup_missing`
- `backup_failed`
- `recovery_sla_risk`
- `recovery_complete`
Telegram credentials must be stored outside the repo, preferably in Windows Credential Manager or machine environment variables.
## 2026-07-09 Recovery Logic To Embed
When AWOOOI or other sites return 502:
1. Do not only inspect nginx.
2. Check 120 K3s workloads first.
3. If AWOOOI pods are `ImagePullBackOff`, check 110 Harbor before deleting pods.
4. If Harbor token endpoint returns 500 or pulls fail, inspect:
- Docker engine.
- Harbor Redis.
- `harbor-core`.
- `registry`.
- `registryctl`.
5. If `harbor-core` is not healthy:
- Restart Redis and `harbor-core`.
- If Docker/systemd hangs, execute controlled 110 guest restart via VMware.
6. After `harbor-core` is healthy:
- Delete only failed AWOOOI pods.
- Wait for deployment readiness.
- Verify public HTTP.
## First Implementation Milestone
Milestone M1 is complete when:
- Agent99 directory exists on 99.
- Config file exists.
- Startup and heartbeat scheduled tasks exist.
- Scorecard can run in observe mode.
- Recovery can run in controlled apply mode.
- Evidence JSON is written.
- No secrets are stored in repo files.
## 2026-07-09 M1 Receipt
M1 is operational on 99 as of 2026-07-09 16:49 Asia/Taipei.
- `C:\Wooo\Agent99` exists on `WOOO-SUPER`.
- `agent99-control-plane.ps1`, `host-reboot-scorecard.ps1`, and task registration scripts are deployed.
- `agent99.config.json` includes VMX selectors for 110, 188, 112, 120, and 121.
- `Wooo-Agent99-Heartbeat` is registered and produced `LastTaskResult=0`.
- `Wooo-Agent99-Startup-Recovery` is registered and produced `LastTaskResult=0`.
- Latest heartbeat evidence showed Harbor healthy, AWOOOI deploy ready, and public routes non-502.
- Latest recovery evidence showed all five configured VM hosts already reachable and no recovery action required.
Remaining M2 work:
- Install Telegram credentials with environment variables or Windows Credential Manager, then verify real delivery.
- Add backup-job freshness readback and alert classes.
- Add Windows Update no-surprise-reboot policy verification.
- Promote this into a long-running Windows service after the scheduled-task lane stays clean.
## 2026-07-09 M2 Performance Guard Receipt
Agent99 performance guard is operational on 99 as of 2026-07-09 17:15 Asia/Taipei.
- `Wooo-Agent99-Performance-Guard` is registered in Windows Task Scheduler.
- Trigger interval: every 1 minute.
- Last manual verification: `LastTaskResult=0`.
- Latest evidence: `C:\Wooo\Agent99\evidence\agent99-Perf-20260709-171502.json`.
- Readback coverage:
- 110: OK, load-per-core 0.35, memory available 62.93%, disk used 82%.
- 112: OK, load-per-core 0.07, memory available 61.06%, disk used 47%.
- 120: OK, load-per-core 0.05, memory available 91.76%, disk used 44%.
- 121: OK, load-per-core 0.00, memory available 92.79%, disk used 27%.
- 188: OK, load-per-core 0.08, memory available 90.19%, disk used 89%.
- 99 SSH access was completed for:
- 112 with user `kali`.
- 188 with user `ollama`.
- Load-shedding is controlled by allowlist only.
- Current allowlisted action: pause `vibework-dedicated-runner` on 110 only when `load_per_core_critical` is present.
- Disk or memory pressure only alerts; it does not trigger the CPU runner pause action.
- Telegram audit is enabled in config and called for Agent99 start, warnings, critical events, load-shed attempts/results, and completion.
- 99 local env aliases are supported for `AGENT99_*`, `AWOOOI_TG_*`, `OPENCLAW_TG_*`, `TELEGRAM_*`, and `TG_*`.
- If 99 does not have local Telegram env values, Agent99 relays through the existing 110 `stockplatform-v2-api-1` container and uses that container's existing `TELEGRAM_BOT_TOKEN` / `TELEGRAM_CHAT_ID`.
- 2026-07-09 17:27 verification: `Wooo-Agent99-Performance-Guard` produced `LastTaskResult=0`, `agent99-Perf-20260709-172712.json`, `tgSent=2`, and `relayOk=True`.
- No token or chat id is stored in repo files or Agent99 evidence.
## 2026-07-09 OpenClaw Receipt
OpenClaw is configured and now included in Agent99 evidence.
- Source truth:
- AWOOOI prod config: `OPENCLAW_URL=http://192.168.0.188:8088`.
- 188 Ansible service target: OpenClaw on port `8088`.
- Runtime truth:
- `192.168.0.188:8088/health` returns `status=healthy`, `version=6.0`, `service=ClawBot`, `environment=production`, and `telegram_bot=connected`.
- `clawbot.service` on 188 is `active`.
- `192.168.0.188:8089` is not the current production endpoint.
- Agent99 truth:
- Latest status evidence: `C:\Wooo\Agent99\evidence\agent99-Status-20260709-173813.json`.
- `aiServices.openclaw`: `ok=True`, `status=healthy`, `url=http://192.168.0.188:8088/health`.
- Telegram relay evidence: `tgSent=2`, `relayOk=2`.
## 2026-07-09 M3 Command And Alert Ingress Receipt
Agent99 no longer depends on Codex as the only command channel.
- Telegram command inbox deployed: `C:\Wooo\Agent99\bin\agent99-telegram-inbox.ps1`.
- SRE alert inbox deployed: `C:\Wooo\Agent99\bin\agent99-sre-alert-inbox.ps1`.
- Scheduled tasks registered:
- `Wooo-Agent99-Telegram-Inbox`
- `Wooo-Agent99-SRE-Alert-Inbox`
- PowerShell parse readback: all five Agent99 scripts parsed successfully on 99.
- Live config includes both scheduled tasks and freshness contracts.
- Telegram inbox evidence: `C:\Wooo\Agent99\evidence\agent99-TelegramInbox-20260709-230031.json`.
- `ok=True`
- `updatesSeen=0`
- `processedCount=0`
- no Python code or token text in evidence.
- SRE synthetic alert evidence: `C:\Wooo\Agent99\evidence\agent99-SreAlertInbox-20260709-230107.json`.
- input kind: `public_route_502`
- source: `awoooi-sre-war-room`
- resolved mode: `Recover`
- queued count: `1`
- failed count: `0`
- `controlExitCode=0`
- Recovery execution evidence: `C:\Wooo\Agent99\evidence\agent99-Recover-20260709-230108.json`.
- public route failures: `0`
- host reachability failures: `0`
- Harbor core healthy: `True`
- AWOOOI deploy ready: `True`
- Queue processor evidence: `C:\Wooo\Agent99\evidence\agent99-ControlTick-20260709-230108.json`.
- processed count: `1`
- source: `agent99-sre-alert-inbox`
- reason: `sre_alert`
- mode: `Recover`
- exit code: `0`
- Telegram relay: `sent=True`, `relayOk=True`
- evidence contains no Python code.
- Self-health evidence: `C:\Wooo\Agent99\evidence\agent99-SelfCheck-20260709-230223.json`.
- severity: `ok`
- task failures: `0`
- evidence critical: `0`
- evidence warning: `0`
- Telegram inbox task: OK
- SRE alert inbox task: OK
- Dashboard JSON: `C:\Wooo\Agent99\dashboard\agent99-control-center.latest.json`.
- queue pending: `0`
- alert pending: `0`
## 2026-07-09 M4 SRE Telegram Command And Problem Memory Receipt
Agent99 now accepts AwoooI SRE Telegram-group commands as first-class work orders.
- Telegram SRE command source mapping:
- source: `telegram-sre-war-room`
- request path: `C:\Wooo\Agent99\requests`
- queue path: `C:\Wooo\Agent99\queue`
- command result event: `operator_command_result`
- Synthetic SRE Telegram command verification:
- instruction: `檢查所有網站是否恢復正常若502就自動修復`
- request id: `request-20260709-231157-d53f53d8`
- resolved mode: `Recover`
- control exit code: `0`
- evidence: `C:\Wooo\Agent99\evidence\agent99-ControlTick-20260709-231158.json`
- source: `telegram-sre-war-room`
- processed mode: `Recover`
- processed ok: `True`
- processed exit code: `0`
- Telegram relay: sent `1`
- Telegram evidence contains no Python code.
- Problem memory readback:
- problem key: `telegram-sre-war-room_agent99_operator_instruction`
- occurrences: `1`
- resolved: `1`
- failed: `0`
- stats path: `C:\Wooo\Agent99\state\problem-counts.json`
- KM exists: `True`
- RAG feed exists: `True`
- dashboard problem events: `1`
- dashboard resolved: `1`
- dashboard failed: `0`
- Post-check:
- Telegram inbox evidence: `ok=True`, `updatesSeen=0`, `processedCount=0`
- Self-health evidence: `C:\Wooo\Agent99\evidence\agent99-SelfCheck-20260709-231255.json`
- self-health severity: `ok`
- task failures: `0`
- queue pending: `0`
- alert pending: `0`
- SRE group routing hardening:
- Existing relay default chat readback was a private chat, so group commands must not depend only on `TELEGRAM_CHAT_ID`.
- Live config was updated with `allowedCommandChatTitlePattern=AwoooI SRE|AWOOOI SRE|SRE`.
- Telegram inbox accepts matched group commands and carries a redacted `telegram-origin` reply route for completion reports.
## 2026-07-09 M5 SRE Telegram Monitoring Alert Auto-Ingest Receipt
Agent99 now ingests monitoring alerts from the AwoooI SRE Telegram group, not only `/agent99` commands.
- Runtime switch:
- `autoIngestMonitoringAlerts=True`
- `allowedCommandChatTitlePattern=AwoooI SRE|AWOOOI SRE|SRE`
- Encoding hardening:
- Runtime PowerShell scripts are ASCII-only.
- Chinese alert keyword support uses Unicode codepoint checks instead of raw Chinese strings, avoiding Windows PowerShell 5 parser failures.
- Synthetic SRE Telegram monitoring alert:
- text: `Alertmanager FIRING critical public_route_502 service=awoooi host=192.168.0.120 HTTP 502`
- no `/agent99` prefix.
- Telegram inbox evidence: `C:\Wooo\Agent99\evidence\agent99-TelegramInbox-20260709-233230.json`
- `inboxOk=True`
- `alertedCount=1`
- SRE alert bridge evidence:
- `C:\Wooo\Agent99\evidence\agent99-SreAlertInbox-20260709-233231.json`
- queued count: `1`
- failed count: `0`
- Control/recovery evidence:
- `C:\Wooo\Agent99\evidence\agent99-ControlTick-20260709-233232.json`
- source: `telegram-sre-war-room-monitoring`
- kind: `public_route_502`
- mode: `Recover`
- processed ok: `True`
- exit code: `0`
- Telegram attempts: `1`
- Telegram sent: `1`
- evidence contains no Python code.
- Problem memory:
- total events before: `2`
- total events after: `3`
- total resolved after: `3`
- total failed after: `0`
## 2026-07-09 M6 Provider Freshness Signal Receipt
Provider freshness alerts must not be treated as passive route health.
- Gap found:
- Telegram Inbox and SRE Alert Inbox were running, but `provider_freshness_signal` was not present in Agent99 alerts, queue, or execution evidence after 2026-07-09 23:30 Asia/Taipei.
- Only SelfCheck `evidenceFreshness` fields matched the word `freshness`, so the original alert was not processed as an actionable provider freshness signal.
- Runtime behavior added:
- Telegram auto-ingest recognizes `provider_freshness_signal`, `source_provider_freshness_triage`, `raw_signal_normalized`, `controlled_runtime_candidate`, `freshness`, `last seen`, `last_seen`, and `ingestion`.
- SRE Alert Inbox routes freshness signals to `ProviderFreshness`.
- `ProviderFreshness` creates a read-only candidate under `C:\Wooo\Agent99\state\provider-freshness-candidates`.
- Candidate evidence must require provider window, last seen, direct reference, and verifier readback.
- Guardrail remains: no provider switch, no paid model call, no env change, no reload.
## 2026-07-09 M7 AWOOI API To Agent99 Alert Bridge
Agent99 must not depend only on Telegram text polling for AwoooI SRE alerts.
- Producer-side bridge:
- AWOOI API `POST /api/v1/webhooks/alertmanager` now schedules a fail-open Agent99 SRE bridge after alert normalization and fingerprinting.
- The bridge POSTs the same Agent99 alert contract to `AGENT99_SRE_ALERT_RELAY_URL` when configured.
- If relay URL is empty, the bridge writes the contract expected by `C:\Wooo\Agent99\alerts\incoming` when `AGENT99_SRE_ALERT_INBOX_PATH` is configured.
- Empty relay/path means bridge disabled; Alertmanager response must not fail or retry-storm because Agent99 inbox is unavailable.
- Safety:
- Payloads are sanitized before writing.
- Labels with token, secret, password, authorization, cookie, session, or private-key names are redacted.
- Provider freshness signals map to `ProviderFreshness`.
- Route 502 maps to `Recover`; CPU/memory/disk/load maps to `Perf`; backup maps to `BackupCheck`; Harbor maps to `HarborRepair`; K3s/pod/image-pull maps to `AwoooRepair`.
- Guardrail:
- This bridge does not switch provider, call paid models, edit env, reload services, or read raw secrets.
- problem key: `agent99-sre-alert-inbox_awoooi_192.168.0.120_public_route_502`
- problem occurrences: `2`
## 2026-07-10 M8 Agent99 Direct SRE Relay Contract
Telegram text polling is not enough for SLA-grade SRE automation.
- 99-side relay:
- Script: `C:\Wooo\Agent99\bin\agent99-sre-alert-relay.ps1`.
- Scheduled task: `Wooo-Agent99-SRE-Alert-Relay`.
- Default prefix: `http://+:8787/agent99/sre-alert/`.
- Accepted sources: local and `192.168.0.*` by default.
- Optional token env: `AGENT99_SRE_RELAY_TOKEN`.
- Production producer:
- `k8s/awoooi-prod/04-configmap.yaml` sets `AGENT99_SRE_ALERT_RELAY_URL=http://192.168.0.99:8787/agent99/sre-alert/`.
- `AGENT99_SRE_ALERT_RELAY_TOKEN` is optional and must come from Secret/env only.
- API bridge uses relay first, then mounted file inbox fallback.
- Runtime behavior:
- Relay validates POST + JSON, writes into `C:\Wooo\Agent99\alerts\incoming`, and triggers `agent99-sre-alert-inbox.ps1 -RunNow`.
- SRE Alert Inbox resolves mode and queues only allowlisted work.
- Control Loop writes evidence, Telegram completion, problem counts, KM, and RAG import records.
- Relay must not execute arbitrary shell, read secrets, switch providers, reload services, or perform destructive actions.
## 2026-07-09 Windows Update Policy Receipt
99 is configured to prevent surprise Windows Update reboots.
- Evidence: `C:\Wooo\Agent99\evidence\agent99-windows-update-policy-20260709-173933.json`.
- `NoAutoRebootWithLoggedOnUsers=1`.
- `AlwaysAutoRebootAtScheduledTime=0`.
- `AUOptions=3`.
- `ActiveHoursStart=0`, `ActiveHoursEnd=23`.
- `gpupdateOk=True`.
## 2026-07-09 M3 Self-Health Receipt
Agent99 now monitors its own automation loop.
- Scheduled task: `Wooo-Agent99-Self-Health`.
- Trigger interval: every 5 minutes.
- Task Scheduler verification:
- `LastTaskResult=0`.
- Latest triggered evidence: `C:\Wooo\Agent99\evidence\agent99-SelfCheck-20260709-175218.json`.
- `evidenceAfterTrigger=True`.
- Self-check coverage:
- Startup recovery task, heartbeat task, performance guard task, and self-health task.
- Evidence freshness for `agent99-Perf-*.json`, `agent99-Status-*.json`, `agent99-SelfCheck-*.json`, and `agent99-Recover-*.json`.
- Latest performance evidence must include all five hosts: 110, 112, 120, 121, and 188.
- Recent attempted Telegram delivery must be successful through direct env credentials or the 110 relay; a quiet interval with no alert attempts is OK and must not create a false critical self-check.
- Latest result:
- `selfHealth.severity=ok`.
- `taskFailures=0`.
- `performanceSeverity=ok`.
- `telegramSeverity=ok`.
- `tgSent=3`, `relayOk=3`.
## 2026-07-09 M4 110 Load Auto-Reduction Receipt
Agent99 now handles the 110 CPU/load pressure pattern found after reboot.
- Incident findings:
- The high-load source was not the runner.
- Normal high consumers included Gitea and StockPlatform Postgres/API.
- Backup load came from `/backup/scripts/backup-awoooi.sh`, `gzip`, and remote `pg_dump`.
- A random `alpine "tar czf - /data"` container named `mystifying_napier` was consuming CPU and logging compressed binary output.
- Manual controlled action applied once:
- Backup PIDs were set to nice 15 / ionice best-effort 7.
- `mystifying_napier` was stopped and retained as a dead container for evidence.
- Automation added to `performance.loadShedding.actions`:
- `stop-110-runaway-alpine-tar-data`: warning-or-critical action for allowlisted runaway `alpine tar czf - /data` containers.
- `renice-110-awoooi-backup-pressure`: warning-or-critical action for backup/gzip/remote-pg-dump pressure.
- `pause-110-ci-runner-pressure`: critical-only action for runner pressure, unchanged.
- Verification:
- Evidence: `C:\Wooo\Agent99\evidence\agent99-Perf-20260709-180704.json`.
- 110: severity OK, load-per-core 0.67, memory available 62.02%, disk used 80%.
- 112, 120, 121, and 188: severity OK.
- `perfCritical=0`, `perfWarning=0`.
- Telegram relay evidence: `tgSent=2`, `relayOk=2`.
## 2026-07-09 M5 Backup Health Receipt
Agent99 now monitors backup freshness and backup-job contracts.
- Scheduled task: `Wooo-Agent99-Backup-Health`.
- Trigger interval: every 15 minutes.
- Task Scheduler verification:
- `LastTaskResult=0`.
- Latest triggered evidence: `C:\Wooo\Agent99\evidence\agent99-BackupCheck-20260709-190617.json`.
- `evidenceAfterTrigger=True`.
- BackupCheck coverage:
- 13 snapshot targets: `awoooi`, `gitea`, `harbor`, `momo`, `langfuse`, `monitoring`, `signoz`, `clawbot`, `open-webui`, `sentry`, `ai-artifacts`, `configs`, and `public-routes`.
- 7 file/status checks: `backup.log`, `backup-status.log`, legacy `status.log`, legacy `cron.log`, integrity check status, restore drill status, and configs last-status JSON.
- 6 cron contract patterns: `backup-all.sh`, `backup-awoooi-frequent.sh`, `backup-health-textfile-exporter.py`, `check-backup-integrity.sh`, `sync-offsite-backups.sh --mode status`, and `backup-status.sh`.
- Latest result:
- `backupHealth.severity=ok`.
- `targetCritical=0`, `targetWarning=0`.
- `fileCritical=0`, `fileWarning=0`.
- `cronCritical=0`, `cronWarning=0`.
- `tgSent=2`, `relayOk=2`.
- Agent self-health now includes backup monitoring:
- Evidence: `C:\Wooo\Agent99\evidence\agent99-SelfCheck-20260709-190714.json`.
- `Wooo-Agent99-Backup-Health`: OK, `LastTaskResult=0`.
- Backup evidence freshness: OK, latest file `agent99-BackupCheck-20260709-190617.json`.
## 2026-07-09 M6 Alert-to-Remediation Receipt
Agent99 now treats alerts as action triggers, not as the final output.
- Telegram relay bug fixed:
- The relay sender now decodes the message argument from `sys.argv[2]`, so Telegram receives the alert text instead of the Python relay source.
- Info/start/complete events are suppressed by default: `alertAllOperations=false`, `alertInfoEvents=false`.
- Duplicate warning/critical events are deduped for 30 minutes using stable event keys.
- Controlled remediation behavior:
- `Perf -ControlledApply` suppresses immediate performance spam, runs allowlisted load-shedding/remediation first, then alerts the controlled remediation result.
- If no remediation applies, Agent99 still alerts the original warning/critical performance issue.
- 188 disk pressure now has an allowlisted action: `prune-188-docker-build-cache-disk-pressure`.
- The 188 prune action uses `timeoutSeconds=240` and a post-apply verifier: `postVerifyMaxDiskUsedPercent=89`.
- A command timeout is not reported as final critical when the post-verifier proves the host is back under the disk threshold.
- False critical prevention:
- Self-health no longer treats "no recent alerts were needed" as Telegram failure.
- Telegram health is critical only when recent alert attempts exist and delivery failed.
- Telegram delivery health uses a 60-minute lookback across recent evidence files, not only the newest file per evidence family.
- Transient performance warnings are no longer promoted to self-health critical; only missing or critical host performance evidence is critical.
- Backup monitor false critical prevention:
- Backup snapshot readback now supports `snapshotReadTimeoutSeconds`.
- 99 live config sets `snapshotReadTimeoutSeconds=90`.
- Harbor backup timeout at 22:10 was verified as a readback timeout, not a missing backup: `/backup/harbor/snapshots` had latest metadata from 2026-07-09 17:48 Asia/Taipei and remained within the 1800-minute SLA.
- Evidence hygiene:
- Harbor status readback no longer stores raw `docker inspect` JSON.
- Agent99 now records only selected Harbor health fields and sets `inspectOutput=[redacted: raw docker inspect omitted]`.
- Existing Status evidence on 99 was sanitized: 77 files checked; `inspectOutputNotRedactedCount=0`; `unsafeHarborFieldCount=0`.
- Runtime readback after deployment:
- Performance evidence: `C:\Wooo\Agent99\evidence\agent99-Perf-20260709-220718.json`.
- Self-health evidence: `C:\Wooo\Agent99\evidence\agent99-SelfCheck-20260709-220829.json`, `selfHealth.severity=ok`.
- Status evidence: `C:\Wooo\Agent99\evidence\agent99-Status-20260709-221147.json`.
- Host reachability: 110, 112, 120, 121, and 188 all ping OK.
- Public routes: AWOOOI, StockPlatform, FIFA, Gitea, and Harbor all HTTP 200 / non-502.
- Harbor: core, Redis, and registry healthy with redacted evidence.
- AWOOOI: `deployReady=true`, no image pull failure, no crash failure.
- Backup evidence after timeout fix: `C:\Wooo\Agent99\evidence\agent99-BackupCheck-20260709-221759.json`, `backupHealth.severity=ok`, all target/file/cron critical and warning counts are 0.
- Final self-health evidence: `C:\Wooo\Agent99\evidence\agent99-SelfCheck-20260709-222236.json`, `selfHealth.severity=ok`, `taskFailures=0`, `performanceSeverity=ok`, `telegramSeverity=ok`.
## 2026-07-09 M7 Visible Control Center Receipt
Agent99 is now visible and operable on the 99 Windows desktop instead of being only background scheduled tasks.
- New mode:
- `ControlTick`.
- Runs the Agent99 visible control loop.
- Updates dashboard HTML/JSON.
- Processes allowlisted operator command queue files.
- Writes evidence and alerts operator command results to Telegram.
- Scheduled task:
- `Wooo-Agent99-Control-Loop`.
- Trigger interval: every 1 minute.
- Self-health now checks this task.
- Desktop shortcuts created:
- `C:\Users\Public\Desktop\Agent99 Control Center.lnk`.
- `C:\Users\Public\Desktop\Agent99 Queue.lnk`.
- `C:\Users\Public\Desktop\Agent99 Evidence.lnk`.
- `D:\Desktop\Agent99 Control Center.lnk`.
- `D:\Desktop\Agent99 Queue.lnk`.
- `D:\Desktop\Agent99 Evidence.lnk`.
- Dashboard paths:
- HTML: `C:\Wooo\Agent99\dashboard\index.html`.
- JSON: `C:\Wooo\Agent99\dashboard\agent99-control-center.latest.json`.
- Queue processor:
- Input: `C:\Wooo\Agent99\queue\*.json`.
- Processed output: `C:\Wooo\Agent99\queue\processed\processed-<id>.json`.
- Failed output: `C:\Wooo\Agent99\queue\failed\failed-<id>.json`.
- Allowed modes: `Status`, `Recover`, `StartVMs`, `PublicSmoke`, `HarborRepair`, `AwoooRepair`, `Perf`, `LoadShed`, `SelfCheck`, `BackupCheck`.
- Operator command results are Telegram-alerted even when info/start/complete noise is suppressed.
- Verification:
- Control evidence: `C:\Wooo\Agent99\evidence\agent99-ControlTick-20260709-223309.json`.
- Processed command: `codex-visible-publicsmoke-20260709-223308`.
- Processed result: `ok=true`, `exitCode=0`, `durationSeconds=2.2`.
- Command evidence: `C:\Wooo\Agent99\evidence\agent99-PublicSmoke-20260709-223309.json`.
- Public smoke: AWOOOI, StockPlatform, FIFA, Gitea, and Harbor all HTTP 200 / non-502.
- Telegram result: `operator_command_result`, `severity=info`, `sent=true`, `relayOk=true`.
- Self-health evidence after control-loop install: `C:\Wooo\Agent99\evidence\agent99-SelfCheck-20260709-223340.json`, `selfHealth.severity=ok`, `Wooo-Agent99-Control-Loop=ok`.
- Latest performance evidence: `C:\Wooo\Agent99\evidence\agent99-Perf-20260709-223419.json`, `perfCritical=0`, `perfWarning=0`.
## 2026-07-09 M8 Operator Instruction Intake Receipt
Agent99 now accepts operator text instructions and turns them into allowlisted queue commands.
- New submit tool:
- `C:\Wooo\Agent99\bin\agent99-submit-request.ps1`.
- Interactive launcher: `C:\Wooo\Agent99\agent99-submit-request.cmd`.
- Desktop shortcut: `Agent99 Submit Request.lnk`.
- Request flow:
- Operator enters one text instruction.
- Agent99 writes the original request to `C:\Wooo\Agent99\requests`.
- Agent99 resolves the request into an allowlisted mode.
- Agent99 writes a queue command to `C:\Wooo\Agent99\queue`.
- `ControlTick` processes the command, writes evidence, and sends Telegram `operator_command_result`.
- Current allowlisted instruction targets:
- website / route / 502 checks -> `PublicSmoke`.
- reboot / restore / repair -> `Recover`.
- VM / VMware start -> `StartVMs`.
- CPU / load / memory / disk / performance -> `Perf`.
- backup / snapshot / cron -> `BackupCheck`.
- self / agent / health -> `SelfCheck`.
- Harbor / registry -> `HarborRepair`.
- AWOOOI / K3s / pod -> `AwoooRepair`.
- Verification:
- Test instruction: `check all websites are normal` equivalent in Traditional Chinese.
- Request ID: `request-20260709-224133-0dc045fe`.
- Resolved mode: `PublicSmoke`.
- Processed result: `ok=true`, `exitCode=0`, `durationSeconds=1.9`.
- Evidence: `C:\Wooo\Agent99\evidence\agent99-PublicSmoke-20260709-224134.json`.
- Public smoke: AWOOOI, StockPlatform, FIFA, Gitea, and Harbor all HTTP 200 / non-502.
- Telegram result: `operator_command_result`, `severity=info`, `sent=true`, `relayOk=true`.

View File

@@ -0,0 +1,589 @@
# Host Reboot AI Automation SOP
Last updated: 2026-07-09
## P0 Goal
After host 99 or any VM host is rebooted, the automation must detect the reboot, restore VM reachability, restore critical services, and prove public sites are no longer returning 502.
Target recovery bar:
- Detect 99 / 110 / 112 / 120 / 121 / 188 reboot or down state.
- Ensure VMware Workstation on 99 is running and required VMs are powered on.
- Restore Harbor / registry on 110 before forcing K3s workloads to repull images.
- Restore AWOOOI production pods on 120.
- Verify public HTTP readback for AWOOOI, StockPlatform, FIFA, Gitea, and Harbor.
- Emit operator-visible evidence and Telegram alerts for down, recovering, recovered, and degraded states.
## 2026-07-09 Incident Findings To Automate
The reboot incident was not a generic 502 problem. The confirmed chain was:
1. 110 rebooted and Docker/Harbor did not fully recover.
2. `harbor-core` entered a failed/restarting state because Harbor Redis / Docker DNS dependency was not clean.
3. K3s on 120 could not pull AWOOOI images from `192.168.0.110:5000`.
4. AWOOOI prod pods stayed in `ImagePullBackOff`.
5. Public nginx returned 502 because upstream pods were unavailable.
6. Restarting / rebooting 110 cleared the Docker / systemd / Harbor dependency state.
7. Once `harbor-core` became healthy, 120 pods repulled images and AWOOOI recovered.
This failure mode must be treated as a first-class automated recovery path.
## Recovery Priority Order
P0. Control-plane and VM reachability
- 99 Windows host reachable.
- VMware Workstation process running on 99.
- VMs 110, 112, 120, 121, and 188 powered on.
- Hosts 110, 112, 120, 121, and 188 respond to ICMP or SSH.
P1. 110 Harbor and registry dependency chain
- Docker engine active.
- Harbor Redis running.
- `harbor-core` running and healthy.
- Harbor registry / registryctl running and healthy.
- `harbor.wooo.work` returns HTTP 200 after redirect.
- Internal registry pull path from K3s nodes is reachable.
P2. 120 K3s production workloads
- `awoooi-prod` deployments ready:
- `awoooi-api` 1/1
- `awoooi-web` 2/2
- `awoooi-worker` 1/1
- `awoooi-auto-repair-canary` 1/1
- No critical pods stuck in `ImagePullBackOff`, `ErrImagePull`, `CrashLoopBackOff`, or `Pending`.
- If image pull failures remain after Harbor is healthy, delete only the failed pods and let controllers recreate them.
P3. Public user-visible surfaces
- `awoooi.wooo.work` returns HTTP 200 for web.
- `awoooi.wooo.work/api/v1/health` returns JSON and not 502.
- `stock.wooo.work` returns HTTP 200.
- `2026fifa.wooo.work` returns HTTP 200.
- `gitea.wooo.work` returns HTTP 200.
- `harbor.wooo.work` returns HTTP 200.
P4. Degraded provider follow-up
- AWOOOI health can be `degraded` even when 502 is fixed.
- Treat AI provider health separately from user-visible 502 restoration.
- Follow-up checks include `ollama_gcp_a`, `ollama_gcp_b`, and `ollama_local`.
- Do not block public-site 502 recovery on non-primary AI provider cooldown unless the app is user-impacting.
## Automated Decision Tree
1. Detect reboot / outage.
- Ping 110, 112, 120, 121, 188.
- SSH to 110 and 120 if keys are available.
- Query VMware running VM list on 99 if `vmrun.exe` is available.
2. If a VM is not reachable.
- Check VMware VM state.
- Start the VM from 99.
- Wait for guest ping / SSH.
- Alert Telegram: `host_recovering`.
3. If AWOOOI public route is 502.
- Check 120 `awoooi-prod` deployments.
- If pods are `ImagePullBackOff`, check 110 Harbor first.
- Do not repeatedly delete pods before Harbor is healthy.
4. If 110 Harbor is unhealthy.
- Check Docker active state.
- Check `redis`, `harbor-core`, `registry`, and `registryctl`.
- Restart Harbor Redis and `harbor-core` first.
- If Docker/systemd/cgroup operations hang, use controlled 110 guest reboot through VMware.
- After 110 returns, wait for Harbor core healthy before touching K3s pods.
5. If Harbor is healthy and pods are still in image pull failure.
- Delete only failed AWOOOI pods in `awoooi-prod`.
- Wait for deployments to become ready.
- Read back public HTTP.
6. If public sites are HTTP 200 but AWOOOI health is degraded.
- Record degraded providers.
- Alert `service_degraded`.
- Continue provider-specific remediation separately.
## 99 Windows AI Agent Control Node
99 must become the automation entrypoint for reboot recovery.
Required capabilities:
- Run a local AI Agent service on Windows 99 with least-privilege operator permissions.
- Execute the reboot scorecard on a schedule and on boot.
- Control VMware Workstation VM startup through `vmrun.exe`.
- Use SSH keys to 110 and 120 for service-level checks and controlled repair.
- Send Telegram alerts for down, recovering, recovered, degraded, failed, and controlled-remediation-result states.
- Do not stop at alerting. Every warning/critical alert must either trigger an allowlisted remediation, record that no remediation matched, or record that the action was skipped by cooldown/min-severity.
- Do not send start/complete/info noise to Telegram by default. Human-facing alerts must be deduped and action-oriented.
- Persist scorecard evidence to a local non-secret log directory.
- Never store raw passwords, tokens, cookies, sessions, or `.env` in the repo.
- Accept operator commands through the 99 desktop submitter, Telegram `/agent99 ...`, and local queue files without depending on Codex quota.
- Accept SRE war-room alert JSON at `C:\Wooo\Agent99\alerts\incoming`, resolve each alert into an allowlisted mode, and either run controlled remediation or record why no remediation matched.
Candidate implementation layers:
- P0: Windows Task Scheduler runs `host-reboot-scorecard.ps1` at startup and every minute for the first 15 minutes after 99 boot.
- P1: Install a local Windows service wrapper for the scorecard loop.
- P2: Add an AI Agent runtime that consumes scorecard JSON and chooses approved recovery actions.
- P3: Add a UI/console dashboard on 99 for operator evidence and manual override.
Current command and alert loops:
- `Wooo-Agent99-Control-Loop`: processes `C:\Wooo\Agent99\queue`, updates the dashboard, and writes execution evidence.
- `Wooo-Agent99-Telegram-Inbox`: reads Telegram group commands with Agent99 prefixes and submits allowlisted queue work.
- `Wooo-Agent99-SRE-Alert-Inbox`: reads `C:\Wooo\Agent99\alerts\incoming`, converts actionable alerts into queue work, and triggers the control loop.
- `Wooo-Agent99-SRE-Alert-Relay`: runs the 99-side HTTP relay for sanitized AWOOI Alertmanager JSON and writes accepted alerts into `C:\Wooo\Agent99\alerts\incoming`.
- `Wooo-Agent99-Performance-Guard`: runs every minute and performs allowlisted load shedding before escalating unresolved performance alerts.
- `Wooo-Agent99-Self-Health`: verifies the scheduled tasks and evidence freshness for the control plane itself.
AwoooI SRE Telegram command loop:
- Operators and project AI Agents can issue commands in the SRE Telegram group with `/agent99 ...`.
- Agent99 records these as `source=telegram-sre-war-room`.
- Completed automated repairs send `operator_command_result` back to Telegram.
- Results include source, instruction, mode, exit code, evidence path, problem count, and KM path.
- The command path must not depend on Codex quota.
- The command path must not depend only on the default `TELEGRAM_CHAT_ID`, because that can point to a private chat. Agent99 also accepts command updates from Telegram chats whose title matches `AwoooI SRE|AWOOOI SRE|SRE`.
- When the command comes from a matched Telegram group, the completion report is sent back to that same Telegram origin; stored evidence redacts the raw chat id as `telegram-origin`.
- Non-command monitoring alerts in the SRE Telegram group are auto-ingested when they look like Alertmanager/FIRING/critical/warning/502/CPU/disk/Harbor/K3s/backup/host-down/provider-freshness alerts.
- Auto-ingested monitoring alerts use `source=telegram-sre-war-room-monitoring` and enter the same SRE alert inbox / queue / recovery / Telegram completion / KM-RAG path.
Alert-to-action routing:
- Public 502 / route down / website down -> `Recover`.
- Host down / reboot detected / VM not running -> `Recover`.
- CPU, memory, disk, or load pressure -> `Perf` with controlled apply.
- Harbor or registry unhealthy -> `HarborRepair`.
- AWOOOI K3s, pod, deployment, or image pull failure -> `AwoooRepair`.
- Backup freshness, snapshot, restore drill, or cron issue -> `BackupCheck`.
- Provider freshness, last-seen, ingestion, raw signal, or no-false-green risk -> `ProviderFreshness`.
- Unknown actionable alert -> `Status` evidence first.
Producer-side Agent99 bridge:
- AWOOI API Alertmanager webhook schedules an Agent99 bridge after normalization and fingerprinting.
- Preferred transport: when `AGENT99_SRE_ALERT_RELAY_URL` is configured, the bridge POSTs sanitized alert JSON to the 99 relay. Production URL is `http://192.168.0.99:8787/agent99/sre-alert/`.
- Fallback transport: when `AGENT99_SRE_ALERT_INBOX_PATH` is configured and relay URL is empty, the bridge writes sanitized alert JSON directly to the Agent99 SRE inbox contract.
- If relay/path is empty or unavailable, the bridge fails open and logs; it must not cause Alertmanager retries.
- Relay safety: only LAN/local source prefixes are accepted by default; optional `AGENT99_SRE_RELAY_TOKEN` / `AGENT99_SRE_ALERT_RELAY_TOKEN` values stay in env/Secret and are never stored in repo or evidence.
- Provider freshness bridge output must require provider window, last seen, direct reference, and verifier readback before dashboard green is trusted.
Approved controlled actions for the 99 agent:
- Start a missing VM.
- Ping and SSH readback.
- Restart Harbor Redis / Harbor core after evidence of Harbor token failure.
- Delete K3s pods stuck in image pull failure only after Harbor is healthy.
- Run public route readback.
- Send Telegram status.
Actions still requiring explicit break-glass:
- Reading or exfiltrating secrets.
- Destructive database operations.
- Firewall cutover.
- Remote delete / prune / force push.
- Production provider route cutover with cost or billing impact.
## Evidence Required Before Calling Recovery Complete
Recovery is complete only when all of the following are captured:
- Five VM hosts have reachability evidence.
- 110 Harbor core is healthy.
- 120 AWOOOI deployments are ready.
- Public routes return non-502, preferably HTTP 200.
- Any degraded health components are listed separately with owner / next action.
## Current 2026-07-09 Receipts
- 110 `harbor-core`: recovered to `Up ... (healthy)`.
- 120 `awoooi-prod`:
- `awoooi-api`: 1/1
- `awoooi-web`: 2/2
- `awoooi-worker`: 1/1
- `awoooi-auto-repair-canary`: 1/1
- Public readback:
- `awoooi.wooo.work`: HTTP 200 after redirect.
- `stock.wooo.work`: HTTP 200 after redirect.
- `2026fifa.wooo.work`: HTTP 200 after redirect.
- `gitea.wooo.work`: HTTP 200 after redirect.
- `harbor.wooo.work`: HTTP 200 after redirect.
- Host reachability:
- 110, 112, 120, 121, 188 responded to ping or SSH.
- Remaining degraded signal:
- AWOOOI health reported degraded AI provider signals including `ollama_gcp_b` timeout and `ollama_local` connection refused / unavailable.
## Agent99 Installation Receipt
2026-07-09 16:49 Asia/Taipei:
- 99 host: `WOOO-SUPER`.
- Agent root: `C:\Wooo\Agent99`.
- VMware CLI: `C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe`.
- Live config: `C:\Wooo\Agent99\config\agent99.config.json`.
- Evidence directory: `C:\Wooo\Agent99\evidence`.
- Scheduled tasks:
- `Wooo-Agent99-Heartbeat`: `LastTaskResult=0`, latest status evidence `agent99-Status-20260709-164845.json`.
- `Wooo-Agent99-Startup-Recovery`: `LastTaskResult=0`, latest recovery evidence `agent99-Recover-20260709-164937.json`.
- Startup recovery readback:
- VM selector: 110, 188, 112, 120, 121 all `host_already_reachable`.
- Harbor: core, Redis, and registry all healthy.
- AWOOOI: deploy ready, no image pull failure, no crash failure.
- Public routes: `awoooi.wooo.work`, `stock.wooo.work`, `2026fifa.wooo.work`, `gitea.wooo.work`, and `harbor.wooo.work` all HTTP 200 / non-502.
## Agent99 Performance Guard Receipt
2026-07-09 17:15 Asia/Taipei:
- Scheduled task: `Wooo-Agent99-Performance-Guard`.
- Trigger interval: every 1 minute.
- Last task result: `0`.
- Latest evidence: `C:\Wooo\Agent99\evidence\agent99-Perf-20260709-171502.json`.
- Host performance readback:
- 110: OK.
- 112: OK.
- 120: OK.
- 121: OK.
- 188: OK.
- Controlled load shedding:
- Only allowlisted actions can run.
- Current 110 runner action requires `load_per_core_critical`.
- Memory/disk pressure alerts do not execute the runner pause action.
- Telegram audit:
- Agent99 emits Telegram attempts for all operation start/complete events, warnings, critical events, and load-shed actions.
- Agent99 supports local env aliases and an existing-token relay.
- Current relay: 110 `stockplatform-v2-api-1`, using that container's existing `TELEGRAM_BOT_TOKEN` / `TELEGRAM_CHAT_ID`.
- 2026-07-09 17:27 verification: `Wooo-Agent99-Performance-Guard` `LastTaskResult=0`; latest evidence `agent99-Perf-20260709-172712.json`; `tgSent=2`; `relayOk=True`.
- Token and chat id values were not printed, copied into repo files, or written to evidence.
## Agent99 Command And SRE Alert Integration Receipt
2026-07-09 23:03 Asia/Taipei:
- Telegram command inbox is deployed and scheduled:
- Script: `C:\Wooo\Agent99\bin\agent99-telegram-inbox.ps1`
- Task: `Wooo-Agent99-Telegram-Inbox`
- Latest evidence: `C:\Wooo\Agent99\evidence\agent99-TelegramInbox-20260709-230031.json`
- Result: `ok=True`, `updatesSeen=0`, `processedCount=0`
- Evidence check: no Python code, no token text.
- SRE war-room alert inbox is deployed and scheduled:
- Script: `C:\Wooo\Agent99\bin\agent99-sre-alert-inbox.ps1`
- Task: `Wooo-Agent99-SRE-Alert-Inbox`
- Incoming path: `C:\Wooo\Agent99\alerts\incoming`
- Latest evidence: `C:\Wooo\Agent99\evidence\agent99-SreAlertInbox-20260709-230226.json`
- Synthetic 502 alert test:
- Alert id: `synthetic-public-502-20260709-230107`
- Alert source: `awoooi-sre-war-room`
- Alert kind: `public_route_502`
- Resolved mode: `Recover`
- Queue processor evidence: `C:\Wooo\Agent99\evidence\agent99-ControlTick-20260709-230108.json`
- Processed count: `1`
- Mode: `Recover`
- Exit code: `0`
- Telegram relay: `sent=True`, `relayOk=True`
- Recovery verifier from the synthetic alert:
- Evidence: `C:\Wooo\Agent99\evidence\agent99-Recover-20260709-230108.json`
- Public route failures: `0`
- Host reachability failures: `0`
- Harbor core healthy: `True`
- AWOOOI deploy ready: `True`
- Agent99 self-health after integration:
- Evidence: `C:\Wooo\Agent99\evidence\agent99-SelfCheck-20260709-230223.json`
- Severity: `ok`
- Task failures: `0`
- Evidence critical: `0`
- Evidence warning: `0`
- Telegram inbox task: OK
- SRE alert inbox task: OK
- Dashboard queue pending: `0`
- Dashboard alert pending: `0`
## Agent99 Problem Count / KM / RAG Receipt
2026-07-09 23:12 Asia/Taipei:
- Synthetic AwoooI SRE Telegram command:
- instruction: `檢查所有網站是否恢復正常若502就自動修復`
- source: `telegram-sre-war-room`
- request id: `request-20260709-231157-d53f53d8`
- resolved mode: `Recover`
- control exit code: `0`
- Execution evidence:
- `C:\Wooo\Agent99\evidence\agent99-ControlTick-20260709-231158.json`
- processed source: `telegram-sre-war-room`
- processed mode: `Recover`
- processed ok: `True`
- processed exit code: `0`
- Telegram relay sent: `1`
- Telegram evidence contains no Python code.
- Problem counters:
- path: `C:\Wooo\Agent99\state\problem-counts.json`
- total events: `1`
- total resolved: `1`
- total failed: `0`
- problem key: `telegram-sre-war-room_agent99_operator_instruction`
- occurrences: `1`
- resolved: `1`
- failed: `0`
- Knowledge outputs:
- full event ledger: `C:\Wooo\Agent99\playbooks\agent99-incident-records.jsonl`
- KM summary: `C:\Wooo\Agent99\playbooks\agent99-incident-km.md`
- RAG import feed: `C:\Wooo\Agent99\playbooks\rag-import\agent99-incidents.jsonl`
- Dashboard:
- problem events: `1`
- resolved: `1`
- failed: `0`
- queue pending: `0`
- alert pending: `0`
- SRE Telegram group routing:
- Default relay chat readback was private chat `OoO`, not the SRE group.
- Live Agent99 config now includes `allowedCommandChatTitlePattern=AwoooI SRE|AWOOOI SRE|SRE`.
- Telegram inbox readback after the change: `ok=True`, `updatesSeen=0`, `processedCount=0`.
- Next real `/agent99 ...` message from the SRE group will be accepted by title match and will reply back to the same Telegram origin.
## Agent99 SRE Telegram Monitoring Alert Auto-Ingest Receipt
2026-07-09 23:32 Asia/Taipei:
- Runtime behavior:
- SRE Telegram group monitoring alerts are now consumed automatically.
- Prefix `/agent99` is no longer required for monitoring alerts.
- Auto-ingest is controlled by `autoIngestMonitoringAlerts=True`.
- Group matching uses `allowedCommandChatTitlePattern=AwoooI SRE|AWOOOI SRE|SRE`.
- Loop prevention:
- Agent99's own `Agent99 Alert` / `operator_command_result` messages are ignored by monitoring auto-ingest.
- Operator command result Telegram reports bypass dedupe so every completed repair is reported.
- Provider freshness signals are not passive dashboard health; they create a read-only freshness candidate and require provider window, last seen, direct reference, and verifier readback.
- Provider freshness guardrail: do not switch provider, call paid model, edit env, or reload.
- Synthetic monitoring alert:
- `Alertmanager FIRING critical public_route_502 service=awoooi host=192.168.0.120 HTTP 502`
- no `/agent99` prefix.
- Evidence chain:
- Telegram inbox: `C:\Wooo\Agent99\evidence\agent99-TelegramInbox-20260709-233230.json`
- `inboxOk=True`
- `alertedCount=1`
- SRE alert inbox: `C:\Wooo\Agent99\evidence\agent99-SreAlertInbox-20260709-233231.json`
- queued count: `1`
- failed count: `0`
- Control loop: `C:\Wooo\Agent99\evidence\agent99-ControlTick-20260709-233232.json`
- alert source: `telegram-sre-war-room-monitoring`
- alert kind: `public_route_502`
- mode: `Recover`
- processed ok: `True`
- exit code: `0`
- Telegram attempts: `1`
- Telegram sent: `1`
- Recovery: `C:\Wooo\Agent99\evidence\agent99-Recover-20260709-233055.json`
- Problem counters:
- total events before: `2`
- total events after: `3`
- total resolved after: `3`
- total failed after: `0`
- problem key: `agent99-sre-alert-inbox_awoooi_192.168.0.120_public_route_502`
- problem occurrences: `2`
## OpenClaw Cold-Start Readback
2026-07-09 17:38 Asia/Taipei:
- Production OpenClaw URL: `http://192.168.0.188:8088`.
- 188 service: `clawbot.service` is `active`.
- Health endpoint: `http://192.168.0.188:8088/health`.
- Health readback: `status=healthy`, `version=6.0`, `service=ClawBot`, `environment=production`, `telegram_bot=connected`.
- Agent99 latest status evidence: `C:\Wooo\Agent99\evidence\agent99-Status-20260709-173813.json`.
- Agent99 `aiServices.openclaw`: `ok=True`, `status=healthy`.
- Telegram relay from the Agent99 status run: `tgSent=2`, `relayOk=2`.
- Note: `192.168.0.188:8089` is not the current production OpenClaw endpoint.
## Windows Update No-Surprise-Reboot Policy
2026-07-09 17:39 Asia/Taipei:
- Evidence: `C:\Wooo\Agent99\evidence\agent99-windows-update-policy-20260709-173933.json`.
- `NoAutoRebootWithLoggedOnUsers=1`.
- `AlwaysAutoRebootAtScheduledTime=0`.
- `AUOptions=3`.
- `ActiveHoursStart=0`, `ActiveHoursEnd=23`.
- `gpupdateOk=True`.
## Agent99 Self-Health Receipt
2026-07-09 17:52 Asia/Taipei:
- Scheduled task: `Wooo-Agent99-Self-Health`.
- Trigger interval: every 5 minutes.
- Task Scheduler trigger verification:
- `LastTaskResult=0`.
- Evidence: `C:\Wooo\Agent99\evidence\agent99-SelfCheck-20260709-175218.json`.
- `evidenceAfterTrigger=True`.
- Self-health checks:
- `Wooo-Agent99-Startup-Recovery`: OK.
- `Wooo-Agent99-Heartbeat`: OK.
- `Wooo-Agent99-Performance-Guard`: OK.
- `Wooo-Agent99-Self-Health`: OK.
- Evidence freshness: performance, status, self-health, and startup recovery OK.
- Latest performance evidence: all five hosts OK.
- Telegram delivery: `tgSent=3`, `relayOk=3`.
- Updated rule: a quiet interval with no alert attempts is OK; only failed alert attempts are Telegram critical.
## 110 Load Pressure Incident Readback
2026-07-09 18:07 Asia/Taipei:
- Symptom: 110 host load stayed above warning after all hosts rebooted.
- Initial findings:
- `gitea` and `stockplatform-v2-postgres-1` were the highest regular service containers.
- `/backup/scripts/backup-awoooi.sh` was running a remote `pg_dump` from 188 and local gzip compression.
- A random container `mystifying_napier` was running `alpine "tar czf - /data"` with no restart policy and no labels, consuming CPU and writing compressed binary output into Docker logs.
- Controlled actions taken:
- Backup pipeline PIDs were reduced to nice 15 / ionice best-effort 7.
- `mystifying_napier` was stopped and left as `Dead`; it was not deleted, so evidence remains available.
- Agent99 automation added:
- `stop-110-runaway-alpine-tar-data`: stops allowlisted `alpine` containers running `tar czf - /data` when 110 is at warning or critical load.
- `renice-110-awoooi-backup-pressure`: lowers priority for `backup-awoooi.sh`, `gzip`, and remote `pg_dump` pipeline PIDs when 110 is at warning or critical load.
- `pause-110-ci-runner-pressure`: still only runs at critical load with `load_per_core_critical`; runner is not touched for backup, disk, memory, or warning-only pressure.
- Warning-level top CPU capture is enabled with a 10-second timeout to avoid diagnostics becoming a new load source.
- Verification:
- Evidence: `C:\Wooo\Agent99\evidence\agent99-Perf-20260709-180704.json`.
- 110 load-per-core: `0.67`, severity `ok`.
- 112, 120, 121, and 188: severity `ok`.
- `perfCritical=0`, `perfWarning=0`.
- Load-shedding actions skipped because host severity was below threshold.
- Telegram relay: `tgSent=2`, `relayOk=2`.
## Agent99 Backup Health Receipt
2026-07-09 19:07 Asia/Taipei:
- Scheduled task: `Wooo-Agent99-Backup-Health`.
- Trigger interval: every 15 minutes.
- Task Scheduler trigger verification:
- `LastTaskResult=0`.
- Evidence: `C:\Wooo\Agent99\evidence\agent99-BackupCheck-20260709-190617.json`.
- `evidenceAfterTrigger=True`.
- BackupCheck coverage:
- Snapshot freshness targets: `awoooi`, `gitea`, `harbor`, `momo`, `langfuse`, `monitoring`, `signoz`, `clawbot`, `open-webui`, `sentry`, `ai-artifacts`, `configs`, and `public-routes`.
- File/status checks: `backup.log`, `backup-status.log`, legacy `status.log`, legacy `cron.log`, integrity check status, restore drill status, and configs last-status JSON.
- Cron contract patterns: `backup-all.sh`, `backup-awoooi-frequent.sh`, `backup-health-textfile-exporter.py`, `check-backup-integrity.sh`, `sync-offsite-backups.sh --mode status`, and `backup-status.sh`.
- Latest result:
- `backupHealth.severity=ok`.
- `targetCritical=0`, `targetWarning=0`.
- `fileCritical=0`, `fileWarning=0`.
- `cronCritical=0`, `cronWarning=0`.
- `targetCount=13`, `fileCheckCount=7`, `cronPatternCount=6`.
- Telegram relay: `tgSent=2`, `relayOk=2`.
- Self-health cross-check:
- Evidence: `C:\Wooo\Agent99\evidence\agent99-SelfCheck-20260709-190714.json`.
- `Wooo-Agent99-Backup-Health`: OK, `LastTaskResult=0`.
- Backup evidence freshness: OK, latest file `agent99-BackupCheck-20260709-190617.json`, age `0.5` minutes, max age `30` minutes.
## Alert-to-Remediation / Evidence Sanitization Readback
2026-07-09 22:11 Asia/Taipei:
- Root cause fixed for unreadable Telegram relay messages:
- Relay sender argument index corrected from `sys.argv[1]` to `sys.argv[2]`.
- Telegram now receives formatted Agent99 alert text, not embedded Python relay code.
- Noise control:
- `alertAllOperations=false`.
- `alertInfoEvents=false`.
- `dedupeMinutes=30`.
- `agent_complete` remains evidence-only and does not alert by default.
- Alert-to-action rule:
- `Perf -ControlledApply` suppresses immediate performance spam, performs allowlisted remediation first, then alerts `load_shed_result`.
- If no remediation applies, Agent99 alerts the unresolved performance issue.
- 188 disk pressure is now allowlisted through `prune-188-docker-build-cache-disk-pressure`.
- The 188 action uses `timeoutSeconds=240` plus a post-apply verifier; `postVerifyMaxDiskUsedPercent=89`.
- Timeout alone is not final truth; post-verifier success can mark remediation effective.
- Self-health false critical fixed:
- A quiet interval with no recent alerts is OK.
- Telegram health is critical only when recent alert attempts exist and all delivery attempts fail.
- Telegram delivery health now uses a 60-minute lookback across recent evidence files.
- Transient performance warnings are no longer promoted to self-health critical; only missing or critical host performance evidence is critical.
- Backup monitor false critical fixed:
- Backup snapshot readback now supports `snapshotReadTimeoutSeconds`.
- 99 live config sets `snapshotReadTimeoutSeconds=90`.
- Harbor backup was verified as present and fresh after a timeout false critical; latest metadata was from 2026-07-09 17:48 Asia/Taipei and within the 1800-minute SLA.
- Evidence security fixed:
- Harbor readback now stores selected health fields only: name, running state, health state.
- Raw `docker inspect` output is not stored.
- Existing Status evidence on 99 was sanitized: 77 files checked; `inspectOutputNotRedactedCount=0`; `unsafeHarborFieldCount=0`.
- Latest recovery proof:
- Status evidence: `C:\Wooo\Agent99\evidence\agent99-Status-20260709-221147.json`.
- Host reachability: 110, 112, 120, 121, and 188 all OK.
- Public routes: `awoooi.wooo.work`, `stock.wooo.work`, `2026fifa.wooo.work`, `gitea.wooo.work`, and `harbor.wooo.work` all HTTP 200 / non-502.
- Performance: `perfCritical=0`, `perfWarning=0`; 188 disk used 87%; 110 load-per-core 0.85.
- Harbor: core, Redis, and registry healthy with redacted evidence.
- AWOOOI: `deployReady=true`, `imagePullFailure=false`, `crashFailure=false`.
- Backup evidence after timeout fix: `C:\Wooo\Agent99\evidence\agent99-BackupCheck-20260709-221759.json`, `backupHealth.severity=ok`, all target/file/cron critical and warning counts are 0.
- Final self-health evidence: `C:\Wooo\Agent99\evidence\agent99-SelfCheck-20260709-222236.json`, `selfHealth.severity=ok`, `taskFailures=0`, `performanceSeverity=ok`, `telegramSeverity=ok`.
## 99 Visible Agent Control Center
2026-07-09 22:33 Asia/Taipei:
- Agent99 is visible and controllable from the 99 Windows desktop.
- New scheduled task:
- `Wooo-Agent99-Control-Loop`.
- Runs every 1 minute.
- Updates dashboard and processes queue commands.
- Desktop shortcuts:
- `C:\Users\Public\Desktop\Agent99 Control Center.lnk`.
- `C:\Users\Public\Desktop\Agent99 Queue.lnk`.
- `C:\Users\Public\Desktop\Agent99 Evidence.lnk`.
- `D:\Desktop\Agent99 Control Center.lnk`.
- `D:\Desktop\Agent99 Queue.lnk`.
- `D:\Desktop\Agent99 Evidence.lnk`.
- Dashboard:
- HTML: `C:\Wooo\Agent99\dashboard\index.html`.
- JSON: `C:\Wooo\Agent99\dashboard\agent99-control-center.latest.json`.
- Latest dashboard timestamp readback: `2026-07-09T22:33:26+08:00`.
- Queue readback: `pending=0`, `processed=2`, `failed=0`.
- Queue command contract:
- Add a JSON command to `C:\Wooo\Agent99\queue`.
- `ControlTick` processes only allowlisted modes.
- Results are written to `C:\Wooo\Agent99\queue\processed` or `C:\Wooo\Agent99\queue\failed`.
- Operator command results are sent to Telegram.
- Verification:
- Command ID: `codex-visible-publicsmoke-20260709-223308`.
- Mode: `PublicSmoke`.
- Result: `ok=true`, `exitCode=0`.
- Evidence: `C:\Wooo\Agent99\evidence\agent99-PublicSmoke-20260709-223309.json`.
- Telegram: `operator_command_result`, `severity=info`, `sent=true`, `relayOk=true`.
- Self-health: `C:\Wooo\Agent99\evidence\agent99-SelfCheck-20260709-223340.json`, `selfHealth.severity=ok`, `Wooo-Agent99-Control-Loop=ok`.
- Latest performance: `C:\Wooo\Agent99\evidence\agent99-Perf-20260709-223419.json`, `perfCritical=0`, `perfWarning=0`.
## Operator Instruction Intake
2026-07-09 22:41 Asia/Taipei:
- Agent99 accepts text instructions on 99 through:
- `C:\Wooo\Agent99\bin\agent99-submit-request.ps1`.
- `C:\Wooo\Agent99\agent99-submit-request.cmd`.
- `Agent99 Submit Request.lnk` on the desktop.
- Flow:
- instruction -> request record -> queue command -> `ControlTick` -> evidence -> Telegram result.
- Current allowlisted request router:
- websites / routes / 502 -> `PublicSmoke`.
- restore / reboot / repair -> `Recover`.
- VM / VMware -> `StartVMs`.
- CPU / load / memory / disk / performance -> `Perf`.
- backup / snapshot / cron -> `BackupCheck`.
- self health / agent health -> `SelfCheck`.
- Harbor / registry -> `HarborRepair`.
- AWOOOI / K3s / pods -> `AwoooRepair`.
- Verification:
- Chinese instruction submitted from Codex to 99: check whether all websites are normal.
- Request ID: `request-20260709-224133-0dc045fe`.
- Resolved mode: `PublicSmoke`.
- Processed result: `ok=true`, `exitCode=0`.
- Evidence: `C:\Wooo\Agent99\evidence\agent99-PublicSmoke-20260709-224134.json`.
- Public routes: AWOOOI, StockPlatform, FIFA, Gitea, and Harbor all HTTP 200 / non-502.
- Telegram: `operator_command_result`, `severity=info`, `sent=true`, `relayOk=true`.

162
agent99-bootstrap.ps1 Normal file
View File

@@ -0,0 +1,162 @@
param(
[string]$AgentRoot = "C:\Wooo\Agent99",
[switch]$InstallScheduledTasks
)
$ErrorActionPreference = "Stop"
$SourceRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
$BinDir = Join-Path $AgentRoot "bin"
$ConfigDir = Join-Path $AgentRoot "config"
$EvidenceDir = Join-Path $AgentRoot "evidence"
$LogDir = Join-Path $AgentRoot "logs"
$QueueDir = Join-Path $AgentRoot "queue"
$RequestDir = Join-Path $AgentRoot "requests"
$AlertDir = Join-Path $AgentRoot "alerts"
$PlaybookDir = Join-Path $AgentRoot "playbooks"
$DashboardDir = Join-Path $AgentRoot "dashboard"
foreach ($dir in @($AgentRoot, $BinDir, $ConfigDir, $EvidenceDir, $LogDir, $QueueDir, $RequestDir, $AlertDir, $PlaybookDir, $DashboardDir)) {
New-Item -ItemType Directory -Force -Path $dir | Out-Null
}
function Copy-AgentFile {
param([string]$Name)
$src = Join-Path $SourceRoot $Name
$dst = Join-Path $BinDir $Name
if (Test-Path $src) {
$srcFull = [System.IO.Path]::GetFullPath($src)
$dstFull = [System.IO.Path]::GetFullPath($dst)
if ($srcFull -ne $dstFull) {
Copy-Item -Force $src $dst
}
}
}
Copy-AgentFile "agent99-control-plane.ps1"
Copy-AgentFile "agent99-register-tasks.ps1"
Copy-AgentFile "agent99-submit-request.ps1"
Copy-AgentFile "agent99-telegram-inbox.ps1"
Copy-AgentFile "agent99-sre-alert-inbox.ps1"
Copy-AgentFile "agent99-sre-alert-relay.ps1"
Copy-AgentFile "host-reboot-scorecard.ps1"
$exampleConfig = Join-Path $SourceRoot "agent99.config.example.json"
$configPath = Join-Path $ConfigDir "agent99.config.json"
if (-not (Test-Path $configPath)) {
if (Test-Path $exampleConfig) {
Copy-Item $exampleConfig $configPath
} else {
@"
{
"agentName": "Agent99",
"agentRoot": "$($AgentRoot.Replace('\', '\\'))",
"sshUser": "wooo",
"sshUsers": {},
"vmrunPath": "",
"hosts": ["192.168.0.110", "192.168.0.112", "192.168.0.120", "192.168.0.121", "192.168.0.188"],
"vms": [],
"publicUrls": ["awoooi.wooo.work", "stock.wooo.work", "2026fifa.wooo.work", "gitea.wooo.work", "harbor.wooo.work"],
"sreAlertRelay": {
"enabled": true,
"prefix": "http://+:8787/agent99/sre-alert/",
"tokenEnv": "AGENT99_SRE_RELAY_TOKEN",
"allowedRemotePrefixes": ["127.", "::1", "192.168.0."]
},
"telegram": {
"enabled": true,
"botTokenEnv": "AGENT99_TELEGRAM_BOT_TOKEN",
"chatIdEnv": "AGENT99_TELEGRAM_CHAT_ID",
"botTokenEnvAliases": ["AWOOOI_TG_BOT_TOKEN", "OPENCLAW_TG_BOT_TOKEN", "TELEGRAM_BOT_TOKEN", "TELEGRAM_TOKEN", "TG_BOT_TOKEN", "AIDER_WATCH_TELEGRAM_TOKEN"],
"chatIdEnvAliases": ["TELEGRAM_CHAT_ID", "TELEGRAM_ALERT_CHAT_ID", "TELEGRAM_P0_CHAT_ID", "TELEGRAM_SECURITY_CHAT_ID", "OPENCLAW_TG_CHAT_ID", "TG_CHAT_ID", "AIDER_WATCH_TELEGRAM_CHAT_ID"],
"relay": { "enabled": true, "host": "192.168.0.110", "container": "stockplatform-v2-api-1" },
"alertAllOperations": false,
"alertInfoEvents": false,
"alertOperatorCommands": true,
"dedupeMinutes": 30
},
"selfHealth": {
"scheduledTasks": [
"Wooo-Agent99-Startup-Recovery",
"Wooo-Agent99-Heartbeat",
"Wooo-Agent99-Performance-Guard",
"Wooo-Agent99-Control-Loop",
"Wooo-Agent99-Telegram-Inbox",
"Wooo-Agent99-SRE-Alert-Inbox",
"Wooo-Agent99-SRE-Alert-Relay",
"Wooo-Agent99-Self-Health",
"Wooo-Agent99-Backup-Health"
]
},
"performance": {
"enabled": true,
"intervalMinutes": 1,
"loadPerCoreWarning": 0.9,
"loadPerCoreCritical": 1.5,
"memoryAvailablePercentCritical": 10,
"diskUsedPercentWarning": 90,
"diskUsedPercentCritical": 95,
"captureTopCpuOnWarning": true,
"topCpuTimeoutSeconds": 10,
"loadShedding": { "enabled": true, "actions": [] }
},
"backupHealth": {
"host": "192.168.0.110",
"targets": [],
"fileChecks": [],
"cronPatterns": []
}
}
"@ | Set-Content -Encoding UTF8 $configPath
}
}
$launcher = Join-Path $AgentRoot "agent99-run.ps1"
@"
param(
[ValidateSet("Status", "Recover", "StartVMs", "PublicSmoke", "HarborRepair", "AwoooRepair", "Perf", "LoadShed", "SelfCheck", "BackupCheck", "ProviderFreshness", "ControlTick")]
[string]`$Mode = "Status",
[switch]`$ControlledApply
)
& "$BinDir\agent99-control-plane.ps1" -Mode `$Mode -ControlledApply:`$ControlledApply -ConfigPath "$configPath" -EvidenceDir "$EvidenceDir"
"@ | Set-Content -Encoding UTF8 $launcher
$submitCmd = Join-Path $AgentRoot "agent99-submit-request.cmd"
@"
@echo off
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$BinDir\agent99-submit-request.ps1" -RunNow
pause
"@ | Set-Content -Encoding ASCII $submitCmd
function Find-Vmrun {
$candidates = @(
"C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe",
"C:\Program Files\VMware\VMware Workstation\vmrun.exe"
)
foreach ($candidate in $candidates) {
if (Test-Path $candidate) { return $candidate }
}
return ""
}
$vmrun = Find-Vmrun
if ($vmrun) {
$config = Get-Content $configPath -Raw | ConvertFrom-Json
$config.vmrunPath = $vmrun
$config | ConvertTo-Json -Depth 8 | Set-Content -Encoding UTF8 $configPath
}
if ($InstallScheduledTasks) {
& (Join-Path $BinDir "agent99-register-tasks.ps1") -AgentRoot $AgentRoot
}
Write-Host "Agent99 bootstrap complete"
Write-Host "AgentRoot: $AgentRoot"
Write-Host "Config: $configPath"
Write-Host "Evidence: $EvidenceDir"
if ($vmrun) {
Write-Host "vmrun: $vmrun"
} else {
Write-Host "vmrun: not detected; update agent99.config.json"
}

2454
agent99-control-plane.ps1 Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,119 @@
param(
[string]$AgentRoot = "C:\Wooo\Agent99",
[string]$StagingDir = "",
[string]$RelayPrefix = "http://+:8787/agent99/sre-alert/",
[string]$TokenEnv = "AGENT99_SRE_RELAY_TOKEN"
)
$ErrorActionPreference = "Stop"
if (-not $StagingDir) {
$StagingDir = Split-Path -Parent $MyInvocation.MyCommand.Path
}
$BinDir = Join-Path $AgentRoot "bin"
$ConfigPath = Join-Path $AgentRoot "config\agent99.config.json"
$PlaybookDir = Join-Path $AgentRoot "playbooks"
$EvidenceDir = Join-Path $AgentRoot "evidence"
New-Item -ItemType Directory -Force -Path $BinDir, $PlaybookDir, $EvidenceDir | Out-Null
foreach ($name in @("agent99-sre-alert-relay.ps1", "agent99-register-tasks.ps1", "agent99-bootstrap.ps1")) {
$src = Join-Path $StagingDir $name
if (-not (Test-Path $src)) {
throw "missing staged file: $name"
}
Copy-Item -Force $src (Join-Path $BinDir $name)
}
foreach ($name in @("agent99.config.example.json", "agent99.config.99.example.json")) {
$src = Join-Path $StagingDir $name
if (Test-Path $src) {
Copy-Item -Force $src (Join-Path $PlaybookDir $name)
}
}
function Set-AgentProp {
param([object]$Object, [string]$Name, [object]$Value)
if ($Object.PSObject.Properties[$Name]) {
$Object.PSObject.Properties[$Name].Value = $Value
} else {
$Object | Add-Member -NotePropertyName $Name -NotePropertyValue $Value
}
}
if (-not (Test-Path $ConfigPath)) {
throw "missing config: $ConfigPath"
}
$config = Get-Content $ConfigPath -Raw | ConvertFrom-Json
if (-not $config.PSObject.Properties["sreAlertRelay"]) {
$config | Add-Member -NotePropertyName "sreAlertRelay" -NotePropertyValue ([pscustomobject]@{})
}
Set-AgentProp $config.sreAlertRelay "enabled" $true
Set-AgentProp $config.sreAlertRelay "prefix" $RelayPrefix
Set-AgentProp $config.sreAlertRelay "tokenEnv" $TokenEnv
Set-AgentProp $config.sreAlertRelay "allowedRemotePrefixes" @("127.", "::1", "192.168.0.")
if (-not $config.PSObject.Properties["selfHealth"]) {
$config | Add-Member -NotePropertyName "selfHealth" -NotePropertyValue ([pscustomobject]@{})
}
$tasks = @()
if ($config.selfHealth.PSObject.Properties["scheduledTasks"]) {
$tasks = @($config.selfHealth.scheduledTasks)
}
foreach ($taskName in @(
"Wooo-Agent99-Startup-Recovery",
"Wooo-Agent99-Heartbeat",
"Wooo-Agent99-Performance-Guard",
"Wooo-Agent99-Control-Loop",
"Wooo-Agent99-Telegram-Inbox",
"Wooo-Agent99-SRE-Alert-Inbox",
"Wooo-Agent99-SRE-Alert-Relay",
"Wooo-Agent99-Self-Health",
"Wooo-Agent99-Backup-Health"
)) {
if ($tasks -notcontains $taskName) {
$tasks += $taskName
}
}
Set-AgentProp $config.selfHealth "scheduledTasks" $tasks
$freshness = @()
if ($config.selfHealth.PSObject.Properties["evidenceFreshness"]) {
$freshness = @($config.selfHealth.evidenceFreshness)
}
if (-not ($freshness | Where-Object { $_.name -eq "sre_alert_relay" } | Select-Object -First 1)) {
$freshness += [pscustomobject]@{
name = "sre_alert_relay"
pattern = "agent99-SreAlertRelay-start-*.json"
maxAgeMinutes = 1440
required = $false
}
}
Set-AgentProp $config.selfHealth "evidenceFreshness" $freshness
$config | ConvertTo-Json -Depth 20 | Set-Content -Encoding UTF8 $ConfigPath
& (Join-Path $BinDir "agent99-register-tasks.ps1") -AgentRoot $AgentRoot | Out-Null
Start-ScheduledTask -TaskName "Wooo-Agent99-SRE-Alert-Relay"
Start-Sleep -Seconds 4
$task = Get-ScheduledTask -TaskName "Wooo-Agent99-SRE-Alert-Relay" -ErrorAction Stop
$info = Get-ScheduledTaskInfo -TaskName "Wooo-Agent99-SRE-Alert-Relay" -ErrorAction Stop
$listener = @(Get-NetTCPConnection -LocalPort 8787 -State Listen -ErrorAction SilentlyContinue)
$latestEvidence = Get-ChildItem -Path $EvidenceDir -Filter "agent99-SreAlertRelay-start-*.json" -File -ErrorAction SilentlyContinue |
Sort-Object LastWriteTime -Descending |
Select-Object -First 1
[pscustomobject]@{
ok = $true
taskState = [string]$task.State
lastTaskResult = $info.LastTaskResult
listenerCount = $listener.Count
listenerLocal = @($listener | Select-Object -First 3 | ForEach-Object { "$($_.LocalAddress):$($_.LocalPort)" })
latestEvidence = if ($latestEvidence) { $latestEvidence.FullName } else { "" }
configRelayEnabled = [bool]$config.sreAlertRelay.enabled
configTokenEnv = [string]$config.sreAlertRelay.tokenEnv
scheduledTaskListed = @($config.selfHealth.scheduledTasks) -contains "Wooo-Agent99-SRE-Alert-Relay"
} | ConvertTo-Json -Depth 6 -Compress

182
agent99-register-tasks.ps1 Normal file
View File

@@ -0,0 +1,182 @@
param(
[string]$AgentRoot = "C:\Wooo\Agent99"
)
$ErrorActionPreference = "Stop"
$launcher = Join-Path $AgentRoot "agent99-run.ps1"
if (-not (Test-Path $launcher)) {
throw "Agent99 launcher not found: $launcher"
}
$ps = Join-Path $env:SystemRoot "System32\WindowsPowerShell\v1.0\powershell.exe"
$principalUser = if ($env:COMPUTERNAME) { "$env:COMPUTERNAME\$env:USERNAME" } else { $env:USERNAME }
$principal = New-ScheduledTaskPrincipal -UserId $principalUser -LogonType S4U -RunLevel Highest
$settings = New-ScheduledTaskSettingsSet `
-AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries `
-StartWhenAvailable `
-MultipleInstances IgnoreNew `
-ExecutionTimeLimit (New-TimeSpan -Minutes 10)
$serviceSettings = New-ScheduledTaskSettingsSet `
-AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries `
-StartWhenAvailable `
-MultipleInstances IgnoreNew `
-ExecutionTimeLimit ([TimeSpan]::Zero)
$recoverArgs = "-NoProfile -ExecutionPolicy Bypass -File `"$launcher`" -Mode Recover -ControlledApply"
$recoverAction = New-ScheduledTaskAction -Execute $ps -Argument $recoverArgs
$startupTrigger = New-ScheduledTaskTrigger -AtStartup
$logonTrigger = New-ScheduledTaskTrigger -AtLogOn -User $principalUser
Register-ScheduledTask `
-TaskName "Wooo-Agent99-Startup-Recovery" `
-Action $recoverAction `
-Trigger @($startupTrigger, $logonTrigger) `
-Principal $principal `
-Settings $settings `
-Description "Agent99 startup recovery for VMs, Harbor, K3s, and public routes." `
-Force | Out-Null
$heartbeatArgs = "-NoProfile -ExecutionPolicy Bypass -File `"$launcher`" -Mode Status"
$heartbeatAction = New-ScheduledTaskAction -Execute $ps -Argument $heartbeatArgs
$heartbeatTrigger = New-ScheduledTaskTrigger `
-Once `
-At (Get-Date).AddMinutes(1) `
-RepetitionInterval (New-TimeSpan -Minutes 5) `
-RepetitionDuration (New-TimeSpan -Days 3650)
Register-ScheduledTask `
-TaskName "Wooo-Agent99-Heartbeat" `
-Action $heartbeatAction `
-Trigger $heartbeatTrigger `
-Principal $principal `
-Settings $settings `
-Description "Agent99 observe-mode heartbeat for host, Harbor, K3s, and public route evidence." `
-Force | Out-Null
$perfArgs = "-NoProfile -ExecutionPolicy Bypass -File `"$launcher`" -Mode Perf -ControlledApply"
$perfAction = New-ScheduledTaskAction -Execute $ps -Argument $perfArgs
$perfTrigger = New-ScheduledTaskTrigger `
-Once `
-At (Get-Date).AddMinutes(1) `
-RepetitionInterval (New-TimeSpan -Minutes 1) `
-RepetitionDuration (New-TimeSpan -Days 3650)
Register-ScheduledTask `
-TaskName "Wooo-Agent99-Performance-Guard" `
-Action $perfAction `
-Trigger $perfTrigger `
-Principal $principal `
-Settings $settings `
-Description "Agent99 minute-level performance guard with Telegram audit and allowlisted controlled load shedding." `
-Force | Out-Null
$controlArgs = "-NoProfile -ExecutionPolicy Bypass -File `"$launcher`" -Mode ControlTick"
$controlAction = New-ScheduledTaskAction -Execute $ps -Argument $controlArgs
$controlTrigger = New-ScheduledTaskTrigger `
-Once `
-At (Get-Date).AddMinutes(1) `
-RepetitionInterval (New-TimeSpan -Minutes 1) `
-RepetitionDuration (New-TimeSpan -Days 3650)
Register-ScheduledTask `
-TaskName "Wooo-Agent99-Control-Loop" `
-Action $controlAction `
-Trigger $controlTrigger `
-Principal $principal `
-Settings $settings `
-Description "Agent99 visible control center, desktop shortcuts, and allowlisted operator command queue processor." `
-Force | Out-Null
$telegramInboxScript = Join-Path $AgentRoot "bin\agent99-telegram-inbox.ps1"
$telegramInboxArgs = "-NoProfile -ExecutionPolicy Bypass -File `"$telegramInboxScript`" -RunNow"
$telegramInboxAction = New-ScheduledTaskAction -Execute $ps -Argument $telegramInboxArgs
$telegramInboxTrigger = New-ScheduledTaskTrigger `
-Once `
-At (Get-Date).AddMinutes(1) `
-RepetitionInterval (New-TimeSpan -Minutes 1) `
-RepetitionDuration (New-TimeSpan -Days 3650)
Register-ScheduledTask `
-TaskName "Wooo-Agent99-Telegram-Inbox" `
-Action $telegramInboxAction `
-Trigger $telegramInboxTrigger `
-Principal $principal `
-Settings $settings `
-Description "Agent99 Telegram command inbox for allowlisted /agent99 operator instructions." `
-Force | Out-Null
$sreAlertInboxScript = Join-Path $AgentRoot "bin\agent99-sre-alert-inbox.ps1"
$sreAlertInboxArgs = "-NoProfile -ExecutionPolicy Bypass -File `"$sreAlertInboxScript`" -RunNow"
$sreAlertInboxAction = New-ScheduledTaskAction -Execute $ps -Argument $sreAlertInboxArgs
$sreAlertInboxTrigger = New-ScheduledTaskTrigger `
-Once `
-At (Get-Date).AddMinutes(1) `
-RepetitionInterval (New-TimeSpan -Minutes 1) `
-RepetitionDuration (New-TimeSpan -Days 3650)
Register-ScheduledTask `
-TaskName "Wooo-Agent99-SRE-Alert-Inbox" `
-Action $sreAlertInboxAction `
-Trigger $sreAlertInboxTrigger `
-Principal $principal `
-Settings $settings `
-Description "Agent99 SRE war-room alert inbox that converts actionable alerts into allowlisted remediation queue entries." `
-Force | Out-Null
$sreAlertRelayScript = Join-Path $AgentRoot "bin\agent99-sre-alert-relay.ps1"
if (Test-Path $sreAlertRelayScript) {
$sreAlertRelayArgs = "-NoProfile -ExecutionPolicy Bypass -File `"$sreAlertRelayScript`""
$sreAlertRelayAction = New-ScheduledTaskAction -Execute $ps -Argument $sreAlertRelayArgs
Register-ScheduledTask `
-TaskName "Wooo-Agent99-SRE-Alert-Relay" `
-Action $sreAlertRelayAction `
-Trigger @($startupTrigger, $logonTrigger) `
-Principal $principal `
-Settings $serviceSettings `
-Description "Agent99 HTTP relay for AWOOI Alertmanager alerts into the SRE alert inbox." `
-Force | Out-Null
}
$selfCheckArgs = "-NoProfile -ExecutionPolicy Bypass -File `"$launcher`" -Mode SelfCheck"
$selfCheckAction = New-ScheduledTaskAction -Execute $ps -Argument $selfCheckArgs
$selfCheckTrigger = New-ScheduledTaskTrigger `
-Once `
-At (Get-Date).AddMinutes(2) `
-RepetitionInterval (New-TimeSpan -Minutes 5) `
-RepetitionDuration (New-TimeSpan -Days 3650)
Register-ScheduledTask `
-TaskName "Wooo-Agent99-Self-Health" `
-Action $selfCheckAction `
-Trigger $selfCheckTrigger `
-Principal $principal `
-Settings $settings `
-Description "Agent99 self-health guard for scheduled tasks, evidence freshness, host perf readback, and Telegram delivery." `
-Force | Out-Null
$backupArgs = "-NoProfile -ExecutionPolicy Bypass -File `"$launcher`" -Mode BackupCheck"
$backupAction = New-ScheduledTaskAction -Execute $ps -Argument $backupArgs
$backupTrigger = New-ScheduledTaskTrigger `
-Once `
-At (Get-Date).AddMinutes(3) `
-RepetitionInterval (New-TimeSpan -Minutes 15) `
-RepetitionDuration (New-TimeSpan -Days 3650)
Register-ScheduledTask `
-TaskName "Wooo-Agent99-Backup-Health" `
-Action $backupAction `
-Trigger $backupTrigger `
-Principal $principal `
-Settings $settings `
-Description "Agent99 backup freshness, integrity-status, cron-contract, and Telegram alert guard." `
-Force | Out-Null
$taskNames = @(
"Wooo-Agent99-Startup-Recovery",
"Wooo-Agent99-Heartbeat",
"Wooo-Agent99-Performance-Guard",
"Wooo-Agent99-Control-Loop",
"Wooo-Agent99-Telegram-Inbox",
"Wooo-Agent99-SRE-Alert-Inbox",
"Wooo-Agent99-SRE-Alert-Relay",
"Wooo-Agent99-Self-Health",
"Wooo-Agent99-Backup-Health"
)
Get-ScheduledTask -TaskName $taskNames -ErrorAction SilentlyContinue |
Select-Object TaskName, State, TaskPath

297
agent99-sre-alert-inbox.ps1 Normal file
View File

@@ -0,0 +1,297 @@
param(
[string]$AgentRoot = "C:\Wooo\Agent99",
[switch]$RunNow,
[int]$MaxAlerts = 10
)
$ErrorActionPreference = "Stop"
$EvidenceDir = Join-Path $AgentRoot "evidence"
$AlertRoot = Join-Path $AgentRoot "alerts"
$IncomingDir = Join-Path $AlertRoot "incoming"
$RunningDir = Join-Path $AlertRoot "running"
$ProcessedDir = Join-Path $AlertRoot "processed"
$FailedDir = Join-Path $AlertRoot "failed"
$IgnoredDir = Join-Path $AlertRoot "ignored"
$QueueDir = Join-Path $AgentRoot "queue"
$RequestDir = Join-Path $AgentRoot "requests"
New-Item -ItemType Directory -Force -Path $EvidenceDir, $IncomingDir, $RunningDir, $ProcessedDir, $FailedDir, $IgnoredDir, $QueueDir, $RequestDir | Out-Null
$allowedModes = @("Status", "Recover", "StartVMs", "PublicSmoke", "HarborRepair", "AwoooRepair", "Perf", "LoadShed", "SelfCheck", "BackupCheck", "ProviderFreshness")
$remediationModes = @("Recover", "StartVMs", "HarborRepair", "AwoooRepair", "Perf", "LoadShed")
function Get-AgentField {
param(
[object]$Object,
[string]$Name,
[object]$Default = $null
)
if ($Object -and $Object.PSObject.Properties[$Name]) {
return $Object.PSObject.Properties[$Name].Value
}
return $Default
}
function Convert-AgentSafeFileToken {
param([string]$Value)
$safe = [regex]::Replace($Value, "[^A-Za-z0-9_.-]", "_")
if (-not $safe) { return "alert" }
if ($safe.Length -gt 80) { return $safe.Substring(0, 80) }
return $safe
}
function Get-AgentAlertText {
param([object]$Alert)
$labels = @()
$rawLabels = Get-AgentField $Alert "labels" @()
foreach ($label in @($rawLabels)) {
if ($label -ne $null) { $labels += [string]$label }
}
@(
(Get-AgentField $Alert "source" ""),
(Get-AgentField $Alert "severity" ""),
(Get-AgentField $Alert "kind" ""),
(Get-AgentField $Alert "service" ""),
(Get-AgentField $Alert "host" ""),
(Get-AgentField $Alert "title" ""),
(Get-AgentField $Alert "message" ""),
($labels -join " ")
) -join " "
}
function Test-AgentAlertActionable {
param([object]$Alert)
$force = [bool](Get-AgentField $Alert "force" $false)
if ($force) { return $true }
$text = (Get-AgentAlertText $Alert).ToLowerInvariant()
$severity = ([string](Get-AgentField $Alert "severity" "")).ToLowerInvariant()
if ($severity -in @("critical", "warning", "error", "degraded", "down", "failed")) {
return $true
}
if ($text -match "502|provider_freshness_signal|source_provider_freshness_triage|raw_signal_normalized|controlled_runtime_candidate|freshness|last_seen|last seen|ingestion|imagepullbackoff|errimagepull|crashloopbackoff|host_down|vm_not_running|backup_failed|backup_missing|disk_used|load_per_core|cpu|memory|harbor|registry|k3s") {
return $true
}
return $false
}
function Resolve-AgentAlertMode {
param([object]$Alert)
$text = (Get-AgentAlertText $Alert).ToLowerInvariant()
$suggestedMode = [string](Get-AgentField $Alert "suggestedMode" "")
if ($text -match "provider_freshness_signal|source_provider_freshness_triage|raw_signal_normalized|controlled_runtime_candidate|freshness|last_seen|last seen|ingestion") {
return "ProviderFreshness"
}
if ($text -match "502|site_502|public_route_502|route_down|website_down|nginx upstream") {
return "Recover"
}
if ($text -match "host_down|host_reboot|reboot_detected|vm_not_running|powered_off|vmware|startup") {
return "Recover"
}
if ($text -match "load_per_core|cpu|memory|mem_|disk_used|performance|pressure") {
return "Perf"
}
if ($text -match "backup|snapshot|restore drill|cron") {
return "BackupCheck"
}
if ($text -match "harbor|registry") {
return "HarborRepair"
}
if ($text -match "awoooi|k3s|imagepullbackoff|errimagepull|crashloopbackoff|pod|deployment") {
return "AwoooRepair"
}
if ($suggestedMode -and $suggestedMode -in $allowedModes) {
return $suggestedMode
}
return "Status"
}
function New-AgentInstructionFromAlert {
param(
[object]$Alert,
[string]$AlertId,
[string]$ModeName
)
$existing = [string](Get-AgentField $Alert "instruction" "")
if ($existing) { return $existing }
$source = [string](Get-AgentField $Alert "source" "sre-war-room")
$severity = [string](Get-AgentField $Alert "severity" "unknown")
$kind = [string](Get-AgentField $Alert "kind" "unknown")
$service = [string](Get-AgentField $Alert "service" "unknown")
$hostName = [string](Get-AgentField $Alert "host" "")
$title = [string](Get-AgentField $Alert "title" "")
$message = [string](Get-AgentField $Alert "message" "")
if ($kind -eq "provider_freshness_signal" -or $message.ToLowerInvariant() -match "provider_freshness_signal|source_provider_freshness_triage|raw_signal_normalized|controlled_runtime_candidate|freshness") {
return "Provider freshness candidate required; alertId=$AlertId; source=$source; severity=$severity; service=$service; mode=$ModeName; require providerWindow,lastSeen,directReference,verifierReadback; mark missing,expired,timeout,no_false_green; do not switch provider, call paid model, edit env, or reload."
}
$parts = @("SRE alert", "id=$AlertId", "source=$source", "severity=$severity", "service=$service", "kind=$kind", "mode=$ModeName")
if ($hostName) { $parts += "host=$hostName" }
if ($title) { $parts += "title=$title" }
if ($message) { $parts += "message=$message" }
return ($parts -join "; ")
}
function Get-AgentControlledApplyForAlert {
param(
[object]$Alert,
[string]$ModeName
)
if ($Alert -and $Alert.PSObject.Properties["controlledApply"]) {
return [bool]$Alert.controlledApply
}
return [bool]($ModeName -in $remediationModes)
}
$stamp = Get-Date -Format "yyyyMMdd-HHmmss"
$evidencePath = Join-Path $EvidenceDir "agent99-SreAlertInbox-$stamp.json"
$queued = @()
$ignored = @()
$failed = @()
$files = @(Get-ChildItem -Path $IncomingDir -Filter "*.json" -File -ErrorAction SilentlyContinue |
Sort-Object LastWriteTime |
Select-Object -First $MaxAlerts)
foreach ($file in $files) {
$runningPath = Join-Path $RunningDir $file.Name
Move-Item -Force $file.FullName $runningPath
try {
$alert = Get-Content $runningPath -Raw | ConvertFrom-Json
} catch {
$failedPath = Join-Path $FailedDir ("failed-" + $file.Name)
Move-Item -Force $runningPath $failedPath
$failed += [pscustomobject]@{
file = $failedPath
ok = $false
reason = "invalid_json"
error = $_.Exception.Message
}
continue
}
$alertId = [string](Get-AgentField $alert "id" $file.BaseName)
$actionable = Test-AgentAlertActionable $alert
if (-not $actionable) {
$ignoredPath = Join-Path $IgnoredDir ("ignored-" + (Convert-AgentSafeFileToken $alertId) + "-" + $file.Name)
Move-Item -Force $runningPath $ignoredPath
$ignored += [pscustomobject]@{
alertId = $alertId
file = $ignoredPath
reason = "not_actionable"
}
continue
}
$modeName = Resolve-AgentAlertMode $alert
if ($modeName -notin $allowedModes) {
$failedPath = Join-Path $FailedDir ("failed-" + (Convert-AgentSafeFileToken $alertId) + "-" + $file.Name)
Move-Item -Force $runningPath $failedPath
$failed += [pscustomobject]@{
alertId = $alertId
file = $failedPath
ok = $false
reason = "mode_not_allowlisted"
mode = $modeName
}
continue
}
$queueId = "sre-alert-" + (Get-Date -Format "yyyyMMdd-HHmmss") + "-" + ([guid]::NewGuid().ToString("N").Substring(0, 8))
$controlled = Get-AgentControlledApplyForAlert $alert $modeName
$instruction = New-AgentInstructionFromAlert $alert $alertId $modeName
$source = [string](Get-AgentField $alert "source" "sre-war-room")
$kind = [string](Get-AgentField $alert "kind" "unknown")
$severity = [string](Get-AgentField $alert "severity" "unknown")
$service = [string](Get-AgentField $alert "service" "unknown")
$hostName = [string](Get-AgentField $alert "host" "")
$replyChatId = [string](Get-AgentField $alert "replyChatId" "")
$requestPath = Join-Path $RequestDir "$queueId.json"
$queuePath = Join-Path $QueueDir "$queueId.json"
$processedPath = Join-Path $ProcessedDir ("processed-" + (Convert-AgentSafeFileToken $alertId) + "-" + $file.Name)
[pscustomobject]@{
id = $queueId
instruction = $instruction
resolvedMode = $modeName
controlledApply = $controlled
requestedBy = $env:USERNAME
source = "agent99-sre-alert-inbox"
alertId = $alertId
alertSource = $source
alertKind = $kind
alertSeverity = $severity
alertService = $service
alertHost = $hostName
alertPath = $processedPath
createdAt = (Get-Date -Format o)
} | ConvertTo-Json -Depth 8 | Set-Content -Path $requestPath -Encoding UTF8
[pscustomobject]@{
id = $queueId
mode = $modeName
controlledApply = $controlled
requestedBy = $env:USERNAME
source = "agent99-sre-alert-inbox"
reason = "sre_alert"
instruction = $instruction
requestPath = $requestPath
alertId = $alertId
alertSource = $source
alertKind = $kind
alertSeverity = $severity
alertService = $service
alertHost = $hostName
alertPath = $processedPath
replyChatId = $replyChatId
createdAt = (Get-Date -Format o)
} | ConvertTo-Json -Depth 8 | Set-Content -Path $queuePath -Encoding UTF8
Move-Item -Force $runningPath $processedPath
$queued += [pscustomobject]@{
alertId = $alertId
source = $source
severity = $severity
kind = $kind
service = $service
host = $hostName
mode = $modeName
controlledApply = $controlled
requestPath = $requestPath
queuePath = $queuePath
alertPath = $processedPath
}
}
$controlExitCode = $null
if ($RunNow -and @($queued).Count -gt 0) {
$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()
$controlExitCode = $process.ExitCode
}
$result = [pscustomobject]@{
timestamp = (Get-Date -Format o)
ok = [bool](@($failed).Count -eq 0)
incomingPath = $IncomingDir
queuedCount = @($queued).Count
ignoredCount = @($ignored).Count
failedCount = @($failed).Count
queued = $queued
ignored = $ignored
failed = $failed
runNow = [bool]$RunNow
controlExitCode = $controlExitCode
}
$result | ConvertTo-Json -Depth 10 | Set-Content -Path $evidencePath -Encoding UTF8
$result | ConvertTo-Json -Depth 10

218
agent99-sre-alert-relay.ps1 Normal file
View File

@@ -0,0 +1,218 @@
param(
[string]$AgentRoot = "C:\Wooo\Agent99",
[string]$Prefix = "http://+:8787/agent99/sre-alert/",
[string]$TokenEnv = "AGENT99_SRE_RELAY_TOKEN",
[string[]]$AllowedRemotePrefixes = @("127.", "::1", "192.168.0."),
[int]$MaxBodyBytes = 262144,
[switch]$RunOnce
)
$ErrorActionPreference = "Stop"
$EvidenceDir = Join-Path $AgentRoot "evidence"
$AlertRoot = Join-Path $AgentRoot "alerts"
$IncomingDir = Join-Path $AlertRoot "incoming"
$LogDir = Join-Path $AgentRoot "logs"
$BinDir = Join-Path $AgentRoot "bin"
$SreAlertInboxScript = Join-Path $BinDir "agent99-sre-alert-inbox.ps1"
New-Item -ItemType Directory -Force -Path $EvidenceDir, $IncomingDir, $LogDir | Out-Null
function Convert-AgentSafeFileToken {
param([string]$Value)
$safe = [regex]::Replace($Value, "[^A-Za-z0-9_.-]", "_")
if (-not $safe) { return "relay-alert" }
if ($safe.Length -gt 96) { return $safe.Substring(0, 96) }
return $safe
}
function Get-AgentField {
param(
[object]$Object,
[string]$Name,
[object]$Default = $null
)
if ($Object -and $Object.PSObject.Properties[$Name]) {
return $Object.PSObject.Properties[$Name].Value
}
return $Default
}
function Get-AgentEnvValue {
param([string]$Name)
foreach ($target in @("Process", "User", "Machine")) {
$value = [Environment]::GetEnvironmentVariable($Name, $target)
if ($value) { return $value }
}
return ""
}
function Write-AgentRelayEvent {
param([hashtable]$Event)
$path = Join-Path $LogDir "agent99-sre-alert-relay.jsonl"
$Event["timestamp"] = (Get-Date).ToString("o")
($Event | ConvertTo-Json -Compress -Depth 8) | Add-Content -Encoding UTF8 $path
}
function Send-AgentRelayResponse {
param(
[System.Net.HttpListenerContext]$Context,
[int]$StatusCode,
[hashtable]$Body
)
$json = $Body | ConvertTo-Json -Compress -Depth 8
$bytes = [System.Text.Encoding]::UTF8.GetBytes($json)
$Context.Response.StatusCode = $StatusCode
$Context.Response.ContentType = "application/json; charset=utf-8"
$Context.Response.ContentLength64 = $bytes.Length
$Context.Response.OutputStream.Write($bytes, 0, $bytes.Length)
$Context.Response.Close()
}
function Test-AgentRelayRemoteAllowed {
param([System.Net.HttpListenerContext]$Context)
$remote = [string]$Context.Request.RemoteEndPoint.Address
foreach ($prefix in $AllowedRemotePrefixes) {
if ($remote.StartsWith($prefix)) { return $true }
}
return $false
}
function Start-AgentSreAlertInbox {
if (-not (Test-Path $SreAlertInboxScript)) {
return [pscustomobject]@{ started = $false; reason = "sre_alert_inbox_script_missing" }
}
$ps = Join-Path $env:SystemRoot "System32\WindowsPowerShell\v1.0\powershell.exe"
$stamp = Get-Date -Format "yyyyMMdd-HHmmss"
$stdout = Join-Path $LogDir "agent99-sre-alert-relay-trigger-$stamp.out"
$stderr = Join-Path $LogDir "agent99-sre-alert-relay-trigger-$stamp.err"
$args = @(
"-NoProfile",
"-ExecutionPolicy", "Bypass",
"-File", $SreAlertInboxScript,
"-AgentRoot", $AgentRoot,
"-RunNow"
)
$proc = Start-Process -FilePath $ps -ArgumentList $args -PassThru -WindowStyle Hidden -RedirectStandardOutput $stdout -RedirectStandardError $stderr
return [pscustomobject]@{
started = $true
processId = $proc.Id
stdout = $stdout
stderr = $stderr
}
}
if (-not $Prefix.EndsWith("/")) {
$Prefix = "$Prefix/"
}
$listener = [System.Net.HttpListener]::new()
$listener.Prefixes.Add($Prefix)
$startupStamp = Get-Date -Format "yyyyMMdd-HHmmss"
$startupEvidence = Join-Path $EvidenceDir "agent99-SreAlertRelay-start-$startupStamp.json"
@{
ok = $true
prefix = $Prefix
tokenEnv = $TokenEnv
tokenConfigured = [bool](Get-AgentEnvValue $TokenEnv)
allowedRemotePrefixes = $AllowedRemotePrefixes
maxBodyBytes = $MaxBodyBytes
runOnce = [bool]$RunOnce
startedAt = (Get-Date).ToString("o")
} | ConvertTo-Json -Depth 6 | Set-Content -Encoding UTF8 $startupEvidence
try {
$listener.Start()
Write-AgentRelayEvent @{
event = "relay_started"
ok = $true
prefix = $Prefix
tokenConfigured = [bool](Get-AgentEnvValue $TokenEnv)
evidence = $startupEvidence
}
do {
$context = $listener.GetContext()
$remote = [string]$context.Request.RemoteEndPoint.Address
try {
if ($context.Request.HttpMethod -ne "POST") {
Send-AgentRelayResponse $context 405 @{ ok = $false; error = "method_not_allowed" }
Write-AgentRelayEvent @{ event = "relay_rejected"; ok = $false; remote = $remote; reason = "method_not_allowed" }
continue
}
if (-not (Test-AgentRelayRemoteAllowed $context)) {
Send-AgentRelayResponse $context 403 @{ ok = $false; error = "remote_not_allowed" }
Write-AgentRelayEvent @{ event = "relay_rejected"; ok = $false; remote = $remote; reason = "remote_not_allowed" }
continue
}
$expectedToken = Get-AgentEnvValue $TokenEnv
if ($expectedToken) {
$actualToken = [string]$context.Request.Headers["X-Agent99-Relay-Token"]
if ($actualToken -ne $expectedToken) {
Send-AgentRelayResponse $context 401 @{ ok = $false; error = "invalid_relay_token" }
Write-AgentRelayEvent @{ event = "relay_rejected"; ok = $false; remote = $remote; reason = "invalid_relay_token" }
continue
}
}
if ($context.Request.ContentLength64 -gt $MaxBodyBytes) {
Send-AgentRelayResponse $context 413 @{ ok = $false; error = "payload_too_large" }
Write-AgentRelayEvent @{ event = "relay_rejected"; ok = $false; remote = $remote; reason = "payload_too_large" }
continue
}
$reader = [System.IO.StreamReader]::new($context.Request.InputStream, $context.Request.ContentEncoding)
$body = $reader.ReadToEnd()
if ($body.Length -gt $MaxBodyBytes) {
Send-AgentRelayResponse $context 413 @{ ok = $false; error = "payload_too_large" }
Write-AgentRelayEvent @{ event = "relay_rejected"; ok = $false; remote = $remote; reason = "payload_too_large_readback" }
continue
}
$alert = $body | ConvertFrom-Json
$alertId = [string](Get-AgentField $alert "id" ("relay-" + [guid]::NewGuid().ToString("N")))
$safeId = Convert-AgentSafeFileToken $alertId
$fileStamp = Get-Date -Format "yyyyMMdd-HHmmss"
$tmpPath = Join-Path $IncomingDir ".$safeId-$fileStamp-$([guid]::NewGuid().ToString("N")).tmp"
$finalPath = Join-Path $IncomingDir "$safeId-$fileStamp.json"
$alert | ConvertTo-Json -Depth 20 | Set-Content -Encoding UTF8 $tmpPath
Move-Item -Force $tmpPath $finalPath
$trigger = Start-AgentSreAlertInbox
Send-AgentRelayResponse $context 202 @{
ok = $true
alertId = $alertId
incomingFile = $finalPath
inboxTriggered = [bool]$trigger.started
inboxProcessId = $trigger.processId
}
Write-AgentRelayEvent @{
event = "relay_accepted"
ok = $true
remote = $remote
alertId = $alertId
incomingFile = $finalPath
inboxTriggered = [bool]$trigger.started
inboxProcessId = $trigger.processId
}
} catch {
try {
Send-AgentRelayResponse $context 500 @{ ok = $false; error = "relay_exception"; errorType = $_.Exception.GetType().Name }
} catch {
}
Write-AgentRelayEvent @{
event = "relay_exception"
ok = $false
remote = $remote
errorType = $_.Exception.GetType().Name
error = $_.Exception.Message
}
}
} while (-not $RunOnce)
} finally {
if ($listener.IsListening) { $listener.Stop() }
$listener.Close()
}

150
agent99-submit-request.ps1 Normal file
View File

@@ -0,0 +1,150 @@
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 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))
$request = [pscustomobject]@{
id = $id
instruction = $Instruction
resolvedMode = $modeName
controlledApply = [bool]$ControlledApply
requestedBy = if ($RequestedBy) { $RequestedBy } else { $env:USERNAME }
source = $Source
externalId = $ExternalId
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
reason = "operator_instruction"
instruction = $Instruction
requestPath = $requestPath
replyChatId = $ReplyChatId
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
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

488
agent99-telegram-inbox.ps1 Normal file
View File

@@ -0,0 +1,488 @@
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, "<redacted>")
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

View File

@@ -0,0 +1,104 @@
param(
[string]$EvidenceDir = "C:\Wooo\Agent99\evidence"
)
$ErrorActionPreference = "Stop"
New-Item -ItemType Directory -Force -Path $EvidenceDir | Out-Null
$stamp = Get-Date -Format "yyyyMMdd-HHmmss"
$evidencePath = Join-Path $EvidenceDir "agent99-windows-update-policy-$stamp.json"
$logPath = Join-Path $EvidenceDir "agent99-windows-update-policy-$stamp.log"
function Write-PolicyLog {
param([string]$Message)
"$(Get-Date -Format o) $Message" | Tee-Object -FilePath $logPath -Append | Out-Null
}
function Get-RegistrySnapshot {
param([string]$Path)
if (-not (Test-Path $Path)) {
return [pscustomobject]@{ path = $Path; exists = $false; values = @{} }
}
$item = Get-ItemProperty -Path $Path
$values = @{}
foreach ($prop in $item.PSObject.Properties) {
if ($prop.Name -notmatch "^PS") {
$values[$prop.Name] = $prop.Value
}
}
[pscustomobject]@{ path = $Path; exists = $true; values = $values }
}
$windowsUpdatePolicy = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate"
$auPolicy = Join-Path $windowsUpdatePolicy "AU"
$uxSettings = "HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings"
$before = @(
Get-RegistrySnapshot $windowsUpdatePolicy
Get-RegistrySnapshot $auPolicy
Get-RegistrySnapshot $uxSettings
)
Write-PolicyLog "apply_start windows_update_no_surprise_reboot"
New-Item -Path $windowsUpdatePolicy -Force | Out-Null
New-Item -Path $auPolicy -Force | Out-Null
New-Item -Path $uxSettings -Force | Out-Null
# Keep Windows Update enabled, but prevent unattended surprise restarts.
New-ItemProperty -Path $auPolicy -Name "NoAutoRebootWithLoggedOnUsers" -Value 1 -PropertyType DWord -Force | Out-Null
New-ItemProperty -Path $auPolicy -Name "AlwaysAutoRebootAtScheduledTime" -Value 0 -PropertyType DWord -Force | Out-Null
New-ItemProperty -Path $auPolicy -Name "AUOptions" -Value 3 -PropertyType DWord -Force | Out-Null
New-ItemProperty -Path $auPolicy -Name "ScheduledInstallDay" -Value 0 -PropertyType DWord -Force | Out-Null
New-ItemProperty -Path $auPolicy -Name "ScheduledInstallTime" -Value 3 -PropertyType DWord -Force | Out-Null
# Make the interactive active-hours UI reflect an ops-safe window.
New-ItemProperty -Path $uxSettings -Name "ActiveHoursStart" -Value 0 -PropertyType DWord -Force | Out-Null
New-ItemProperty -Path $uxSettings -Name "ActiveHoursEnd" -Value 23 -PropertyType DWord -Force | Out-Null
New-ItemProperty -Path $uxSettings -Name "SmartActiveHoursState" -Value 0 -PropertyType DWord -Force | Out-Null
$after = @(
Get-RegistrySnapshot $windowsUpdatePolicy
Get-RegistrySnapshot $auPolicy
Get-RegistrySnapshot $uxSettings
)
$pendingRebootKeys = @(
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending",
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired",
"HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager"
)
$pending = @()
foreach ($key in $pendingRebootKeys) {
if ($key -like "*Session Manager") {
$value = (Get-ItemProperty -Path $key -Name "PendingFileRenameOperations" -ErrorAction SilentlyContinue).PendingFileRenameOperations
$pending += [pscustomobject]@{ path = $key; signal = "PendingFileRenameOperations"; present = [bool]$value }
} else {
$pending += [pscustomobject]@{ path = $key; signal = "key_exists"; present = [bool](Test-Path $key) }
}
}
try {
gpupdate.exe /target:computer /force | Out-File -FilePath $logPath -Append -Encoding UTF8
$gpupdateOk = $true
} catch {
$gpupdateOk = $false
Write-PolicyLog "gpupdate_failed error=$($_.Exception.Message)"
}
$result = [pscustomobject]@{
timestamp = (Get-Date -Format o)
applied = $true
policy = "windows_update_no_surprise_reboot"
gpupdateOk = $gpupdateOk
before = $before
after = $after
pendingRebootSignals = $pending
evidenceLog = $logPath
}
$result | ConvertTo-Json -Depth 8 | Set-Content -Encoding UTF8 $evidencePath
Write-PolicyLog "apply_complete evidence=$evidencePath"
Write-Host $evidencePath

View File

@@ -0,0 +1,273 @@
{
"agentName": "Agent99",
"agentRoot": "C:\\Wooo\\Agent99",
"sshUser": "wooo",
"sshUsers": {
"192.168.0.112": "kali",
"192.168.0.188": "ollama"
},
"vmrunPath": "C:\\Program Files (x86)\\VMware\\VMware Workstation\\vmrun.exe",
"hosts": [
"192.168.0.110",
"192.168.0.112",
"192.168.0.120",
"192.168.0.121",
"192.168.0.188"
],
"vms": [
{
"name": "host110-devops",
"host": "192.168.0.110",
"vmx": "D:\\Documents\\Virtual Machines\\Ubuntu 64-bit (3)\\Ubuntu 64-bit (3).vmx",
"priority": 10
},
{
"name": "host188-ai",
"host": "192.168.0.188",
"vmx": "D:\\Documents\\Virtual Machines\\Ollama_Ubuntu_64-bit\\Ollama_Ubuntu_64-bit.vmx",
"priority": 20
},
{
"name": "host112-kali",
"host": "192.168.0.112",
"vmx": "D:\\Downloads\\kali-linux-2025.4-vmware-amd64\\kali-linux-2025.4-vmware-amd64.vmwarevm\\kali-linux-2025.4-vmware-amd64.vmx",
"priority": 30
},
{
"name": "host120-k3s",
"host": "192.168.0.120",
"vmx": "D:\\Documents\\Virtual Machines\\192.168.0.120_Ubuntu_64-bit\\192.168.0.120_Ubuntu_64-bit.vmx",
"priority": 40
},
{
"name": "host121-k3s",
"host": "192.168.0.121",
"vmx": "D:\\Documents\\Virtual Machines\\192.168.0.121_Ubuntu_64-bit\\192.168.0.121_Ubuntu_64-bit.vmx",
"priority": 50
}
],
"k3s": {
"jumpHost": "192.168.0.110",
"primary": "192.168.0.120",
"namespace": "awoooi-prod"
},
"publicUrls": [
"awoooi.wooo.work",
"stock.wooo.work",
"2026fifa.wooo.work",
"gitea.wooo.work",
"harbor.wooo.work"
],
"aiServices": [
{
"name": "openclaw",
"url": "http://192.168.0.188:8088/health",
"host": "192.168.0.188",
"expectedStatus": "healthy",
"critical": true
}
],
"providerFreshness": {
"enabled": true,
"lookbackMinutes": 120,
"targets": [
{
"name": "provider_freshness_signal",
"providerWindowMinutes": null,
"directReference": null,
"verifier": "agent99-provider-freshness-triage"
}
]
},
"sreAlertRelay": {
"enabled": true,
"prefix": "http://+:8787/agent99/sre-alert/",
"tokenEnv": "AGENT99_SRE_RELAY_TOKEN",
"allowedRemotePrefixes": [
"127.",
"::1",
"192.168.0."
]
},
"telegram": {
"enabled": true,
"botTokenEnv": "AGENT99_TELEGRAM_BOT_TOKEN",
"chatIdEnv": "AGENT99_TELEGRAM_CHAT_ID",
"botTokenEnvAliases": [
"AWOOOI_TG_BOT_TOKEN",
"OPENCLAW_TG_BOT_TOKEN",
"TELEGRAM_BOT_TOKEN",
"TELEGRAM_TOKEN",
"TG_BOT_TOKEN",
"AIDER_WATCH_TELEGRAM_TOKEN"
],
"chatIdEnvAliases": [
"TELEGRAM_CHAT_ID",
"TELEGRAM_ALERT_CHAT_ID",
"TELEGRAM_P0_CHAT_ID",
"TELEGRAM_SECURITY_CHAT_ID",
"OPENCLAW_TG_CHAT_ID",
"TG_CHAT_ID",
"AIDER_WATCH_TELEGRAM_CHAT_ID"
],
"relay": {
"enabled": true,
"host": "192.168.0.110",
"container": "stockplatform-v2-api-1"
},
"allowedCommandChatTitlePattern": "AwoooI SRE|AWOOOI SRE|SRE",
"autoIngestMonitoringAlerts": true,
"alertAllOperations": false,
"alertInfoEvents": false,
"alertOperatorCommands": true,
"dedupeMinutes": 30
},
"performance": {
"enabled": true,
"intervalMinutes": 1,
"loadPerCoreWarning": 0.9,
"loadPerCoreCritical": 1.5,
"memoryAvailablePercentCritical": 10,
"diskUsedPercentWarning": 90,
"diskUsedPercentCritical": 95,
"captureTopCpuOnWarning": true,
"topCpuTimeoutSeconds": 10,
"loadShedding": {
"enabled": true,
"actions": [
{
"name": "prune-188-docker-build-cache-disk-pressure",
"host": "192.168.0.188",
"command": "echo BEFORE_DF=$(df -P / | awk 'NR==2{print $5}'); docker system df; docker builder prune -af --filter until=24h; echo AFTER_DF=$(df -P / | awk 'NR==2{print $5}'); docker system df",
"timeoutSeconds": 240,
"postVerifyCommand": "df -P / | awk 'NR==2{gsub(\"%\",\"\",$5); print \"__DISK_USED__=\"$5}'",
"postVerifyMaxDiskUsedPercent": 89,
"rollback": "echo rollback_not_applicable_for_build_cache_prune",
"cooldownMinutes": 120,
"minSeverity": "warning",
"reasonMatches": [
"disk_used_warning",
"disk_used_high"
],
"enabled": true
},
{
"name": "stop-110-runaway-alpine-tar-data",
"host": "192.168.0.110",
"command": "ids=$(docker ps --filter ancestor=alpine --format '{{.ID}} {{.Command}}' | awk '/tar czf - \\/data/ {print $1}'); [ -z \"$ids\" ] && exit 0; for id in $ids; do docker stop \"$id\"; done",
"rollback": "echo manual_review_required_for_stopped_runaway_container",
"cooldownMinutes": 10,
"minSeverity": "warning",
"reasonMatches": [
"load_per_core_warning",
"load_per_core_critical"
],
"enabled": true
},
{
"name": "renice-110-awoooi-backup-pressure",
"host": "192.168.0.110",
"command": "pids=$(ps -eo pid,args | awk '/backup\\/scripts\\/backup-awoooi\\.sh|gzip -c|pg_dump --no-password/ && !/awk/ {print $1}'); [ -z \"$pids\" ] && exit 0; for p in $pids; do sudo -n renice -n 15 -p $p >/dev/null 2>&1 || true; sudo -n ionice -c2 -n7 -p $p >/dev/null 2>&1 || true; done; ps -o pid,ni,pri,comm,args -p $pids",
"rollback": "pids=$(ps -eo pid,args | awk '/backup\\/scripts\\/backup-awoooi\\.sh|gzip -c|pg_dump --no-password/ && !/awk/ {print $1}'); [ -z \"$pids\" ] && exit 0; for p in $pids; do sudo -n renice -n 0 -p $p >/dev/null 2>&1 || true; sudo -n ionice -c2 -n4 -p $p >/dev/null 2>&1 || true; done",
"cooldownMinutes": 15,
"minSeverity": "warning",
"reasonMatches": [
"load_per_core_warning",
"load_per_core_critical"
],
"enabled": true
},
{
"name": "pause-110-ci-runner-pressure",
"host": "192.168.0.110",
"command": "docker pause vibework-dedicated-runner",
"rollback": "docker unpause vibework-dedicated-runner",
"cooldownMinutes": 30,
"reasonMatches": [
"load_per_core_critical"
],
"enabled": true
}
]
}
},
"selfHealth": {
"scheduledTasks": [
"Wooo-Agent99-Startup-Recovery",
"Wooo-Agent99-Heartbeat",
"Wooo-Agent99-Performance-Guard",
"Wooo-Agent99-Control-Loop",
"Wooo-Agent99-Telegram-Inbox",
"Wooo-Agent99-SRE-Alert-Inbox",
"Wooo-Agent99-SRE-Alert-Relay",
"Wooo-Agent99-Self-Health",
"Wooo-Agent99-Backup-Health"
],
"requiredHosts": [
"192.168.0.110",
"192.168.0.112",
"192.168.0.120",
"192.168.0.121",
"192.168.0.188"
],
"requireRecentTelegramDelivery": true,
"telegramDeliveryLookbackMinutes": 60,
"evidenceFreshness": [
{
"name": "performance_guard",
"pattern": "agent99-Perf-*.json",
"maxAgeMinutes": 5,
"required": true
},
{
"name": "heartbeat_status",
"pattern": "agent99-Status-*.json",
"maxAgeMinutes": 15,
"required": true
},
{
"name": "self_health",
"pattern": "agent99-SelfCheck-*.json",
"maxAgeMinutes": 15,
"required": false
},
{
"name": "backup_health",
"pattern": "agent99-BackupCheck-*.json",
"maxAgeMinutes": 30,
"required": false
},
{
"name": "startup_recovery",
"pattern": "agent99-Recover-*.json",
"maxAgeMinutes": 1440,
"required": false
},
{
"name": "telegram_inbox",
"pattern": "agent99-TelegramInbox-*.json",
"maxAgeMinutes": 15,
"required": false
},
{
"name": "sre_alert_inbox",
"pattern": "agent99-SreAlertInbox-*.json",
"maxAgeMinutes": 15,
"required": false
},
{
"name": "sre_alert_relay",
"pattern": "agent99-SreAlertRelay-start-*.json",
"maxAgeMinutes": 1440,
"required": false
}
]
},
"backupHealth": {
"host": "192.168.0.110",
"snapshotReadTimeoutSeconds": 90,
"targets": [],
"fileChecks": [],
"cronPatterns": []
}
}

205
agent99.config.example.json Normal file
View File

@@ -0,0 +1,205 @@
{
"agentName": "Agent99",
"agentRoot": "C:\\Wooo\\Agent99",
"sshUser": "wooo",
"sshUsers": {},
"vmrunPath": "",
"hosts": [
"192.168.0.110",
"192.168.0.112",
"192.168.0.120",
"192.168.0.121",
"192.168.0.188"
],
"vms": [
{
"name": "110",
"host": "192.168.0.110",
"vmx": ""
},
{
"name": "112",
"host": "192.168.0.112",
"vmx": ""
},
{
"name": "120",
"host": "192.168.0.120",
"vmx": ""
},
{
"name": "121",
"host": "192.168.0.121",
"vmx": ""
},
{
"name": "188",
"host": "192.168.0.188",
"vmx": ""
}
],
"publicUrls": [
"awoooi.wooo.work",
"stock.wooo.work",
"2026fifa.wooo.work",
"gitea.wooo.work",
"harbor.wooo.work"
],
"aiServices": [
{
"name": "openclaw",
"url": "http://192.168.0.188:8088/health",
"host": "192.168.0.188",
"expectedStatus": "healthy",
"critical": true
}
],
"providerFreshness": {
"enabled": true,
"lookbackMinutes": 120,
"targets": [
{
"name": "provider_freshness_signal",
"providerWindowMinutes": null,
"directReference": null,
"verifier": "agent99-provider-freshness-triage"
}
]
},
"sreAlertRelay": {
"enabled": true,
"prefix": "http://+:8787/agent99/sre-alert/",
"tokenEnv": "AGENT99_SRE_RELAY_TOKEN",
"allowedRemotePrefixes": [
"127.",
"::1",
"192.168.0."
]
},
"telegram": {
"enabled": true,
"botTokenEnv": "AGENT99_TELEGRAM_BOT_TOKEN",
"chatIdEnv": "AGENT99_TELEGRAM_CHAT_ID",
"botTokenEnvAliases": [
"AWOOOI_TG_BOT_TOKEN",
"OPENCLAW_TG_BOT_TOKEN",
"TELEGRAM_BOT_TOKEN",
"TELEGRAM_TOKEN",
"TG_BOT_TOKEN",
"AIDER_WATCH_TELEGRAM_TOKEN"
],
"chatIdEnvAliases": [
"TELEGRAM_CHAT_ID",
"TELEGRAM_ALERT_CHAT_ID",
"TELEGRAM_P0_CHAT_ID",
"TELEGRAM_SECURITY_CHAT_ID",
"OPENCLAW_TG_CHAT_ID",
"TG_CHAT_ID",
"AIDER_WATCH_TELEGRAM_CHAT_ID"
],
"relay": {
"enabled": true,
"host": "192.168.0.110",
"container": "stockplatform-v2-api-1"
},
"allowedCommandChatTitlePattern": "AwoooI SRE|AWOOOI SRE|SRE",
"autoIngestMonitoringAlerts": true,
"alertAllOperations": false,
"alertInfoEvents": false,
"alertOperatorCommands": true,
"dedupeMinutes": 30
},
"performance": {
"enabled": true,
"intervalMinutes": 1,
"loadPerCoreWarning": 0.9,
"loadPerCoreCritical": 1.5,
"memoryAvailablePercentCritical": 10,
"diskUsedPercentWarning": 90,
"diskUsedPercentCritical": 95,
"captureTopCpuOnWarning": true,
"topCpuTimeoutSeconds": 10,
"loadShedding": {
"enabled": true,
"actions": []
}
},
"selfHealth": {
"scheduledTasks": [
"Wooo-Agent99-Startup-Recovery",
"Wooo-Agent99-Heartbeat",
"Wooo-Agent99-Performance-Guard",
"Wooo-Agent99-Control-Loop",
"Wooo-Agent99-Telegram-Inbox",
"Wooo-Agent99-SRE-Alert-Inbox",
"Wooo-Agent99-SRE-Alert-Relay",
"Wooo-Agent99-Self-Health",
"Wooo-Agent99-Backup-Health"
],
"requiredHosts": [
"192.168.0.110",
"192.168.0.112",
"192.168.0.120",
"192.168.0.121",
"192.168.0.188"
],
"requireRecentTelegramDelivery": true,
"telegramDeliveryLookbackMinutes": 60,
"evidenceFreshness": [
{
"name": "performance_guard",
"pattern": "agent99-Perf-*.json",
"maxAgeMinutes": 5,
"required": true
},
{
"name": "heartbeat_status",
"pattern": "agent99-Status-*.json",
"maxAgeMinutes": 15,
"required": true
},
{
"name": "self_health",
"pattern": "agent99-SelfCheck-*.json",
"maxAgeMinutes": 15,
"required": false
},
{
"name": "backup_health",
"pattern": "agent99-BackupCheck-*.json",
"maxAgeMinutes": 30,
"required": false
},
{
"name": "startup_recovery",
"pattern": "agent99-Recover-*.json",
"maxAgeMinutes": 1440,
"required": false
},
{
"name": "telegram_inbox",
"pattern": "agent99-TelegramInbox-*.json",
"maxAgeMinutes": 15,
"required": false
},
{
"name": "sre_alert_inbox",
"pattern": "agent99-SreAlertInbox-*.json",
"maxAgeMinutes": 15,
"required": false
},
{
"name": "sre_alert_relay",
"pattern": "agent99-SreAlertRelay-start-*.json",
"maxAgeMinutes": 1440,
"required": false
}
]
},
"backupHealth": {
"host": "192.168.0.110",
"targets": [],
"fileChecks": [],
"cronPatterns": []
}
}

View File

@@ -56,6 +56,7 @@ from src.services.alertmanager_llm_guard import (
from src.services.approval_db import get_approval_service
from src.services.auto_approve import get_auto_approve_policy
from src.services.auto_repair_service import AutoRepairService
from src.services.agent99_sre_bridge import bridge_alertmanager_to_agent99
from src.services.channel_hub import (
record_alertmanager_event,
record_grouped_alert_event,
@@ -2849,6 +2850,21 @@ async def alertmanager_webhook(
labels=dict(alert.labels) if alert.labels else {},
annotations=dict(alert.annotations) if alert.annotations else {},
)
background_tasks.add_task(
bridge_alertmanager_to_agent99,
alert_id=alert_id,
alertname=alertname,
severity=severity,
namespace=namespace,
target_resource=target_resource,
message=message,
labels=dict(alert.labels) if alert.labels else {},
annotations=dict(alert.annotations) if alert.annotations else {},
fingerprint=fingerprint,
alert_category=alert_category,
notification_type=notification_type,
source_url=alert.generatorURL,
)
# ==========================================================================
# ADR-076: 告警聚合引擎 — 5 分鐘滑動視窗,防止告警風暴

View File

@@ -925,6 +925,28 @@ class Settings(BaseSettings):
default="",
description="Telegram Webhook Secret TokensetWebhook 設定的同一值)",
)
AGENT99_SRE_ALERT_INBOX_PATH: str = Field(
default="",
description=(
"Optional mounted path for Agent99 SRE alert inbox. Empty disables the "
"fail-open Alertmanager to Agent99 file bridge."
),
)
AGENT99_SRE_ALERT_RELAY_URL: str = Field(
default="",
description=(
"Optional Agent99 SRE alert relay endpoint. When set, Alertmanager "
"events are posted to Agent99 before falling back to the mounted file inbox."
),
)
AGENT99_SRE_ALERT_RELAY_TOKEN: str = Field(
default="",
description="Optional shared token for Agent99 SRE alert relay POSTs.",
)
AGENT99_SRE_ALERT_RELAY_TIMEOUT_SECONDS: float = Field(
default=3.0,
description="Timeout for fail-open Agent99 SRE alert relay POSTs.",
)
# 2026-04-24 Claude Sonnet 4.6 (ADR-095 WS4): Hermes NL 自然語言閘道
# false=不啟用預設true=啟用 @mention 問答(需 ANTHROPIC_API_KEY
HERMES_NL_ENABLED: bool = Field(

View File

@@ -0,0 +1,357 @@
from __future__ import annotations
import asyncio
import json
import re
import urllib.error
import urllib.parse
import urllib.request
import uuid
from pathlib import Path
from typing import Any
from src.core.config import settings
from src.core.logging import get_logger
from src.utils.timezone import now_taipei
logger = get_logger("awoooi.agent99_sre_bridge")
_SENSITIVE_KEY_RE = re.compile(
r"(token|secret|password|passwd|authorization|cookie|session|private[_-]?key)",
re.IGNORECASE,
)
_SAFE_TOKEN_RE = re.compile(r"[^A-Za-z0-9_.-]+")
def _safe_text(value: Any, *, max_length: int = 1200) -> str:
text = str(value or "")
text = " ".join(text.split())
if len(text) > max_length:
return text[: max_length - 14] + "...(truncated)"
return text
def _safe_label_value(key: str, value: Any) -> str:
if _SENSITIVE_KEY_RE.search(key):
return "[redacted]"
return _safe_text(value, max_length=240)
def _safe_file_token(value: str) -> str:
safe = _SAFE_TOKEN_RE.sub("_", value).strip("._-")
return (safe or "alert")[:96]
def _safe_url_for_log(value: str) -> str:
if not value:
return ""
parsed = urllib.parse.urlsplit(value)
return urllib.parse.urlunsplit((parsed.scheme, parsed.netloc, parsed.path, "", ""))
def _alert_text(
*,
alertname: str,
severity: str,
target_resource: str,
namespace: str,
message: str,
labels: dict[str, Any],
annotations: dict[str, Any],
) -> str:
return " ".join(
[
alertname,
severity,
target_resource,
namespace,
message,
" ".join(f"{k}={v}" for k, v in labels.items()),
" ".join(f"{k}={v}" for k, v in annotations.items()),
]
).lower()
def resolve_agent99_alert_kind(
*,
alertname: str,
severity: str,
target_resource: str,
namespace: str,
message: str,
labels: dict[str, Any] | None = None,
annotations: dict[str, Any] | None = None,
) -> str:
text = _alert_text(
alertname=alertname,
severity=severity,
target_resource=target_resource,
namespace=namespace,
message=message,
labels=labels or {},
annotations=annotations or {},
)
if re.search(
r"provider_freshness_signal|source_provider_freshness_triage|"
r"raw_signal_normalized|controlled_runtime_candidate|freshness|"
r"last_seen|last seen|ingestion",
text,
):
return "provider_freshness_signal"
if re.search(r"502|site_502|public_route|website|route_down|nginx upstream", text):
return "public_route_502"
if re.search(r"cpu|load|memory|mem_|disk|pressure|performance", text):
return "performance_pressure"
if re.search(r"backup|snapshot|restore drill|cron", text):
return "backup_health"
if re.search(r"harbor|registry", text):
return "harbor_registry"
if re.search(r"k3s|imagepullbackoff|errimagepull|crashloopbackoff|pod|deployment", text):
return "awoooi_k3s"
if re.search(r"host_down|hostdown|vm_not_running|reboot|restart|powered_off", text):
return "host_recovery"
return "monitoring_alert"
def _suggested_mode_for_kind(kind: str) -> str | None:
return {
"provider_freshness_signal": "ProviderFreshness",
"public_route_502": "Recover",
"performance_pressure": "Perf",
"backup_health": "BackupCheck",
"harbor_registry": "HarborRepair",
"awoooi_k3s": "AwoooRepair",
"host_recovery": "Recover",
}.get(kind)
def build_agent99_sre_alert(
*,
alert_id: str,
alertname: str,
severity: str,
namespace: str,
target_resource: str,
message: str,
labels: dict[str, Any] | None = None,
annotations: dict[str, Any] | None = None,
fingerprint: str = "",
alert_category: str = "",
notification_type: str = "",
source_url: str | None = None,
) -> dict[str, Any]:
labels = labels or {}
annotations = annotations or {}
normalized_severity = (severity or "warning").lower()
kind = resolve_agent99_alert_kind(
alertname=alertname,
severity=normalized_severity,
target_resource=target_resource,
namespace=namespace,
message=message,
labels=labels,
annotations=annotations,
)
suggested_mode = _suggested_mode_for_kind(kind)
host = str(labels.get("host") or labels.get("node") or labels.get("instance") or "")
if ":" in host:
host = host.split(":", 1)[0]
service = str(
labels.get("service")
or labels.get("app")
or labels.get("component")
or target_resource
or "awoooi"
)
safe_labels = [
"alertmanager",
"awoooi-api",
f"alertname={_safe_label_value('alertname', alertname)}",
f"namespace={_safe_label_value('namespace', namespace)}",
f"target={_safe_label_value('target', target_resource)}",
]
if fingerprint:
safe_labels.append(f"fingerprint={_safe_label_value('fingerprint', fingerprint)}")
for key, value in sorted(labels.items()):
safe_labels.append(f"{_safe_text(key, max_length=80)}={_safe_label_value(key, value)}")
if kind == "provider_freshness_signal":
safe_labels.extend(["provider-freshness", "no-false-green"])
instruction = None
if kind == "provider_freshness_signal":
instruction = (
"Provider freshness candidate required; require providerWindow,lastSeen,"
"directReference,verifierReadback; mark missing,expired,timeout,"
"no_false_green; do not switch provider, call paid model, edit env, or reload."
)
payload: dict[str, Any] = {
"id": f"awoooi-alertmanager-{_safe_file_token(alert_id)}",
"source": "awoooi-api-alertmanager",
"severity": normalized_severity,
"kind": kind,
"service": _safe_text(service, max_length=160),
"host": _safe_text(host, max_length=160),
"title": _safe_text(f"Alertmanager {alertname}", max_length=240),
"message": _safe_text(message, max_length=2000),
"labels": safe_labels,
"force": True,
"controlledApply": True,
"createdAt": now_taipei().isoformat(),
"awoooi": {
"alertId": _safe_text(alert_id, max_length=160),
"alertname": _safe_text(alertname, max_length=160),
"namespace": _safe_text(namespace, max_length=160),
"targetResource": _safe_text(target_resource, max_length=240),
"fingerprint": _safe_text(fingerprint, max_length=160),
"alertCategory": _safe_text(alert_category, max_length=160),
"notificationType": _safe_text(notification_type, max_length=160),
"sourceUrl": _safe_text(source_url or "", max_length=600),
},
}
if suggested_mode:
payload["suggestedMode"] = suggested_mode
if instruction:
payload["instruction"] = instruction
return payload
def write_agent99_sre_alert(
payload: dict[str, Any],
*,
inbox_path: str | None = None,
) -> Path | None:
target_dir_text = inbox_path if inbox_path is not None else settings.AGENT99_SRE_ALERT_INBOX_PATH
if not target_dir_text:
logger.info(
"agent99_sre_bridge_disabled",
reason="AGENT99_SRE_ALERT_INBOX_PATH not configured",
alert_id=payload.get("id"),
kind=payload.get("kind"),
)
return None
target_dir = Path(target_dir_text)
target_dir.mkdir(parents=True, exist_ok=True)
alert_id = _safe_file_token(str(payload.get("id") or uuid.uuid4().hex))
final_path = target_dir / f"{alert_id}.json"
tmp_path = target_dir / f".{alert_id}.{uuid.uuid4().hex}.tmp"
tmp_path.write_text(
json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True),
encoding="utf-8",
)
tmp_path.replace(final_path)
logger.info(
"agent99_sre_alert_written",
path=str(final_path),
alert_id=payload.get("id"),
kind=payload.get("kind"),
)
return final_path
def post_agent99_sre_alert(
payload: dict[str, Any],
*,
relay_url: str | None = None,
relay_token: str | None = None,
timeout_seconds: float | None = None,
) -> dict[str, Any] | None:
target_url = relay_url if relay_url is not None else settings.AGENT99_SRE_ALERT_RELAY_URL
if not target_url:
return None
token = relay_token if relay_token is not None else settings.AGENT99_SRE_ALERT_RELAY_TOKEN
timeout = (
timeout_seconds
if timeout_seconds is not None
else settings.AGENT99_SRE_ALERT_RELAY_TIMEOUT_SECONDS
)
data = json.dumps(payload, ensure_ascii=False, sort_keys=True).encode("utf-8")
headers = {
"Content-Type": "application/json; charset=utf-8",
"User-Agent": "awoooi-agent99-sre-bridge/1",
}
if token:
headers["X-Agent99-Relay-Token"] = token
request = urllib.request.Request(target_url, data=data, headers=headers, method="POST")
try:
with urllib.request.urlopen(request, timeout=timeout) as response:
status = int(response.getcode())
body = response.read(4096).decode("utf-8", errors="replace")
except urllib.error.HTTPError as exc:
body = exc.read(4096).decode("utf-8", errors="replace")
raise RuntimeError(f"Agent99 relay HTTP {exc.code}: {body[:300]}") from exc
if status >= 400:
raise RuntimeError(f"Agent99 relay HTTP {status}: {body[:300]}")
logger.info(
"agent99_sre_alert_relay_posted",
relay_url=_safe_url_for_log(target_url),
alert_id=payload.get("id"),
kind=payload.get("kind"),
status=status,
)
return {"status": status, "body": body}
def dispatch_agent99_sre_alert(payload: dict[str, Any]) -> str:
if settings.AGENT99_SRE_ALERT_RELAY_URL:
post_agent99_sre_alert(payload)
return "relay"
if settings.AGENT99_SRE_ALERT_INBOX_PATH:
written = write_agent99_sre_alert(payload)
if written is not None:
return "file"
logger.info(
"agent99_sre_bridge_disabled",
reason="no relay URL or mounted inbox path configured",
alert_id=payload.get("id"),
kind=payload.get("kind"),
)
return "disabled"
async def bridge_alertmanager_to_agent99(
*,
alert_id: str,
alertname: str,
severity: str,
namespace: str,
target_resource: str,
message: str,
labels: dict[str, Any] | None = None,
annotations: dict[str, Any] | None = None,
fingerprint: str = "",
alert_category: str = "",
notification_type: str = "",
source_url: str | None = None,
) -> None:
try:
payload = build_agent99_sre_alert(
alert_id=alert_id,
alertname=alertname,
severity=severity,
namespace=namespace,
target_resource=target_resource,
message=message,
labels=labels,
annotations=annotations,
fingerprint=fingerprint,
alert_category=alert_category,
notification_type=notification_type,
source_url=source_url,
)
await asyncio.to_thread(dispatch_agent99_sre_alert, payload)
except Exception as exc:
logger.warning(
"agent99_sre_bridge_failed_fail_open",
alert_id=alert_id,
alertname=alertname,
error=str(exc),
)

View File

@@ -0,0 +1,143 @@
from __future__ import annotations
import json
from src.services.agent99_sre_bridge import (
build_agent99_sre_alert,
post_agent99_sre_alert,
resolve_agent99_alert_kind,
write_agent99_sre_alert,
)
def test_provider_freshness_alert_builds_agent99_contract() -> None:
payload = build_agent99_sre_alert(
alert_id="alert-20260709233000",
alertname="provider_freshness_signal",
severity="warning",
namespace="awoooi-prod",
target_resource="provider_freshness_signal",
message=(
"raw_signal_normalized controlled_runtime_candidate freshness "
"last seen missing direct ref verifier readback"
),
labels={
"service": "awoooi",
"token": "must-not-leak",
},
annotations={
"summary": "Provider freshness evidence is missing",
},
fingerprint="fp-provider-freshness",
alert_category="source_provider_freshness_triage",
notification_type="raw_signal_normalized",
source_url="https://awoooi.wooo.work/api/v1/webhooks/alertmanager",
)
assert payload["source"] == "awoooi-api-alertmanager"
assert payload["kind"] == "provider_freshness_signal"
assert payload["suggestedMode"] == "ProviderFreshness"
assert payload["controlledApply"] is True
assert payload["force"] is True
assert "provider-freshness" in payload["labels"]
assert "no-false-green" in payload["labels"]
assert "token=[redacted]" in payload["labels"]
assert "must-not-leak" not in json.dumps(payload)
assert "do not switch provider" in payload["instruction"]
def test_alert_kind_mapping_for_route_and_performance() -> None:
assert (
resolve_agent99_alert_kind(
alertname="PublicRoute502",
severity="critical",
namespace="awoooi-prod",
target_resource="awoooi-web",
message="HTTP 502",
)
== "public_route_502"
)
assert (
resolve_agent99_alert_kind(
alertname="HostHighCpuLoad",
severity="warning",
namespace="node",
target_resource="192.168.0.110",
message="load per core is high",
)
== "performance_pressure"
)
def test_write_agent99_sre_alert_to_configured_inbox(tmp_path) -> None:
payload = build_agent99_sre_alert(
alert_id="alert-write-smoke",
alertname="HostHighCpuLoad",
severity="warning",
namespace="node",
target_resource="192.168.0.110",
message="CPU load per core is high",
labels={"host": "192.168.0.110"},
)
written = write_agent99_sre_alert(payload, inbox_path=str(tmp_path))
assert written is not None
data = json.loads(written.read_text(encoding="utf-8"))
assert data["id"] == "awoooi-alertmanager-alert-write-smoke"
assert data["kind"] == "performance_pressure"
assert data["suggestedMode"] == "Perf"
def test_post_agent99_sre_alert_to_relay(monkeypatch) -> None:
payload = build_agent99_sre_alert(
alert_id="relay-smoke",
alertname="provider_freshness_signal",
severity="warning",
namespace="awoooi-prod",
target_resource="provider_freshness_signal",
message="freshness last_seen missing",
labels={"service": "awoooi", "authorization": "must-not-leak"},
)
seen: dict[str, object] = {}
class FakeResponse:
def getcode(self) -> int:
return 202
def read(self, _limit: int) -> bytes:
return b'{"ok":true}'
def __enter__(self) -> "FakeResponse":
return self
def __exit__(self, *_args: object) -> None:
return None
def fake_urlopen(request, timeout): # type: ignore[no-untyped-def]
seen["url"] = request.full_url
seen["timeout"] = timeout
seen["headers"] = dict(request.header_items())
seen["payload"] = json.loads(request.data.decode("utf-8"))
return FakeResponse()
monkeypatch.setattr(
"src.services.agent99_sre_bridge.urllib.request.urlopen",
fake_urlopen,
)
receipt = post_agent99_sre_alert(
payload,
relay_url="http://192.168.0.99:8787/agent99/sre-alert/",
relay_token="relay-token",
timeout_seconds=1.5,
)
assert receipt == {"status": 202, "body": '{"ok":true}'}
assert seen["url"] == "http://192.168.0.99:8787/agent99/sre-alert/"
assert seen["timeout"] == 1.5
assert seen["payload"]["kind"] == "provider_freshness_signal"
assert seen["payload"]["suggestedMode"] == "ProviderFreshness"
assert "authorization=[redacted]" in seen["payload"]["labels"]
assert "must-not-leak" not in json.dumps(seen["payload"])
assert seen["headers"]["X-agent99-relay-token"] == "relay-token"

197
host-reboot-scorecard.ps1 Normal file
View File

@@ -0,0 +1,197 @@
param(
[switch]$ControlledApply,
[string]$SshUser = "wooo",
[string]$EvidenceDir = "$env:ProgramData\Wooo\host-reboot-scorecard",
[string[]]$Hosts = @("192.168.0.110", "192.168.0.112", "192.168.0.120", "192.168.0.121", "192.168.0.188"),
[string[]]$PublicUrls = @(
"awoooi.wooo.work",
"stock.wooo.work",
"2026fifa.wooo.work",
"gitea.wooo.work",
"harbor.wooo.work"
)
)
$ErrorActionPreference = "Continue"
New-Item -ItemType Directory -Force -Path $EvidenceDir | Out-Null
$stamp = Get-Date -Format "yyyyMMdd-HHmmss"
$jsonPath = Join-Path $EvidenceDir "scorecard-$stamp.json"
$logPath = Join-Path $EvidenceDir "scorecard-$stamp.log"
function Write-Evidence {
param([string]$Message)
$line = "$(Get-Date -Format o) $Message"
$line | Tee-Object -FilePath $logPath -Append
}
function Test-PingHost {
param([string]$Host)
$ok = Test-Connection -ComputerName $Host -Count 1 -Quiet -ErrorAction SilentlyContinue
[pscustomobject]@{
host = $Host
ping = [bool]$ok
}
}
function Invoke-SshText {
param(
[string]$Host,
[string]$Command,
[int]$TimeoutSeconds = 20
)
$args = @(
"-o", "BatchMode=yes",
"-o", "ConnectTimeout=$TimeoutSeconds",
"-l", $SshUser,
$Host,
$Command
)
try {
$output = & ssh @args 2>&1
[pscustomobject]@{
ok = ($LASTEXITCODE -eq 0)
exit_code = $LASTEXITCODE
output = ($output -join "`n")
}
} catch {
[pscustomobject]@{
ok = $false
exit_code = -1
output = $_.Exception.Message
}
}
}
function Test-HttpHead {
param([string]$Url)
try {
$output = & curl.exe --location --head --max-time 12 $Url 2>&1
$text = $output -join "`n"
$statusMatches = [regex]::Matches($text, "HTTP/\S+\s+(\d+)")
$lastStatus = if ($statusMatches.Count -gt 0) { [int]$statusMatches[$statusMatches.Count - 1].Groups[1].Value } else { $null }
[pscustomobject]@{
url = $Url
status = $lastStatus
non_502 = ($lastStatus -ne 502 -and $null -ne $lastStatus)
ok = ($lastStatus -ge 200 -and $lastStatus -lt 500)
output = $text
}
} catch {
[pscustomobject]@{
url = $Url
status = $null
non_502 = $false
ok = $false
output = $_.Exception.Message
}
}
}
function Get-HarborState {
$core = Invoke-SshText "192.168.0.110" "docker ps --filter name=harbor-core"
$registry = Invoke-SshText "192.168.0.110" "docker ps --filter name=registry"
$redis = Invoke-SshText "192.168.0.110" "docker ps --filter name=redis"
[pscustomobject]@{
core_ok = ($core.output -match "harbor-core" -and $core.output -match "healthy")
registry_ok = ($registry.output -match "registry" -and $registry.output -match "healthy")
redis_ok = ($redis.output -match "redis" -and $redis.output -match "Up")
core = $core.output
registry = $registry.output
redis = $redis.output
}
}
function Get-AwoooDeployState {
$deploy = Invoke-SshText "192.168.0.120" "sudo -n kubectl get deploy -n awoooi-prod"
$pods = Invoke-SshText "192.168.0.120" "sudo -n kubectl get pods -n awoooi-prod"
[pscustomobject]@{
deploy_ok = (
$deploy.output -match "awoooi-api\s+1/1" -and
$deploy.output -match "awoooi-web\s+2/2" -and
$deploy.output -match "awoooi-worker\s+1/1"
)
image_pull_failure = ($pods.output -match "ImagePullBackOff|ErrImagePull")
crash_failure = ($pods.output -match "CrashLoopBackOff|Pending")
deploy = $deploy.output
pods = $pods.output
}
}
function Repair-HarborIfAllowed {
param([object]$Harbor)
if (-not $ControlledApply) { return }
if (-not $Harbor.redis_ok) {
Write-Evidence "CONTROLLED_APPLY restart harbor redis"
Invoke-SshText "192.168.0.110" "docker restart redis" | Out-Null
}
if (-not $Harbor.core_ok) {
Write-Evidence "CONTROLLED_APPLY restart harbor-core"
Invoke-SshText "192.168.0.110" "docker restart harbor-core" | Out-Null
}
}
function Repair-AwoooPodsIfAllowed {
param([object]$Awooo, [object]$Harbor)
if (-not $ControlledApply) { return }
if (-not $Harbor.core_ok) { return }
if (-not $Awooo.image_pull_failure) { return }
Write-Evidence "CONTROLLED_APPLY delete AWOOOI ImagePullBackOff pods after Harbor core healthy"
Invoke-SshText "192.168.0.120" "sudo -n kubectl delete pod -n awoooi-prod --selector app=awoooi-api" | Out-Null
Invoke-SshText "192.168.0.120" "sudo -n kubectl delete pod -n awoooi-prod --selector app=awoooi-web" | Out-Null
Invoke-SshText "192.168.0.120" "sudo -n kubectl delete pod -n awoooi-prod --selector app=awoooi-worker" | Out-Null
}
Write-Evidence "scorecard_start controlled_apply=$ControlledApply"
$hostResults = @()
foreach ($h in $Hosts) {
$result = Test-PingHost $h
$hostResults += $result
Write-Evidence "host_ping host=$($result.host) ok=$($result.ping)"
}
$harbor = Get-HarborState
Write-Evidence "harbor core_ok=$($harbor.core_ok) registry_ok=$($harbor.registry_ok) redis_ok=$($harbor.redis_ok)"
Repair-HarborIfAllowed $harbor
$awooo = Get-AwoooDeployState
Write-Evidence "awoooi deploy_ok=$($awooo.deploy_ok) image_pull_failure=$($awooo.image_pull_failure) crash_failure=$($awooo.crash_failure)"
Repair-AwoooPodsIfAllowed $awooo $harbor
$httpResults = @()
foreach ($u in $PublicUrls) {
$result = Test-HttpHead $u
$httpResults += $result
Write-Evidence "http url=$($result.url) status=$($result.status) non_502=$($result.non_502) ok=$($result.ok)"
}
$summary = [pscustomobject]@{
timestamp = (Get-Date -Format o)
controlled_apply = [bool]$ControlledApply
hosts = $hostResults
harbor = $harbor
awoooi = $awooo
public_http = $httpResults
completed = (
($hostResults | Where-Object { -not $_.ping }).Count -eq 0 -and
$harbor.core_ok -and
$awooo.deploy_ok -and
($httpResults | Where-Object { -not $_.non_502 }).Count -eq 0
)
}
$summary | ConvertTo-Json -Depth 8 | Set-Content -Encoding UTF8 $jsonPath
Write-Evidence "scorecard_complete completed=$($summary.completed) json=$jsonPath"
if (-not $summary.completed) {
exit 2
}
exit 0

View File

@@ -107,6 +107,12 @@ data:
# OPENCLAW_BOT_TOKEN / NEMOTRON_BOT_TOKEN 在 Secrets 中配置
# ============================================================================
SRE_GROUP_CHAT_ID: "-1003711974679"
# 2026-07-10 Codex: Agent99 direct SRE relay. Alertmanager still lands in
# AWOOI API first; the sanitized bridge then POSTs to 99 so Agent99 can queue
# allowlisted recovery and write evidence/KM. Optional relay token stays in
# Secret/env only; no token value is stored here.
AGENT99_SRE_ALERT_RELAY_URL: "http://192.168.0.99:8787/agent99/sre-alert/"
AGENT99_SRE_ALERT_RELAY_TIMEOUT_SECONDS: "3"
# 快取 TTL (秒)
CACHE_TTL_DASHBOARD: "300"