Compare commits
1 Commits
codex/depl
...
drift/adop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75b7d338e0 |
@@ -10,11 +10,11 @@
|
||||
|
||||
| 欄位 | 值 |
|
||||
|------|-----|
|
||||
| **版本** | v1.8 |
|
||||
| **版本** | v1.7 |
|
||||
| **建立日期** | 2026-03-20 (台北) |
|
||||
| **建立者** | Claude Code |
|
||||
| **最後修改** | 2026-05-01 15:30 (台北) |
|
||||
| **修改者** | Codex |
|
||||
| **最後修改** | 2026-03-31 18:00 (台北) |
|
||||
| **修改者** | Claude Code (首席架構師) |
|
||||
|
||||
### 變更紀錄
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
| v1.5 | 2026-03-27 | Claude Code | Stream Key 統一 + 告警去重機制 |
|
||||
| v1.6 | 2026-03-27 | Claude Code | **P1 優化: 稍後/靜默按鈕** |
|
||||
| v1.7 | 2026-03-31 | Claude Code | **Phase 22: OpenClaw + Nemotron 協作 (ADR-044)** |
|
||||
| v1.8 | 2026-05-01 | Codex | **LLM 鬼循環治理: stable alert cache key + no裸奔重試** |
|
||||
|
||||
---
|
||||
|
||||
@@ -116,18 +115,6 @@ async def analyze_with_ai(context: str) -> str:
|
||||
response = await _call_ollama(context)
|
||||
```
|
||||
|
||||
#### 2.1 告警快取鍵必須使用穩定維度
|
||||
|
||||
告警分析的 prompt 會包含 annotations、SignOz 即時數值、MCP evidence 等動態資料;不得把完整 prompt 當成同一告警的唯一 cache key,否則 firing 告警每 20 秒都會 miss cache。
|
||||
|
||||
正確維度:
|
||||
|
||||
```
|
||||
prompt_family + alertname + alert_category + namespace + target_resource + severity + fingerprint
|
||||
```
|
||||
|
||||
禁止把 `annotations.description`、`message`、即時 metrics 數值、trace URL 當成重複告警 cache key 的必要組成。需要重新分析時,應由 fingerprint 變化、人工刷新、Playbook/KM 版本變化、或明確 TTL 到期觸發。
|
||||
|
||||
### 3. Multi-Sig 動作必須 Dry-Run
|
||||
|
||||
```python
|
||||
|
||||
@@ -38,8 +38,6 @@
|
||||
| v2.5 | 2026-04-09 | Claude Sonnet 4.6 | **🔴 SSH 自動修復全鏈路 — 雙主機 E2E 閉環 + 12 Bug 修復** |
|
||||
| v2.6 | 2026-04-11 | Claude Sonnet 4.6 | **Sprint B-1 Ansible IaC 骨架 + Architecture Review 安全修復** |
|
||||
| v2.7 | 2026-04-11 | Claude Sonnet 4.6 | **Sprint B-2/B-3 ArgoCD GitOps + Sprint C Velero/rsync DR + ADR-070 MCP Phase 1-4 全自動 AIOps 閉環 + ADR-071 告警通知四類型** |
|
||||
| v2.8 | 2026-04-25 | Claude Sonnet 4.6 | **🔴 Prometheus 記憶體指標選擇規範(working_set vs usage_bytes)+ Gitea HMAC Webhook 規範** |
|
||||
| v2.9 | 2026-05-01 | Codex | **ArgoCD deploy revision gate:CD 不得以舊 revision Synced/Healthy 誤判成功** |
|
||||
|
||||
---
|
||||
|
||||
@@ -625,23 +623,6 @@ concurrency:
|
||||
- Session Conflict 錯誤
|
||||
- set_output 檔案遺失
|
||||
|
||||
### ArgoCD Deploy Revision Gate (2026-05-01)
|
||||
|
||||
GitOps CD 在 `kustomization.yaml` commit/push 後,禁止只用 `Synced + Healthy` 判定完成;那可能是上一個 revision 已同步。正確條件:
|
||||
|
||||
```bash
|
||||
DEPLOY_REVISION=$(git rev-parse HEAD) # chore(cd): deploy ... commit
|
||||
kubectl annotate application awoooi-prod -n argocd \
|
||||
argocd.argoproj.io/refresh=hard --overwrite
|
||||
|
||||
# 必須同時成立
|
||||
status.sync.status == Synced
|
||||
status.health.status == Healthy
|
||||
status.sync.revision == DEPLOY_REVISION
|
||||
```
|
||||
|
||||
超時必須 `exit 1`,不可繼續 rollout/health check 舊 image,否則會把「舊版健康」誤報成「新版已部署」。
|
||||
|
||||
---
|
||||
|
||||
## 🚨 Runner 殭屍進程修復 (2026-03-26 教訓)
|
||||
@@ -1235,9 +1216,9 @@ links = DeepLinking.get_all_links(
|
||||
|------|-------|------|
|
||||
| Dockerfile | `openssh-client` | 生產 stage 必須安裝,ssh binary 才存在 |
|
||||
| K8s Pod securityContext | `fsGroup: 1000` | 讓 appuser 有 group read on 0400 Secret |
|
||||
| NetworkPolicy egress | port 22 → 110/120/121/188 | 預設拒絕,必須明確開放 |
|
||||
| NetworkPolicy egress | port 22 → 110 + 188 | 預設拒絕,必須明確開放 |
|
||||
| Secret defaultMode | `0400` (八進位) | SSH 要求 owner-only,group read 靠 fsGroup |
|
||||
| known_hosts Secret | `awoooi-repair-known-hosts` + `ssh-mcp-key.known_hosts` | optional: true,含 110/120/121/188 指紋;`ssh-mcp-key` 給 asyncssh 使用 |
|
||||
| known_hosts Secret | `awoooi-repair-known-hosts` | optional: true,含 110+188 hashed 指紋 |
|
||||
|
||||
### repair-bot 白名單 (當前完整清單)
|
||||
|
||||
@@ -1277,7 +1258,7 @@ links = DeepLinking.get_all_links(
|
||||
|
||||
1. 在目標主機建立 `~/bin/repair-bot-{host}.sh`(複製模板)
|
||||
2. 將 `awoooi-repair-ssh-key.pub` 加入 `~/.ssh/authorized_keys`(加 `command=` 限制)
|
||||
3. `ssh-keyscan {host_ip}` → 更新 `awoooi-repair-known-hosts` Secret 與 `ssh-mcp-key.known_hosts`
|
||||
3. `ssh-keyscan -H {host_ip}` → 更新 `awoooi-repair-known-hosts` Secret
|
||||
4. NetworkPolicy 新增 `{host_ip}:22` egress
|
||||
5. `LAYER_SSH_CONFIG` 新增 layer 設定(`host_repair_agent.py`)
|
||||
6. service-registry.yaml 新增服務分級
|
||||
@@ -1291,8 +1272,8 @@ links = DeepLinking.get_all_links(
|
||||
❌ kubectl apply 06-deployment-api.yaml → IMAGE_TAG_PLACEHOLDER 覆蓋真實 SHA → ImagePullBackOff
|
||||
✅ 修改 K8s Deployment 配置用 kubectl patch,不用 kubectl apply
|
||||
|
||||
❌ ssh-mcp-key known_hosts 是空檔或只更新 Secret 未重啟 subPath pod → asyncssh `Host key is not trusted`
|
||||
✅ 用 `wc -c /etc/ssh-mcp/known_hosts` 驗證非 0;subPath 掛載更新後 rollout restart API/worker
|
||||
❌ known_hosts hashed 格式,grep IP 會得 0 → 以為沒寫進去
|
||||
✅ 用 wc -l 或 ssh 實測驗證,hashed 格式是正常的
|
||||
|
||||
❌ StrictHostKeyChecking=no(舊設定)
|
||||
✅ known_hosts Secret 已建立,改用 StrictHostKeyChecking=yes
|
||||
@@ -1362,51 +1343,6 @@ Architecture Review 發現的安全要求(2026-04-11):
|
||||
|
||||
3. **群組 B 工具需 trust_score >= 0.8**(硬編碼守衛)
|
||||
|
||||
### Host/Backup SSH Route Invariants (2026-05-01)
|
||||
|
||||
`backup_failure` is a host-layer category. Keep it aligned anywhere
|
||||
`host_resource` is routed, especially:
|
||||
|
||||
- `DecisionManager`: non-`kubectl` actions must route to SSH MCP before
|
||||
`parse_kubectl_action()`. Otherwise SSH diagnosis strings with shell syntax
|
||||
are blocked as `forbidden_shell_metachar`.
|
||||
- `DecisionManager`: `kubectl` actions from `host_resource` or
|
||||
`backup_failure` must be blocked and escalated to emergency intervention.
|
||||
- `AutoRepairService`: host/backup incidents must not fall back to K8s
|
||||
rollout Playbooks.
|
||||
- `SSHProvider`: `ssh_diagnose` is a first-class read-only tool. A successful
|
||||
diagnosis is evidence collection, not auto-repair completion.
|
||||
- `SSHProvider`: host user overrides are required for topology drift. Current
|
||||
baseline is `SSH_MCP_HOST_USERS=192.168.0.188=ollama`; 110/120/121 use
|
||||
default `wooo`.
|
||||
- `DecisionManager`: SSH MCP failure must set `mcp_all_failed=True` and raise
|
||||
emergency intervention. Never mark failed SSH or diagnosis-only paths
|
||||
`COMPLETED`.
|
||||
|
||||
Runtime baseline for host/backup repair:
|
||||
|
||||
```bash
|
||||
kubectl -n awoooi-prod get secret ssh-mcp-key awoooi-repair-ssh-key awoooi-repair-known-hosts
|
||||
|
||||
kubectl -n awoooi-prod exec deploy/awoooi-api -- sh -lc '
|
||||
ls -l /run/secrets/ssh_mcp_key /etc/ssh-mcp/known_hosts \
|
||||
/etc/repair-ssh/id_ed25519 /etc/repair-known-hosts/known_hosts
|
||||
'
|
||||
|
||||
kubectl -n awoooi-prod exec deploy/awoooi-api -- sh -lc '
|
||||
for h in 192.168.0.110 192.168.0.120 192.168.0.121; do
|
||||
ssh -i /run/secrets/ssh_mcp_key -o BatchMode=yes \
|
||||
-o StrictHostKeyChecking=yes -o ConnectTimeout=5 wooo@$h "echo OK:$h"
|
||||
done
|
||||
ssh -i /run/secrets/ssh_mcp_key -o BatchMode=yes \
|
||||
-o StrictHostKeyChecking=yes -o ConnectTimeout=5 ollama@192.168.0.188 "echo OK:188"
|
||||
'
|
||||
```
|
||||
|
||||
`awoooi-executor` RBAC must include read-only backup evidence:
|
||||
`jobs.batch`, `cronjobs.batch`, PVCs, and Velero backup resources. It may patch
|
||||
`statefulsets.apps` / `daemonsets.apps` only for safe rollout restart.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Sprint C — DR 備份與恢復 (2026-04-11) ✅
|
||||
@@ -1433,100 +1369,6 @@ kubectl -n awoooi-prod exec deploy/awoooi-api -- sh -lc '
|
||||
|
||||
---
|
||||
|
||||
## 🔴 Prometheus 記憶體指標選擇規範 (2026-04-25)
|
||||
|
||||
> **事故**: ClickHouse 在 2026-04-23 23:13 觸發假警報,`usage_bytes`=88.5% 但實際壓力 `working_set_bytes`=7.8%
|
||||
> **根因**: 指標選錯,不是閾值設定問題
|
||||
|
||||
### 兩個指標的本質差異
|
||||
|
||||
| 指標 | 含義 | OOM Killer 管 | 告警應用 |
|
||||
|------|------|--------------|---------|
|
||||
| `container_memory_usage_bytes` | RSS + page cache(含 OS inactive 緩存) | ❌ 不管 | ❌ 禁止用於記憶體壓力告警 |
|
||||
| `container_memory_working_set_bytes` | RSS + active cache(K8s kubectl top 同源) | ✅ 真實壓力 | ✅ 必須用於記憶體壓力告警 |
|
||||
|
||||
### 鐵律
|
||||
|
||||
```yaml
|
||||
# ❌ 絕對禁止:包含 page cache,產生假警報
|
||||
- alert: MemoryPressure
|
||||
expr: container_memory_usage_bytes / container_spec_memory_limit_bytes > 0.8
|
||||
|
||||
# ✅ 必須使用:業界標準,K8s kubectl top 同源,OOM killer 基準
|
||||
- alert: MemoryPressure
|
||||
expr: container_memory_working_set_bytes{container!="", container!="POD"} / container_spec_memory_limit_bytes{container!="", container!="POD"} > 0.85
|
||||
for: 10m
|
||||
```
|
||||
|
||||
**Why 0.85(非 0.8)**: `working_set` 語意下 85% 才代表真實記憶體壓力,0.8 偏保守
|
||||
**Why `for: 10m`**: 防止瞬間抖動,真實壓力需持續 10 分鐘才觸發
|
||||
|
||||
### PromQL 測試(必須)
|
||||
|
||||
新增或修改記憶體告警規則時,必須用 `promtool test rules` 加 4 個 test cases:
|
||||
- 負測 1:`usage_bytes` 高 + `working_set` 低 → 不觸發
|
||||
- 負測 2:`working_set` 略低於閾值 → 不觸發
|
||||
- 正測 1:`working_set` 超閾值持續 10 分鐘 → 觸發
|
||||
- 正測 2:`working_set` 超閾值但不足 10 分鐘 → 不觸發
|
||||
|
||||
**測試檔案位置**: `ops/monitoring/tests/`
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Gitea CI/CD Webhook 整合 (2026-04-25)
|
||||
|
||||
> **新增端點**: POST `/api/v1/webhooks/gitea`
|
||||
> **實作**: `apps/api/src/integrations/gitea_webhook.py`
|
||||
|
||||
### 驗簽機制
|
||||
|
||||
```python
|
||||
# Gitea 使用 X-Gitea-Signature header(與 GitHub 不同)
|
||||
def _verify_gitea_signature(payload: bytes, signature: str, secret: str) -> bool:
|
||||
expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
|
||||
return hmac.compare_digest(expected, signature)
|
||||
```
|
||||
|
||||
### 三類事件 + URL 路由
|
||||
|
||||
| 事件 | 觸發條件 | Telegram 訊息格式 |
|
||||
|------|---------|-----------------|
|
||||
| PR merged | `pull_request.merged == true` | 🔀 PR merged 通知 |
|
||||
| CI failure | `workflow_run.conclusion == "failure"` | 🔴 CI 失敗告警 |
|
||||
| Deploy failure | `check_run.conclusion == "failure" && name contains "deploy"` | 🚨 部署失敗告警 |
|
||||
|
||||
### K8s 配置要求
|
||||
|
||||
```yaml
|
||||
# K8s Secret 必須包含(在 03-secrets.yaml 有佔位)
|
||||
GITEA_WEBHOOK_SECRET: <base64>
|
||||
|
||||
# Gitea UI 設定
|
||||
URL: https://api.awoooi.wooo.work/api/v1/webhooks/gitea
|
||||
Content-Type: application/json
|
||||
Secret: <同 K8s Secret>
|
||||
Events: Pull Request + Workflow Run
|
||||
```
|
||||
|
||||
### 去重保護
|
||||
|
||||
Redis SET NX EX 600s(`dedup:gitea:{event}:{sha[:8]}`),同一事件 10 分鐘不重複推送。
|
||||
|
||||
### E2E 驗證
|
||||
|
||||
```bash
|
||||
# 確認 Secret 注入
|
||||
kubectl get secret awoooi-secrets -n awoooi-prod -o jsonpath='{.data.GITEA_WEBHOOK_SECRET}' | base64 -d
|
||||
|
||||
# 直接測試 endpoint 可達
|
||||
curl -s -X POST https://api.awoooi.wooo.work/api/v1/webhooks/gitea \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}' | jq '.detail'
|
||||
# 預期: "Missing signature" 或 "Invalid signature"(代表端點存在,驗簽生效)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🤖 ADR-070 全自動 AIOps 閉環 — MCP Phase 1-4 (2026-04-11) ✅
|
||||
|
||||
> 10 MCP Providers 全部生產驗收完成
|
||||
@@ -1550,7 +1392,6 @@ curl -s -X POST https://api.awoooi.wooo.work/api/v1/webhooks/gitea \
|
||||
```yaml
|
||||
SSH_MCP_ENABLED: "true"
|
||||
SSH_MCP_KNOWN_HOSTS_FILE: "/etc/ssh-mcp/known_hosts"
|
||||
SSH_MCP_HOST_USERS: "192.168.0.188=ollama"
|
||||
ARGOCD_MCP_ENABLED: "true"
|
||||
ARGOCD_URL: "https://192.168.0.125:30443"
|
||||
SENTRY_MCP_ENABLED: "true"
|
||||
@@ -1567,3 +1408,4 @@ ssh-mcp-key ✅ (ssh_mcp_key + known_hosts)
|
||||
|
||||
### Runbook
|
||||
`docs/runbooks/ssh-mcp-setup.md`
|
||||
|
||||
|
||||
@@ -784,48 +784,8 @@ kubectl -n awoooi-prod logs -l app=awoooi-api --tail=50 | \
|
||||
| `can_auto_repair: false` | service-registry BLOCK/HITL | 查 `blocked_by` 欄位 |
|
||||
| `ssh: command not found` | Dockerfile 缺 openssh-client | Pod exec `which ssh` |
|
||||
| `Permission denied (publickey)` | known_hosts 缺少該主機 | Pod exec SSH 看錯誤訊息 |
|
||||
| `Permission denied (publickey)` only on `192.168.0.188` | 188 需要 `ollama` 使用者,不是預設 `wooo` | 查 `SSH_MCP_HOST_USERS=192.168.0.188=ollama`,用 `ollama@192.168.0.188` 測 |
|
||||
| `Host key is not trusted for host ...` | `/etc/ssh-mcp/known_hosts` 空檔、過期,或 Secret 已 patch 但 subPath pod 未重啟 | patch `ssh-mcp-key.known_hosts`,rollout restart API/worker,再用 `ssh_diagnose` 驗證 |
|
||||
| `Load key ... Permission denied` | fsGroup 未設定 | Pod exec `ls -la /etc/repair-ssh/` |
|
||||
| `Connection refused/timeout` | NetworkPolicy 封鎖 22 | Pod exec `ssh -v` 看連線過程 |
|
||||
| `forbidden_shell_metachar` 且 action 是 `ssh ... '...'` | host/backup category 沒在 DecisionManager kubectl parser 前路由 SSH | 查 `alert_category` 是否為 `backup_failure`,確認 `_is_host_layer_ssh_category()` 覆蓋 |
|
||||
| SSH diagnosis success but incident still needs action | `ssh_diagnose` 是只讀證據蒐集,不是修復 | 應看到 `ssh_diagnosis_collected=True` 並走 emergency/human/AI intervention |
|
||||
|
||||
### Telegram 按鈕 E2E 檢查 (2026-05-01)
|
||||
|
||||
告警卡片按鈕不是純 UI。每個按鈕都必須能在
|
||||
`callback_action_spec.yaml` 找到 callback pattern,並經
|
||||
`callback_dispatcher.py` 路由到實際 handler。
|
||||
|
||||
| 卡片/情境 | 必要按鈕 | 預期處理 |
|
||||
|-----------|----------|----------|
|
||||
| Approval / LLM action | approve, reject, details, ignore | 寫 approval decision、執行或拒絕、查詳情、忽略告警 |
|
||||
| Auto repair unavailable / emergency | investigate, escalate/assign, rollback when applicable | 通知人工/AI Agent 介入,不可靜默 |
|
||||
| Drift TYPE-4D | view diff, adopt, rollback, ignore | 看 diff、採納變更、回滾、忽略 |
|
||||
| Backup / host diagnosis | restart only when rule allows, charts/logs/details, cleanup when safe | 不得提供 K8s-only repair button 當 host/backup 主動作 |
|
||||
| Post-verification degraded/failed | rollback proposal, investigate, details | 不自動 rollback,需人工或 emergency AI Agent 接手 |
|
||||
| SecOps authorize/isolate/block | record authorization, multi-sig gate | 不直接執行危險隔離;必須寫 Redis TTL、AOL、timeline |
|
||||
|
||||
Regression test target: button callback names emitted by `telegram_gateway.py`
|
||||
must stay in sync with `callback_action_spec.yaml`; stale buttons are a
|
||||
production bug because Telegram cards can outlive code deploys.
|
||||
|
||||
Provider name drift is also a ghost-button bug. `callback_action_spec.yaml`
|
||||
may use friendly names (`k8s`, `ssh`), but dispatcher must normalize to actual
|
||||
registered MCP providers (`kubernetes`, `ssh_host`) before `get_provider()`.
|
||||
`backup_failure` cards must expose read-only diagnostics before any write
|
||||
action: host disk, backup jobs, and Velero backup status.
|
||||
|
||||
Emergency intervention is not complete until it is queryable later. Any
|
||||
auto-repair-unavailable, drift-auto-adopt-blocked, or SecOps authorization path
|
||||
must write both `alert_operation_log` and `timeline_events` using existing enum
|
||||
values (`APPROVAL_ESCALATED` / `USER_ACTION`) unless a migration has already
|
||||
landed. Telegram-only escalation is a silent learning-loop failure.
|
||||
|
||||
All Telegram alert lifecycle operations must use `TelegramGateway.alert_chat_id`:
|
||||
initial send, analyzing placeholder, delete, editMessageText,
|
||||
editMessageReplyMarkup, CI progress, and action-result updates. Sending the
|
||||
card to the SRE group but editing/deleting the DM is a ghost-button bug.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -10,11 +10,11 @@
|
||||
|
||||
| 欄位 | 值 |
|
||||
|------|-----|
|
||||
| **版本** | v1.6 |
|
||||
| **版本** | v1.5 |
|
||||
| **建立日期** | 2026-03-20 (台北) |
|
||||
| **建立者** | Claude Code |
|
||||
| **最後修改** | 2026-04-24 22:30 (台北) |
|
||||
| **修改者** | Codex |
|
||||
| **最後修改** | 2026-03-26 15:40 (台北) |
|
||||
| **修改者** | Claude Code |
|
||||
|
||||
### 變更紀錄
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
| v1.3 | 2026-03-26 | Claude Code | 首席架構師審查流程 + 審查週期調整 (每週) |
|
||||
| v1.4 | 2026-03-26 | Claude Code | 🔴 新增「封存而非刪除」策略 (統帥裁示) |
|
||||
| v1.5 | 2026-03-26 | Claude Code | **dependency-cruiser 依賴治理整合 (Phase 14.2)** |
|
||||
| v1.6 | 2026-04-24 | Codex | **新增 12-agent 協作治理:任務判型、主責/協作 agent、9 skills 對照** |
|
||||
|
||||
---
|
||||
|
||||
@@ -141,54 +140,6 @@ Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
||||
| 架構變更 | ✅ |
|
||||
| 部署成功 | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 12-Agent 協作治理 (2026-04-24 新增)
|
||||
|
||||
> 目的:讓專案任務分工有固定語法,不再只靠臨場口頭約定。
|
||||
|
||||
### 定位
|
||||
|
||||
- `12 agents` 是任務角色分工
|
||||
- `.agents/skills/*.md` 是工程守則
|
||||
- 實際工作流:**先判型與派工,再依對應 skills 執行**
|
||||
|
||||
### 最小必要組隊原則
|
||||
|
||||
1. 每個任務只能有 1 個主責 agent
|
||||
2. 協作 agent 預設 1-3 位,避免過度編排
|
||||
3. 涉及紅區、Telegram、learning loop、deploy 時,自動補 `critic`
|
||||
|
||||
### 常用派工規則
|
||||
|
||||
| 任務類型 | 主責 agent | 協作 agent |
|
||||
|----------|-----------|-----------|
|
||||
| 查 bug / 查斷點 / 找根因 | `debugger` | `db-expert`, `tool-expert`, `critic` |
|
||||
| migration / SQL / playbook / KM / learning | `db-expert` | `debugger`, `refactor-specialist` |
|
||||
| 前端頁面 / UI / i18n / 戰情中心 | `frontend-designer` | `fullstack-engineer`, `critic` |
|
||||
| 前後端一起改 / API 對 UI / 完整落地 | `fullstack-engineer` | `frontend-designer`, `debugger`, `db-expert` |
|
||||
| 重構 / 抽層 / 技術債 | `refactor-specialist` | `migration-engineer`, `critic`, `db-expert` |
|
||||
| Gitea / webhook / CI/CD / deploy | `migration-engineer` | `tool-expert`, `vuln-verifier`, `critic` |
|
||||
| Telegram / approval / callback / 權限 / 安全 | `vuln-verifier` | `debugger`, `db-expert`, `critic` |
|
||||
| 規劃 / 拆階段 / 驗收 | `planner` | `critic`, `onboarder` |
|
||||
| 專案導覽 / 建立上下文 | `onboarder` | `planner`, `critic` |
|
||||
| 官方規格 / SDK / 外部方案查證 | `web-researcher` | `planner`, `critic` |
|
||||
|
||||
### 與 9 Skills 的關係
|
||||
|
||||
| 12-agent | 最接近的 skills |
|
||||
|----------|------------------|
|
||||
| `frontend-designer` | `01-awoooi-frontend-aesthetics` |
|
||||
| `fullstack-engineer` | `01 + 02 + 06` |
|
||||
| `debugger` | `02 + 05` |
|
||||
| `db-expert` | `02` |
|
||||
| `refactor-specialist` | `09 + 02` |
|
||||
| `migration-engineer` | `09 + 06 + 04` |
|
||||
| `tool-expert` | `07` |
|
||||
| `critic` | `05` |
|
||||
|
||||
完整規則見 `docs/12-agent-game-rules.md`
|
||||
|
||||
### 格式範例
|
||||
|
||||
```markdown
|
||||
|
||||
@@ -10,19 +10,16 @@
|
||||
|
||||
| 欄位 | 值 |
|
||||
|------|-----|
|
||||
| **版本** | v1.6 |
|
||||
| **版本** | v1.3 |
|
||||
| **建立日期** | 2026-03-25 23:30 (台北) |
|
||||
| **建立者** | Claude Code |
|
||||
| **最後修改** | 2026-05-01 15:45 (台北) |
|
||||
| **修改者** | Codex |
|
||||
| **最後修改** | 2026-03-26 18:00 (台北) |
|
||||
| **修改者** | Claude Code |
|
||||
|
||||
### 變更紀錄
|
||||
|
||||
| 版本 | 日期 | 執行者 | 變更內容 |
|
||||
|------|------|--------|----------|
|
||||
| v1.6 | 2026-05-01 | Codex | Agent Loop shadow structured metadata, non-decisive confidence delta guard |
|
||||
| v1.5 | 2026-05-01 | Codex | OpenClaw Agent Loop read-only shadow canary + prod feature flag |
|
||||
| v1.4 | 2026-05-01 | Codex | MCP Agent Loop governance、audit schema、Agent role tool permissions |
|
||||
| v1.3 | 2026-03-26 18:00 | Claude Code | 新增 Grafana MCP (#83) + SignOz query_logs |
|
||||
| v1.2 | 2026-03-26 23:30 | Claude Code | 新增 Filesystem MCP Tool (#82 已完成) |
|
||||
| v1.1 | 2026-03-26 14:20 | Claude Code | 更新 MCP Tool 狀態 (#79/#80/#81 已完成) |
|
||||
@@ -51,17 +48,6 @@ Phase 13.2 Tool 實作 (P0 最優先):
|
||||
| **Grafana** | ✅ 真實 | `providers/grafana_provider.py` | #83 ✅ |
|
||||
| 維運手冊 RAG | 📋 設計完成 | - | #84 (待實作) |
|
||||
|
||||
## Agent Loop MCP 鐵律 (ADR-105)
|
||||
|
||||
- MCP Provider 已存在時,不要重複安裝外部 MCP server;先接入 `ProviderRegistry` / `MCPToolRegistry`,再補 audit 與權限。
|
||||
- 所有 provider `execute()` 必須經過 audited wrapper,寫入 `mcp_audit_log` 與 `mcp_daily_stats`。
|
||||
- Agent Loop 工具 schema 必須由 `ai_providers/tool_schema.py` 產生,禁止 provider 各自手刻不同命名規則。
|
||||
- OpenClaw / NemoTron / Hermes / ElephantAlpha 的工具白名單必須由 `ai_providers/permissions.py` 控制。
|
||||
- Internal RAG/MCP 知識層沿用 PostgreSQL + pgvector + Redis hot cache;不得為「MCP RAG」另建孤立資料庫,除非已有量級、隔離或延遲證據。
|
||||
- `incident_id` 在 MCP audit schema 中使用 `VARCHAR(64)`,因為 AWOOOI incident 是 `INC-*` 字串,不是 UUID。
|
||||
- OpenClaw Agent Loop 初期只可用 shadow canary:`ENABLE_OPENCLAW_AGENT_LOOP_SHADOW=true` 時,先給 read-only tools 且不改主決策;確認 `mcp_audit_log`、latency、LLM quality 後才允許升級成 decisive path。
|
||||
- Shadow canary output 必須正規化為 `agent_loop_shadow.structured`,並固定 `decision_impact=none`;`confidence_delta` 初期只能記錄 0 到 -0.15 的保守 metadata,禁止用 shadow 結果提高信心或覆蓋主決策。
|
||||
|
||||
### 已完成 Tool 功能
|
||||
|
||||
**SignOz MCP (#79)**:
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Skill 08: Model Router Expert
|
||||
|
||||
> 版本: v1.2
|
||||
> 版本: v1.1
|
||||
> 建立: 2026-03-26 (台北時區)
|
||||
> 更新: 2026-05-01 (加入 LLM ghost-loop 成本治理)
|
||||
> 更新: 2026-03-29 (加入 NVIDIA Nemotron 整合)
|
||||
> 管轄: Phase 13.3 智能路由、複雜度評估、意圖分類、Tool Calling 路由
|
||||
|
||||
---
|
||||
@@ -138,20 +138,6 @@ alerts:
|
||||
action: notify_admin
|
||||
```
|
||||
|
||||
### Provider 成本治理鐵律
|
||||
|
||||
外部 AI 費用不是第一層問題。當同一告警形成鬼循環時,任何 provider 都會被放大;先修 dedupe/cache/retry,再調 provider。
|
||||
|
||||
| 狀態 | Router 行為 |
|
||||
|------|-------------|
|
||||
| 同 fingerprint 10 分鐘內重複 delivery | 命中 Alertmanager in-flight lock / DB convergence,不進 provider routing |
|
||||
| 同告警 annotations 或 metrics 變動 | 命中 stable LLM cache,不因動態 prompt 重新計費 |
|
||||
| provider timeout / 500 | 走 circuit breaker + fallback,但 webhook 不得回 500 造成 Alertmanager retry storm |
|
||||
| 高複雜度且本地模型信心不足 | 才允許 Gemini/Groq/Claude/OpenRouter 等外部 capped fallback |
|
||||
| 訂閱方案評估 | 以「新問題數」估算,不以 retry storm 的 delivery 數估算 |
|
||||
|
||||
健康飛輪下,外部 provider 用量應接近每天新告警/新 incident 數,而不是 Alertmanager 重送次數。Gemini/Groq/Claude 只能補專業度與 fallback 韌性,不能拿來遮住收斂失效。
|
||||
|
||||
---
|
||||
|
||||
## Fallback 策略 (ADR-006 v1.3 + ADR-036)
|
||||
|
||||
1
.claude/scheduled_tasks.lock
Normal file
1
.claude/scheduled_tasks.lock
Normal file
@@ -0,0 +1 @@
|
||||
{"sessionId":"412c1507-44d4-4702-bb80-f37e97b804a7","pid":5408,"acquiredAt":1774326092203}
|
||||
732
.claude/settings.json
Normal file
732
.claude/settings.json
Normal file
@@ -0,0 +1,732 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Read(**)",
|
||||
"Glob(**)",
|
||||
"Grep(**)",
|
||||
"Bash(curl *)",
|
||||
"Bash(kubectl get *)",
|
||||
"Bash(kubectl describe *)",
|
||||
"Bash(kubectl logs *)",
|
||||
"Bash(kubectl rollout status *)",
|
||||
"Bash(docker ps *)",
|
||||
"Bash(docker logs *)",
|
||||
"Bash(ls *)",
|
||||
"Bash(cat *)",
|
||||
"Bash(head *)",
|
||||
"Bash(tail *)",
|
||||
"Bash(grep *)",
|
||||
"Bash(find *)",
|
||||
"Bash(pwd)",
|
||||
"Bash(which *)",
|
||||
"Bash(echo *)",
|
||||
"Bash(git status *)",
|
||||
"Bash(git log *)",
|
||||
"Bash(git diff *)",
|
||||
"Bash(git branch *)",
|
||||
"Bash(git remote *)",
|
||||
"Edit(**)",
|
||||
"Write(apps/**)",
|
||||
"Write(packages/**)",
|
||||
"Write(docs/**)",
|
||||
"Write(.agents/**)",
|
||||
"Write(k8s/**)",
|
||||
"Write(scripts/**)",
|
||||
"Bash(pnpm *)",
|
||||
"Bash(npm *)",
|
||||
"Bash(npx *)",
|
||||
"Bash(node *)",
|
||||
"Bash(python *)",
|
||||
"Bash(python3 *)",
|
||||
"Bash(pip *)",
|
||||
"Bash(cd *)",
|
||||
"Bash(mkdir *)",
|
||||
"Bash(touch *)",
|
||||
"Bash(cp *)",
|
||||
"Bash(mv *)",
|
||||
"Bash(chmod *)",
|
||||
"Bash(pytest *)",
|
||||
"Bash(playwright *)",
|
||||
"Bash(git add *)",
|
||||
"Bash(git commit *)",
|
||||
"Bash(git stash *)",
|
||||
"Bash(ssh *)",
|
||||
"Bash(scp *)",
|
||||
"Bash(export KUBECONFIG=*)",
|
||||
"Bash(git push:*)",
|
||||
"Bash(claude --version)",
|
||||
"Bash(git check-ignore:*)",
|
||||
"WebSearch",
|
||||
"Bash(claude plugin:*)",
|
||||
"Bash(claude --channels)",
|
||||
"Bash(claude --channels plugin:telegram@claude-plugins-official --help)",
|
||||
"Bash(bash)",
|
||||
"Bash(source ~/.zshrc)",
|
||||
"Bash(~/.bun/bin/bun --version)",
|
||||
"Bash(env)",
|
||||
"Bash(claude upgrade:*)",
|
||||
"Bash(/Users/ogt/.local/bin/claude --help)",
|
||||
"Bash(CLAUDE_CODE_EXPERIMENTAL_CHANNELS=1 claude --help)",
|
||||
"Bash(claude --channels plugin:telegram@claude-plugins-official --print \"hello\")",
|
||||
"Bash(mkdir -p ~/.claude/channels/telegram)",
|
||||
"Bash(~/.claude/channels/telegram/.env)",
|
||||
"Bash(~/.bun/bin/bun run:*)",
|
||||
"Bash(sudo ln:*)",
|
||||
"Bash(ln -sf ~/.bun/bin/bun /opt/homebrew/bin/bun)",
|
||||
"Bash(xargs python:*)",
|
||||
"Bash(uv --version)",
|
||||
"Bash(pip3 install:*)",
|
||||
"Bash(pip3 show:*)",
|
||||
"Bash(ruff *)",
|
||||
"Bash(mypy *)",
|
||||
"Bash(black *)",
|
||||
"Bash(isort *)",
|
||||
"Bash(timeout *)",
|
||||
"Bash(wc *)",
|
||||
"Bash(sort *)",
|
||||
"Bash(uniq *)",
|
||||
"Bash(awk *)",
|
||||
"Bash(sed *)",
|
||||
"Bash(tr *)",
|
||||
"Bash(tee *)",
|
||||
"Bash(xargs *)",
|
||||
"Bash(test *)",
|
||||
"Bash([ *)",
|
||||
"Bash(true)",
|
||||
"Bash(false)",
|
||||
"Bash(date *)",
|
||||
"Bash(sleep *)",
|
||||
"Bash(kill *)",
|
||||
"Bash(pkill *)",
|
||||
"Bash(ps *)",
|
||||
"Bash(top *)",
|
||||
"Bash(htop *)",
|
||||
"Bash(df *)",
|
||||
"Bash(du *)",
|
||||
"Bash(free *)",
|
||||
"Bash(uname *)",
|
||||
"Bash(hostname *)",
|
||||
"Bash(whoami)",
|
||||
"Bash(id *)",
|
||||
"Bash(groups *)",
|
||||
"Bash(stat *)",
|
||||
"Bash(file *)",
|
||||
"Bash(realpath *)",
|
||||
"Bash(dirname *)",
|
||||
"Bash(basename *)",
|
||||
"Bash(type *)",
|
||||
"Bash(command *)",
|
||||
"Bash(hash *)",
|
||||
"Bash(alias *)",
|
||||
"Bash(set *)",
|
||||
"Bash(unset *)",
|
||||
"Bash(printenv *)",
|
||||
"Bash(diff *)",
|
||||
"Bash(cmp *)",
|
||||
"Bash(comm *)",
|
||||
"Bash(join *)",
|
||||
"Bash(paste *)",
|
||||
"Bash(cut *)",
|
||||
"Bash(rev *)",
|
||||
"Bash(nl *)",
|
||||
"Bash(fmt *)",
|
||||
"Bash(fold *)",
|
||||
"Bash(pr *)",
|
||||
"Bash(expand *)",
|
||||
"Bash(unexpand *)",
|
||||
"Bash(od *)",
|
||||
"Bash(xxd *)",
|
||||
"Bash(hexdump *)",
|
||||
"Bash(strings *)",
|
||||
"Bash(base64 *)",
|
||||
"Bash(md5sum *)",
|
||||
"Bash(sha256sum *)",
|
||||
"Bash(jq *)",
|
||||
"Bash(yq *)",
|
||||
"Bash(gh *)",
|
||||
"Bash(docker build *)",
|
||||
"Bash(docker run *)",
|
||||
"Bash(docker exec *)",
|
||||
"Bash(docker compose *)",
|
||||
"Bash(docker-compose *)",
|
||||
"Bash(docker images *)",
|
||||
"Bash(docker inspect *)",
|
||||
"Bash(docker network *)",
|
||||
"Bash(docker volume *)",
|
||||
"Bash(kubectl apply *)",
|
||||
"Bash(kubectl create *)",
|
||||
"Bash(kubectl exec *)",
|
||||
"Bash(kubectl port-forward *)",
|
||||
"Bash(kubectl config *)",
|
||||
"Bash(helm *)",
|
||||
"Bash(terraform *)",
|
||||
"Bash(ansible *)",
|
||||
"Bash(bun *)",
|
||||
"Bash(deno *)",
|
||||
"Bash(cargo *)",
|
||||
"Bash(rustc *)",
|
||||
"Bash(go *)",
|
||||
"Bash(java *)",
|
||||
"Bash(javac *)",
|
||||
"Bash(gradle *)",
|
||||
"Bash(mvn *)",
|
||||
"Bash(make *)",
|
||||
"Bash(cmake *)",
|
||||
"Bash(ninja *)",
|
||||
"Bash(uv *)",
|
||||
"Bash(poetry *)",
|
||||
"Bash(pipx *)",
|
||||
"Bash(virtualenv *)",
|
||||
"Bash(venv *)",
|
||||
"Bash(conda *)",
|
||||
"Bash(brew *)",
|
||||
"Bash(apt *)",
|
||||
"Bash(apt-get *)",
|
||||
"Bash(yum *)",
|
||||
"Bash(dnf *)",
|
||||
"Bash(pacman *)",
|
||||
"Bash(snap *)",
|
||||
"Bash(flatpak *)",
|
||||
"Bash(systemctl status *)",
|
||||
"Bash(journalctl *)",
|
||||
"Bash(service * status)",
|
||||
"Bash(nc *)",
|
||||
"Bash(netstat *)",
|
||||
"Bash(ss *)",
|
||||
"Bash(lsof *)",
|
||||
"Bash(nmap *)",
|
||||
"Bash(dig *)",
|
||||
"Bash(nslookup *)",
|
||||
"Bash(host *)",
|
||||
"Bash(ping *)",
|
||||
"Bash(traceroute *)",
|
||||
"Bash(mtr *)",
|
||||
"Bash(wget *)",
|
||||
"Bash(http *)",
|
||||
"Bash(httpie *)",
|
||||
"Bash(hadolint apps/api/Dockerfile)",
|
||||
"Bash(docker info:*)",
|
||||
"Bash(kubectl cluster-info:*)",
|
||||
"Read(//var/run/**)",
|
||||
"Bash(open -a Docker)",
|
||||
"Bash(git rm:*)",
|
||||
"Bash(git reset:*)",
|
||||
"Bash(kubectl --kubeconfig ~/.kube/config get pods -n awoooi -o wide)",
|
||||
"Bash(kubectl scale:*)",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollam@192.168.0.188 \"docker ps -a | grep -i claw\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker ps -a | grep -i claw\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker start clawbot && sleep 3 && docker logs clawbot --tail=10\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker ps | grep clawbot && docker port clawbot\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker logs clawbot --tail=30\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"cat /home/ollama/clawbot/.env | grep -E ''\\(TG_|TELEGRAM\\)'' | head -5\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker inspect clawbot --format=''{{range .Mounts}}{{.Source}}:{{.Destination}} {{end}}''\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker inspect clawbot --format=''{{range .Config.Env}}{{println .}}{{end}}'' | grep -E ''\\(TG_|TELEGRAM|ENABLED\\)''\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker logs clawbot 2>&1 | grep -i ''logout\\\\|log.out\\\\|shutdown\\\\|stop'' | tail -20\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker logs clawbot 2>&1 | grep -E ''\\(getMe|getUpdates|sendMessage\\).*200'' | tail -5\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker logs clawbot 2>&1 | grep -i ''success\\\\|started\\\\|初始化'' | head -20\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker logs clawbot 2>&1 | grep -E ''2026-03-\\(19|20|21\\)'' | grep -i ''error\\\\|fail\\\\|logout\\\\|400\\\\|401'' | head -20\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker stop clawbot && docker rm clawbot && echo ''✅ OpenClaw 已永久停用''\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"cd /home/ollama/clawbot-v5 && docker-compose ps 2>/dev/null || ls -la docker-compose.yml 2>/dev/null || find /home/ollama -name ''docker-compose*.yml'' -type f 2>/dev/null | head -5\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"cd /home/ollama/clawbot-v5 && docker-compose up -d && sleep 3 && docker-compose ps\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"cd /home/ollama/clawbot-v5 && docker compose up -d 2>&1 || docker run -d --name clawbot --restart unless-stopped -p 8088:8088 -v /var/run/docker.sock:/var/run/docker.sock 192.168.0.110:5000/library/clawbot:stable-v6 2>&1\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker logs clawbot --tail=15 2>&1\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker ps --format ''table {{.Names}}\\\\t{{.Status}}'' | grep -E ''clawbot|litellm''\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"cd /home/ollama/clawbot-v5 && sed -i ''s|TELEGRAM_BOT_TOKEN=.*|TELEGRAM_BOT_TOKEN=8569720657:AAHrJ5CMOb4rP0IYJrCUiDViLsnpK69uEUI|'' .env && grep TELEGRAM_BOT_TOKEN .env\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"cd /home/ollama/clawbot-v5 && docker compose down && docker compose up -d && sleep 5 && docker logs clawbot --tail=10\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker ps --format ''{{.Names}}'' | grep -i alert\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker stop alertmanager && docker rm alertmanager && echo ''✅ 舊 AIOPS Alertmanager 已停用''\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker ps --format ''table {{.Names}}\\\\t{{.Image}}\\\\t{{.Status}}''\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"cat /home/ollama/momo-pro/monitoring/prometheus/alert_rules.yml 2>/dev/null | grep -A5 ''ClawbotDown\\\\|telegram\\\\|AIOPS'' | head -30\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"find /home/ollama -name ''*.yml'' -type f 2>/dev/null | xargs grep -l ''ClawbotDown\\\\|telegram'' 2>/dev/null | head -5\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker exec clawbot grep -r ''協同警報\\\\|ClawbotDown'' /app 2>/dev/null | head -5\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker exec prometheus cat /etc/prometheus/prometheus.yml 2>/dev/null | grep -A10 ''alerting\\\\|alertmanager''\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker ps | grep -i alert || echo ''✅ 沒有 alertmanager 在運行''\")",
|
||||
"Bash(jq -r '.status, .components | to_entries[] | \"\"\"\"\\\\\\(.key\\): \\\\\\(.value.status\\)\"\"\"\"')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker ps --format ''table {{.Names}}\\\\t{{.Status}}'' | grep clawbot && docker logs clawbot --tail=15\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker inspect clawbot --format=''{{range .Config.Env}}{{println .}}{{end}}'' | grep TELEGRAM\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"cd /home/ollama/clawbot-v5 && sed -i ''s|TELEGRAM_BOT_TOKEN=.*|TELEGRAM_BOT_TOKEN=8569720657:AAFjDyjAN94QQrjn1gBnFXAyS20EUyozH8c|'' .env && docker compose down && docker compose up -d && sleep 5 && docker logs clawbot --tail=10\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker exec clawbot grep -r ''ClawBotDown\\\\|ClawbotDown'' /app 2>/dev/null | head -5 || echo ''在程式碼中找不到''\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker exec prometheus cat /etc/prometheus/alerts.yml 2>/dev/null | grep -A10 ''ClawBot\\\\|clawbot'' | head -30\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker exec prometheus cat /etc/prometheus/alerts.yml 2>/dev/null | grep -i ''clawbot\\\\|claw'' -A5 -B5\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker logs clawbot --since=5m 2>&1 | grep -i ''clawbot\\\\|incident\\\\|alert'' | tail -20\")",
|
||||
"Bash(sshpass -p \"0936223270\" ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker logs clawbot --tail 50 2>&1\")",
|
||||
"Bash(sshpass -p \"0936223270\" ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker logs clawbot 2>&1 | grep -i ''telegram\\\\|polling\\\\|bot'' | tail -20\")",
|
||||
"Bash(sshpass -p \"0936223270\" ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker ps --format ''table {{.Names}}\\\\t{{.Status}}\\\\t{{.Ports}}'' | grep -E ''claw|NAME''\")",
|
||||
"Bash(sshpass -p \"0936223270\" ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker logs clawbot 2>&1 | grep -E ''telegram|Telegram|error|Error'' | tail -20\")",
|
||||
"Bash(sshpass -p \"0936223270\" ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker ps | grep ollama\")",
|
||||
"Bash(sshpass -p \"0936223270\" ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker ps -a --format ''table {{.Names}}\\\\t{{.Status}}'' | head -20\")",
|
||||
"Bash(sshpass -p \"0936223270\" ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"sed -i ''s|host.docker.internal|172.17.0.1|g'' /home/ollama/clawbot-v5/.env && cat /home/ollama/clawbot-v5/.env | grep OLLAMA\")",
|
||||
"Bash(sshpass -p \"0936223270\" ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"cd /home/ollama/clawbot-v5 && docker-compose restart clawbot && sleep 3 && docker logs clawbot --tail 30 2>&1\")",
|
||||
"Bash(sshpass -p \"0936223270\" ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"cd /home/ollama/clawbot-v5 && docker compose restart clawbot && sleep 5 && docker logs clawbot --tail 30 2>&1\")",
|
||||
"Bash(sshpass -p \"0936223270\" ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker exec clawbot curl -s http://172.17.0.1:11434/api/tags | head -c 200\")",
|
||||
"Bash(sshpass -p \"0936223270\" ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker logs clawbot 2>&1 | tail -10\")",
|
||||
"Bash(sshpass -p \"0936223270\" ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker logs clawbot 2>&1 | grep -iE ''error|telegram|polling|alert|send'' | tail -30\")",
|
||||
"Bash(sshpass -p \"0936223270\" ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"cat /home/ollama/clawbot-v5/.env | grep OLLAMA\")",
|
||||
"Bash(sshpass -p \"0936223270\" ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"cd /home/ollama/clawbot-v5 && docker compose up -d --force-recreate clawbot && sleep 5 && docker logs clawbot 2>&1 | tail -20\")",
|
||||
"Bash(sshpass -p \"0936223270\" ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker exec clawbot curl -s http://172.17.0.1:11434/api/tags | head -c 100\")",
|
||||
"Bash(sshpass -p \"0936223270\" ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker logs clawbot --since 5m 2>&1 | tail -30\")",
|
||||
"Bash(sshpass -p \"0936223270\" ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker exec momo-db psql -U postgres -d clawbot -c \"\"SELECT enum_range\\(NULL::approvalstatus\\);\"\"\")",
|
||||
"Bash(sshpass -p \"0936223270\" ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker exec -e PGPASSWORD=clawbot123 momo-db psql -U clawbot -d clawbot -c \"\"SELECT enum_range\\(NULL::approvalstatus\\);\"\"\")",
|
||||
"Bash(sshpass -p \"0936223270\" ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker ps | grep -E ''postgres|db''\")",
|
||||
"Bash(sshpass -p \"0936223270\" ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker exec momo-db env | grep -i postgres\")",
|
||||
"Bash(sshpass -p \"0936223270\" ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"PGPASSWORD=AwoooiProd2026 psql -h localhost -U awoooi -d awoooi_prod -c \"\"SELECT enum_range\\(NULL::approvalstatus\\);\"\"\")",
|
||||
"Bash(KUBECONFIG=~/.kube/config kubectl config get-contexts)",
|
||||
"Bash(docker tag:*)",
|
||||
"Bash(docker push:*)",
|
||||
"Bash(ssh ollama@192.168.0.188 \"cd ~/awoooi-build && find apps/web/src -name ''''*.ts'''' -o -name ''''*.tsx'''' | head -30 | xargs md5sum\")",
|
||||
"Bash(rsync -avz --exclude 'node_modules' --exclude '.next' --exclude '.turbo' --exclude '*.log' /Users/ogt/awoooi/ ollama@192.168.0.188:~/awoooi-build/)",
|
||||
"Bash(gh run:*)",
|
||||
"Bash(APPROVAL_ID=\"ea43578e-17cd-40b9-b4c3-8fe8e92f225c\" __NEW_LINE_76dc92b2699cd7d5__ echo \"=== 檢查 Approval Metadata ===\" curl -s \"https://awoooi.wooo.work/api/v1/approvals/pending\")",
|
||||
"Bash(APPROVAL_ID=\"865ab726-c3b9-447e-86a9-65a6227516e6\" __NEW_LINE_db14ef76ca26af32__ echo \"=== 簽核 ===\" curl -s -X POST \"https://awoooi.wooo.work/api/v1/approvals/$APPROVAL_ID/sign\" -H \"Content-Type: application/json\" -d '{\"\"\"\"signer_id\"\"\"\":\"\"\"\"commander\"\"\"\",\"\"\"\"signer_name\"\"\"\":\"\"\"\"Commander\"\"\"\",\"\"\"\"comment\"\"\"\":\"\"\"\"Test resolution\"\"\"\"}')",
|
||||
"Read(//Users/ogt/awoooi/**)",
|
||||
"Bash(APPROVAL_ID=\"e9445e68-6c3e-4899-b507-3b9b7bcaf0a7\" __NEW_LINE_680ad94d4896e58a__ echo \"=== 簽核 ===\" curl -s -X POST \"https://awoooi.wooo.work/api/v1/approvals/$APPROVAL_ID/sign\" -H \"Content-Type: application/json\" -d '{\"\"\"\"signer_id\"\"\"\":\"\"\"\"commander\"\"\"\",\"\"\"\"signer_name\"\"\"\":\"\"\"\"Commander\"\"\"\",\"\"\"\"comment\"\"\"\":\"\"\"\"Final test\"\"\"\"}')",
|
||||
"Bash(APPROVAL_ID=\"eb0afb4e-834b-4af7-9ae0-3c58232fdd99\" INCIDENT=\"INC-20260323-F05CD6\" __NEW_LINE_47f1c3803a64b43c__ echo \"=== 簽核前 Incident 狀態 ===\" curl -s \"https://awoooi.wooo.work/api/v1/incidents/$INCIDENT\")",
|
||||
"Bash(mkdir -p /Users/ogt/awoooi/.claude/hooks)",
|
||||
"Bash(/Users/ogt/awoooi/.claude/hooks/pre-commit-check.sh:*)",
|
||||
"Bash(git -C /Users/ogt/awoooi status packages/lewooogo-core/)",
|
||||
"Bash(git -C /Users/ogt/awoooi ls-files packages/lewooogo-core/src/)",
|
||||
"Bash(git -C /Users/ogt/awoooi status --short)",
|
||||
"Bash(git -C /Users/ogt/awoooi add apps/api/pyproject.toml apps/api/scripts/ apps/api/src/ apps/web/.eslintrc.js apps/web/src/ packages/lewooogo-core/.eslintrc.js)",
|
||||
"Bash(git -C /Users/ogt/awoooi diff --cached --stat)",
|
||||
"Bash(git -C:*)",
|
||||
"Bash(for wf:*)",
|
||||
"Bash(do)",
|
||||
"Bash(done)",
|
||||
"Bash(jq 'if type == \"\"\"\"array\"\"\"\" then .[0] | {incident_id, status, decision} else . end')",
|
||||
"Bash(PYTHONPATH=. python -c \"from src.api.v1.stats import router; print\\(''✅ stats.py 載入成功,路由數:'', len\\(router.routes\\)\\)\")",
|
||||
"Bash(PYTHONPATH=. pytest tests/ -v --tb=short)",
|
||||
"Bash(PYTHONPATH=. pytest tests/test_stats_api.py -v --tb=short)",
|
||||
"Bash(PYTHONPATH=. pytest tests/test_webhook_telegram_integration.py::TestNewAlertTelegramPush -v --tb=long)",
|
||||
"Bash(PYTHONPATH=. pytest tests/test_webhook_telegram_integration.py::TestNewAlertTelegramPush -v --tb=short)",
|
||||
"Bash(PYTHONPATH=. pytest tests/test_webhook_telegram_integration.py -v --tb=short)",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 'kubectl get pods -n awoooi')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 'kubectl get ns awoooi && kubectl get all -n awoooi')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 'kubectl get ns | head -20')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 'kubectl get pods -n awoooi-prod')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 'kubectl logs awoooi-worker-bb89b5ffc-bpf45 -n awoooi-prod --tail=50')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 'kubectl logs awoooi-worker-bb89b5ffc-bpf45 -n awoooi-prod --tail=100 | grep -i telegram')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 'kubectl logs awoooi-api-8c9489b6c-cm8g5 -n awoooi-prod --tail=50 | grep -i webhook')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 'kubectl logs awoooi-api-8c9489b6c-cm8g5 -n awoooi-prod --tail=30')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 'kubectl get pods -n monitoring | grep alertmanager')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 \"kubectl get configmap alertmanager-config -n monitoring -o jsonpath=''{.data.alertmanager\\\\.yml}'' | head -50\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 'kubectl get svc -n awoooi-prod')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 \"kubectl patch configmap alertmanager-config -n monitoring --type merge -p ''{\"\"data\"\":{\"\"alertmanager.yml\"\":\"\"global:\\\\n resolve_timeout: 5m\\\\n\\\\nroute:\\\\n group_by: [\\\\\"\"alertname\\\\\"\", \\\\\"\"severity\\\\\"\"]\\\\n group_wait: 30s\\\\n group_interval: 5m\\\\n repeat_interval: 4h\\\\n receiver: \\\\\"\"awoooi-webhook\\\\\"\"\\\\n routes:\\\\n - match:\\\\n severity: critical\\\\n receiver: \\\\\"\"awoooi-webhook\\\\\"\"\\\\n group_wait: 10s\\\\n repeat_interval: 1h\\\\n - match:\\\\n severity: warning\\\\n receiver: \\\\\"\"awoooi-webhook\\\\\"\"\\\\n group_wait: 1m\\\\n repeat_interval: 4h\\\\n\\\\nreceivers:\\\\n - name: \\\\\"\"awoooi-webhook\\\\\"\"\\\\n webhook_configs:\\\\n - url: \\\\\"\"http://192.168.0.120:32334/api/v1/webhook/alertmanager\\\\\"\"\\\\n send_resolved: true\\\\n\\\\ninhibit_rules:\\\\n - source_match:\\\\n severity: \\\\\"\"critical\\\\\"\"\\\\n target_match:\\\\n severity: \\\\\"\"warning\\\\\"\"\\\\n equal: [\\\\\"\"alertname\\\\\"\", \\\\\"\"instance\\\\\"\"]\\\\n\"\"}}''\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 'kubectl rollout restart deployment/alertmanager -n monitoring && kubectl rollout status deployment/alertmanager -n monitoring')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 \"kubectl get configmap alertmanager-config -n monitoring -o jsonpath=''{.data.alertmanager\\\\.yml}'' | grep -A 3 ''url:''\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 'kubectl get pods -n awoooi-prod -o jsonpath=\"\"{range .items[*]}{.metadata.name}{\\\\\"\" \\\\\"\"}{.spec.containers[*].image}{\\\\\"\"\\\\\\\\n\\\\\"\"}{end}\"\"')",
|
||||
"Bash(git mv:*)",
|
||||
"Bash(for file:*)",
|
||||
"Bash(do echo:*)",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 wooo@192.168.0.120 \"echo ''Connected''\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 \"kubectl get deployment -n awoooi-prod -o jsonpath=''{range .items[*]}{.metadata.name}{\"\" selector: \"\"}{.spec.selector.matchLabels}{\"\"\\\\n\"\"}{end}''\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 \"kubectl delete deployment awoooi-api awoooi-web awoooi-worker -n awoooi-prod\")",
|
||||
"WebFetch(domain:awoooi.wooo.work)",
|
||||
"WebFetch(domain:api.awoooi.wooo.work)",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 'kubectl get pods -n awoooi-prod -o wide')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 'kubectl get svc,ingress -n awoooi-prod')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 'kubectl exec -n awoooi-prod deploy/awoooi-api -- curl -sf http://localhost:8000/api/v1/health 2>&1')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 'curl -sf http://10.43.125.201:8000/api/v1/health 2>&1 || echo \"\"FAILED\"\"')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 'sudo nginx -t 2>&1 && sudo cat /etc/nginx/sites-enabled/awoooi* 2>/dev/null || sudo cat /etc/nginx/conf.d/awoooi* 2>/dev/null || echo \"\"No awoooi nginx config found\"\"')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 'cat /etc/nginx/sites-enabled/* 2>/dev/null | grep -A5 awoooi || cat /etc/nginx/conf.d/* 2>/dev/null | grep -A5 awoooi || ls -la /etc/nginx/ 2>/dev/null || echo \"\"No nginx on this host\"\"')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.110 'ls /etc/nginx/sites-enabled/ 2>/dev/null && cat /etc/nginx/sites-enabled/*awoooi* 2>/dev/null || echo \"\"Checking conf.d...\"\" && ls /etc/nginx/conf.d/ 2>/dev/null')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.110 'grep -l awoooi /etc/nginx/sites-enabled/* 2>/dev/null || grep -r \"\"awoooi\"\" /etc/nginx/sites-enabled/ 2>/dev/null | head -20')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.110 'grep -r \"\"awoooi\\\\|32334\\\\|32335\"\" /etc/nginx/ 2>/dev/null | head -20')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 'echo \"\"0936223270\"\" | sudo -S cp /tmp/awoooi-prod.conf /etc/nginx/conf.d/ && echo \"\"Config copied\"\" && sudo nginx -t 2>&1')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 'echo \"\"0936223270\"\" | sudo -S ls -la /etc/nginx/ssl/ 2>/dev/null || echo \"\"No ssl dir\"\" && sudo ls -la /etc/letsencrypt/live/ 2>/dev/null | head -10')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 'echo \"\"0936223270\"\" | sudo -S sed -i \"\"s|/etc/nginx/ssl/awoooi.crt|/etc/letsencrypt/live/awoooi.wooo.work/fullchain.pem|g\"\" /etc/nginx/conf.d/awoooi-prod.conf && sudo sed -i \"\"s|/etc/nginx/ssl/awoooi.key|/etc/letsencrypt/live/awoooi.wooo.work/privkey.pem|g\"\" /etc/nginx/conf.d/awoooi-prod.conf && echo \"\"Paths fixed\"\" && sudo nginx -t 2>&1')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 'echo \"\"0936223270\"\" | sudo -S nginx -s reload && echo \"\"Nginx reloaded!\"\" && sleep 2')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 'grep -r \"\"awoooi\"\" /etc/nginx/sites-enabled/ 2>/dev/null | head -5')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 'echo \"\"0936223270\"\" | sudo -S grep -rl \"\"awoooi.wooo.work\"\" /etc/nginx/ 2>/dev/null')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 'curl -sf http://192.168.0.121:32334/api/v1/health 2>&1 || echo \"\"FAILED to reach 121\"\"')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 'echo \"\"0936223270\"\" | sudo -S rm /etc/nginx/conf.d/awoooi-prod.conf && sudo nginx -t && sudo nginx -s reload && echo \"\"Cleaned up duplicate config\"\"')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 'echo \"\"0936223270\"\" | sudo -S tail -30 /var/log/nginx/error.log 2>/dev/null')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 'grep -r \"\"api.awoooi\"\" /etc/nginx/ 2>/dev/null || echo \"\"No api.awoooi config found\"\"')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 'kubectl get configmap awoooi-config -n awoooi-prod -o yaml | grep -A5 NEXT_PUBLIC')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 'kubectl get deployment awoooi-web -n awoooi-prod -o yaml | grep -A20 \"\"env:\"\" | head -25')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 'echo \"\"0936223270\"\" | sudo -S tail -10 /var/log/nginx/access.log 2>/dev/null | grep awoooi')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 'echo \"\"0936223270\"\" | sudo -S tail -5 /var/log/nginx/error.log 2>/dev/null')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 'echo \"\"0936223270\"\" | sudo -S stat /etc/nginx/sites-available/awoooi.wooo.work.conf 2>/dev/null | grep -E \"\"Modify|Change|Birth\"\"')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 'kubectl logs -n awoooi-prod -l app=awoooi-web --tail=30 2>/dev/null | grep -i \"\"api\\\\|error\\\\|fetch\"\" | head -20')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 'echo \"\"0936223270\"\" | sudo -S tail -20 /var/log/nginx/access.log 2>/dev/null | grep -E \"\"awoooi.*api\"\"')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 'echo \"\"0936223270\"\" | sudo -S tail -20 /var/log/nginx/awoooi-prod-access.log 2>/dev/null')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 'kubectl exec -n awoooi-prod deploy/awoooi-web -- env | grep -i api')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 'kubectl exec -n awoooi-prod deploy/awoooi-web -- sh -c \"\"grep -r \\\\\"\"NEXT_PUBLIC_API_URL\\\\|api.awoooi\\\\\"\" /app/.next/static/chunks/*.js 2>/dev/null | head -5 || grep -r \\\\\"\"awoooi.wooo.work\\\\\"\" /app/.next/static/chunks/*.js 2>/dev/null | head -3\"\"')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 'kubectl exec -n awoooi-prod deploy/awoooi-web -- sh -c \"\"find /app/.next -name \\\\\"\"*.js\\\\\"\" -exec grep -l \\\\\"\"awoooi\\\\\"\" {} \\\\; 2>/dev/null | head -3\"\"')",
|
||||
"Bash(./scripts/qa-zero-touch.sh)",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 'echo \"\"0936223270\"\" | sudo -S cat /etc/nginx/sites-available/awoooi.wooo.work.conf')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 'echo \"\"0936223270\"\" | sudo -S cp /tmp/awoooi.wooo.work.conf /etc/nginx/sites-available/awoooi.wooo.work.conf && sudo nginx -t 2>&1')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 'echo \"\"0936223270\"\" | sudo -S nginx -s reload && echo \"\"✅ Nginx reloaded with load balancing!\"\"')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.110 'cd /opt && sudo ls -la sentry 2>/dev/null || echo \"\"Sentry 目錄不存在,需要建立\"\"')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.110 'sudo mkdir -p /opt/sentry && sudo chown wooo:wooo /opt/sentry && cd /opt/sentry && git clone https://github.com/getsentry/self-hosted.git . 2>&1 | tail -5')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.110 'echo \"\"0936223270\"\" | sudo -S mkdir -p /opt/sentry && echo \"\"0936223270\"\" | sudo -S chown wooo:wooo /opt/sentry && cd /opt/sentry && git clone https://github.com/getsentry/self-hosted.git . 2>&1 | tail -10')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.110 'cd /opt/sentry && ls -la 2>&1 | head -20')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.110 'cd /opt/sentry && git describe --tags 2>/dev/null || git rev-parse --short HEAD')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.110 'cd /opt/sentry && ./install.sh --help 2>&1 | head -30 || echo \"\"No help available, checking script...\"\"')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.110 'cd /opt/sentry && nohup ./install.sh --skip-user-creation --no-report-self-hosted-issues > /tmp/sentry-install.log 2>&1 &')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.110 'tail -30 /tmp/sentry-install.log 2>/dev/null || echo \"\"日誌檔案尚未建立,等待中...\"\"')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.110 'grep -E \"\"^\\\\▶|^Creating|^Starting|^Error|^✓|Pulling\"\" /tmp/sentry-install.log 2>/dev/null | tail -40')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.110 'echo \"\"=== 日誌行數 ===\"\" && wc -l /tmp/sentry-install.log && echo \"\"\"\" && echo \"\"=== 最近進度 ===\"\" && tail -10 /tmp/sentry-install.log')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.110 'echo \"\"=== 日誌行數 ===\"\" && wc -l /tmp/sentry-install.log && echo \"\"\"\" && echo \"\"=== 關鍵階段 ===\"\" && grep -E \"\"^▶|✓|Error|Creating|Starting\"\" /tmp/sentry-install.log | tail -20')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.110 'echo \"\"=== 日誌行數 ===\"\" && wc -l /tmp/sentry-install.log && echo \"\"\"\" && echo \"\"=== 最近 20 行 ===\"\" && tail -20 /tmp/sentry-install.log')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.110 'echo \"\"=== 日誌行數 ===\"\" && wc -l /tmp/sentry-install.log && echo \"\"\"\" && echo \"\"=== 關鍵階段 ===\"\" && grep -E \"\"^▶|✓|Error|Creating|Starting|Building|DONE\"\" /tmp/sentry-install.log | tail -30')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.110 'echo \"\"=== 日誌行數 ===\"\" && wc -l /tmp/sentry-install.log && echo \"\"\"\" && echo \"\"=== 最近關鍵階段 ===\"\" && grep -E \"\"^▶|✓|Error|Creating|Starting|DONE|Completed|success\"\" /tmp/sentry-install.log | tail -25')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.110 'grep -E \"\"^▶|✓|Error|Completed|success|fail\"\" /tmp/sentry-install.log | tail -15')",
|
||||
"Bash(redis-cli -h 192.168.0.188 -p 6380 KEYS incident:*)",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"cat /home/ollama/momo-pro/monitoring/alertmanager.yml 2>/dev/null || cat /etc/alertmanager/alertmanager.yml 2>/dev/null || echo ''Config not found''\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker logs clawbot --tail 30 2>&1\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker logs clawbot --tail 20 2>&1 | grep -iE ''telegram|send|alert|incident|error''\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"cat /home/ollama/clawbot-v5/.env | grep -E ''TELEGRAM|TG_'' | head -5\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"cat /home/ollama/clawbot-v5/.env | grep -E ''REDIS|POSTGRES|DATABASE'' | head -5\")",
|
||||
"Bash(ssh ollama@192.168.0.188 'curl -s \"\"http://localhost:9093/api/v2/alerts?active=true\"\" | python3 -c \"\"import sys,json; alerts=json.load\\(sys.stdin\\); print\\(f\\\\\"\"Active alerts: {len\\(alerts\\)}\\\\\"\"\\)\"\"')",
|
||||
"Bash(ssh ollama@192.168.0.188 'curl -s \"\"http://localhost:9093/api/v2/alerts\"\" | python3 -c \"\"import sys,json; alerts=json.load\\(sys.stdin\\); print\\(f\\\\\"\"Total alerts: {len\\(alerts\\)}\\\\\"\"\\); [print\\(a[\\\\\"\"labels\\\\\"\"][\\\\\"\"alertname\\\\\"\"]\\) for a in alerts[:5]]\"\"')",
|
||||
"Bash(ssh ollama@192.168.0.188 'redis-cli -p 6380 -n 0 GET incident:INC-20260324-36AF55 | python3 -c \"\"import sys,json; d=json.load\\(sys.stdin\\); print\\(f\\\\\"\"Status: {d.get\\(\\\\\"\"status\\\\\"\"\\)}\\\\\"\"\\); print\\(f\\\\\"\"message_id: {d.get\\(\\\\\"\"message_id\\\\\"\", \\\\\"\"NONE\\\\\"\"\\)}\\\\\"\"\\); print\\(f\\\\\"\"chat_id: {d.get\\(\\\\\"\"chat_id\\\\\"\", \\\\\"\"NONE\\\\\"\"\\)}\\\\\"\"\\)\"\"')",
|
||||
"Bash(ssh ollama@192.168.0.188 'redis-cli -p 6380 -n 0 GET incident:INC-20260324-36AF55 | python3 -c \"\"import sys,json; d=json.load\\(sys.stdin\\); print\\(f\\\\\"\"status: {d.get\\('status'\\)}\\\\\"\"\\); print\\(f\\\\\"\"message_id: {d.get\\('message_id'\\)}\\\\\"\"\\); print\\(f\\\\\"\"created_at: {d.get\\('created_at'\\)}\\\\\"\"\\)\"\"')",
|
||||
"Bash(redis-cli -h 192.168.0.188 -p 6380 -n 0 KEYS *approval*)",
|
||||
"Bash(redis-cli -h 192.168.0.188 -p 6380 -n 0 KEYS *incident*)",
|
||||
"Bash(redis-cli -h 192.168.0.188 -p 6380 -n 0 KEYS *pending*)",
|
||||
"Bash(redis-cli -h 192.168.0.188 -p 6380 -n 0 KEYS *)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/k3s-prod.yaml kubectl get pods -n awoooi-prod -o wide)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/k3s-prod.yaml kubectl get deployment awoooi-api -n awoooi-prod -o jsonpath='{.spec.template.spec.containers[0].image}')",
|
||||
"Bash(kubectl --kubeconfig=/Users/ogt/awoooi/k3s-prod.yaml get deployment awoooi-api -n awoooi-prod -o jsonpath='{.spec.template.spec.containers[0].image}')",
|
||||
"Bash(python3 -c \":*)",
|
||||
"Bash(/tmp/awoooi-tg-secret.yaml:*)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/k3s-prod.yaml kubectl apply -f /tmp/awoooi-tg-secret.yaml)",
|
||||
"Bash(for pod:*)",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.188 \"curl -fsSL https://ollama.com/install.sh | sh\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no -o PreferredAuthentications=password wooo@192.168.0.188 \"echo connected && ollama --version\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no -o PreferredAuthentications=password ollama@192.168.0.188 \"curl -fsSL https://ollama.com/install.sh | sh\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"echo ''0936223270'' | sudo -S curl -fsSL https://ollama.com/install.sh | sudo -S sh\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"ollama --version\")",
|
||||
"Bash(__NEW_LINE_95e9df111552805b__ echo:*)",
|
||||
"Bash(sshpass -p '0936223270' scp /Users/ogt/awoooi/k8s/nginx/awoooi-prod.conf ollama@192.168.0.188:/tmp/awoooi-prod.conf)",
|
||||
"Bash(sshpass -p '0936223270' ssh ollama@192.168.0.188 \"echo ''0936223270'' | sudo -S cp /tmp/awoooi-prod.conf /etc/nginx/conf.d/awoooi-prod.conf && echo ''0936223270'' | sudo -S nginx -t 2>&1\")",
|
||||
"Bash(sshpass -p '0936223270' ssh ollama@192.168.0.188 \"echo ''0936223270'' | sudo -S ls -la /etc/nginx/ssl/ 2>/dev/null || echo ''No ssl dir''; echo ''0936223270'' | sudo -S ls -la /etc/nginx/conf.d/ 2>/dev/null | head -10\")",
|
||||
"Bash(sshpass -p '0936223270' ssh ollama@192.168.0.188 \"echo ''0936223270'' | sudo -S grep -r ''ssl_certificate'' /etc/nginx/ 2>/dev/null | head -5\")",
|
||||
"Bash(sshpass -p '0936223270' ssh ollama@192.168.0.188 \"echo ''0936223270'' | sudo -S grep -A 20 ''server_name awoooi'' /etc/nginx/sites-enabled/all-sites.conf 2>/dev/null | head -30\")",
|
||||
"Bash(sshpass -p '0936223270' ssh ollama@192.168.0.188 \"echo ''0936223270'' | sudo -S ls -la /etc/nginx/sites-enabled/ 2>/dev/null\")",
|
||||
"Bash(sshpass -p '0936223270' ssh ollama@192.168.0.188 \"echo ''0936223270'' | sudo -S cat /etc/nginx/sites-available/awoooi.wooo.work.conf 2>/dev/null\")",
|
||||
"Bash(sshpass -p '0936223270' ssh ollama@192.168.0.188 \"echo ''0936223270'' | sudo -S rm /etc/nginx/conf.d/awoooi-prod.conf && echo ''0936223270'' | sudo -S nginx -t 2>&1\")",
|
||||
"Bash(sshpass -p '0936223270' ssh ollama@192.168.0.188 \"echo ''0936223270'' | sudo -S nginx -s reload 2>&1\")",
|
||||
"Bash(sshpass -p '0936223270' ssh ollama@192.168.0.188 \"echo ''0936223270'' | sudo -S systemctl reload nginx 2>&1\")",
|
||||
"Bash(sshpass -p '0936223270' ssh ollama@192.168.0.188 \"docker logs openclaw 2>&1 | tail -30\")",
|
||||
"Bash(sshpass -p '0936223270' ssh ollama@192.168.0.188 \"docker ps -a --format ''table {{.Names}}\\\\t{{.Status}}\\\\t{{.Image}}'' 2>&1 | head -15\")",
|
||||
"Bash(sshpass -p '0936223270' ssh ollama@192.168.0.188 \"docker logs clawbot 2>&1 | grep -i telegram | tail -20\")",
|
||||
"Bash(sshpass -p '0936223270' ssh ollama@192.168.0.188 \"docker logs clawbot 2>&1 | tail -30\")",
|
||||
"Bash(sshpass -p '0936223270' ssh ollama@192.168.0.188 \"docker exec alertmanager cat /etc/alertmanager/alertmanager.yml 2>&1 | head -30\")",
|
||||
"Bash(sshpass -p '0936223270' ssh ollama@192.168.0.188 \"curl -sf ''http://localhost:9093/api/v1/alerts'' | jq ''.data | length'' 2>/dev/null || curl -sf ''http://localhost:9093/api/v2/alerts'' | jq ''length'' 2>/dev/null\")",
|
||||
"Bash(sshpass -p '0936223270' ssh ollama@192.168.0.188 \"docker exec alertmanager wget -qO- ''http://localhost:9093/api/v2/alerts'' 2>&1 | head -100\")",
|
||||
"Bash(sshpass -p '0936223270' ssh ollama@192.168.0.188 \"KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n awoooi-prod logs -l app=awoooi-worker --tail=50 2>&1\")",
|
||||
"Bash(sshpass -p '0936223270' ssh ollama@192.168.0.188 \"cat /home/ollama/alertmanager/alertmanager.yml 2>/dev/null || docker exec alertmanager cat /etc/alertmanager/alertmanager.yml\")",
|
||||
"Bash(sshpass -p '0936223270' ssh ollama@192.168.0.188 \"docker cp /tmp/alertmanager.yml alertmanager:/etc/alertmanager/alertmanager.yml && docker exec alertmanager amtool check-config /etc/alertmanager/alertmanager.yml && docker kill -s SIGHUP alertmanager\")",
|
||||
"Bash(sshpass -p '0936223270' ssh ollama@192.168.0.188 \"docker inspect alertmanager --format ''{{range .Mounts}}{{.Source}} -> {{.Destination}}{{println}}{{end}}''\")",
|
||||
"Bash(sshpass -p '0936223270' ssh ollama@192.168.0.188 \"docker exec alertmanager cat /etc/alertmanager/alertmanager.yml\")",
|
||||
"Bash(sshpass -p '0936223270' ssh ollama@192.168.0.188 \"docker restart alertmanager && sleep 3 && docker exec alertmanager cat /etc/alertmanager/alertmanager.yml\")",
|
||||
"Bash(sshpass -p '0936223270' ssh ollama@192.168.0.188 \"docker logs clawbot 2>&1 | grep -i ''telegram\\\\|webhook\\\\|alert'' | tail -10\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl logs deployment/awoooi-api -n awoooi-prod --tail=30 2>/dev/null | grep -E ''''POST|webhook|alertmanager|ManualTest''''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl logs deployment/awoooi-api -n awoooi-prod --tail=30 2>/dev/null | grep -iE ''''POST|webhook''''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl logs deployment/awoooi-api -n awoooi-prod --tail=50 2>/dev/null | grep -iE ''''POST.*webhook|alertmanager_webhook|NewFingerprint''''\")",
|
||||
"Bash(kustomize build:*)",
|
||||
"Bash(KUBECONFIG=~/.kube/config kubectl get secret awoooi-secrets -n awoooi-prod -o jsonpath='{.data}')",
|
||||
"Bash(KUBECONFIG=/Users/ogt/.kube/config kubectl exec deploy/awoooi-api -n awoooi-prod -- env)",
|
||||
"Bash(git checkout:*)",
|
||||
"Bash(jq -r '.status // \"\"\"\"failed\"\"\"\"')",
|
||||
"Bash(jq -r '.total // \"\"\"\"error\"\"\"\"')",
|
||||
"Bash(redis-cli -h 192.168.0.188 -p 6380 -n 10 XLEN awoooi:signals)",
|
||||
"Bash(redis-cli -h 192.168.0.188 -p 6380 -n 10 XRANGE awoooi:signals - + COUNT 5)",
|
||||
"Bash(SENTRY_TOKEN=\"2b73050606d2b32f54095b4e177f4842f2bfe69d4b17da25f6daa4739148a972\" curl -s \"http://192.168.0.110:9000/api/0/organizations/\" -H \"Authorization: Bearer $SENTRY_TOKEN\")",
|
||||
"Bash(SENTRY_TOKEN=\"2b73050606d2b32f54095b4e177f4842f2bfe69d4b17da25f6daa4739148a972\" curl -s \"http://192.168.0.110:9000/api/0/organizations/sentry/projects/\" -H \"Authorization: Bearer $SENTRY_TOKEN\")",
|
||||
"Bash(SENTRY_TOKEN=\"2b73050606d2b32f54095b4e177f4842f2bfe69d4b17da25f6daa4739148a972\" curl -s \"http://192.168.0.110:9000/api/0/projects/sentry/awoooi-api/rules/\" -H \"Authorization: Bearer $SENTRY_TOKEN\")",
|
||||
"Bash(SENTRY_TOKEN=\"2b73050606d2b32f54095b4e177f4842f2bfe69d4b17da25f6daa4739148a972\" __NEW_LINE_583db0bbb6875db0__ echo \"=== Alert Rules ===\" curl -s \"http://192.168.0.110:9000/api/0/projects/sentry/awoooi-api/rules/\" -H \"Authorization: Bearer $SENTRY_TOKEN\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get nodes -o wide && echo ''---'' && kubectl top nodes 2>/dev/null || echo ''metrics-server not installed''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get pods -n awoooi-prod -o wide && echo ''---'' && kubectl get pvc -n awoooi-prod 2>/dev/null && echo ''---'' && kubectl get sc 2>/dev/null && echo ''---'' && kubectl get deploy -n awoooi-prod\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get ns && echo ''---'' && kubectl get svc -A | grep -E ''prometheus|grafana|metrics|signoz|longhorn|argocd'' || echo ''No monitoring/gitops services found''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"cat /etc/rancher/k3s/config.yaml 2>/dev/null || echo ''--- K3s default config \\(no custom config.yaml\\) ---'' && echo ''---'' && sudo k3s check-config 2>/dev/null | head -30 || echo ''check-config not available''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"free -h && echo ''---'' && swapon --show && echo ''---'' && df -h /var/lib/rancher/k3s\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get pods -n cnpg-system && echo ''---'' && kubectl get svc -n monitoring\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get all -n awoooi-prod -o wide 2>/dev/null && echo ''---QUOTA---'' && kubectl describe quota -n awoooi-prod 2>/dev/null && echo ''---EVENTS---'' && kubectl get events -n awoooi-prod --sort-by=''.lastTimestamp'' 2>/dev/null | tail -20\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get helmcharts -A 2>/dev/null || echo ''No HelmCharts'' && echo ''---'' && kubectl get helmreleases -A 2>/dev/null || echo ''No HelmReleases'' && echo ''---'' && kubectl api-resources | grep -E ''argo|flux|velero|longhorn'' || echo ''No GitOps/Backup CRDs''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get ds -A && echo ''---'' && kubectl get cm -n kube-system | grep -E ''traefik|coredns'' && echo ''---REGISTRIES---'' && sudo cat /etc/rancher/k3s/registries.yaml 2>/dev/null || echo ''No registries.yaml''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get ingress -A 2>/dev/null || echo ''No Ingress'' && echo ''---HPA---'' && kubectl get hpa -A 2>/dev/null || echo ''No HPA'' && echo ''---PDB---'' && kubectl get pdb -A 2>/dev/null || echo ''No PDB'' && echo ''---SYSCTL---'' && cat /proc/sys/net/core/somaxconn && cat /proc/sys/fs/file-max\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"systemctl status k3s | head -20 && echo ''---K3S-VERSION---'' && k3s --version && echo ''---ETCD-STATUS---'' && sudo k3s etcd-snapshot list 2>/dev/null | head -5 || echo ''No etcd snapshots''\")",
|
||||
"Bash(ssh wooo@192.168.0.121 \"free -h && swapon --show && echo ''---DISK---'' && df -h /var/lib/rancher/k3s 2>/dev/null\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"sudo ls -la /var/lib/rancher/k3s/server/db/ 2>/dev/null && echo ''---TOKEN---'' && sudo cat /var/lib/rancher/k3s/server/token 2>/dev/null | head -1 | cut -c1-20\")",
|
||||
"Bash(ssh -o ConnectTimeout=10 wooo@192.168.0.120 \"ps aux | grep k3s | grep -v grep | head -3 && echo ''---'' && sudo cat /etc/systemd/system/k3s.service 2>/dev/null | grep -E ''ExecStart|datastore''\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"echo ''0936223270'' | sudo -S mkdir -p /backup/k3s_etcd 2>/dev/null && echo ''0936223270'' | sudo -S chown ollama:ollama /backup/k3s_etcd 2>/dev/null && echo ''=== 188 備份目錄 ==='' && ls -la /backup/\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"mkdir -p ~/.ssh && chmod 700 ~/.ssh && echo ''ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCnTnbjtSPwrI/pN6DByDxsFDOR4+sVnk7hb+eOr+Pb4e7o7QGbyKaJC2eKP7uRBilPqeScuvNKZhwmY8ZOuhjId+ZyLK0jZXHdq3a6tjsQ4MwPGyT2aMaD7x2jKzPbFojR0P5lmQWH2zjxeVuB7UeBIejaYk3gQEMFVES8Xh84yxFvy9jlwKmZFAI0gIhx0nPOTPB7onTyb8L5snUbwQQntoHWYFbb83+wui/kM15aLT5r8uvS2yZdsWWrDvAyuIShde1ceTBevwwqxezH1egXGoGkvZYYF7vHFu3X6jF7Nfp4qVfo0EfFV3omy90HzoFvoEXCC+jIWU0TjUqdEgGIEj2b+YXw3bIs+k+g/0/iJzA5LLUNb2vHVHoUmah4ZNlfiGU7e6hTYXjLjoXJlz9gfv6LYywhgktdThi9sUCn6rzbatlMrY0HNUE6uOwRTugMq1YUEJCvRqeFmtX5yF6xGp+FbOjIr1kMmplbRQRqKIrpQoqEn0+UBXC7OwJNCk8= wooo@mon'' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys && echo ''SSH key 已加入''\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"echo ''ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCs3nQ11B+V/VEchNR9Uzj57JoKXOJ8S1UVjCTHkUDL8FnrbdPFr0zvpYgX0a/Ipj9wHkqU6z6Ho6MQj3X2+HaK5fC0fZ3aZE1QT2df/x0xXdyka9XSaTFaymKzNTvfmum40koBkNccKyO5SLSjTcoTZCDHP4RqHHu/MYjQMejG7yeyCFmgumrHh5T/0DXPf5zl0Ff1C5U3VCLPxz5vq63JB2dTfrjQLg3sO0ZI3KTZE8aFj3txKz5snDZX3nE1tHZMKLecwwEqi130BtVZcm8zXDqX83gtUDp/WLfPyKCmzZzGf6YgEofIsyrVup8XnD9xNoFmbEeBdFocGWeoIVIn+faOpU22fvQ34L57GHhNQwygZOPKsZa9XNKjayKdKQl3gcAA2wnkZgN0cyIEYvTd3O+Z5Xvff2dat+0sDMK571V+0JEdAMOpQjFO7DkwjKHn/gHLmvRjYLiUOItX9JysFgYuHs8omad2LmeUIkQrBD2I2hyvY49HaJKWctk4Jm0= root@mon'' >> ~/.ssh/authorized_keys && echo ''Root SSH key added''\")",
|
||||
"Bash(grep -r \"\"\"zod\"\"\" /Users/ogt/awoooi/package.json /Users/ogt/awoooi/apps/*/package.json /Users/ogt/awoooi/packages/*/package.json)",
|
||||
"Bash(__NEW_LINE_144503b060dfd3dd__ echo:*)",
|
||||
"Bash(__NEW_LINE_ae2a22b14586d7aa__ echo:*)",
|
||||
"Bash(__NEW_LINE_e17561a4e55f74d4__ echo:*)",
|
||||
"Bash(ssh wooo@192.168.0.120 \"echo ''''0936223270'''' | sudo -S cat /etc/rancher/k3s/k3s.yaml 2>/dev/null | sed ''''s|https://127.0.0.1:6443|https://192.168.0.125:6443|g''''\")",
|
||||
"Bash(KUBECONFIG=/tmp/kubeconfig-vip.yaml kubectl get nodes)",
|
||||
"Bash(kubectl --kubeconfig=/tmp/kubeconfig-vip.yaml get rs -n awoooi-prod)",
|
||||
"Bash(kubectl --kubeconfig=/tmp/kubeconfig-vip.yaml get pods -A --no-headers)",
|
||||
"Bash(kubectl --kubeconfig=/tmp/kubeconfig-vip.yaml get jobs -A --no-headers)",
|
||||
"Bash(kubectl --kubeconfig=/tmp/kubeconfig-vip.yaml get rs -n awoooi-prod --no-headers)",
|
||||
"Bash(kubectl --kubeconfig=/tmp/kubeconfig-vip.yaml delete job api-watchdog-29556380 -n wooo-aiops-uat)",
|
||||
"Bash(kubectl --kubeconfig=/tmp/kubeconfig-vip.yaml get pods -n awoooi-prod)",
|
||||
"Bash(kubectl --kubeconfig=/tmp/kubeconfig-vip.yaml get pods -A)",
|
||||
"Bash(kubectl --kubeconfig=/tmp/kubeconfig-vip.yaml get svc -A)",
|
||||
"Bash(PGPASSWORD=changeme psql -h 192.168.0.188 -U awoooi -d awoooi_prod -f /Users/ogt/awoooi/apps/api/scripts/migrate_phase18_audit_logs.sql)",
|
||||
"Bash(PLAYWRIGHT_BASE_URL=http://192.168.0.125:32335 npx playwright test phase11-conversational.spec.ts --reporter=list)",
|
||||
"Bash(PLAYWRIGHT_BASE_URL=http://192.168.0.125:32335 npx playwright test phase11-conversational.spec.ts --reporter=list --workers=1)",
|
||||
"Bash(KUBECONFIG=~/.kube/config kubectl get nodes --server=https://192.168.0.125:6443 --insecure-skip-tls-verify)",
|
||||
"Bash(source .venv/bin/activate)",
|
||||
"Read(//etc/postgresql/14/main/**)",
|
||||
"Bash(for port:*)",
|
||||
"Bash(kubectl top:*)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl top pods -n awoooi-prod)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl get pods -n awoooi-prod -o wide)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl get svc -n awoooi-prod)",
|
||||
"Bash(jq -r '.components | to_entries[] | \"\"\"\"\\\\\\(.key\\): \\\\\\(.value.status\\)\"\"\"\"')",
|
||||
"Bash(tar -xzf velero-v1.13.0-darwin-arm64.tar.gz)",
|
||||
"Bash(sudo mv:*)",
|
||||
"Bash(velero version:*)",
|
||||
"Bash(mkdir -p ~/bin)",
|
||||
"Bash(mv velero-v1.13.0-darwin-arm64/velero ~/bin/)",
|
||||
"Bash(~/bin/velero version:*)",
|
||||
"Bash(k8s/velero/00-namespace.yaml:*)",
|
||||
"Bash(k8s/velero/01-credentials.yaml:*)",
|
||||
"Bash(k8s/velero/02-velero-install.yaml:*)",
|
||||
"Bash(tar -xzf velero.tar.gz)",
|
||||
"Bash(/tmp/velero-credentials:*)",
|
||||
"Bash(__NEW_LINE_e85d95513fc16492__ ~/bin/velero install --provider aws --plugins velero/velero-plugin-for-aws:v1.9.0 --bucket velero-backups --secret-file /tmp/velero-credentials --backup-location-config region=minio,s3ForcePathStyle=true,s3Url=http://192.168.0.188:9000 --use-volume-snapshots=false --dry-run -o yaml)",
|
||||
"Bash(__NEW_LINE_e85d95513fc16492__ head:*)",
|
||||
"Bash(k8s/velero/README.md:*)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/.kube/config kubectl apply -f /Users/ogt/awoooi/k8s/velero/velero-install-full.yaml)",
|
||||
"Bash(sshpass -p '09362233270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 \"whoami && hostname && cat /etc/sudoers.d/* 2>/dev/null | head -5 || echo ''no sudoers.d files''\")",
|
||||
"Bash(sshpass -p '09362233270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 \"kubectl get nodes 2>&1 || echo ''kubectl failed, checking k3s kubeconfig...'' && ls -la /etc/rancher/k3s/k3s.yaml 2>&1\")",
|
||||
"Bash(sshpass -p '09362233270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 \"sudo -l 2>&1 | head -20\")",
|
||||
"Bash(sshpass -p '09362233270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 \"echo ''09362233270'' | sudo -S -l 2>&1\")",
|
||||
"Bash(sshpass -p '09362233270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 \"echo ''0936223270'' | sudo -S kubectl get nodes 2>&1\")",
|
||||
"Bash(sshpass -p '0936223270' scp /Users/ogt/awoooi/k8s/velero/velero-install-full.yaml wooo@192.168.0.120:/tmp/velero-install-full.yaml)",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 \"echo ''''0936223270'''' | sudo -S kubectl apply -f /tmp/velero-install-full.yaml 2>&1\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 \"echo ''0936223270'' | sudo -S kubectl get pods -n velero 2>&1\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 \"echo ''0936223270'' | sudo -S kubectl get backupstoragelocation -n velero 2>&1\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 \"echo ''0936223270'' | sudo -S kubectl logs -n velero deploy/velero --tail=30 2>&1\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 \"echo ''0936223270'' | sudo -S kubectl logs -n velero deploy/velero --tail=10 2>&1\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 \"echo ''0936223270'' | sudo -S kubectl get secret cloud-credentials -n velero -o jsonpath=''{.data.cloud}'' 2>&1 | base64 -d\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 \"echo ''0936223270'' | sudo -S curl -s http://192.168.0.188:9000/velero-backups/ 2>&1\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 \"echo ''0936223270'' | sudo -S kubectl rollout restart deployment/velero -n velero 2>&1\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 \"echo ''0936223270'' | sudo -S kubectl get backups -n velero 2>&1\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 \"echo ''0936223270'' | sudo -S kubectl describe backup test-backup-20260328-2114 -n velero 2>&1 | tail -30\")",
|
||||
"Bash(sshpass -p:*)",
|
||||
"Read(//Users/ogt/awoooi/=== 測試 /approvals/**)",
|
||||
"Bash(kubectl --kubeconfig=/Users/ogt/.kube/config get svc -n velero -o wide)",
|
||||
"Bash(kubectl --kubeconfig=/Users/ogt/.kube/config get pods -n velero -o wide)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/.kube/config kubectl get svc -n velero)",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 'echo \"\"0936223270\"\" | sudo -S sh -c \"\"kubectl get pods -A | grep -E \\\\\"\"kube-state|state-metrics\\\\\"\"\"\"')",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 'echo \"\"0936223270\"\" | sudo -S sh -c \"\"kubectl get ns | grep -E \\\\\"\"wooo|aiops|legacy|old\\\\\"\"\"\"')",
|
||||
"Bash(KUBECONFIG=~/.kube/config kubectl get ns --no-headers)",
|
||||
"WebFetch(domain:build.nvidia.com)",
|
||||
"WebFetch(domain:ollama.com)",
|
||||
"WebFetch(domain:docs.api.nvidia.com)",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"curl -s ''http://admin:admin@localhost:3002/api/search?type=dash-db'' | python3 -c \"\"import sys,json; d=json.load\\(sys.stdin\\); print\\(f''Dashboard 數量: {len\\(d\\)}''\\); [print\\(f\\\\\"\" - {i[''title'']}\\\\\"\"\\) for i in d[:10]]\"\"\")",
|
||||
"Bash(jq '.ai_provider // .data.ai_provider // \"\"\"\"not found\"\"\"\"')",
|
||||
"Bash(KUBECONFIG=~/.kube/config kubectl logs -n awoooi-prod deployment/awoooi-api --tail=50)",
|
||||
"Bash(export NVIDIA_API_KEY=\"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\")",
|
||||
"Bash(curl -s -X POST \"https://integrate.api.nvidia.com/v1/chat/completions\" -H \"Content-Type: application/json\" -H \"Authorization: Bearer $NVIDIA_API_KEY\" -d '{:*)",
|
||||
"Bash(/tmp/fix-network-policy.yaml:*)",
|
||||
"Bash(__NEW_LINE_acde7a92ceae01f6__ scp:*)",
|
||||
"Bash(curl -s -X POST https://awoooi.wooo.work/api/v1/webhooks/alertmanager -H 'Content-Type: application/json' -d '{:*)",
|
||||
"Bash(ssh ollama@192.168.0.188 'curl -s \"\"http://localhost:9090/api/v1/targets\"\" 2>/dev/null | grep -o \"\"\\\\\"\"health\\\\\"\":\\\\\"\"[^\\\\\"\"]*\\\\\"\"\"\" | sort | uniq -c')",
|
||||
"Bash(ssh ollama@192.168.0.188 'curl -s \"\"http://localhost:9090/api/v1/rules\"\" 2>/dev/null | grep -o \"\"\\\\\"\"name\\\\\"\":\\\\\"\"[^\\\\\"\"]*\\\\\"\"\"\" | sort | uniq')",
|
||||
"Bash(ssh ollama@192.168.0.188 'curl -s \"\"http://localhost:9090/api/v1/targets\"\" 2>/dev/null | grep -o \"\"\\\\\"\"job\\\\\"\":\\\\\"\"[^\\\\\"\"]*\\\\\"\"\"\" | sort | uniq -c | sort -rn')",
|
||||
"Bash(ssh ollama@192.168.0.188 'curl -s \"\"http://localhost:9090/api/v1/query?query=up\"\" 2>/dev/null | grep -o \"\"\\\\\"\"instance\\\\\"\":\\\\\"\"[^\\\\\"\"]*\\\\\"\"\"\" | sort | uniq')",
|
||||
"Bash(for i:*)",
|
||||
"Bash(do sleep:*)",
|
||||
"Bash(kubectl patch:*)",
|
||||
"Bash(ssh wooo@192.168.0.110 \"cat /tmp/runner_clean.log 2>/dev/null; echo ''---''; ps aux | grep ''Runner.Listener'' | grep -v grep | wc -l\")",
|
||||
"Bash(KUBECONFIG=~/.kube/config kubectl logs -n awoooi-prod -l app=awoooi-api --tail=200)",
|
||||
"Bash(/Users/ogt/awoooi/ops/monitoring/deploy-exporters.sh:*)",
|
||||
"WebFetch(domain:github.com)",
|
||||
"WebFetch(domain:docs.ollama.com)",
|
||||
"Skill(telegram:configure)",
|
||||
"Skill(telegram:configure:*)",
|
||||
"Bash(USE_NEW_ENGINE=true pytest tests/test_incident*.py -v --tb=short -x)",
|
||||
"Bash(USE_NEW_ENGINE=true pytest tests/test_approval_field_alignment.py tests/test_learning_service.py -v --tb=short)",
|
||||
"Bash(/tmp/debug_approval.py:*)",
|
||||
"Bash(/tmp/debug_approval2.py:*)",
|
||||
"Bash(/tmp/bulk_sign.sh:*)",
|
||||
"Bash(bash /tmp/bulk_sign.sh)",
|
||||
"Bash(/tmp/check_deploy.py:*)",
|
||||
"Bash(/tmp/check_buttons.py:*)",
|
||||
"Bash(ssh ollama@192.168.0.188 \"docker logs openclaw --since=10s 2>&1 | grep -Ev ''\\(GET|POST\\) /health'' | tail -10 && echo ''---'' && docker exec openclaw env | grep OPENAI_API_KEY | cut -c1-30\")",
|
||||
"Read(//Users/ogt/awoooi/https:/awoooi.wooo.work/_next/static/chunks/app/%5Blocale%5D/**)",
|
||||
"Bash(find /Users/ogt/awoooi/apps/web -type f \\\\\\(-name *.spec.ts -o -name *.spec.tsx \\\\\\))",
|
||||
"Bash(kubectl -n awoooi-prod get pods)",
|
||||
"Bash(kubectl -n production get pods)",
|
||||
"Bash(ssh -o StrictHostKeyChecking=no wooo@192.168.0.121 \"export KUBECONFIG=/etc/rancher/k3s/k3s.yaml && sudo kubectl get deployment awoooi-web -n awoooi-prod -o jsonpath=''{.spec.template.spec.containers[0].image}'' && echo '''' && sudo kubectl get pods -n awoooi-prod -l app=awoooi-web --no-headers\")",
|
||||
"Bash(KUBECONFIG=/Users/ogt/.kube/config kubectl get pods -n awoooi-prod)",
|
||||
"Bash(for run_id in 166 165)",
|
||||
"mcp__plugin_playwright_playwright__browser_navigate",
|
||||
"mcp__plugin_playwright_playwright__browser_take_screenshot",
|
||||
"Bash(open \"http://192.168.0.110:3001/wooo/awoooi/actions\")",
|
||||
"Bash(TOKEN=\"2fa33d4e6d8ef1806c18875ed6fec216c8a10e78\" curl -s \"http://192.168.0.110:3001/api/v1/repos/wooo/awoooi/actions/runs?limit=5\" -H \"Authorization: token $TOKEN\")",
|
||||
"Bash(TOKEN=\"2fa33d4e6d8ef1806c18875ed6fec216c8a10e78\" curl -s \"http://192.168.0.110:3001/api/v1/repos/wooo/awoooi/actions/runs/166/jobs\" -H \"Authorization: token $TOKEN\")",
|
||||
"Bash(TOKEN=\"2fa33d4e6d8ef1806c18875ed6fec216c8a10e78\" curl -s \"http://192.168.0.110:3001/api/v1/repos/wooo/awoooi/actions/runs?limit=10\" -H \"Authorization: token $TOKEN\")",
|
||||
"Bash(TOKEN=\"2fa33d4e6d8ef1806c18875ed6fec216c8a10e78\" curl -s \"http://192.168.0.110:3001/api/v1/repos/wooo/awoooi/actions/runners\" -H \"Authorization: token $TOKEN\")",
|
||||
"Bash(TOKEN=\"2fa33d4e6d8ef1806c18875ed6fec216c8a10e78\" curl -s \"http://192.168.0.110:3001/api/v1/admin/runners\" -H \"Authorization: token $TOKEN\")",
|
||||
"Bash(TOKEN=\"2fa33d4e6d8ef1806c18875ed6fec216c8a10e78\")",
|
||||
"Bash(TOKEN=\"2fa33d4e6d8ef1806c18875ed6fec216c8a10e78\" curl -s \"http://192.168.0.110:3001/api/v1/repos/wooo/awoooi/actions/runs?limit=3\" -H \"Authorization: token $TOKEN\")",
|
||||
"Bash(TOKEN=\"2fa33d4e6d8ef1806c18875ed6fec216c8a10e78\" curl -s \"http://192.168.0.110:3001/api/v1/repos/wooo/awoooi/actions/runs/169/jobs\" -H \"Authorization: token $TOKEN\")",
|
||||
"Bash(TOKEN=\"2fa33d4e6d8ef1806c18875ed6fec216c8a10e78\" curl -s \"http://192.168.0.110:3001/api/v1/repos/wooo/awoooi/actions/jobs/179/logs\" -H \"Authorization: token $TOKEN\")",
|
||||
"Bash(TOKEN=\"2fa33d4e6d8ef1806c18875ed6fec216c8a10e78\" JOB_ID=180 curl -s \"http://192.168.0.110:3001/api/v1/repos/wooo/awoooi/actions/jobs/$JOB_ID/logs\" -H \"Authorization: token $TOKEN\")",
|
||||
"Bash(TOKEN=\"2fa33d4e6d8ef1806c18875ed6fec216c8a10e78\" curl -s \"http://192.168.0.110:3001/api/v1/repos/wooo/awoooi/actions/runs?limit=2\" -H \"Authorization: token $TOKEN\")",
|
||||
"Bash(TOKEN=\"2fa33d4e6d8ef1806c18875ed6fec216c8a10e78\" JOB_ID=181 curl -s \"http://192.168.0.110:3001/api/v1/repos/wooo/awoooi/actions/jobs/$JOB_ID/logs\" -H \"Authorization: token $TOKEN\")",
|
||||
"Bash(TOKEN=\"2fa33d4e6d8ef1806c18875ed6fec216c8a10e78\" curl -s \"http://192.168.0.110:3001/api/v1/repos/wooo/awoooi/actions/runs/172/jobs\" -H \"Authorization: token $TOKEN\")",
|
||||
"Bash(TOKEN=\"2fa33d4e6d8ef1806c18875ed6fec216c8a10e78\" curl -s \"http://192.168.0.110:3001/api/v1/repos/wooo/awoooi/actions/jobs/182/logs\" -H \"Authorization: token $TOKEN\")",
|
||||
"Bash(TOKEN=\"2fa33d4e6d8ef1806c18875ed6fec216c8a10e78\" curl -s \"http://192.168.0.110:3001/api/v1/repos/wooo/awoooi/actions/runs/178\" -H \"Authorization: token $TOKEN\")",
|
||||
"mcp__plugin_playwright_playwright__browser_snapshot",
|
||||
"mcp__plugin_playwright_playwright__browser_fill_form",
|
||||
"mcp__plugin_playwright_playwright__browser_click",
|
||||
"Bash(GITEA_TOKEN=\"e6c9fecb1f0148939493ae0fa30407d28c91279d\" curl -s \"http://192.168.0.110:3001/api/v1/repos/wooo/awoooi/actions/runs?limit=5\" -H \"Authorization: token $GITEA_TOKEN\")",
|
||||
"Bash(/Users/ogt/.pyenv/versions/3.11.7/bin/python3 /tmp/a4_smoke.py)",
|
||||
"Bash(/Users/ogt/.pyenv/versions/3.11.7/bin/python3 -c \"from src.repositories.aider_event_repository import AiderEventRepository; print\\('import OK'\\)\")",
|
||||
"Bash(/Users/ogt/.pyenv/versions/3.11.7/bin/python3 -m pytest tests/test_aider_event_service.py -v)",
|
||||
"Bash(/Users/ogt/.pyenv/versions/3.11.7/bin/python3 -m pytest tests/test_aider_event_service.py -v --tb=short)",
|
||||
"Bash(/Users/ogt/.pyenv/versions/3.11.7/bin/python3 -c \"from src.services.aider_event_service import classify_severity, should_create_incident, build_signal_data; print\\('✓ All imports successful'\\)\")",
|
||||
"Bash(/Users/ogt/.pyenv/versions/3.11.7/bin/python3 -m pytest tests/test_aider_event_service.py::test_build_signal_data_redacts_secrets_in_annotations -v)",
|
||||
"Bash(/Users/ogt/.pyenv/versions/3.11.7/bin/python3 -m pytest tests/test_aider_events_api.py -v)",
|
||||
"Bash(/Users/ogt/.pyenv/versions/3.11.7/bin/python3 -m pytest tests/test_aider_event_service.py tests/test_aider_events_api.py tests/test_aider_event_models.py tests/test_secret_redactor.py -v)",
|
||||
"Bash(/Users/ogt/.pyenv/versions/3.11.7/bin/python3 -m pytest tests/test_aider_event_processor.py -v)",
|
||||
"Bash(/Users/ogt/.pyenv/versions/3.11.7/bin/python3 -m pytest tests/test_aider_event_processor.py tests/test_aider_event_service.py tests/test_aider_events_api.py tests/test_aider_event_models.py tests/test_secret_redactor.py -v)",
|
||||
"Bash(/Users/ogt/.pyenv/versions/3.11.7/bin/python3 -c \"from src.workers.aider_event_processor import AiderEventProcessor, get_aider_event_processor, run_aider_event_processor_loop; print\\('✓ All imports successful'\\)\")",
|
||||
"Bash(/Users/ogt/.pyenv/versions/3.11.7/bin/python3 -m pytest tests/test_aider_event_processor.py -v --tb=short)",
|
||||
"Bash(/Users/ogt/.pyenv/versions/3.11.7/bin/python3 -m pytest tests/test_aider_event_processor.py tests/test_aider_event_service.py tests/test_aider_events_api.py tests/test_aider_event_models.py tests/test_secret_redactor.py --tb=short)",
|
||||
"Bash(/Users/ogt/.pyenv/versions/3.11.7/bin/python3 -m pytest tests/test_ai_router_feedback.py -v)",
|
||||
"Bash(/Users/ogt/.pyenv/versions/3.11.7/bin/python3 -m pytest tests/test_aider_event_service.py tests/test_aider_events_api.py tests/test_aider_event_models.py tests/test_secret_redactor.py tests/test_aider_event_processor.py tests/test_ai_router_feedback.py -v)",
|
||||
"Bash(/Users/ogt/.pyenv/versions/3.11.7/bin/python3 -c \"from src.services.ai_router import AIRouter; from src.db.base import get_session_factory; print\\('✓ Imports successful, no circular imports'\\)\")",
|
||||
"Bash(/Users/ogt/.pyenv/versions/3.11.7/bin/python3)",
|
||||
"Bash(/Users/ogt/.pyenv/versions/3.11.7/bin/python3 -m pytest tests/test_ai_router_feedback.py tests/test_aider_event_service.py -v --tb=short)",
|
||||
"Bash(/Users/ogt/.pyenv/versions/3.11.7/bin/python3 -c \"from src.api.v1 import aider_events; from src.workers.aider_event_processor import run_aider_event_processor_loop; from src.core.config import settings; print\\('AIDER_WEBHOOK_SECRET' in settings.__fields__, 'USE_AIDER_FEEDBACK' in settings.__fields__\\)\")",
|
||||
"Bash(AIDER_WEBHOOK_SECRET=testsecret /Users/ogt/.pyenv/versions/3.11.7/bin/python3 -c \"from src.main import app; print\\('app OK; title:', app.title\\)\")",
|
||||
"Bash(/Users/ogt/.pyenv/versions/3.11.7/bin/python3 -m pytest tests/test_action_parsing.py tests/test_aider_event_service.py tests/test_aider_events_api.py tests/test_aider_event_models.py tests/test_secret_redactor.py tests/test_aider_event_processor.py tests/test_ai_router_feedback.py -v)",
|
||||
"Bash(/Users/ogt/.pyenv/versions/3.11.7/bin/python3 -m pytest tests/test_action_parsing.py tests/test_aider_event_service.py tests/test_aider_events_api.py tests/test_aider_event_models.py tests/test_secret_redactor.py tests/test_aider_event_processor.py tests/test_ai_router_feedback.py -q)",
|
||||
"Bash(/Users/ogt/.pyenv/versions/3.11.7/bin/python3 -m pip install -e .[dev] --quiet)",
|
||||
"Bash(/Users/ogt/.pyenv/versions/3.11.7/bin/python3 -m pip install -e '.[dev]' --quiet)",
|
||||
"Bash(/Users/ogt/.pyenv/versions/3.11.7/bin/python3 -m pytest tests/ -v)",
|
||||
"Bash(/Users/ogt/.pyenv/versions/3.11.7/bin/python3 -c \"from aider_watch_client.aiderw import main as awmain; from aider_watch_client.cli import main as climain; print\\('✓ imports ok'\\)\")",
|
||||
"Bash(/Users/ogt/.pyenv/versions/3.11.7/bin/python3 -m pip show aider-watch-client)",
|
||||
"Bash(tailscale status *)",
|
||||
"Bash(kubectl rollout *)",
|
||||
"Bash(bash /Users/ogt/awoooi/scripts/aider_watch_client/scripts/install.sh)",
|
||||
"Bash(git rebase *)",
|
||||
"Bash(/opt/homebrew/bin/aiderw --message \"add docstring to hello function\" --exit)",
|
||||
"Bash(kubectl -n awoooi-prod get pod -l app=awoooi-api -o jsonpath='{.items[0].metadata.name}')",
|
||||
"Bash(kubectl -n awoooi-prod exec awoooi-api-7b9464c969-8ml88 -- python -c ' *)",
|
||||
"Bash(kubectl -n awoooi-prod rollout restart deployment/awoooi-api)",
|
||||
"Bash(kubectl -n awoooi-prod get pod -l app=awoooi-api --no-headers)",
|
||||
"Bash(kubectl -n awoooi-prod rollout status deployment/awoooi-api --timeout=120s)",
|
||||
"Bash(/opt/homebrew/bin/aider-watch flush *)",
|
||||
"Bash(kubectl -n awoooi-prod get pod -l app=awoooi-api -o wide)",
|
||||
"Bash(kubectl -n awoooi-prod rollout status deployment/awoooi-api --timeout=30s)",
|
||||
"Bash(kubectl -n awoooi-prod exec awoooi-api-6657fb9cf7-47lcg -- python -c \"import src.services.telegram_gateway as tg; import inspect; lines = inspect.getsource\\(tg\\); idx = lines.find\\('response_body=e.response.text'\\); print\\('FOUND' if idx >= 0 else 'NOT FOUND'\\)\")",
|
||||
"Read(//opt/gitea/**)",
|
||||
"Bash(/Users/ogt/.pyenv/versions/3.11.7/bin/python3 -m pytest tests/ -q)",
|
||||
"Bash(/Users/ogt/.pyenv/versions/3.11.7/bin/python3 -m pytest tests/unit/test_aider_event_service.py tests/unit/test_aider_model.py -v)",
|
||||
"Bash(/Users/ogt/.pyenv/versions/3.11.7/bin/python3 -m pytest tests/test_aider_events_api.py tests/test_aider_event_models.py tests/test_aider_event_service.py tests/test_aider_event_processor.py -v)",
|
||||
"Bash(kubectl -n awoooi-prod get svc)",
|
||||
"Bash(kubectl -n openclaw get pod)",
|
||||
"Bash(kubectl -n awoooi-prod exec awoooi-api-7cd784c875-r4qkz -- python -c ' *)",
|
||||
"Bash(kubectl -n awoooi-prod logs awoooi-api-7cd784c875-qt6j2 --since=10m)",
|
||||
"Bash(kubectl -n awoooi-prod logs awoooi-api-7cd784c875-qt6j2 --since=15m)",
|
||||
"Bash(kubectl -n awoooi-prod logs awoooi-api-7cd784c875-qt6j2 --since=20m)",
|
||||
"Bash(kubectl -n awoooi-prod get secret awoooi-secrets -o yaml)",
|
||||
"Bash(kubectl -n awoooi-prod logs awoooi-api-7cd784c875-qt6j2 --since=30m)",
|
||||
"Bash(kubectl -n awoooi-prod logs awoooi-api-7cd784c875-qt6j2 --since=2h)",
|
||||
"Bash(kubectl -n awoooi-prod logs awoooi-api-7cd784c875-qt6j2)",
|
||||
"Bash(kubectl -n awoooi-prod get pod -l app=awoooi-api -o jsonpath='{range .items[*]}{.metadata.name} {.status.containerStatuses[0].imageID}{\"\\\\n\"}{end}')",
|
||||
"Bash(kubectl -n awoooi-prod get ingress)",
|
||||
"Bash(kubectl -n awoooi-prod get svc awoooi-api-svc)",
|
||||
"Bash(kubectl -n awoooi-prod logs -l app=awoooi-api --since=60s --prefix)",
|
||||
"Bash(kubectl -n awoooi-prod logs -l app=awoooi-api --since=5m --prefix)",
|
||||
"Bash(kubectl -n awoooi-prod logs pod/awoooi-api-86bc79766d-dn5ll --since=5m)",
|
||||
"Bash(kubectl -n awoooi-prod logs pod/awoooi-api-86bc79766d-dn5ll --since=10m)",
|
||||
"Bash(kubectl -n awoooi-prod logs pod/awoooi-api-86bc79766d-dn5ll)",
|
||||
"Bash(kubectl -n awoooi-prod logs -l app=awoooi-api --since=90s --prefix)",
|
||||
"Bash(kubectl -n awoooi-prod logs pod/awoooi-api-86bc79766d-4x69p --since=5m)",
|
||||
"Bash(redis-cli -h 192.168.0.188 -p 6380 -n 10 SCAN 0 MATCH \"playbook:PB-*\" COUNT 500)",
|
||||
"Bash(redis-cli -h 192.168.0.188 -p 6380 -n 10 DBSIZE)",
|
||||
"Bash(wait)",
|
||||
"Read(//Users/**)",
|
||||
"Read(//Users/ooo/.claude/**)",
|
||||
"Bash(mkdir -p /Users/ogt/awoooi/.claude/agents)",
|
||||
"Bash(cp /Users/ogt/.claude/agents/*.md /Users/ogt/awoooi/.claude/agents/)"
|
||||
],
|
||||
"deny": [
|
||||
"Bash(rm -rf *)",
|
||||
"Bash(git push --force *)",
|
||||
"Bash(git reset --hard *)",
|
||||
"Bash(kubectl delete *)",
|
||||
"Bash(docker rm -f *)"
|
||||
],
|
||||
"additionalDirectories": [
|
||||
"/Users/ogt/.claude/projects/-Users-ogt-awoooi/memory",
|
||||
"/Users/ogt/awoooi/.claude/hooks",
|
||||
"/Users/ogt/.claude/channels/telegram",
|
||||
"/Users/ogt",
|
||||
"/Users/ogt/.claude"
|
||||
]
|
||||
},
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/awoooi-guard.js 2>/dev/null || true"
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node /Users/ogt/.claude/hooks/branch-protection.js"
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node /Users/ogt/.claude/hooks/commit-quality.js"
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node /Users/ogt/.claude/hooks/large-file-warner.js"
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node /Users/ogt/.claude/hooks/mcp-health.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node /Users/ogt/.claude/hooks/audit-log.js"
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node /Users/ogt/.claude/hooks/suggest-compact.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Stop": [
|
||||
{
|
||||
"matcher": "",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node /Users/ogt/.claude/hooks/cost-tracker.js"
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node /Users/ogt/.claude/hooks/session-summary.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
827
.claude/settings.json.bak.20260323
Normal file
827
.claude/settings.json.bak.20260323
Normal file
@@ -0,0 +1,827 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(pnpm install:*)",
|
||||
"Bash(npm --version)",
|
||||
"Bash(npm install:*)",
|
||||
"Bash(pnpm --version)",
|
||||
"Bash(pnpm dev:*)",
|
||||
"Bash(pnpm add:*)",
|
||||
"Bash(ls -la /Users/ogt/awoooi/apps/web/next.config.*)",
|
||||
"Bash(pkill -f \"next dev\")",
|
||||
"Bash(curl -sL http://localhost:3000/zh-TW)",
|
||||
"Bash(curl -s http://localhost:3000/zh-TW)",
|
||||
"Bash(pnpm --filter web build)",
|
||||
"Bash(curl -s http://localhost:3001/zh-TW)",
|
||||
"Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:3000/zh-TW)",
|
||||
"Bash(kubectl apply:*)",
|
||||
"Bash(chmod +x /Users/ogt/awoooi/deploy-infra.sh)",
|
||||
"Bash(./deploy-infra.sh)",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 \"mkdir -p /tmp/awoooi-k8s\")",
|
||||
"Bash(sshpass -p '0936223270' scp -o StrictHostKeyChecking=no /Users/ogt/awoooi/k8s/awoooi-prod/01-namespace-quota.yaml /Users/ogt/awoooi/k8s/awoooi-prod/02-network-policy.yaml /Users/ogt/awoooi/k8s/awoooi-prod/04-configmap.yaml wooo@192.168.0.120:/tmp/awoooi-k8s/)",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 \"sudo kubectl apply -f /tmp/awoooi-k8s/01-namespace-quota.yaml\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 \"echo ''0936223270'' | sudo -S kubectl apply -f /tmp/awoooi-k8s/01-namespace-quota.yaml 2>/dev/null\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 \"echo ''0936223270'' | sudo -S kubectl apply -f /tmp/awoooi-k8s/02-network-policy.yaml 2>/dev/null\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 \"echo ''0936223270'' | sudo -S kubectl apply -f /tmp/awoooi-k8s/04-configmap.yaml 2>/dev/null\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 \"echo ''0936223270'' | sudo -S kubectl get ns awoooi-prod -o wide 2>/dev/null\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 \"echo ''0936223270'' | sudo -S kubectl get networkpolicy -n awoooi-prod 2>/dev/null\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 \"echo ''0936223270'' | sudo -S kubectl get resourcequota,limitrange,configmap -n awoooi-prod 2>/dev/null\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 \"rm -rf /tmp/awoooi-k8s\")",
|
||||
"Bash(PYTHONPATH=. python -c \"from src.main import app; print\\(''Import OK''\\)\")",
|
||||
"Bash(curl -s http://localhost:8000/api/v1/health/ready)",
|
||||
"Bash(curl -s http://localhost:8000/api/v1/health/live)",
|
||||
"Bash(curl -s http://localhost:8000/)",
|
||||
"Bash(pkill -f \"uvicorn src.main:app\")",
|
||||
"Bash(pkill -f \"node.*next\")",
|
||||
"Bash(curl -s http://localhost:8000/api/v1/health)",
|
||||
"Read(//Users/ogt/awoooi/apps/api/**)",
|
||||
"Bash(pnpm typecheck:*)",
|
||||
"Read(//Users/ogt/awoooi/apps/web/**)",
|
||||
"Bash(curl -s -X POST http://localhost:8000/api/v1/dashboard/demo/spike/clear)",
|
||||
"Read(//Users/ogt/awoooi/=== 驗證英文頁面 \\(/en/**)",
|
||||
"Bash(jq \".devDependencies | keys | map\\(select\\(startswith\\(\"\"@playwright\"\"\\) or startswith\\(\"\"playwright\"\"\\)\\)\\)\")",
|
||||
"Bash(npx playwright:*)",
|
||||
"Bash(curl -s http://localhost:3000/zh-TW/demo -o /dev/null -w \"Frontend: HTTP %{http_code}\\\\n\")",
|
||||
"Bash(__NEW_LINE_ef548029029cdfac__ echo:*)",
|
||||
"Bash(curl -s http://localhost:8000/api/v1/health -o /dev/null -w \"Backend: HTTP %{http_code}\\\\n\")",
|
||||
"Bash(echo '=== 已產出的截圖 ===' find /Users/ogt/awoooi/apps/web/test-results -name *.png)",
|
||||
"Bash(echo '=== Playwright E2E 測試結果 ===' echo echo '📸 截圖證據 \\(test-results/screenshots/\\):' ls -la /Users/ogt/awoooi/apps/web/test-results/screenshots/ __NEW_LINE_db74e5f56e34db17__ echo echo '🎬 錄影證據 \\(.webm\\):' find /Users/ogt/awoooi/apps/web/test-results -name *.webm -exec ls -la {})",
|
||||
"Bash(__NEW_LINE_db74e5f56e34db17__ echo:*)",
|
||||
"Bash(source .venv/bin/activate)",
|
||||
"Bash(python scripts/demo_multisig.py)",
|
||||
"Bash(python -c \"from src.api.v1.approvals import router; print\\(''✅ Approvals router loaded:'', len\\(router.routes\\), ''routes''\\)\")",
|
||||
"Bash(npx tsc:*)",
|
||||
"Bash(chmod +x /Users/ogt/awoooi/scripts/demo-multisig-flow.sh)",
|
||||
"Bash(python -c \"from src.main import app; print\\(''✅ API loads successfully''\\)\")",
|
||||
"Bash(jq)",
|
||||
"Bash(/Users/ogt/awoooi/scripts/demo-multisig-flow.sh)",
|
||||
"Bash(curl -s -X POST \"http://localhost:8000/api/v1/approvals\" -H \"Content-Type: application/json\" -d '{:*)",
|
||||
"Bash(curl -s http://localhost:8000/api/v1/openapi.json)",
|
||||
"Bash(python -c \":*)",
|
||||
"Bash(curl -s http://localhost:3000 -o /dev/null -w \"%{http_code}\")",
|
||||
"Bash(lsof -ti:3000,3001,8000)",
|
||||
"Bash(curl -s http://localhost:8000/health)",
|
||||
"Bash(curl -s http://localhost:8000/api/v1/approvals/pending)",
|
||||
"Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:3001/zh-TW/demo)",
|
||||
"Bash(ls -la test-results/*.png)",
|
||||
"Bash(cp test-results/cpo102-*.png /Users/ogt/awoooi/docs/screenshots/)",
|
||||
"Bash(ssh ogt@192.168.0.120 'cat /etc/rancher/k3s/k3s.yaml')",
|
||||
"Bash(python -c \"from src.main import app; print\\(''✅ main.py imports OK''\\)\")",
|
||||
"Bash(curl -s http://localhost:8000/api/v1/approvals/k8s-test)",
|
||||
"Bash(sqlite3 awoooi.db \".tables\")",
|
||||
"Bash(sshpass -p 0936223270 ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 'sudo cat /etc/rancher/k3s/k3s.yaml')",
|
||||
"Bash(kubectl --kubeconfig=/Users/ogt/awoooi/apps/api/k3s-prod.yaml get deployments -n awoooi-prod)",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 \"echo ''0936223270'' | sudo -S kubectl get deployments -n awoooi-prod 2>/dev/null\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 \"echo ''0936223270'' | sudo -S kubectl get deployments -A 2>/dev/null\")",
|
||||
"Bash(curl -s -X POST http://localhost:8000/api/v1/approvals -H \"Content-Type: application/json\" -d '{:*)",
|
||||
"Bash(APPROVAL_ID=\"b58a0d86-fa4e-43ca-881c-02e978cd7943\")",
|
||||
"Bash(curl -s -X POST \"http://localhost:8000/api/v1/approvals/$APPROVAL_ID/sign\" -H \"Content-Type: application/json\" -d '{:*)",
|
||||
"Bash(sqlite3 /Users/ogt/awoooi/apps/api/awoooi.db \"SELECT operation_type, target_resource, namespace, success, dry_run_passed, dry_run_message, error_message, execution_duration_ms, created_at FROM audit_logs ORDER BY created_at DESC LIMIT 1;\" -header -column)",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 \"echo ''0936223270'' | sudo -S kubectl get pods -n monitoring -l app=grafana 2>/dev/null\")",
|
||||
"Bash(curl -s http://192.168.0.188:11434/api/tags)",
|
||||
"Bash(python -c \"from src.main import app; print\\(''✅ Compile OK''\\)\")",
|
||||
"Bash(curl -s http://localhost:8000/api/v1/ai/status)",
|
||||
"Bash(curl -s -X POST http://localhost:8000/api/v1/ai/analyze-and-propose -H \"Content-Type: application/json\" -d '{}')",
|
||||
"Bash(curl -s -X POST http://192.168.0.188:11434/api/generate -H \"Content-Type: application/json\" -d '{\"\"\"\"model\"\"\"\":\"\"\"\"llama3.2:1b\"\"\"\",\"\"\"\"prompt\"\"\"\":\"\"\"\"Output only JSON: {\\\\\"\"\"\"action\\\\\"\"\"\":\\\\\"\"\"\"test\\\\\"\"\"\"}\"\"\"\",\"\"\"\"stream\"\"\"\":false,\"\"\"\"format\"\"\"\":\"\"\"\"json\"\"\"\"}' --max-time 30)",
|
||||
"Bash(curl -s -X POST http://localhost:8000/api/v1/ai/analyze-and-propose -H \"Content-Type: application/json\" -d '{}' --max-time 60)",
|
||||
"Bash(PROMPT='你是 ClawBot AI。分析以下監控數據,輸出純 JSON(無其他文字)。:*)",
|
||||
"Bash(curl -s -X POST http://192.168.0.188:11434/api/generate -H \"Content-Type: application/json\" -d \"{\"\"model\"\":\"\"llama3.2:1b\"\",\"\"prompt\"\":\"\"$PROMPT\"\",\"\"stream\"\":false,\"\"format\"\":\"\"json\"\",\"\"options\"\":{\"\"num_predict\"\":256,\"\"temperature\"\":0.1}}\" --max-time 60)",
|
||||
"Bash(curl -s -X POST http://192.168.0.188:11434/api/generate -H \"Content-Type: application/json\" -d '{\"\"\"\"model\"\"\"\":\"\"\"\"llama3.2:1b\"\"\"\",\"\"\"\"prompt\"\"\"\":\"\"\"\"Harbor service returning 404. Output JSON: {\\\\\"\"\"\"suggested_action\\\\\"\"\"\":\\\\\"\"\"\"RESTART_DEPLOYMENT\\\\\"\"\"\",\\\\\"\"\"\"target_resource\\\\\"\"\"\":\\\\\"\"\"\"harbor\\\\\"\"\"\",\\\\\"\"\"\"namespace\\\\\"\"\"\":\\\\\"\"\"\"default\\\\\"\"\"\",\\\\\"\"\"\"risk_level\\\\\"\"\"\":\\\\\"\"\"\"medium\\\\\"\"\"\",\\\\\"\"\"\"reasoning\\\\\"\"\"\":\\\\\"\"\"\"Service down\\\\\"\"\"\",\\\\\"\"\"\"confidence\\\\\"\"\"\":0.8,\\\\\"\"\"\"affected_services\\\\\"\"\"\":[]}\"\"\"\",\"\"\"\"stream\"\"\"\":false,\"\"\"\"format\"\"\"\":\"\"\"\"json\"\"\"\",\"\"\"\"options\"\"\"\":{\"\"\"\"num_predict\"\"\"\":128,\"\"\"\"temperature\"\"\"\":0.1}}' --max-time 30)",
|
||||
"Bash(curl -v -X POST http://192.168.0.188:11434/api/generate -H \"Content-Type: application/json\" -d '{\"\"\"\"model\"\"\"\":\"\"\"\"llama3.2:1b\"\"\"\",\"\"\"\"prompt\"\"\"\":\"\"\"\"Say hello\"\"\"\",\"\"\"\"stream\"\"\"\":false}' --max-time 30)",
|
||||
"Bash(curl -s -X POST http://localhost:8000/api/v1/ai/analyze-and-propose -H \"Content-Type: application/json\" -d '{}' --max-time 120)",
|
||||
"Bash(curl -s http://localhost:8000/api/v1/ai/analyze-and-propose -X POST -H \"Content-Type: application/json\")",
|
||||
"Bash(curl -s http://localhost:8000/api/v1/dashboard)",
|
||||
"Bash(ls -la ~/Downloads/image*.png)",
|
||||
"Bash(ls -la ~/Desktop/image*.png)",
|
||||
"Bash(ls -la /Users/ogt/awoooi/apps/web/public/*.png)",
|
||||
"WebFetch(domain:openclaw.ai)",
|
||||
"Bash(ls -la /Users/ogt/Downloads/*.png)",
|
||||
"Bash(ls -la /Users/ogt/.gemini/antigravity/brain/*/image*.png)",
|
||||
"Bash(ls -lat /Users/ogt/Downloads/*.png)",
|
||||
"Bash(curl -s http://localhost:8000/api/v1/approvals)",
|
||||
"Bash(curl -s -X GET http://localhost:8000/api/v1/approvals/)",
|
||||
"Bash(APPROVAL_ID=\"4989729e-e518-4e7e-8dff-5c3269e0c82b\")",
|
||||
"Bash(curl -s -X POST \"http://localhost:8000/api/v1/approvals/$APPROVAL_ID/sign\" -H \"Content-Type: application/json\" -d '{\"\"\"\"signer_id\"\"\"\": \"\"\"\"ciso-001\"\"\"\", \"\"\"\"signer_name\"\"\"\": \"\"\"\"Demo CISO\"\"\"\", \"\"\"\"comment\"\"\"\": \"\"\"\"資安確認,核准執行\"\"\"\"}')",
|
||||
"Bash(curl -s http://localhost:8000/api/v1/webhooks/health)",
|
||||
"Bash(curl -s -X POST http://localhost:8000/api/v1/webhooks/alerts -H \"Content-Type: application/json\" -d '{:*)",
|
||||
"Bash(curl -s http://localhost:3000)",
|
||||
"Bash(ls -la apps/web/test-results/*.png)",
|
||||
"Bash(curl -s http://localhost:3000/zh-TW/demo)",
|
||||
"Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:3333/zh-TW/demo)",
|
||||
"Bash(curl -s http://localhost:8001/api/v1/approvals/pending)",
|
||||
"Bash(curl -s -X POST http://localhost:8001/api/v1/approvals -H \"Content-Type: application/json\" -d '{:*)",
|
||||
"Bash(curl -s http://localhost:8001/openapi.json)",
|
||||
"Bash(curl -s http://localhost:8001/docs)",
|
||||
"Bash(curl -s http://localhost:8001/api/v1/webhooks/grafana -X OPTIONS)",
|
||||
"Bash(pnpm run:*)",
|
||||
"Bash(node scripts/screenshot-rbac.mjs)",
|
||||
"Bash(pnpm exec:*)",
|
||||
"Bash(curl -s http://localhost:3333 -o /dev/null -w \"%{http_code}\")",
|
||||
"Bash(curl -s http://localhost:3333/zh-TW/demo -o /dev/null -w \"%{http_code}\")",
|
||||
"Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\(f''''Count: {d[count]}''''\\); [print\\(f''''- {a[id][:8]}... risk={a[risk_level]}''''\\) for a in d[''''approvals''''][:3]]\")",
|
||||
"Bash(curl -s http://localhost:3000/zh-TW/demo -o /dev/null -w \"%{http_code}\")",
|
||||
"Bash(python -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\(f'''' Connected: {d[\"\"success\"\"]}''''\\); print\\(f'''' Namespaces: {d[\"\"namespaces\"\"][:3]}...''''\\)\" __NEW_LINE_57ae1c1c812968e7__ echo \"\" echo \"3. 資料庫持久化:\" sqlite3 /Users/ogt/awoooi/apps/api/awoooi.db \"SELECT COUNT\\(*\\) as approvals FROM approval_records;\" sqlite3 /Users/ogt/awoooi/apps/api/awoooi.db \"SELECT COUNT\\(*\\) as timeline FROM timeline_events;\" sqlite3 /Users/ogt/awoooi/apps/api/awoooi.db \"SELECT COUNT\\(*\\) as audits FROM audit_logs;\")",
|
||||
"Bash(head -2 __NEW_LINE_9bf9481fbdf30d4e__ echo \"\" echo \"2. 告警收斂跳過 LLM 日誌 \\(應該有 4 次\\):\" grep -c \"alert_converged_skip_llm\" /tmp/api-server.log)",
|
||||
"Bash(python -m json.tool)",
|
||||
"Bash(__NEW_LINE_7463bff94cecc20f__ echo:*)",
|
||||
"Bash(__NEW_LINE_13846c8488c5fa9a__ echo:*)",
|
||||
"Bash(__NEW_LINE_13846c8488c5fa9a__ ls:*)",
|
||||
"Bash(python -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\(f'''' Status: {d[\"\"status\"\"]}''''\\)\" __NEW_LINE_32366ca1bb050259__ echo \"\" echo \"2. 待簽核記錄 \\(含 hit_count\\):\" curl -s http://localhost:8000/api/v1/approvals/pending)",
|
||||
"Read(//Users/ogt/awoooi/**)",
|
||||
"Bash(curl -s http://localhost:8000/api/v1/timeline/events?limit=10)",
|
||||
"Bash(curl -s http://localhost:8000/api/v1/timeline/events?limit=5)",
|
||||
"Bash(ls -la /Users/ogt/awoooi/apps/api/*.txt /Users/ogt/awoooi/apps/api/*.toml)",
|
||||
"Bash(ls -la /Users/ogt/awoooi/docker-compose*.yml)",
|
||||
"Bash(ls /Users/ogt/awoooi/k8s/awoooi-prod/*rbac* /Users/ogt/awoooi/k8s/awoooi-prod/*service-account*)",
|
||||
"Bash(kubectl kustomize:*)",
|
||||
"Bash(docker compose:*)",
|
||||
"Bash(docker info:*)",
|
||||
"Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\(''''API Status:'''', d.get\\(''''status'''', ''''unknown''''\\)\\)\")",
|
||||
"Bash(pkill -9 -f uvicorn)",
|
||||
"Bash(lsof -ti:8000)",
|
||||
"Bash(open -a Docker)",
|
||||
"Bash(docker stop:*)",
|
||||
"Bash(lsof -ti:3000)",
|
||||
"Bash(docker start:*)",
|
||||
"Bash(docker ps:*)",
|
||||
"Bash(curl -s http://localhost:3000 -o /dev/null -w 'HTTP Status: %{http_code}\\\\n')",
|
||||
"Bash(curl -I http://localhost:8000/api/v1/dashboard/stream)",
|
||||
"Bash(curl -s http://localhost:8000/openapi.json)",
|
||||
"Bash(curl -s http://localhost:8000/api/v1/dashboard/stream --max-time 3 -w \"\\\\n--- HTTP Status: %{http_code} ---\\\\n\")",
|
||||
"Bash(curl -s http://localhost:8000/api/v1/dashboard/stream --max-time 3)",
|
||||
"Bash(curl -s http://localhost:3000/zh-TW -o /dev/null -w \"HTTP Status: %{http_code}\\\\n\")",
|
||||
"Bash(curl -s -D - http://localhost:8000/api/v1/dashboard/stream --max-time 2)",
|
||||
"Bash(chmod +x /Users/ogt/awoooi/scripts/deploy-infra.sh)",
|
||||
"Bash(./scripts/deploy-infra.sh)",
|
||||
"Bash(pnpm --filter @awoooi/web build)",
|
||||
"Bash(timeout 10 env MOCK_MODE=true OTEL_ENABLED=false uvicorn src.main:app --host 0.0.0.0 --port 8099)",
|
||||
"Bash(timeout 8 pnpm --filter @awoooi/web dev)",
|
||||
"Bash(git diff:*)",
|
||||
"Bash(curl -s -I http://localhost:8000/api/v1/dashboard/stream)",
|
||||
"Bash(timeout 3 curl -s -N http://localhost:8000/api/v1/dashboard/stream)",
|
||||
"Bash(grep -n \"NEXT_PUBLIC\\\\|API_URL\\\\|localhost\" /Users/ogt/awoooi/apps/web/.env*)",
|
||||
"Bash(timeout 2 curl -s -D - -N http://localhost:8000/api/v1/dashboard/stream)",
|
||||
"Bash(curl -s http://localhost:3000/)",
|
||||
"Bash(python -m py_compile scripts/fire_test_alert.py)",
|
||||
"Bash(python -m scripts.fire_test_alert --help)",
|
||||
"Bash(python -m scripts.fire_test_alert)",
|
||||
"Bash(python -m scripts.fire_test_alert --type k8s_pod_crash)",
|
||||
"Bash(timeout 3 curl -s -N -H \"Origin: http://localhost:3000\" http://localhost:8000/api/v1/dashboard/stream)",
|
||||
"Bash(python -m scripts.fire_test_alert --type disk_full)",
|
||||
"Bash(docker restart:*)",
|
||||
"Bash(curl -s -w \"\\\\nHTTP_CODE: %{http_code}\\\\n\" http://localhost:3000)",
|
||||
"Bash(docker exec:*)",
|
||||
"Bash(docker rmi:*)",
|
||||
"Bash(timeout 5 curl -s -N http://localhost:8000/api/v1/dashboard/stream)",
|
||||
"Bash(curl -s http://localhost:3000 -w \"\\\\nHTTP: %{http_code}\\\\n\")",
|
||||
"Bash(timeout 120 docker logs awoooi-api -f --since 1s)",
|
||||
"Bash(curl -s -I -H \"Origin: http://localhost:3000\" http://localhost:8000/api/v1/dashboard/stream)",
|
||||
"Bash(curl -s -X OPTIONS -H \"Origin: http://localhost:3000\" -H \"Access-Control-Request-Method: GET\" http://localhost:8000/api/v1/dashboard/stream -I)",
|
||||
"Bash(node /Users/ogt/awoooi/scripts/verify-sse.js)",
|
||||
"Bash(python -m scripts.fire_test_alert --type db_connection_timeout)",
|
||||
"Bash(npm run:*)",
|
||||
"Bash(docker-compose down:*)",
|
||||
"Bash(docker-compose build:*)",
|
||||
"Bash(docker-compose up:*)",
|
||||
"Bash(pkill -f 'next dev')",
|
||||
"Bash(node /Users/ogt/awoooi/scripts/test-approval-flow.js)",
|
||||
"Bash(python -m scripts.fire_test_alert --type pod_crash)",
|
||||
"Bash(node /Users/ogt/awoooi/scripts/test-k8s-executor.js)",
|
||||
"Bash(kubectl cluster-info:*)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl cluster-info)",
|
||||
"Bash(ls -la /Users/ogt/awoooi/apps/web/src/app/[locale]/)",
|
||||
"Bash(python -c \"from src.api.v1 import audit_logs; print\\(''API module loads OK''\\)\")",
|
||||
"Bash(curl -s http://localhost:3000/zh-TW/action-logs)",
|
||||
"Bash(pnpm build:*)",
|
||||
"Bash(curl -s http://localhost:8000/api/v1/audit-logs)",
|
||||
"Bash(xargs -r kill -9 2)",
|
||||
"Bash(/dev/null source:*)",
|
||||
"Bash(python -c \"from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor; print\\(''''httpx ok''''\\)\")",
|
||||
"Bash(sqlite3 /Users/ogt/awoooi/apps/api/awoooi.db \"SELECT * FROM audit_logs ORDER BY created_at DESC LIMIT 5;\")",
|
||||
"Bash(sqlite3 /Users/ogt/awoooi/apps/api/awoooi.db \"SELECT name FROM sqlite_master WHERE type=''table'';\")",
|
||||
"Bash(sqlite3 /Users/ogt/awoooi/apps/api/awoooi.db \"SELECT id, event_type, status, title, created_at FROM timeline_events ORDER BY created_at DESC LIMIT 5;\")",
|
||||
"Bash(curl -s http://localhost:8000/api/v1/audit-logs/stats)",
|
||||
"Bash(curl -s http://localhost:8000/api/v1/timeline?limit=10)",
|
||||
"Bash(curl -s \"http://localhost:8000/api/v1/timeline\")",
|
||||
"Bash(curl -s http://localhost:8000/api/v1/docs)",
|
||||
"Bash(chmod +x /Users/ogt/awoooi/scripts/setup-guardrails.sh /Users/ogt/awoooi/scripts/ai_code_reviewer.py)",
|
||||
"Bash(ls -la /Users/ogt/awoooi/apps/web/.eslintrc*)",
|
||||
"Bash(ls -la scripts/*.py scripts/*.sh .pre-commit-config.yaml .secrets.baseline apps/web/.eslintrc.js)",
|
||||
"Bash(python -m src.services.test_context_gatherer)",
|
||||
"Bash(python -m pytest src/services/test_context_gatherer.py -v)",
|
||||
"Bash(grep -r \"ClawBot\\\\|clawbot\\\\|CLAWBOT\" --include=*.py --include=*.ts --include=*.tsx apps/)",
|
||||
"Bash(python scripts/e2e_openclaw_test.py)",
|
||||
"Bash(python -m pytest tests/e2e_network_test.py -v --tb=short)",
|
||||
"Bash(chmod +x /Users/ogt/awoooi/apps/api/scripts/apply_prometheus_config.sh /Users/ogt/awoooi/apps/api/scripts/fire_live_alert.py)",
|
||||
"Bash(./scripts/apply_prometheus_config.sh)",
|
||||
"Bash(python scripts/fire_live_alert.py oomkilled)",
|
||||
"Bash(python scripts/fire_live_alert.py oomkilled --api-url http://localhost:8000)",
|
||||
"Bash(python scripts/fire_live_alert.py highcpu --api-url http://localhost:8000)",
|
||||
"Bash(python scripts/fire_live_alert.py podcrash --api-url http://localhost:8000)",
|
||||
"Bash(python -m pytest tests/test_webhook_telegram_integration.py -v)",
|
||||
"Bash(ls -la /Users/ogt/awoooi/apps/api/.env*)",
|
||||
"Bash(ls -la /Users/ogt/wooo-aiops/.env*)",
|
||||
"Bash(ls -la /Users/ogt/AIOps/.env*)",
|
||||
"Bash(/Users/ogt/awoooi/apps/api/.env:*)",
|
||||
"Bash(/tmp/deploy-188-home.sh:*)",
|
||||
"Bash(chmod +x /tmp/deploy-188-home.sh)",
|
||||
"Bash(scp /tmp/awoooi-api-deploy.tar.gz /tmp/deploy-188-home.sh ollama@192.168.0.188:/tmp/)",
|
||||
"Bash(ssh ollama@192.168.0.188 \"bash /tmp/deploy-188-home.sh\")",
|
||||
"Bash(ssh ollama@192.168.0.188 \"curl -s http://localhost:8000/api/v1/webhooks/health\")",
|
||||
"Bash(ssh ollama@192.168.0.188 \"tail -50 /tmp/openclaw.log\")",
|
||||
"Bash(ssh ollama@192.168.0.188 \"cd /home/ollama/awoooi-api && source .venv/bin/activate && pip install sqlalchemy aiosqlite -q && pip install httpx python-dotenv pydantic-settings -q\")",
|
||||
"Bash(ssh ollama@192.168.0.188 \"cd /home/ollama/awoooi-api && pkill -f ''uvicorn src.main:app'' 2>/dev/null; sleep 1; source .venv/bin/activate && nohup uvicorn src.main:app --host 0.0.0.0 --port 8000 > /tmp/openclaw.log 2>&1 & sleep 3 && curl -s http://localhost:8000/api/v1/webhooks/health\")",
|
||||
"Bash(ssh ollama@192.168.0.188:*)",
|
||||
"Bash(pkill -f ngrok)",
|
||||
"Bash(pkill -f \"ssh -fN.*8001\")",
|
||||
"Bash(ssh -fN -L 8001:localhost:8000 ollama@192.168.0.188)",
|
||||
"Bash(curl -s http://localhost:8001/api/v1/webhooks/health)",
|
||||
"Bash(BOT_TOKEN=\"8569720657:AAHdvKf_P2ms-QKFTyqTLtLiqEggz8cpjMk\" curl -s \"https://api.telegram.org/bot$BOT_TOKEN/getWebhookInfo\")",
|
||||
"Bash(curl -s https://api.telegram.org/bot$BOT_TOKEN/getWebhookInfo)",
|
||||
"Bash(curl -s http://localhost:8001/api/v1/webhooks/)",
|
||||
"Bash(curl -s http://localhost:8001/)",
|
||||
"Bash(curl -s http://localhost:8001/api/v1/health)",
|
||||
"Bash(scp /tmp/awoooi-api-v7.tar.gz ollama@192.168.0.188:/tmp/)",
|
||||
"Bash(tar -czvf /tmp/awoooi-api-v7.1.tar.gz src/ requirements.txt pyproject.toml)",
|
||||
"Bash(scp /tmp/awoooi-api-v7.1.tar.gz ollama@192.168.0.188:/tmp/)",
|
||||
"Bash(ssh ollama@192.168.0.188 \"tail -10 /tmp/openclaw.log | grep -E ''''clickhouse|signoz_gold''''\")",
|
||||
"Bash(ssh ogt@192.168.0.188 \"cd /home/ollama/awoooi-api && tail -50 nohup.out 2>/dev/null || journalctl -u awoooi-api --no-pager -n 50 2>/dev/null || echo ''請手動檢查日誌''\")",
|
||||
"Bash(curl -s --connect-timeout 5 http://192.168.0.188:8123/ -d \"SELECT 1 FORMAT JSONEachRow\")",
|
||||
"Bash(curl -s --connect-timeout 5 http://192.168.0.188:11434/api/tags)",
|
||||
"Bash(ssh -o StrictHostKeyChecking=no -o PasswordAuthentication=no -o BatchMode=yes -o ConnectTimeout=5 ollama@192.168.0.188 \"echo ok\")",
|
||||
"Bash(ssh -o StrictHostKeyChecking=no -o PasswordAuthentication=no -o BatchMode=yes -o ConnectTimeout=5 wooo@192.168.0.188 \"echo ok\")",
|
||||
"Bash(ssh -o StrictHostKeyChecking=no -o PasswordAuthentication=no -o BatchMode=yes -o ConnectTimeout=5 root@192.168.0.188 \"echo ok\")",
|
||||
"Bash(curl -s --connect-timeout 5 http://192.168.0.188:8001/health)",
|
||||
"Bash(ssh root@192.168.0.188 \"cat /tmp/openclaw.log 2>/dev/null | tail -100 || echo ''Log file not found''\")",
|
||||
"Bash(ssh -o StrictHostKeyChecking=no -o BatchMode=yes -o ConnectTimeout=5 ollama@192.168.0.188 \"echo ok\")",
|
||||
"Bash(ssh -o StrictHostKeyChecking=no -o BatchMode=yes -o ConnectTimeout=5 wooo@192.168.0.188 \"echo ok\")",
|
||||
"Bash(scp /Users/ogt/awoooi/apps/api/src/services/signoz_client.py ollama@192.168.0.188:/home/ollama/awoooi-api/src/services/)",
|
||||
"Bash(scp /Users/ogt/awoooi/apps/api/src/services/openclaw.py ollama@192.168.0.188:/home/ollama/awoooi-api/src/services/)",
|
||||
"Bash(scp /Users/ogt/awoooi/apps/api/src/services/telegram_gateway.py ollama@192.168.0.188:/home/ollama/awoooi-api/src/services/)",
|
||||
"Bash(scp /Users/ogt/awoooi/apps/api/src/api/v1/webhooks.py ollama@192.168.0.188:/home/ollama/awoooi-api/src/api/v1/)",
|
||||
"Bash(scp /Users/ogt/awoooi/apps/api/src/models/ai.py ollama@192.168.0.188:/home/ollama/awoooi-api/src/models/)",
|
||||
"Bash(ssh ollama@192.168.0.188 \"cd /home/ollama/awoooi-api && pkill -f ''''uvicorn src.main:app'''' && sleep 2 && nohup .venv/bin/python3 -m uvicorn src.main:app --host 0.0.0.0 --port 8000 > nohup.out 2>&1 &\")",
|
||||
"Bash(curl -s --connect-timeout 5 http://192.168.0.188:8000/health)",
|
||||
"Bash(curl -s --connect-timeout 10 http://192.168.0.188:8000/health)",
|
||||
"Bash(curl -s -X POST http://192.168.0.188:8000/api/v1/webhooks/alerts -H \"Content-Type: application/json\" -d '{:*)",
|
||||
"Bash(curl -s -X POST http://192.168.0.188:8000/api/v1/webhooks/alerts -H \"Content-Type: application/json\" -d '{\"\"alert_type\"\":\"\"high_cpu\"\",\"\"severity\"\":\"\"critical\"\",\"\"source\"\":\"\"signoz\"\",\"\"target_resource\"\":\"\"api-gateway\"\",\"\"namespace\"\":\"\"awoooi-prod\"\",\"\"message\"\":\"\"CPU 92% test\"\"}')",
|
||||
"Bash(curl -s --connect-timeout 5 http://192.168.0.188:8000/api/v1/webhooks/alerts -X POST -H \"Content-Type: application/json\" -d '{\"\"alert_type\"\":\"\"high_cpu\"\",\"\"severity\"\":\"\"critical\"\",\"\"source\"\":\"\"signoz\"\",\"\"target_resource\"\":\"\"api-gateway\"\",\"\"namespace\"\":\"\"awoooi-prod\"\",\"\"message\"\":\"\"CPU 92% - 統帥全自主驗收 v2\"\"}')",
|
||||
"Bash(curl -s --connect-timeout 30 --max-time 120 -X POST http://192.168.0.188:8000/api/v1/webhooks/alerts -H \"Content-Type: application/json\" -d '{:*)",
|
||||
"Bash(curl -s --connect-timeout 30 --max-time 180 -X POST http://192.168.0.188:8000/api/v1/webhooks/alerts -H \"Content-Type: application/json\" -d '{:*)",
|
||||
"Bash(curl -s http://192.168.0.188:8000/api/v1/webhooks/alerts -X POST -H \"Content-Type: application/json\" -d '{\"\"alert_type\"\":\"\"k8s_pod_crash\"\",\"\"severity\"\":\"\"critical\"\",\"\"source\"\":\"\"signoz\"\",\"\"target_resource\"\":\"\"inventory-api\"\",\"\"namespace\"\":\"\"commerce\"\",\"\"message\"\":\"\"Pod crash - 統帥終極驗收\"\"}' --connect-timeout 30 --max-time 180)",
|
||||
"Bash(ssh -o ConnectTimeout=10 ollama@192.168.0.188 \"echo OK && ps aux | grep uvicorn | grep -v grep | head -2\")",
|
||||
"Bash(curl -s http://192.168.0.188:8000/api/v1/webhooks/alerts -X POST -H \"Content-Type: application/json\" -d '{\"\"alert_type\"\":\"\"ssl_expiry\"\",\"\"severity\"\":\"\"critical\"\",\"\"source\"\":\"\"signoz\"\",\"\"target_resource\"\":\"\"nginx-ingress\"\",\"\"namespace\"\":\"\"ingress\"\",\"\"message\"\":\"\"SSL 即將過期 - 終極驗收\"\"}' --connect-timeout 30 --max-time 180)",
|
||||
"Bash(curl -s http://192.168.0.188:8000/api/v1/webhooks/alerts -X POST -H \"Content-Type: application/json\" -d '{\"\"alert_type\"\":\"\"db_connection_timeout\"\",\"\"severity\"\":\"\"critical\"\",\"\"source\"\":\"\"signoz\"\",\"\"target_resource\"\":\"\"postgres-primary\"\",\"\"namespace\"\":\"\"database\"\",\"\"message\"\":\"\"DB 連線逾時 - SignOz 整合終極測試\"\"}' --connect-timeout 30 --max-time 180)",
|
||||
"Bash(curl -s http://192.168.0.188:8000/api/v1/webhooks/alerts -X POST -H \"Content-Type: application/json\" -d '{\"\"alert_type\"\":\"\"service_404\"\",\"\"severity\"\":\"\"critical\"\",\"\"source\"\":\"\"signoz\"\",\"\"target_resource\"\":\"\"auth-service\"\",\"\"namespace\"\":\"\"identity\"\",\"\"message\"\":\"\"Service 404 - SignOz + Ollama 整合終極測試\"\"}' --connect-timeout 30 --max-time 180)",
|
||||
"Bash(curl -s http://192.168.0.188:8000/api/v1/webhooks/alerts -X POST -H \"Content-Type: application/json\" -d '{\"\"alert_type\"\":\"\"high_cpu\"\",\"\"severity\"\":\"\"warning\"\",\"\"source\"\":\"\"signoz\"\",\"\"target_resource\"\":\"\"recommendation-engine\"\",\"\"namespace\"\":\"\"ml\"\",\"\"message\"\":\"\"CPU 78% - Ollama 最終測試\"\"}' --connect-timeout 30 --max-time 200)",
|
||||
"Bash(scp apps/api/src/services/openclaw.py ollama@192.168.0.188:/home/ollama/awoooi-api/src/services/openclaw.py)",
|
||||
"Bash(scp /Users/ogt/awoooi/apps/api/src/core/http_client.py ollama@192.168.0.188:/home/ollama/awoooi-api/src/core/)",
|
||||
"Bash(scp /Users/ogt/awoooi/apps/api/src/main.py ollama@192.168.0.188:/home/ollama/awoooi-api/src/)",
|
||||
"Bash(scp /Users/ogt/awoooi/apps/api/src/core/config.py ollama@192.168.0.188:/home/ollama/awoooi-api/src/core/)",
|
||||
"Bash(scp /Users/ogt/awoooi/apps/api/src/api/v1/health.py ollama@192.168.0.188:/home/ollama/awoooi-api/src/api/v1/)",
|
||||
"Bash(ssh -o ConnectTimeout=5 ollama@192.168.0.188 \"ps aux | grep uvicorn | grep -v grep\")",
|
||||
"Bash(curl -s -H \"Origin: http://localhost:3000\" -H \"Access-Control-Request-Method: GET\" -X OPTIONS http://192.168.0.188:8000/api/v1/health -v)",
|
||||
"Bash(curl -s http://192.168.0.188:8000/api/v1/health)",
|
||||
"Bash(curl -s -N --max-time 3 http://192.168.0.188:8000/api/v1/dashboard/stream)",
|
||||
"Bash(curl -s http://localhost:3000/zh-TW -o /dev/null -w \"%{http_code}\")",
|
||||
"Bash(open http://localhost:3000/zh-TW)",
|
||||
"Bash(open http://localhost:3001/zh-TW)",
|
||||
"Bash(curl -s -H \"Origin: http://localhost:3001\" http://192.168.0.188:8000/api/v1/dashboard/stream --max-time 3)",
|
||||
"Bash(curl -s -I -H \"Origin: http://localhost:3001\" http://192.168.0.188:8000/api/v1/health)",
|
||||
"Bash(curl -s http://192.168.0.188:8000/api/v1/approvals/pending)",
|
||||
"Bash(curl -s http://192.168.0.188:8000/api/v1/approvals)",
|
||||
"Bash(curl -s \"http://192.168.0.188:8000/api/v1/approvals?status=pending_approval\")",
|
||||
"Bash(xargs sed:*)",
|
||||
"Bash(curl -s \"http://192.168.0.188:8000/api/v1/approvals/history?limit=5\")",
|
||||
"Bash(curl -s http://192.168.0.188:8000/api/v1/approvals/approved)",
|
||||
"Bash(curl -s \"http://192.168.0.188:8000/api/v1/timeline?limit=10\")",
|
||||
"Bash(curl -s \"http://192.168.0.188:8000/api/v1/action-logs\")",
|
||||
"Bash(curl -s \"http://192.168.0.188:8000/api/v1/timeline/events?limit=10\")",
|
||||
"Bash(ssh ogt@192.168.0.188 \"kubectl get nodes\")",
|
||||
"Bash(curl -s \"http://192.168.0.188:8000/api/v1/approvals/k8s-test\")",
|
||||
"Bash(scp /Users/ogt/awoooi/apps/api/k3s-prod.yaml ogt@192.168.0.188:~/awoooi-api/k3s-prod.yaml)",
|
||||
"Bash(curl -s \"http://192.168.0.188:8000/api/v1/timeline/events?limit=5\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.120 \"cat /etc/rancher/k3s/k3s.yaml\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no wooo@192.168.0.188 \"echo ''SSH OK'' && pwd\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"echo ''SSH OK'' && pwd && ls -la ~/awoooi-api/ 2>/dev/null || echo ''Directory not found''\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"sshpass -p ''0936223270'' scp -o StrictHostKeyChecking=no wooo@192.168.0.120:/etc/rancher/k3s/k3s.yaml ~/awoooi-api/k3s-prod.yaml && sed -i ''s/127.0.0.1/192.168.0.120/g'' ~/awoooi-api/k3s-prod.yaml && echo ''Kubeconfig deployed!'' && head -10 ~/awoooi-api/k3s-prod.yaml\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"cd ~/awoooi-api && pkill -f ''uvicorn'' 2>/dev/null; sleep 1; nohup .venv/bin/uvicorn src.main:app --host 0.0.0.0 --port 8000 --reload > nohup.out 2>&1 & sleep 3; echo ''=== API Restarted ==='' && tail -20 nohup.out\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 \"cd ~/awoooi-api && pkill -f ''uvicorn src.main'' || true\")",
|
||||
"Bash(curl -s \"http://192.168.0.188:8000/api/v1/health\" --connect-timeout 5)",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 ollama@192.168.0.188 \"cd ~/awoooi-api && source .venv/bin/activate && nohup uvicorn src.main:app --host 0.0.0.0 --port 8000 > nohup.out 2>&1 &\")",
|
||||
"Bash(sshpass -p:*)",
|
||||
"Bash(curl -s \"http://192.168.0.188:8000/api/v1/health\" --connect-timeout 10)",
|
||||
"Bash(curl -s \"http://192.168.0.188:8000/api/v1/timeline/events?limit=8\")",
|
||||
"Bash(curl -s http://localhost:3000/zh-TW -o /dev/null -w \"Frontend: HTTP %{http_code}\\\\n\")",
|
||||
"Bash(sshpass -p '0936223270' ssh -o StrictHostKeyChecking=no ollama@192.168.0.188 'curl -s http://localhost:8000/api/v1/approvals/pending | jq -r \"\".approvals[] | \\\\\"\"ID: \\\\\\(.id\\) | Action: \\\\\\(.action\\)\\\\\"\"\"\"')",
|
||||
"Bash(curl -s --connect-timeout 5 https://awoooi.wooo.tw/api/v1/health)",
|
||||
"Bash(curl -s --connect-timeout 5 https://awoooi.wooo.tw/api/v1/approvals/pending)",
|
||||
"Bash(ssh ollama@192.168.70.188 \"ps aux | grep uvicorn | grep -v grep | head -3\")",
|
||||
"Bash(ssh -o ConnectTimeout=10 ollama@192.168.70.188 \"echo ''SSH Connected''\")",
|
||||
"Bash(ping -c 2 -t 5 192.168.70.188)",
|
||||
"Bash(curl -s --connect-timeout 10 https://awoooi.wooo.tw/api/v1/health)",
|
||||
"Bash(ssh -o ConnectTimeout=10 ollama@192.168.0.188 \"echo ''SSH Connected to 188 Base''\")",
|
||||
"Bash(grep -B 5 -A 30 \"async def add_signature\" /Users/ogt/awoooi/apps/api/src/services/*.py)",
|
||||
"Bash(ssh ogt@192.168.0.188 \"cd /home/ogt/awoooi && docker compose ps\")",
|
||||
"Bash(ls -la .env*)",
|
||||
"Bash(.env:*)",
|
||||
"Bash(timeout 15 python -m uvicorn src.main:app --host 0.0.0.0 --port 8001)",
|
||||
"Bash(timeout 20 python -m uvicorn src.main:app --host 0.0.0.0 --port 8001)",
|
||||
"Bash(timeout 25 python -m uvicorn src.main:app --host 0.0.0.0 --port 8001)",
|
||||
"Bash(ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no ogt@192.168.0.188 \"cd /home/ogt/wooo-aiops && docker compose ps clawbot 2>/dev/null || docker ps | grep -i claw\")",
|
||||
"Bash(ls -la ~/.ssh/*.pub)",
|
||||
"Bash(ssh -i ~/.ssh/id_rsa -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o PasswordAuthentication=no ogt@192.168.0.188 \"echo connected\")",
|
||||
"Bash(curl -s \"https://api.telegram.org/bot8569720657:AAHdvKf_P2ms-QKFTyqTLtLiqEggz8cpjMk/logOut\")",
|
||||
"Bash(curl -s \"https://api.telegram.org/bot8569720657:AAHdvKf_P2ms-QKFTyqTLtLiqEggz8cpjMk/close\")",
|
||||
"Bash(curl -s \"https://api.telegram.org/bot8569720657:AAHdvKf_P2ms-QKFTyqTLtLiqEggz8cpjMk/getUpdates?timeout=3&limit=1\")",
|
||||
"Bash(ping -c 1 192.168.0.188)",
|
||||
"Bash(python -m tests.test_redis_multisig)",
|
||||
"Bash(curl -v -X POST http://localhost:8000/api/v1/webhooks/signals -H \"Content-Type: application/json\" -d '{:*)",
|
||||
"Bash(python3 -c \":*)",
|
||||
"Bash(echo ' 無法連線' __NEW_LINE_8fc87454f9798a7d__ echo echo [結論]: echo ' /signals 端點尚未部署到 .188' echo ' 程式碼已完成,需要執行:' echo \" cd apps/api && docker build -t awoooi-api . && docker-compose up -d\")",
|
||||
"Bash(__NEW_LINE_dc88f37970737861__ cd:*)",
|
||||
"Bash(__NEW_LINE_dc88f37970737861__ echo:*)",
|
||||
"Read(//Users/**)",
|
||||
"Bash(tail -20 __NEW_LINE_8b049957a9782734__ echo \"\" echo \"[Step 2] 等待容器啟動 \\(10 秒\\)...\" sleep 10 __NEW_LINE_8b049957a9782734__ echo \"\" echo \"[Step 3] 檢查容器狀態...\" docker compose ps)",
|
||||
"Bash(tail -5 __NEW_LINE_275e0094e9dcb44a__ echo \"\" echo \"[1.2] 重建 API 容器 \\(含 Signal Worker\\)...\" docker compose build api)",
|
||||
"Bash(1 __NEW_LINE_275e0094e9dcb44a__ echo \"\" echo \"[1.4] 等待服務就緒 \\(15 秒\\)...\" sleep 15 __NEW_LINE_275e0094e9dcb44a__ echo \"\" echo \"[1.5] 檢查容器狀態...\" docker compose ps)",
|
||||
"Bash(__NEW_LINE_f4c8301ec5249760__ echo:*)",
|
||||
"Bash(__NEW_LINE_21ba3cf3700d942d__ cd:*)",
|
||||
"Bash(1 __NEW_LINE_9a14b79fc58c11ba__ echo \"\" echo \"[1.3] 等待服務就緒 \\(15 秒\\)...\" sleep 15 __NEW_LINE_9a14b79fc58c11ba__ echo \"\" echo \"[1.4] 檢查容器狀態...\" docker compose ps api)",
|
||||
"Bash(1 __NEW_LINE_6b654ca5be87c137__ echo \"\" echo \"[2] 等待服務就緒 \\(15 秒\\)...\" sleep 15 __NEW_LINE_6b654ca5be87c137__ echo \"\" echo \"[3] 發送測試 Signal...\" curl -s -X POST http://localhost:8000/api/v1/webhooks/signals -H \"Content-Type: application/json\" -d '{:*)",
|
||||
"Bash(__NEW_LINE_564908ddf866c081__ echo:*)",
|
||||
"Bash(chmod +x /Users/ogt/awoooi/apps/api/scripts/test_phase63_aggregation.py)",
|
||||
"Bash(python scripts/test_phase63_aggregation.py)",
|
||||
"Bash(xargs -r docker exec -i awoooi-redis redis-cli DEL)",
|
||||
"Bash(chmod +x /Users/ogt/awoooi/apps/api/scripts/test_race_condition.py)",
|
||||
"Bash(python scripts/test_race_condition.py)",
|
||||
"Bash(chmod +x /Users/ogt/awoooi/apps/api/scripts/test_phase64_proposal.py)",
|
||||
"Bash(python scripts/test_phase64_proposal.py)",
|
||||
"Bash(python agent.py --alert FINAL_PHASE_6_TEST)",
|
||||
"Bash(AWOOOI_REDIS_URL=\"redis://localhost:6379/0\" python agent.py --alert FINAL_PHASE_6_TEST)",
|
||||
"Bash(curl -s http://localhost:8000/api/v1/incidents)",
|
||||
"Bash(curl -s -X POST http://localhost:8000/api/v1/incidents/INC-20260322-06085B/proposal)",
|
||||
"Bash(grep -r \"mock\\\\|Mock\\\\|MOCK\\\\|fake\\\\|Fake\\\\|dummy\\\\|hardcode\" /Users/ogt/awoooi/apps/web/src --include=*.tsx --include=*.ts -l)",
|
||||
"Bash(NEXT_PUBLIC_API_URL=http://localhost:8000 pnpm next build --no-lint)",
|
||||
"Bash(grep -v \"Traceback\\\\|File \"\"/usr\\\\|^\\\\s*$\")",
|
||||
"Bash(python -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\(f''''Signal Count: {len\\(d[\"\"signals\"\"]\\)}''''\\); [print\\(f'''' - {s[\"\"alert_name\"\"]} \\({s[\"\"signal_id\"\"]}\\)''''\\) for s in d[''''signals'''']]\")",
|
||||
"Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:3003/zh-TW)",
|
||||
"Bash(curl -s -X GET \"http://localhost:8000/api/v1/incidents\" -H \"Origin: http://localhost:3003\" -H \"Access-Control-Request-Method: GET\" -v)",
|
||||
"Bash(grep -r TELEGRAM /Users/ogt/awoooi/apps/api/.env*)",
|
||||
"Bash(grep -r TELEGRAM_BOT_TOKEN /Users/ogt/awoooi --include=*.env* --include=*.yaml --include=*.yml)",
|
||||
"Bash(curl -s -I -X OPTIONS \"http://localhost:8000/api/v1/incidents\" -H \"Origin: http://localhost:3000\" -H \"Access-Control-Request-Method: GET\")",
|
||||
"Bash(curl -s \"http://localhost:8000/api/v1/incidents\" -H \"Origin: http://localhost:3000\")",
|
||||
"Bash(python /tmp/e2e_drill.py)",
|
||||
"Bash(python -c \"import sys,json; d=json.load\\(sys.stdin\\); i=[x for x in d[''''incidents''''] if x[''''incident_id'''']==''''INC-20260322-06085B''''][0]; print\\(f\"\"Incident: {i[''''incident_id'''']}\"\"\\); print\\(f\"\"Signals: {i[''''signal_count'''']}\"\"\\); print\\(f\"\"Updated: {i[''''updated_at'''']}\"\"\\)\")",
|
||||
"Bash(curl -s -X POST \"http://localhost:8000/api/v1/telegram/test\")",
|
||||
"Bash(curl -s -X POST \"http://localhost:8000/api/v1/telegram/test-push\" -H \"Content-Type: application/json\" -d '{\"\"\"\"approval_id\"\"\"\": \"\"\"\"15ab6844-ca4e-4a13-aead-dc71cd342445\"\"\"\", \"\"\"\"risk_level\"\"\"\": \"\"\"\"critical\"\"\"\", \"\"\"\"resource_name\"\"\"\": \"\"\"\"api-gateway\"\"\"\", \"\"\"\"root_cause\"\"\"\": \"\"\"\"E2E DRILL - PodCrashLoopBackOff\"\"\"\", \"\"\"\"suggested_action\"\"\"\": \"\"\"\"RESTART_DEPLOYMENT\"\"\"\", \"\"\"\"estimated_downtime\"\"\"\": \"\"\"\"5-15 min\"\"\"\"}')",
|
||||
"Bash(curl -s -o /dev/null -w \"HTTP Status: %{http_code}\\\\n\" http://localhost:3000/zh-TW)",
|
||||
"Bash(curl -s -I \"http://localhost:8000/api/v1/incidents\" -H \"Origin: http://localhost:3000\")",
|
||||
"Bash(curl -s -X POST http://localhost:8000/api/v1/incidents/INC-20260322-19DF60/proposal)",
|
||||
"Bash(curl -s -X POST \"http://localhost:8000/api/v1/telegram/test-push\" -H \"Content-Type: application/json\" -d '{\"\"\"\"approval_id\"\"\"\": \"\"\"\"942e762e-fb97-480f-b21a-d3be67fa70b1\"\"\"\", \"\"\"\"risk_level\"\"\"\": \"\"\"\"critical\"\"\"\", \"\"\"\"resource_name\"\"\"\": \"\"\"\"core-system\"\"\"\", \"\"\"\"root_cause\"\"\"\": \"\"\"\"E2E DRILL TAKE 2 - 二次實彈演習\"\"\"\", \"\"\"\"suggested_action\"\"\"\": \"\"\"\"INVESTIGATE_SERVICE\"\"\"\", \"\"\"\"estimated_downtime\"\"\"\": \"\"\"\"5-15 min\"\"\"\"}')",
|
||||
"Bash(curl -s \"http://localhost:8000/api/v1/incidents\" -H \"Origin: http://localhost:3000\" -H \"Accept: application/json\")",
|
||||
"Bash(python -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\(f''''Incidents: {d[\"\"count\"\"]}''''\\); [print\\(f'''' - {i[\"\"incident_id\"\"]} | {i[\"\"severity\"\"]} | {i[\"\"signal_count\"\"]} signals | {i[\"\"affected_services\"\"]}''''\\) for i in d[''''incidents'''']]\")",
|
||||
"Bash(curl -s \"http://localhost:8000/api/v1/approvals/pending\" -H \"Origin: http://localhost:3000\")",
|
||||
"Bash(python -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\(f''''Pending: {d[\"\"count\"\"]} approvals''''\\); [print\\(f'''' - {a[\"\"id\"\"][:8]}... | {a[\"\"risk_level\"\"]} | {a[\"\"action\"\"][:30]}...''''\\) for a in d[''''approvals''''][:3]]\")",
|
||||
"Bash(mkdir -p /Users/ogt/awoooi/apps/web/public/fonts)",
|
||||
"Bash(curl -sL -o DSEG7Classic-Bold.woff2 \"https://cdn.jsdelivr.net/npm/dseg@0.46.0/fonts/DSEG7-Classic/DSEG7Classic-Bold.woff2\")",
|
||||
"Bash(curl -sL -o DSEG7Classic-Bold.woff \"https://cdn.jsdelivr.net/npm/dseg@0.46.0/fonts/DSEG7-Classic/DSEG7Classic-Bold.woff\")",
|
||||
"Bash(curl -sL -o DSEG7Classic-Regular.woff2 \"https://cdn.jsdelivr.net/npm/dseg@0.46.0/fonts/DSEG7-Classic/DSEG7Classic-Regular.woff2\")",
|
||||
"Bash(curl -sL -o DSEG7Classic-Regular.woff \"https://cdn.jsdelivr.net/npm/dseg@0.46.0/fonts/DSEG7-Classic/DSEG7Classic-Regular.woff\")",
|
||||
"Bash(pnpm next:*)",
|
||||
"Bash(chmod +x /Users/ogt/awoooi/scripts/bootstrap_prod.sh)",
|
||||
"Bash(/Users/ogt/awoooi/.env:*)",
|
||||
"Bash(grep -E \"^\\\\.env$|03-secrets\\\\.yaml\" .gitignore)",
|
||||
"Bash(echo 'Adding to .gitignore...' if ! grep -q ^.env$ .gitignore)",
|
||||
"Bash(then echo:*)",
|
||||
"Bash(git add:*)",
|
||||
"Bash(git commit:*)",
|
||||
"Bash(git push:*)",
|
||||
"Bash(git remote:*)",
|
||||
"Bash(gh repo:*)",
|
||||
"Bash(gh api:*)",
|
||||
"Bash(gh run:*)",
|
||||
"Bash(ls -la pnpm-*.yaml package.json turbo.json)",
|
||||
"Bash(git status:*)",
|
||||
"Bash(gh workflow:*)",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get pods -n awoooi-prod -o wide\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl logs awoooi-api-77545758fc-xnncc -n awoooi-prod --tail=50\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl logs awoooi-api-77545758fc-xnncc -n awoooi-prod 2>&1 | grep -i ''cors'' -A 5 -B 5\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl logs awoooi-api-79948cbbbf-b8cgj -n awoooi-prod --tail=100\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get pods -n awoooi-prod -l app=awoooi-api --sort-by=.metadata.creationTimestamp -o name | tail -1 | xargs kubectl logs -n awoooi-prod --tail=50\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get secret awoooi-secrets -n awoooi-prod -o jsonpath=''{.data.OPENCLAW_TG_USER_WHITELIST}'' | base64 -d\")",
|
||||
"Bash(ssh wooo@192.168.0.120 'kubectl patch secret awoooi-secrets -n awoooi-prod --type='\"''\"'json'\"''\"' -p='\"''\"'[:*)",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl rollout restart deployment/awoooi-api -n awoooi-prod && kubectl rollout status deployment/awoooi-api -n awoooi-prod --timeout=120s\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl rollout restart deployment/awoooi-worker -n awoooi-prod && kubectl rollout status deployment/awoooi-worker -n awoooi-prod --timeout=120s\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl logs awoooi-worker-747967b787-fcx2r -n awoooi-prod --tail=30\")",
|
||||
"Bash(ssh wooo@192.168.0.110 \"ps aux | grep -E ''actions-runner|Runner'' | grep -v grep\")",
|
||||
"Bash(curl -sf http://192.168.0.120:32334/api/v1/health)",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl logs awoooi-api-fd795cd87-rdpgn -n awoooi-prod --tail=30\")",
|
||||
"Bash(ssh wooo@192.168.0.110 \"curl -sf http://192.168.0.120:32334/api/v1/health | jq .status\")",
|
||||
"Bash(ssh wooo@192.168.0.110 \"curl -sf http://192.168.0.120:32334/api/v1/health\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"curl -sf http://localhost:32334/api/v1/health\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get svc -n awoooi-prod\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"curl -sf http://10.43.125.201:8000/api/v1/health\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"curl -sf http://10.43.105.105:3000/ -o /dev/null && echo ''Web OK''\")",
|
||||
"Bash(ssh ogt@192.168.0.188 \"ls -la /etc/nginx/sites-available/\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl logs deployment/awoooi-api -n awoooi-prod --tail=50\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl logs awoooi-api-795c95ff76-wch2p -n awoooi-prod --tail=30\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get pods -n awoooi-prod && ss -tlnp | grep 32334\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"curl -sf http://127.0.0.1:32334/api/v1/health | head -c 200\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"sudo ufw status 2>/dev/null || sudo iptables -L INPUT -n | head -20\")",
|
||||
"Bash(ssh wooo@192.168.0.110 \"curl -sf --connect-timeout 5 http://192.168.0.120:32334/api/v1/health | head -c 100\")",
|
||||
"Bash(ssh wooo@192.168.0.110 \"curl -v --connect-timeout 5 http://192.168.0.120:32334/api/v1/health 2>&1 | head -30\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"cat /etc/systemd/system/k3s.service 2>/dev/null | grep -i exec || ps aux | grep k3s | head -3\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"cat /etc/systemd/system/k3s.service\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"netstat -tlnp 2>/dev/null | grep 32334 || ss -tlnp | grep 32334\")",
|
||||
"Bash(ssh wooo@192.168.0.110 \"curl -sf --connect-timeout 5 http://192.168.0.120:31234/health 2>&1 | head -c 100\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get networkpolicy -n awoooi-prod\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get networkpolicy allow-nginx-ingress -n awoooi-prod -o yaml\")",
|
||||
"Bash(curl -sk https://awoooi.wooo.work/api/v1/health)",
|
||||
"Bash(curl -sk -I -X OPTIONS https://awoooi.wooo.work/api/v1/health -H \"Origin: https://awoooi.wooo.work\" -H \"Access-Control-Request-Method: GET\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"curl -sI --connect-timeout 3 http://127.0.0.1:32334/api/v1/health 2>&1 | head -5\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"curl -sI --connect-timeout 3 http://127.0.0.1:32335/ 2>&1 | head -5\")",
|
||||
"Bash(ssh wooo@192.168.0.121 \"curl -sI --connect-timeout 3 http://127.0.0.1:32334/api/v1/health 2>&1 | head -5\")",
|
||||
"Bash(ssh wooo@192.168.0.121 \"curl -sI --connect-timeout 3 http://127.0.0.1:32335/ 2>&1 | head -5\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"sudo iptables -t nat -L KUBE-NODEPORTS -n 2>/dev/null | head -20\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"sudo netstat -tlnp | grep -E ''32334|32335''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"ss -tlnp 2>/dev/null | grep -E ''32334|32335'' || netstat -tln | grep -E ''32334|32335''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"ss -tln | grep -E ''32334|32335|:323''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"ss -tln\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"export KUBECONFIG=/home/wooo/.kube/config-120; /home/wooo/bin/kubectl get svc -n awoooi-prod -o wide\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"which kubectl || find /usr -name kubectl 2>/dev/null | head -1\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get svc -n awoooi-prod && kubectl get pods -n awoooi-prod -o wide\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"export KUBECONFIG=/home/wooo/.kube/config-120 && kubectl logs awoooi-api-546b88465d-lb8zm -n awoooi-prod --tail 80\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"KUBECONFIG=/home/wooo/.kube/config-120 kubectl logs awoooi-api-546b88465d-lb8zm -n awoooi-prod --tail 80 2>&1\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"ls -la /home/wooo/.kube/ && cat /home/wooo/.kube/config-120 2>/dev/null | head -20 || cat /etc/rancher/k3s/k3s.yaml 2>/dev/null | head -20\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"sudo cat /etc/rancher/k3s/k3s.yaml | head -20\")",
|
||||
"Bash(ssh wooo@192.168.0.110 \"export KUBECONFIG=/home/wooo/.kube/config-120 && kubectl logs awoooi-api-546b88465d-lb8zm -n awoooi-prod --tail 100 2>&1\")",
|
||||
"Bash(ssh wooo@192.168.0.110 \"which kubectl 2>/dev/null || find /home/wooo -name kubectl 2>/dev/null | head -1 || ls -la /home/wooo/bin/\")",
|
||||
"Bash(ssh wooo@192.168.0.110 \"export KUBECONFIG=/home/wooo/.kube/config-120 && /home/wooo/kubectl logs awoooi-api-546b88465d-lb8zm -n awoooi-prod --tail 100 2>&1\")",
|
||||
"Bash(ssh wooo@192.168.0.110 \"export KUBECONFIG=/home/wooo/.kube/config-120 && /home/wooo/kubectl describe pod awoooi-api-546b88465d-lb8zm -n awoooi-prod | tail -40\")",
|
||||
"Bash(ssh wooo@192.168.0.110 \"export KUBECONFIG=/home/wooo/.kube/config-120 && /home/wooo/kubectl get svc -n awoooi-prod -o wide\")",
|
||||
"Bash(ssh wooo@192.168.0.110 \"export KUBECONFIG=/home/wooo/.kube/config-120 && /home/wooo/kubectl exec -n awoooi-prod deploy/awoooi-api -- curl -sf http://localhost:8000/api/v1/health 2>&1\")",
|
||||
"Bash(ssh wooo@192.168.0.110 \"export KUBECONFIG=/home/wooo/.kube/config-120 && /home/wooo/kubectl exec -n awoooi-prod deploy/awoooi-api -- wget -qO- http://localhost:8000/api/v1/health 2>&1\")",
|
||||
"Bash(ssh wooo@192.168.0.110 \"export KUBECONFIG=/home/wooo/.kube/config-120 && /home/wooo/kubectl logs deployment/awoooi-api -n awoooi-prod --tail 20 2>&1\")",
|
||||
"Bash(ssh wooo@192.168.0.110 \"curl -sf http://192.168.0.120:32334/api/v1/health 2>&1 || echo ''FAILED to connect to 120:32334''\")",
|
||||
"Bash(ssh wooo@192.168.0.110 \"curl -sf http://192.168.0.121:32334/api/v1/health 2>&1 || echo ''FAILED to connect to 121:32334''\")",
|
||||
"Bash(ssh wooo@192.168.0.110 \"ssh wooo@192.168.0.120 ''cat /etc/rancher/k3s/k3s.yaml 2>/dev/null || echo No k3s.yaml''\")",
|
||||
"Bash(ssh wooo@192.168.0.110 \"export KUBECONFIG=/home/wooo/.kube/config-120 && /home/wooo/kubectl get pods -n awoooi-prod -o wide | grep Running\")",
|
||||
"Bash(ssh -o ConnectTimeout=5 wooo@192.168.0.120 \"ufw status 2>/dev/null || firewall-cmd --state 2>/dev/null || echo ''No firewall command found''\")",
|
||||
"Bash(ssh -o ConnectTimeout=5 wooo@192.168.0.121 \"ufw status 2>/dev/null || firewall-cmd --state 2>/dev/null || echo ''No firewall command found''\")",
|
||||
"Bash(pip3 show:*)",
|
||||
"Bash(docker build:*)",
|
||||
"Bash(docker version:*)",
|
||||
"Bash(docker run:*)",
|
||||
"Bash(curl -vI -H \"Origin: https://awoooi.wooo.work\" http://localhost:8889/api/v1/health)",
|
||||
"Bash(ssh wooo@192.168.0.110 \"export KUBECONFIG=/home/wooo/.kube/config-120 && /home/wooo/kubectl get endpoints awoooi-api-svc -n awoooi-prod\")",
|
||||
"Bash(ssh wooo@192.168.0.110 \"export KUBECONFIG=/home/wooo/.kube/config-120 && /home/wooo/kubectl get pods -n awoooi-prod -o wide\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"sudo -n ufw status 2>/dev/null || sudo -n iptables -L INPUT -n 2>/dev/null | head -20 || echo ''Need sudo for firewall check''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"ss -tln | grep -E ''32334|32335|:323'' || echo ''No NodePort listeners found''\")",
|
||||
"Bash(ssh wooo@192.168.0.121 \"ss -tln | grep -E ''32334|32335|:323'' || echo ''No NodePort listeners found''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"ps aux | grep -E ''kube-proxy|k3s'' | grep -v grep | head -5\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"cat /proc/sys/net/ipv4/ip_forward\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"systemctl status k3s 2>/dev/null | head -15 || ps aux | grep ''k3s server'' | grep -v grep\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"curl -sf --connect-timeout 5 http://127.0.0.1:32334/api/v1/health 2>&1 || echo ''LOCALHOST NodePort FAILED''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"curl -sf --connect-timeout 5 http://192.168.0.120:32334/api/v1/health 2>&1 || echo ''EXTERNAL IP NodePort FAILED''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"cat /etc/iptables/rules.v4 2>/dev/null || iptables-save 2>/dev/null | grep -E ''DROP|REJECT|32334|32335'' | head -10 || echo ''Cannot read iptables without sudo''\")",
|
||||
"Bash(ssh wooo@192.168.0.121 \"curl -sf --connect-timeout 5 http://192.168.0.120:32334/api/v1/health 2>&1 || echo ''Worker->Master NodePort FAILED''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"cat /etc/rancher/k3s/config.yaml 2>/dev/null || ls -la /etc/rancher/k3s/ 2>/dev/null || echo ''No K3s config found''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"netstat -an 2>/dev/null | grep 32334 || ss -an | grep 32334 || echo ''No socket found for 32334''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"echo ''0936223270'' | sudo -S iptables -L INPUT -n 2>&1 | head -20\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"echo ''0936223270'' | sudo -S iptables -t nat -L KUBE-NODEPORTS -n 2>&1 | head -20\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"echo ''0936223270'' | sudo -S iptables -L KUBE-ROUTER-INPUT -n 2>&1 | head -30\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"echo ''0936223270'' | sudo -S iptables -t nat -L KUBE-NODEPORTS -n 2>&1 | grep -i awoooi || echo ''NO AWOOOI RULES FOUND''\")",
|
||||
"Bash(ssh wooo@192.168.0.110 \"export KUBECONFIG=/home/wooo/.kube/config-120 && /home/wooo/kubectl get svc awoooi-api-svc -n awoooi-prod -o yaml | grep -A5 ''spec:''\")",
|
||||
"Bash(ssh wooo@192.168.0.110 \"export KUBECONFIG=/home/wooo/.kube/config-120 && /home/wooo/kubectl get networkpolicy -n awoooi-prod\")",
|
||||
"Bash(ssh wooo@192.168.0.110 \"export KUBECONFIG=/home/wooo/.kube/config-120 && /home/wooo/kubectl apply -f - 2>&1\")",
|
||||
"Bash(curl -sf --connect-timeout 10 https://awoooi.wooo.work/api/v1/health)",
|
||||
"Bash(curl -skf --connect-timeout 10 https://awoooi.wooo.work/api/v1/health)",
|
||||
"Bash(curl -sI https://awoooi.wooo.work/)",
|
||||
"Bash(curl -skI https://awoooi.wooo.work/)",
|
||||
"Bash(ssh wooo@192.168.0.110 \"export KUBECONFIG=/home/wooo/.kube/config-120 && /home/wooo/kubectl logs deployment/awoooi-api -n awoooi-prod --tail 50 2>&1\")",
|
||||
"Bash(ssh wooo@192.168.0.110 \"export KUBECONFIG=/home/wooo/.kube/config-120 && /home/wooo/kubectl rollout restart deployment/awoooi-api -n awoooi-prod && /home/wooo/kubectl rollout status deployment/awoooi-api -n awoooi-prod --timeout=120s\")",
|
||||
"Bash(curl -sf https://awoooi.wooo.work/api/v1/health)",
|
||||
"Bash(curl -skf https://awoooi.wooo.work/api/v1/health)",
|
||||
"Bash(ssh wooo@192.168.0.110 \"export KUBECONFIG=/home/wooo/.kube/config-120 && /home/wooo/kubectl logs deployment/awoooi-api -n awoooi-prod --tail 40 2>&1\")",
|
||||
"Bash(for i:*)",
|
||||
"Bash(do curl:*)",
|
||||
"Bash(echo \"Request $i sent\")",
|
||||
"Bash(done)",
|
||||
"Bash(ssh wooo@192.168.0.110 \"export KUBECONFIG=/home/wooo/.kube/config-120 && /home/wooo/kubectl logs deployment/awoooi-api -n awoooi-prod --tail 100 2>&1\")",
|
||||
"Bash(ssh wooo@192.168.0.110 \"export KUBECONFIG=/home/wooo/.kube/config-120 && /home/wooo/kubectl logs deployment/awoooi-api -n awoooi-prod --tail 30 2>&1\")",
|
||||
"Bash(ssh wooo@192.168.0.110 \"export KUBECONFIG=/home/wooo/.kube/config-120 && /home/wooo/kubectl get configmap awoooi-config -n awoooi-prod -o yaml | grep OTEL\")",
|
||||
"Bash(ssh wooo@192.168.0.110 \"export KUBECONFIG=/home/wooo/.kube/config-120 && /home/wooo/kubectl exec deployment/awoooi-api -n awoooi-prod -- env | grep OTEL\")",
|
||||
"Bash(ssh wooo@192.168.0.110 \"export KUBECONFIG=/home/wooo/.kube/config-120 && /home/wooo/kubectl exec deployment/awoooi-api -n awoooi-prod -- python -c \"\"import socket; s=socket.socket\\(\\); s.settimeout\\(5\\); s.connect\\(\\(''192.168.0.188'', 24317\\)\\); print\\(''✅ Connection to 24317 OK''\\); s.close\\(\\)\"\" 2>&1\")",
|
||||
"Bash(curl -vI https://awoooi.wooo.work)",
|
||||
"Bash(curl -vI https://awoooi.wooo.work/api/v1/health)",
|
||||
"Bash(curl -sf -X POST https://awoooi.wooo.work/api/v1/webhooks/signals -H \"Content-Type: application/json\" -d '{:*)",
|
||||
"Bash(curl -s -X POST https://awoooi.wooo.work/api/v1/webhooks/signals -H \"Content-Type: application/json\" -d '{\"\"source\"\": \"\"prometheus\"\", \"\"severity\"\": \"\"P1\"\", \"\"message\"\": \"\"Test alert from CLI\"\"}')",
|
||||
"Bash(curl -s -X POST https://awoooi.wooo.work/api/v1/webhooks/signals -H \"Content-Type: application/json\" -d '{:*)",
|
||||
"Bash(ssh wooo@192.168.0.110 \"export KUBECONFIG=/home/wooo/.kube/config-120 && /home/wooo/kubectl get secret awoooi-secrets -n awoooi-prod -o jsonpath=''''{.data.WEBHOOK_HMAC_SECRET}'''' 2>/dev/null\")",
|
||||
"Bash(timeout 15 curl -N -s https://awoooi.wooo.work/api/v1/dashboard/stream)",
|
||||
"Bash(bash:*)",
|
||||
"Bash(curl -s https://awoooi.wooo.work/api/v1/metrics/gold)",
|
||||
"Bash(curl -s 'http://192.168.0.188:8123/' --data \"SELECT DISTINCT metric_name FROM signoz_metrics.distributed_samples_v4 WHERE unix_milli > \\(toUnixTimestamp\\(now\\(\\)\\) - 1800\\) * 1000 LIMIT 20 FORMAT TabSeparated\")",
|
||||
"Bash(curl -s 'http://192.168.0.188:8123/' --data \"SELECT count\\(\\) as trace_count FROM signoz_traces.distributed_signoz_index_v2 WHERE timestamp > now\\(\\) - INTERVAL 30 MINUTE FORMAT TabSeparated\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"KUBECONFIG=/home/wooo/.kube/config-120 /home/wooo/bin/kubectl get configmap awoooi-config -n awoooi-prod -o jsonpath=''{.data}'' | python3 -m json.tool 2>/dev/null | head -30\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"KUBECONFIG=/home/wooo/.kube/config-120 /home/wooo/bin/kubectl logs deployment/awoooi-api -n awoooi-prod --tail 50 2>&1\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"which kubectl || ls -la ~/bin/kubectl 2>/dev/null || ls -la /usr/local/bin/kubectl 2>/dev/null || echo ''kubectl not found''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"export KUBECONFIG=/home/wooo/.kube/config-120 && kubectl get configmap awoooi-config -n awoooi-prod -o jsonpath=''{.data}'' 2>&1\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"ls -la ~/.kube/ 2>/dev/null; cat ~/.kube/config 2>/dev/null | head -20 || echo ''checking k3s default...''; sudo cat /etc/rancher/k3s/k3s.yaml 2>/dev/null | head -5 || echo ''no k3s config''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"sudo k3s kubectl get configmap awoooi-config -n awoooi-prod -o yaml 2>&1\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"sudo k3s kubectl logs deployment/awoooi-api -n awoooi-prod --tail 100 2>&1\")",
|
||||
"Bash(nc -zv 192.168.0.188 24317)",
|
||||
"Bash(curl -s http://192.168.0.188:24318/v1/traces -X POST -H \"Content-Type: application/json\" -d '{}')",
|
||||
"Bash(curl -s 'http://192.168.0.188:8123/' --data \"SELECT DISTINCT serviceName, count\\(\\) as cnt FROM signoz_traces.distributed_signoz_index_v2 WHERE timestamp > now\\(\\) - INTERVAL 24 HOUR GROUP BY serviceName ORDER BY cnt DESC LIMIT 20 FORMAT TabSeparated\")",
|
||||
"Bash(curl -s 'http://192.168.0.188:8123/' --data \"DESCRIBE TABLE signoz_traces.distributed_signoz_index_v2 FORMAT TabSeparated\")",
|
||||
"Bash(curl -s 'http://192.168.0.188:8123/' --data \"SELECT serviceName, count\\(\\) as cnt FROM signoz_traces.distributed_signoz_index_v2 WHERE timestamp > now\\(\\) - INTERVAL 5 MINUTE GROUP BY serviceName ORDER BY cnt DESC LIMIT 10 FORMAT TabSeparated\")",
|
||||
"Bash(curl -s https://awoooi.wooo.work/api/v1/health)",
|
||||
"Bash(curl -s 'http://192.168.0.188:8123/' --data \"SELECT serviceName, count\\(\\) as cnt FROM signoz_traces.distributed_signoz_index_v2 WHERE timestamp > now\\(\\) - INTERVAL 10 MINUTE GROUP BY serviceName ORDER BY cnt DESC LIMIT 10 FORMAT TabSeparated\")",
|
||||
"Bash(curl -s 'http://192.168.0.188:8123/' --data \"SELECT service_name, count\\(\\) as cnt FROM signoz_logs.distributed_logs WHERE timestamp > now\\(\\) - INTERVAL 30 MINUTE GROUP BY service_name ORDER BY cnt DESC LIMIT 10 FORMAT TabSeparated\")",
|
||||
"Bash(curl -s 'http://192.168.0.188:8123/' --data \"SHOW TABLES FROM signoz_logs FORMAT TabSeparated\")",
|
||||
"Bash(curl -s 'http://192.168.0.188:8123/' --data \"SELECT count\\(\\) as total FROM signoz_logs.distributed_logs_v2 WHERE timestamp > now\\(\\) - INTERVAL 30 MINUTE FORMAT TabSeparated\")",
|
||||
"Bash(curl -s 'http://192.168.0.188:8123/' --data \"SELECT JSONExtractString\\(resources_string, ''service.name''\\) as svc, count\\(\\) as cnt FROM signoz_logs.distributed_logs_v2 WHERE timestamp > now\\(\\) - INTERVAL 5 MINUTE GROUP BY svc ORDER BY cnt DESC LIMIT 10 FORMAT TabSeparated\")",
|
||||
"Bash(curl -s 'http://192.168.0.188:8123/' --data \"DESCRIBE TABLE signoz_logs.distributed_logs_v2 FORMAT TabSeparated\")",
|
||||
"Bash(curl -s 'http://192.168.0.188:8123/' --data \"SELECT resources_string[''service.name''] as svc, count\\(\\) as cnt FROM signoz_logs.distributed_logs_v2 WHERE timestamp > \\(toUnixTimestamp64Nano\\(now64\\(\\)\\) - 300000000000\\) GROUP BY svc ORDER BY cnt DESC LIMIT 10 FORMAT TabSeparated\")",
|
||||
"Bash(curl -s 'http://192.168.0.188:8123/' --data \"SELECT body, resources_string FROM signoz_logs.distributed_logs_v2 WHERE timestamp > \\(toUnixTimestamp64Nano\\(now64\\(\\)\\) - 60000000000\\) LIMIT 1 FORMAT JSONEachRow\")",
|
||||
"Bash(curl -s 'http://192.168.0.188:8123/' --data \"SELECT serviceName, count\\(\\) as cnt FROM signoz_traces.distributed_signoz_index_v2 WHERE timestamp > now\\(\\) - INTERVAL 2 MINUTE GROUP BY serviceName ORDER BY cnt DESC LIMIT 10 FORMAT TabSeparated\")",
|
||||
"Bash(curl -s 'http://192.168.0.188:8123/' --data \"SELECT serviceName, name, timestamp FROM signoz_traces.distributed_signoz_index_v2 WHERE timestamp > now\\(\\) - INTERVAL 5 MINUTE ORDER BY timestamp DESC LIMIT 5 FORMAT TabSeparated\")",
|
||||
"Bash(curl -s 'http://192.168.0.188:8123/' --data \"SELECT serviceName, name, formatDateTime\\(timestamp, ''%Y-%m-%d %H:%M:%S''\\) as ts FROM signoz_traces.distributed_signoz_index_v2 ORDER BY timestamp DESC LIMIT 10 FORMAT TabSeparated\")",
|
||||
"Bash(curl -s 'http://192.168.0.188:8123/' --data \"SELECT count\\(\\) FROM signoz_traces.distributed_signoz_index_v2 FORMAT TabSeparated\")",
|
||||
"Bash(curl -s 'http://192.168.0.188:8123/' --data \"SELECT count\\(\\) FROM signoz_traces.distributed_signoz_spans FORMAT TabSeparated\")",
|
||||
"Bash(ssh wooo@192.168.0.188 \"docker ps | grep -E ''otel|signoz''\")",
|
||||
"Bash(curl -s 'http://192.168.0.188:8123/' --data \"SELECT metric_name, sum\\(value\\) as total FROM signoz_metrics.distributed_samples_v4 WHERE metric_name LIKE ''otelcol%span%'' AND unix_milli > \\(toUnixTimestamp\\(now\\(\\)\\) - 300\\) * 1000 GROUP BY metric_name FORMAT TabSeparated\")",
|
||||
"Bash(for t:*)",
|
||||
"Bash(do)",
|
||||
"Bash(echo -n \"$t: \")",
|
||||
"Bash(curl -s 'http://192.168.0.188:8123/' --data \"SELECT count\\(\\) FROM signoz_traces.$t FORMAT TabSeparated\")",
|
||||
"Bash(curl -s 'http://192.168.0.188:8123/' --data \"SELECT serviceName, count\\(\\) as cnt FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp > now\\(\\) - INTERVAL 10 MINUTE GROUP BY serviceName ORDER BY cnt DESC LIMIT 10 FORMAT TabSeparated\")",
|
||||
"Bash(curl -s 'http://192.168.0.188:8123/' --data \":*)",
|
||||
"Bash(curl -s 'http://192.168.0.188:8123/' --data \"DESCRIBE TABLE signoz_traces.distributed_signoz_index_v3 FORMAT TabSeparated\")",
|
||||
"Bash(AWOOOI_API_URL=https://awoooi.wooo.work WEBHOOK_HMAC_SECRET=\"CHANGE_ME_TO_RANDOM_64_CHARS\" python scripts/fire_live_alert.py oomkilled)",
|
||||
"Bash(timeout 10 curl -sN https://awoooi.wooo.work/api/v1/dashboard/stream)",
|
||||
"Bash(curl -s https://awoooi.wooo.work/api/v1/dashboard)",
|
||||
"Bash(npm list:*)",
|
||||
"Bash(node scripts/verify-frontend.js)",
|
||||
"Bash(node /Users/ogt/awoooi/scripts/verify-frontend.js)",
|
||||
"Bash(python -c \"from src.services.proposal_service import ProposalService; print\\(''''✅ ProposalService OK''''\\)\")",
|
||||
"Bash(python -c \"from src.services.openclaw import OpenClawService; print\\(''''✅ OpenClawService OK''''\\)\")",
|
||||
"Bash(curl -s http://192.168.0.120:32334/api/v1/incidents)",
|
||||
"Bash(jq -r \".incidents[:2] | .[] | \"\"\\\\\\(.incident_id\\) - \\\\\\(.status\\) - \\\\\\(.severity\\)\"\"\")",
|
||||
"Bash(curl -s -X POST \"http://192.168.0.120:32334/api/v1/incidents/INC-20260322-4B3152/propose\" -H \"Content-Type: application/json\")",
|
||||
"Bash(kubectl logs:*)",
|
||||
"Bash(ssh ogt@192.168.0.120 \"kubectl logs deployment/awoooi-api -n awoooi-prod --tail 30\")",
|
||||
"Bash(curl -sv -X POST \"http://192.168.0.120:32334/api/v1/incidents/INC-20260322-4B3152/propose\" -H \"Content-Type: application/json\")",
|
||||
"Bash(curl -s http://192.168.0.120:32334/api/v1/health)",
|
||||
"Bash(curl -s \"http://192.168.0.120:32334/api/v1/incidents/INC-20260322-4B3152\")",
|
||||
"Bash(curl -sv \"http://192.168.0.120:32334/api/v1/incidents\")",
|
||||
"Bash(curl -s --retry 3 --retry-delay 2 \"http://192.168.0.120:32334/api/v1/health\")",
|
||||
"Bash(curl -s --retry 3 --retry-delay 2 http://192.168.0.120:32334/api/v1/health)",
|
||||
"Bash(do echo:*)",
|
||||
"Bash(curl -s -X POST \"https://awoooi.wooo.work/api/v1/incidents/INC-20260322-4B3152/propose\" -H \"Content-Type: application/json\")",
|
||||
"Bash(curl -s -X POST \"https://awoooi.wooo.work/api/v1/incidents/INC-20260322-4B3152/proposal\" -H \"Content-Type: application/json\")",
|
||||
"Bash(curl -s -X POST \"https://awoooi.wooo.work/api/v1/incidents/INC-20260322-D6C6A0/proposal\" -H \"Content-Type: application/json\")",
|
||||
"Bash(curl -s http://192.168.0.120:32334/api/v1/approvals/pending)",
|
||||
"Bash(kubectl get:*)",
|
||||
"Bash(curl -s -w \"\\\\nHTTP_CODE: %{http_code}\\\\n\" http://192.168.0.120:32334/api/v1/health)",
|
||||
"Bash(curl -s http://awoooi.wooo.work/api/v1/health)",
|
||||
"Bash(curl -s http://awoooi.wooo.work/api/v1/approvals/pending)",
|
||||
"Bash(curl -sL https://awoooi.wooo.work/api/v1/approvals/pending -k)",
|
||||
"Bash(ssh root@192.168.0.120 \"kubectl get pods -n awoooi-prod -o wide\")",
|
||||
"Bash(ssh root@192.168.0.120 \"kubectl logs -n awoooi-prod -l app=awoooi-api --tail=30\")",
|
||||
"Bash(curl -sL https://awoooi.wooo.work/api/v1/timeline -k)",
|
||||
"Bash(curl -sL https://awoooi.wooo.work/api/v1/incidents -k)",
|
||||
"Bash(curl -sL \"https://awoooi.wooo.work/api/v1/approvals?include_history=true\" -k)",
|
||||
"Bash(curl -sL \"https://awoooi.wooo.work/api/v1/incidents/INC-20260322-4B3152\" -k)",
|
||||
"Bash(curl -sL \"https://awoooi.wooo.work/api/v1/audit-logs?limit=10\" -k)",
|
||||
"Bash(curl -sL https://awoooi.wooo.work/api/v1/audit-logs?limit=10 -k)",
|
||||
"Bash(ssh ogt@192.168.0.120 \"kubectl logs -n awoooi-prod -l app=awoooi-api --tail=100\")",
|
||||
"Bash(ssh ogt@192.168.0.120 \"kubectl logs -n awoooi-prod -l app=awoooi-web --tail=50\")",
|
||||
"Bash(ssh ogt@192.168.0.188 \"kubectl --kubeconfig=/etc/rancher/k3s/k3s.yaml logs -n awoooi-prod -l app=awoooi-api --tail=100 2>/dev/null || docker logs awoooi-api --tail=100 2>/dev/null\")",
|
||||
"Bash(curl -sL \"https://awoooi.wooo.work/api/v1/approvals/pending\" -k -w \"\\\\n\\\\nHTTP: %{http_code}\\\\nTime: %{time_total}s\\\\n\")",
|
||||
"Bash(curl -sL -X POST https://awoooi.wooo.work/api/v1/approvals/182e07c1-118a-49d7-b71c-7d33c5484d9b/sign -H 'Content-Type: application/json' -d '{\"\"\"\"signer_id\"\"\"\": \"\"\"\"test-debug\"\"\"\", \"\"\"\"signer_name\"\"\"\": \"\"\"\"Debug Test\"\"\"\", \"\"\"\"comment\"\"\"\": \"\"\"\"Testing\"\"\"\"}' -k)",
|
||||
"Bash(curl -s https://wwooo.aiops.tw/api/v1/health)",
|
||||
"Bash(curl -s https://wwooo.aiops.tw/api/v1/incidents?limit=5)",
|
||||
"Bash(curl -s https://wwooo.aiops.tw/api/v1/approvals/pending)",
|
||||
"Bash(curl -v -s \"https://wwooo.aiops.tw/api/v1/health\")",
|
||||
"Bash(curl -s \"https://wwooo.aiops.tw/\")",
|
||||
"Bash(curl -s --connect-timeout 5 \"http://192.168.0.120:32334/api/v1/health\")",
|
||||
"Bash(curl -s --connect-timeout 5 \"http://192.168.0.120:32334/api/v1/incidents?limit=5\")",
|
||||
"Bash(ssh -o ConnectTimeout=5 wooo@192.168.0.120 \"kubectl get pods -n awoooi-prod\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl logs awoooi-worker-867f67f55d-kvdl2 -n awoooi-prod --tail=50\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get pods -n awoooi-prod | grep -E ''NAME|worker''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get pods -n awoooi-prod | grep worker\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl logs awoooi-worker-5bdc5699bb-kcv9q -n awoooi-prod --tail=30\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get networkpolicy -n awoooi-prod -o wide\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get pods -n awoooi-prod --show-labels | grep worker\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get networkpolicy allow-required-egress -n awoooi-prod -o yaml\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl patch networkpolicy allow-required-egress -n awoooi-prod --type=''json'' -p=''[{\"\"op\"\": \"\"replace\"\", \"\"path\"\": \"\"/spec/podSelector/matchLabels\"\", \"\"value\"\": {\"\"system\"\": \"\"awoooi\"\"}}]''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl rollout restart deployment/awoooi-worker -n awoooi-prod\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl logs awoooi-worker-5bdc5699bb-kcv9q -n awoooi-prod --tail=15\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl logs deployment/awoooi-worker -n awoooi-prod --tail=40\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl logs deployment/awoooi-worker -n awoooi-prod 2>&1 | grep -E ''signal_worker|redis_pool|INFO'' | tail -10\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"curl -s http://localhost:32334/api/v1/health\")",
|
||||
"Bash(ssh wooo@192.168.0.120 'curl -s -X POST \"\"http://localhost:32334/api/v1/webhooks/signals\"\" -H \"\"Content-Type: application/json\"\" -d \"\"{:*)",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get pods -n awoooi-prod | grep -E ''NAME|worker|api''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get pods -n awoooi-prod && echo ''==='' && kubectl logs deployment/awoooi-worker -n awoooi-prod --tail=30\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"curl -s http://localhost:32334/api/v1/incidents?limit=5\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"curl -s http://localhost:32334/api/v1/approvals/pending\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl logs deployment/awoooi-worker -n awoooi-prod 2>&1 | head -50\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"curl -s http://localhost:32334/api/v1/health | jq ''.components''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get secret -n awoooi-prod -o name\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get secret awoooi-secrets -n awoooi-prod -o jsonpath=''{.data.WEBHOOK_HMAC_SECRET}'' | base64 -d\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl logs deployment/awoooi-worker -n awoooi-prod --tail=20 2>&1 | grep -E ''signal|incident|telegram|INFO''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl logs deployment/awoooi-worker -n awoooi-prod --tail=30\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"curl -s ''http://localhost:32334/api/v1/incidents?limit=5''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl logs deployment/awoooi-worker -n awoooi-prod 2>&1 | grep -iE ''telegram|notification|send'' | tail -10\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"curl -s ''http://localhost:32334/api/v1/approvals/pending''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"curl -s ''http://localhost:32334/api/v1/incidents?limit=2'' && echo ''---'' && curl -s ''http://localhost:32334/api/v1/approvals/pending''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get pods -n awoooi-prod | grep worker && echo ''---'' && kubectl logs deployment/awoooi-worker -n awoooi-prod --tail=30\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl logs awoooi-worker-6b8cc94d9c-xjdwr -n awoooi-prod --tail=40\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get networkpolicy allow-required-egress -n awoooi-prod -o jsonpath=''{.spec.podSelector}''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl patch networkpolicy allow-required-egress -n awoooi-prod --type=''json'' -p=''[{\"\"op\"\": \"\"replace\"\", \"\"path\"\": \"\"/spec/podSelector\"\", \"\"value\"\": {\"\"matchLabels\"\": {\"\"system\"\": \"\"awoooi\"\"}}}]''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl delete pod awoooi-worker-6b8cc94d9c-xjdwr -n awoooi-prod\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl logs awoooi-worker-6b8cc94d9c-pmzj7 -n awoooi-prod --tail=30\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl logs awoooi-worker-6b8cc94d9c-pmzj7 -n awoooi-prod --tail=20\")",
|
||||
"Bash(ls -la /Users/ogt/awoooi/apps/api/scripts/fire*.py)",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl logs deployment/awoooi-worker -n awoooi-prod --tail=50\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"curl -s ''http://localhost:32334/api/v1/incidents?limit=3''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl logs deployment/awoooi-worker -n awoooi-prod 2>&1 | grep -iE ''proposal|approval|llm|ai|ollama|generate'' | tail -20\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get deployment awoooi-worker -n awoooi-prod -o jsonpath=''{.spec.template.spec.containers[0].envFrom}''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get deployment awoooi-api -n awoooi-prod -o jsonpath=''{.spec.template.spec.containers[0].envFrom}''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get configmap awoooi-config -n awoooi-prod -o jsonpath=''''{.data}''''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get secret awoooi-secrets -n awoooi-prod -o jsonpath=''{.data}'' | tr '','' ''\\\\n''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl exec deployment/awoooi-api -n awoooi-prod -- python -c ''import os; print\\(os.getenv\\(\"\"DATABASE_URL\"\", \"\"NOT SET\"\"\\)[:50]\\)''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl logs awoooi-api-75ffbfb88b-2htfh -n awoooi-prod --tail=50\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl exec awoooi-api-6687db5564-rv755 -n awoooi-prod -- env | grep DATABASE\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"PGPASSWORD=''CHANGE_ME'' psql -h 192.168.0.188 -U awoooi -d awoooi_prod -c ''SELECT 1'' 2>&1 || echo ''Connection failed''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get pods -n awoooi-prod\")",
|
||||
"Bash(curl -sv http://192.168.0.120:32334/api/v1/health)",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl logs awoooi-api-75ffbfb88b-2htfh -n awoooi-prod --tail=20 2>&1\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl logs awoooi-worker-7fb7d5b55f-n48gk -n awoooi-prod --tail=20 2>&1\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get rs -n awoooi-prod\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl scale rs awoooi-api-75ffbfb88b -n awoooi-prod --replicas=0\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl scale rs awoooi-worker-7fb7d5b55f -n awoooi-prod --replicas=0\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl logs deployment/awoooi-worker -n awoooi-prod --tail=10\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get deploy -n awoooi-prod -o wide\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get deploy awoooi-api -n awoooi-prod -o jsonpath=''{.spec.replicas}''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get deploy awoooi-worker -n awoooi-prod -o jsonpath=''{.spec.replicas}''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl rollout status deployment/awoooi-api -n awoooi-prod --timeout=5s\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl rollout history deployment/awoooi-api -n awoooi-prod\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl rollout undo deployment/awoooi-api -n awoooi-prod\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl rollout undo deployment/awoooi-worker -n awoooi-prod\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl rollout status deployment/awoooi-api -n awoooi-prod --timeout=30s\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get rs awoooi-api-6687db5564 -n awoooi-prod -o jsonpath=''{.metadata.annotations.deployment\\\\.kubernetes\\\\.io/revision}''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl delete pod awoooi-api-7f487f7cbb-5f88g -n awoooi-prod\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl rollout undo deployment/awoooi-api -n awoooi-prod --to-revision=46\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl logs deployment/awoooi-worker -n awoooi-prod --tail=15\")",
|
||||
"Bash(curl -s http://192.168.0.120:32334/api/v1/incidents?limit=3)",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl logs deployment/awoooi-worker -n awoooi-prod --since=2m\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl logs deployment/awoooi-api -n awoooi-prod --since=2m | grep -i webhook\")",
|
||||
"Bash(curl -sv -X POST http://192.168.0.120:32334/api/v1/webhooks/alertmanager -H \"Content-Type: application/json\" -d '{:*)",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get endpoints -n awoooi-prod\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"curl -s http://localhost:32334/api/v1/health | jq ''{status}''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl logs deployment/awoooi-worker -n awoooi-prod --since=30s\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl logs awoooi-api-fc4744758-7wfv5 -n awoooi-prod --tail=30 2>&1\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl logs awoooi-worker-6fc548887b-b9mtf -n awoooi-prod --tail=30 2>&1\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get configmap awoooi-config -n awoooi-prod -o yaml\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get secret awoooi-secrets -n awoooi-prod -o jsonpath=''''{.data}''''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get pod awoooi-worker-6fc548887b-b9mtf -n awoooi-prod -o jsonpath=''{.metadata.labels}''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get networkpolicy -n awoooi-prod -o yaml\")",
|
||||
"Bash(ssh wooo@192.168.0.120 'kubectl patch networkpolicy allow-required-egress -n awoooi-prod --type=json -p=\"\"[{\\\\\"\"op\\\\\"\": \\\\\"\"replace\\\\\"\", \\\\\"\"path\\\\\"\": \\\\\"\"/spec/podSelector/matchLabels\\\\\"\", \\\\\"\"value\\\\\"\": {\\\\\"\"system\\\\\"\": \\\\\"\"awoooi\\\\\"\"}}]\"\"')",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl rollout restart deployment/awoooi-api deployment/awoooi-worker -n awoooi-prod\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl logs awoooi-api-6c69b77894-d6jqq -n awoooi-prod --tail=20\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl run nc-test --rm -it --restart=Never --image=busybox -- nc -zv 192.168.0.188 5432\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get pods -n awoooi-prod -o=custom-columns=''NAME:.metadata.name,IMAGE:.spec.containers[0].image''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl exec awoooi-api-6687db5564-rv755 -n awoooi-prod -- ls -la *.db 2>/dev/null || echo ''No SQLite files''\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl exec awoooi-api-6687db5564-rv755 -n awoooi-prod -- env | grep -E ''MOCK|DATABASE|SQLITE''\")",
|
||||
"Bash(curl -s \"http://192.168.0.120:32334/api/v1/approvals\")",
|
||||
"Bash(python -m py_compile src/lewooogo_brain/engines/incident_engine.py src/lewooogo_brain/engines/proposal_engine.py src/lewooogo_brain/skills/loader.py)",
|
||||
"Bash(python packages/lewooogo-brain/tests/test_skill_loader.py)",
|
||||
"Bash(python packages/lewooogo-brain/tests/test_incident_engine.py)",
|
||||
"Bash(python packages/lewooogo-brain/tests/test_guardrails.py)",
|
||||
"Bash(python -m py_compile src/lewooogo_brain/engines/proposal_engine.py src/lewooogo_brain/engines/incident_engine.py src/lewooogo_brain/skills/loader.py)",
|
||||
"Bash(PYTHONPATH=/Users/ogt/awoooi/packages/lewooogo-brain/src python -c \":*)",
|
||||
"Bash(curl -s --connect-timeout 5 http://192.168.0.188:8000/api/v1/health)",
|
||||
"Bash(curl -s \"https://awoooi.wooo.work/api/v1/approvals/pending\")",
|
||||
"Bash(curl -s \"https://awoooi.wooo.work/api/v1/approvals?status=pending\")",
|
||||
"Bash(curl -s \"https://awoooi.wooo.work/api/v1/incidents\")",
|
||||
"Bash(uv sync:*)",
|
||||
"Bash(python -c \"from src.routers.proposals import router; print\\(''✅ Router 語法驗證通過''\\)\")",
|
||||
"Bash(curl -s -X GET \"https://awoooi.wooo.work/api/v1/health\" --connect-timeout 10)",
|
||||
"Bash(curl -s -X GET \"https://awoooi.wooo.work/api/v1/incidents\" --connect-timeout 10)",
|
||||
"Bash(curl -s -o /dev/null -w \"%{http_code}\" \"https://awoooi.wooo.work\" --connect-timeout 10)",
|
||||
"Bash(curl -s -o /dev/null -w \"%{http_code}\" -L \"https://awoooi.wooo.work\" --connect-timeout 10)",
|
||||
"Bash(curl -s -X POST \"https://awoooi.wooo.work/api/v1/incidents/test-123/propose\" -H \"Content-Type: application/json\" -d '{\"\"require_dry_run\"\": true}' --connect-timeout 10)",
|
||||
"Bash(ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no ollama@192.168.0.120 \"kubectl get pods -n awoooi-prod -o wide\")",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl get pods -n awoooi-prod)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl logs awoooi-api-64c8659cff-grslz -n awoooi-prod --tail=50)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl get secret awoooi-secrets -n awoooi-prod -o jsonpath='{.data.DATABASE_URL}')",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl rollout restart deployment/awoooi-api -n awoooi-prod)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl get pods -n awoooi-prod -l app=awoooi-api)",
|
||||
"Bash(curl -s \"https://awoooi.wooo.work/api/v1/health\" --connect-timeout 10)",
|
||||
"Bash(curl -s -o /dev/null -w \"%{http_code}\" -L \"https://awoooi.wooo.work/zh-TW\" --connect-timeout 10)",
|
||||
"Bash(python -c \"from src.routers.proposals import router; print\\(''✅ Router import successful''\\)\")",
|
||||
"Bash(PGPASSWORD=postgres psql -h 192.168.0.188 -U awoooi -d awoooi_dev -c \"SELECT incident_id, status, severity FROM incidents LIMIT 5;\")",
|
||||
"Bash(PGPASSWORD=AwoooiProd2026 psql -h 192.168.0.188 -U awoooi -d awoooi_prod -c \"SELECT incident_id, status, severity FROM incidents LIMIT 5;\")",
|
||||
"Bash(curl -sf http://192.168.0.120:32334/api/v1/incidents)",
|
||||
"Bash(curl -v \"http://192.168.0.120:32334/api/v1/incidents\")",
|
||||
"Bash(export KUBECONFIG=/Users/ogt/.kube/config-120)",
|
||||
"Bash(curl -sI \"http://awoooi.wooo.work/\")",
|
||||
"Bash(openssl s_client -servername awoooi.wooo.work -connect awoooi.wooo.work:443)",
|
||||
"Bash(openssl x509:*)",
|
||||
"Bash(curl -s -X POST \"http://192.168.0.120:32334/api/v1/incidents/INC-20260323-7DE10B/propose\" -H \"Content-Type: application/json\" -d '{\"\"\"\"require_dry_run\"\"\"\": true}')",
|
||||
"Bash(python -c \"from src.services.executor import execute_approved_proposal, get_executor, ActionExecutor; print\\(''✅ Import successful''\\)\")",
|
||||
"Bash(curl -s https://awoooi.woooo.cc/api/v1/incidents)",
|
||||
"Bash(curl -s https://awoooi.woooo.cc/api/v1/health)",
|
||||
"Bash(curl -s --connect-timeout 10 https://awoooi.woooo.cc/api/v1/health)",
|
||||
"Bash(ssh ogt@192.168.70.202 \"sudo kubectl get pods -n awoooi 2>/dev/null\")",
|
||||
"Bash(curl -s --connect-timeout 5 http://192.168.70.200:8000/api/v1/health)",
|
||||
"Bash(ssh ogt@192.168.70.202 \"sudo kubectl get pods -n awoooi-prod\")",
|
||||
"Bash(ssh -o StrictHostKeyChecking=no ogt@192.168.70.202 \"sudo kubectl get pods -n awoooi-prod\")",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl get pods -A)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl logs -n awoooi-prod awoooi-worker-7479556d76-jbbps --tail 30)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl logs -n awoooi-prod -l app=awoooi-api --tail 20)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl exec -n awoooi-prod deployment/awoooi-api -- curl -s http://localhost:8000/api/v1/incidents)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl exec -n awoooi-prod deployment/awoooi-api -- python -c \"import httpx; r = httpx.get\\(''http://localhost:8000/api/v1/incidents''\\); print\\(r.text\\)\")",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl get ingress -n awoooi-prod -o wide)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl get svc -n awoooi-prod)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl get deployment awoooi-worker -n awoooi-prod -o jsonpath='{.spec.template.spec.containers[0].env}')",
|
||||
"Bash(curl -s --connect-timeout 5 http://192.168.70.202:32334/api/v1/health)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl describe deployment awoooi-worker -n awoooi-prod)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl get configmap -n awoooi-prod)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl describe deployment awoooi-api -n awoooi-prod)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl get configmap awoooi-config -n awoooi-prod -o yaml)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl get secrets -n awoooi-prod)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl get secret awoooi-secrets -n awoooi-prod -o jsonpath='{.data}')",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl get secret awoooi-secrets -n awoooi-prod -o jsonpath='{.data.REDIS_URL}')",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl rollout restart deployment/awoooi-worker -n awoooi-prod)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl get pods -n awoooi-prod -l app=awoooi-worker)",
|
||||
"Bash(curl -s --connect-timeout 5 https://awoooi.wooo.work/api/v1/health)",
|
||||
"Bash(curl -s https://awoooi.wooo.work/api/v1/incidents)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl logs -n awoooi-prod -l app=awoooi-worker --tail 10)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl get svc -n wooo-aiops-prod)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl get svc -A)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl logs -n awoooi-prod awoooi-worker-76bdf9786d-rvtmz --tail 15)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl exec -n awoooi-prod deployment/awoooi-api -- python -c \"import os; print\\(os.getenv\\(''REDIS_URL'', ''NOT_SET''\\)\\)\")",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl get deployment awoooi-api -n awoooi-prod -o yaml)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl rollout restart deployment/awoooi-api deployment/awoooi-worker -n awoooi-prod)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl logs -n awoooi-prod awoooi-api-865cdc97db-6mpzz --tail 20)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl get pods -n wooo-aiops-prod -l app=redis)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl get pods -n wooo-aiops-prod)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl exec -n wooo-aiops-prod redis-6c6fcd64b8-8wznx -- redis-cli ping)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl exec -n awoooi-prod awoooi-api-6445c76797-mrl7p -- python -c \"import redis; r=redis.Redis\\(host=''10.43.239.47'', port=6379, db=10\\); print\\(r.ping\\(\\)\\)\")",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl get networkpolicy -A)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl get networkpolicy allow-required-egress -n awoooi-prod -o yaml)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl patch networkpolicy allow-required-egress -n awoooi-prod --type='json' -p='[{\"\"op\"\": \"\"add\"\", \"\"path\"\": \"\"/spec/egress/0/ports/-\"\", \"\"value\"\": {\"\"port\"\": 6379, \"\"protocol\"\": \"\"TCP\"\"}}]')",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl logs -n awoooi-prod awoooi-api-5fcc484b85-qpwt6 --tail 15)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl exec -n awoooi-prod awoooi-api-6445c76797-mrl7p -- python -c \"import os; print\\(''REDIS_URL:'', os.getenv\\(''REDIS_URL''\\)\\); import redis; r=redis.Redis.from_url\\(os.getenv\\(''REDIS_URL''\\)\\); print\\(''PING:'', r.ping\\(\\)\\)\")",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl logs -n awoooi-prod awoooi-worker-59d7588d75-p5tht --tail 20)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl logs -n awoooi-prod -l app=awoooi-worker --tail 30)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl get deployment awoooi-worker -n awoooi-prod -o yaml)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl get networkpolicy -n awoooi-prod -o wide)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl apply -f -)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl logs -n awoooi-prod awoooi-worker-6cd7dcbc9-5mtfq --tail 15)",
|
||||
"Bash(jq .incidents[0])",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl get configmap awoooi-config -n awoooi-prod -o jsonpath='{.data.OPENCLAW_URL}')",
|
||||
"Bash(curl -s --connect-timeout 5 http://192.168.0.188:8088/health)",
|
||||
"Bash(curl -s --connect-timeout 5 http://192.168.0.188:8088/)",
|
||||
"Bash(nc -zv 192.168.0.188 8088 -w 5)",
|
||||
"Bash(ping -c 2 192.168.0.188)",
|
||||
"Bash(ping -c 2 192.168.70.202)",
|
||||
"Bash(grep -n \"mapToDualState\" /Users/ogt/awoooi/apps/web/src/app/[locale]/page.tsx -A 30)",
|
||||
"Bash(head -40 /Users/ogt/awoooi/apps/web/src/app/[locale]/page.tsx)",
|
||||
"Bash(ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no ollama@192.168.0.188 \"docker ps -a | grep -i claw; docker start openclaw 2>/dev/null || docker start clawbot 2>/dev/null || echo ''Container not found, listing all:'' && docker ps -a --format ''table {{.Names}}\\\\t{{.Status}}'' | head -10\")",
|
||||
"Bash(curl -s --connect-timeout 5 http://192.168.0.188:8089/health)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl rollout status deployment/awoooi-web -n awoooi-prod --timeout=60s)",
|
||||
"Bash(grep -rn \"clawbot\\\\|ClawBot\" /Users/ogt/awoooi/ --include=*.yaml --include=*.yml --include=*.json)",
|
||||
"Bash(grep -rn \"ClawBot\\\\|clawbot\" /Users/ogt/awoooi/apps/ --include=*.py --include=*.ts --include=*.tsx)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl logs deployment/awoooi-api -n awoooi-prod --tail=100)",
|
||||
"Bash(KUBECONFIG=/Users/ogt/awoooi/apps/api/k3s-prod.yaml kubectl logs deployment/awoooi-api -n awoooi-prod --tail=200)",
|
||||
"Bash(export KUBECONFIG=/Users/ogt/awoooi/k3s-prod.yaml)",
|
||||
"Bash(ssh root@192.168.0.120 \"kubectl logs deployment/awoooi-api -n awoooi-prod --tail=200 2>&1 | grep -iE ''error|fail|exception|execute|background|parse'' | tail -40\")",
|
||||
"Bash(curl -s https://awoooi.wooo.work/api/v1/approvals)",
|
||||
"Bash(ssh k3s@192.168.0.120 \"kubectl logs deployment/awoooi-api -n awoooi-prod --tail=200 2>&1 | grep -iE ''error|fail|execute|background|parse'' | tail -40\")",
|
||||
"Bash(ssh ubuntu@192.168.0.120 \"kubectl logs deployment/awoooi-api -n awoooi-prod --tail=200 2>&1 | grep -iE ''error|fail|execute|background|parse'' | tail -40\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl logs deployment/awoooi-api -n awoooi-prod --tail=200 2>&1 | grep -iE ''error|fail|execute|background|parse|skip'' | tail -50\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl logs deployment/awoooi-api -n awoooi-prod --tail=500 2>&1 | grep -iE ''background_execution|approve_action|reject|k8s_executor'' | tail -30\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl get deploy,sts -n awoooi-prod\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl rollout status deployment/awoooi-api -n awoooi-prod --timeout=120s 2>&1\")",
|
||||
"Bash(ssh wooo@192.168.0.120 \"kubectl logs deployment/awoooi-api -n awoooi-prod --tail=50 2>&1 | grep -iE ''background_execution|k8s_executor|parse'' | tail -10\")"
|
||||
],
|
||||
"additionalDirectories": [
|
||||
"/Users/ogt/awoooi/docs",
|
||||
"/Users/ogt/.claude/projects/-Users-ogt-awoooi/memory",
|
||||
"/Users/ogt/awoooi/apps/web/src/app",
|
||||
"/Users/ogt/awoooi/apps/api",
|
||||
"/Users/ogt/awoooi/apps/api/http:/localhost:8000/api/v1",
|
||||
"/Users/ogt/awoooi/apps/web/public",
|
||||
"/Users/ogt/Downloads",
|
||||
"/Users/ogt/awoooi/apps/web/test-results",
|
||||
"/Users/ogt/awoooi",
|
||||
"/Users/ogt/awoooi/apps/web/src/app/[locale]",
|
||||
"/tmp"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -19,18 +19,10 @@
|
||||
|
||||
# 文件與腳本(不需要進 image)
|
||||
# 注意: docs/runbooks/, docs/adr/, .agents/skills/ 供 RAG 索引 (ADR-067 Phase 33)
|
||||
# scripts/ 大部分不需要進 image,僅白名單 production runtime/ops 種子腳本
|
||||
# scripts/ 大部分不需要進 image,但 CronJob 腳本需要
|
||||
# 2026-04-12 ogt (ADR-073 P2-1): 白名單允許 cron_km_vectorize.py
|
||||
# 2026-05-13 codex: 白名單 T16 auto-repair canary PlayBook seed script
|
||||
# 2026-05-31 codex: MOMO backup Ansible playbook copies the backup script from
|
||||
# the controller image; keep only this backup script in the runtime context.
|
||||
scripts/**
|
||||
!scripts/
|
||||
scripts
|
||||
!scripts/cron_km_vectorize.py
|
||||
!scripts/backup/
|
||||
!scripts/backup/backup-momo-188-pg.sh
|
||||
!scripts/ops/
|
||||
!scripts/ops/awooop-seed-auto-repair-canary-playbook.py
|
||||
|
||||
# Node 快取(monorepo 根目錄)
|
||||
node_modules
|
||||
@@ -58,8 +50,3 @@ apps/web/.env*
|
||||
|
||||
# memory/ADR(不影響 build)
|
||||
memory
|
||||
# 2026-05-02 trigger CI rebuild after runner restart
|
||||
# 2026-06-12 Codex: trigger P2-403N production verification deploy, no runtime behavior change.
|
||||
# 2026-06-12 Codex: retry P2-404 deploy after transient Harbor 502, no runtime behavior change.
|
||||
# 2026-06-19 Codex: trigger P2-111 Code Review Gate production deploy, no runtime behavior change.
|
||||
# 2026-06-26 Codex: trigger IA shell production deploy after skipped image publish, no runtime behavior change.
|
||||
|
||||
@@ -1,581 +0,0 @@
|
||||
# =============================================================================
|
||||
# AWOOOI Agent Market Watch (Gitea Actions)
|
||||
# =============================================================================
|
||||
# Weekly read-only AI Agent market scan. This workflow detects primary-source
|
||||
# changes only; it does not install SDKs, call LLM APIs, commit reports, approve
|
||||
# shadow/canary, or change production routing.
|
||||
|
||||
name: Agent Market Watch
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 1 * * 1' # 每週一 09:00 台北 (UTC+8)
|
||||
|
||||
env:
|
||||
GITEA_ACTIONS_URL: http://192.168.0.110:3001/wooo/awoooi/actions
|
||||
SRE_GROUP_CHAT_ID: "-1003711974679"
|
||||
|
||||
jobs:
|
||||
market-watch:
|
||||
runs-on: awoooi-ubuntu
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Run read-only market watch
|
||||
id: watch
|
||||
run: |
|
||||
set -euo pipefail
|
||||
REPORT="/tmp/agent_market_watch_report.json"
|
||||
PREVIOUS_REPORT="$(find docs/evaluations -maxdepth 1 -type f -name 'agent_market_watch_report_*.json' | sort | tail -n 1 || true)"
|
||||
PREVIOUS_ARGS=()
|
||||
if [ -n "$PREVIOUS_REPORT" ]; then
|
||||
PREVIOUS_ARGS=(--previous-report "$PREVIOUS_REPORT")
|
||||
echo "Using previous committed market watch baseline: $PREVIOUS_REPORT"
|
||||
else
|
||||
echo "No previous committed market watch baseline found; running first live baseline."
|
||||
fi
|
||||
|
||||
python3 scripts/agents/agent-market-watch.py \
|
||||
--registry docs/ai/agent-market-watch-sources.v1.json \
|
||||
--output "$REPORT" \
|
||||
--mode live \
|
||||
--timeout-seconds 12 \
|
||||
"${PREVIOUS_ARGS[@]}"
|
||||
|
||||
python3 -m json.tool "$REPORT" >/dev/null
|
||||
python3 - "$REPORT" <<'PY'
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
report_path = sys.argv[1]
|
||||
with open(report_path, encoding="utf-8") as handle:
|
||||
data = json.load(handle)
|
||||
|
||||
if data.get("schema_version") != "agent_market_watch_report_v1":
|
||||
raise SystemExit("unexpected market watch schema_version")
|
||||
if data.get("mode") != "live":
|
||||
raise SystemExit("market watch workflow must run in live mode")
|
||||
|
||||
summary = data.get("summary")
|
||||
if not isinstance(summary, dict):
|
||||
raise SystemExit("missing market watch summary")
|
||||
|
||||
required = [
|
||||
"candidate_count",
|
||||
"source_count",
|
||||
"changed_candidates",
|
||||
"watch_only_candidates",
|
||||
"integration_queue_count",
|
||||
"failure_count",
|
||||
]
|
||||
missing = [key for key in required if key not in summary]
|
||||
if missing:
|
||||
raise SystemExit(f"missing market watch summary keys: {missing}")
|
||||
|
||||
integration_queue = data.get("integration_queue")
|
||||
if not isinstance(integration_queue, list):
|
||||
raise SystemExit("integration_queue must be a list")
|
||||
|
||||
output_path = os.environ.get("GITHUB_OUTPUT")
|
||||
if output_path:
|
||||
with open(output_path, "a", encoding="utf-8") as handle:
|
||||
for key in required:
|
||||
handle.write(f"{key}={summary.get(key, 0)}\n")
|
||||
|
||||
step_summary_path = os.environ.get("GITHUB_STEP_SUMMARY")
|
||||
if step_summary_path:
|
||||
with open(step_summary_path, "a", encoding="utf-8") as handle:
|
||||
handle.write("## Agent Market Watch\n\n")
|
||||
handle.write(f"- Candidates: {summary['candidate_count']}\n")
|
||||
handle.write(f"- Sources: {summary['source_count']}\n")
|
||||
handle.write(f"- Changed candidates: {summary['changed_candidates']}\n")
|
||||
handle.write(f"- Integration queue: {summary['integration_queue_count']}\n")
|
||||
handle.write(f"- Source failures: {summary['failure_count']}\n")
|
||||
handle.write("\nPolicy: read-only watch; no SDK/API/prod change is approved by this workflow.\n")
|
||||
|
||||
print(json.dumps(summary, ensure_ascii=False, sort_keys=True))
|
||||
PY
|
||||
|
||||
- name: Run read-only integration review
|
||||
id: review
|
||||
run: |
|
||||
set -euo pipefail
|
||||
REVIEW="/tmp/agent_market_integration_review.json"
|
||||
python3 scripts/agents/agent-market-integration-review.py \
|
||||
--watch-report /tmp/agent_market_watch_report.json \
|
||||
--candidates docs/ai/agent-replacement-candidates.v1.json \
|
||||
--scorecard docs/evaluations/agent_market_capability_scorecard_2026-06-01.json \
|
||||
--review-scope all \
|
||||
--output "$REVIEW"
|
||||
|
||||
python3 -m json.tool "$REVIEW" >/dev/null
|
||||
python3 - "$REVIEW" <<'PY'
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
review_path = sys.argv[1]
|
||||
with open(review_path, encoding="utf-8") as handle:
|
||||
data = json.load(handle)
|
||||
|
||||
if data.get("schema_version") != "agent_market_integration_review_v1":
|
||||
raise SystemExit("unexpected integration review schema_version")
|
||||
policy = data.get("policy") or {}
|
||||
forbidden = [
|
||||
"production_changes_approved",
|
||||
"replacement_decision_allowed",
|
||||
"sdk_installation_approved",
|
||||
"paid_api_calls_approved",
|
||||
"shadow_or_canary_approved",
|
||||
]
|
||||
unsafe = [key for key in forbidden if policy.get(key) is not False]
|
||||
if unsafe:
|
||||
raise SystemExit(f"integration review policy must stay false: {unsafe}")
|
||||
|
||||
summary = data.get("summary")
|
||||
if not isinstance(summary, dict):
|
||||
raise SystemExit("missing integration review summary")
|
||||
required = [
|
||||
"reviewed_candidates",
|
||||
"blocked_from_integration",
|
||||
"requires_cost_approval",
|
||||
"requires_dependency_approval",
|
||||
"source_failures",
|
||||
"production_changes_approved",
|
||||
"shadow_or_canary_approved",
|
||||
]
|
||||
missing = [key for key in required if key not in summary]
|
||||
if missing:
|
||||
raise SystemExit(f"missing integration review summary keys: {missing}")
|
||||
|
||||
output_path = os.environ.get("GITHUB_OUTPUT")
|
||||
if output_path:
|
||||
with open(output_path, "a", encoding="utf-8") as handle:
|
||||
for key in required:
|
||||
handle.write(f"{key}={summary.get(key, 0)}\n")
|
||||
|
||||
step_summary_path = os.environ.get("GITHUB_STEP_SUMMARY")
|
||||
if step_summary_path:
|
||||
with open(step_summary_path, "a", encoding="utf-8") as handle:
|
||||
handle.write("\n## Agent Integration Review\n\n")
|
||||
handle.write("- Review scope: all candidates\n")
|
||||
handle.write(f"- Reviewed candidates: {summary['reviewed_candidates']}\n")
|
||||
handle.write(f"- Blocked from integration: {summary['blocked_from_integration']}\n")
|
||||
handle.write(f"- Cost approvals required: {summary['requires_cost_approval']}\n")
|
||||
handle.write(f"- Dependency approvals required: {summary['requires_dependency_approval']}\n")
|
||||
handle.write(f"- Production changes approved: {summary['production_changes_approved']}\n")
|
||||
handle.write(f"- Shadow/canary approved: {summary['shadow_or_canary_approved']}\n")
|
||||
|
||||
print(json.dumps(summary, ensure_ascii=False, sort_keys=True))
|
||||
PY
|
||||
|
||||
- name: Run read-only discovery review
|
||||
id: discovery
|
||||
run: |
|
||||
set -euo pipefail
|
||||
DISCOVERY="/tmp/agent_market_discovery_review.json"
|
||||
PREVIOUS_DISCOVERY="$(find docs/evaluations -maxdepth 1 -type f -name 'agent_market_discovery_review_*.json' | sort | tail -n 1 || true)"
|
||||
PREVIOUS_ARGS=()
|
||||
if [ -n "$PREVIOUS_DISCOVERY" ]; then
|
||||
PREVIOUS_ARGS=(--previous-review "$PREVIOUS_DISCOVERY")
|
||||
echo "Using previous committed discovery review baseline: $PREVIOUS_DISCOVERY"
|
||||
else
|
||||
echo "No previous committed discovery review baseline found; running first discovery intake."
|
||||
fi
|
||||
|
||||
python3 scripts/agents/agent-market-discovery-review.py \
|
||||
--watch-report /tmp/agent_market_watch_report.json \
|
||||
--candidates docs/ai/agent-replacement-candidates.v1.json \
|
||||
--source-registry docs/ai/agent-market-watch-sources.v1.json \
|
||||
--output "$DISCOVERY" \
|
||||
"${PREVIOUS_ARGS[@]}"
|
||||
|
||||
python3 -m json.tool "$DISCOVERY" >/dev/null
|
||||
python3 - "$DISCOVERY" <<'PY'
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
discovery_path = sys.argv[1]
|
||||
with open(discovery_path, encoding="utf-8") as handle:
|
||||
data = json.load(handle)
|
||||
|
||||
if data.get("schema_version") != "agent_market_discovery_review_v1":
|
||||
raise SystemExit("unexpected discovery review schema_version")
|
||||
policy = data.get("policy") or {}
|
||||
forbidden = [
|
||||
"auto_registry_addition_approved",
|
||||
"sdk_installation_approved",
|
||||
"paid_api_calls_approved",
|
||||
"production_changes_approved",
|
||||
"shadow_or_canary_approved",
|
||||
"replacement_decision_allowed",
|
||||
]
|
||||
unsafe = [key for key in forbidden if policy.get(key) is not False]
|
||||
if unsafe:
|
||||
raise SystemExit(f"discovery review policy must stay false: {unsafe}")
|
||||
|
||||
summary = data.get("summary")
|
||||
if not isinstance(summary, dict):
|
||||
raise SystemExit("missing discovery review summary")
|
||||
required = [
|
||||
"discovery_sources",
|
||||
"discovered_items",
|
||||
"unique_repositories",
|
||||
"already_watched_or_registered",
|
||||
"manual_classification_required",
|
||||
"new_manual_classification_required",
|
||||
"source_failures",
|
||||
]
|
||||
missing = [key for key in required if key not in summary]
|
||||
if missing:
|
||||
raise SystemExit(f"missing discovery review summary keys: {missing}")
|
||||
|
||||
output_path = os.environ.get("GITHUB_OUTPUT")
|
||||
if output_path:
|
||||
with open(output_path, "a", encoding="utf-8") as handle:
|
||||
for key in required:
|
||||
handle.write(f"{key}={summary.get(key, 0)}\n")
|
||||
|
||||
step_summary_path = os.environ.get("GITHUB_STEP_SUMMARY")
|
||||
if step_summary_path:
|
||||
with open(step_summary_path, "a", encoding="utf-8") as handle:
|
||||
handle.write("\n## Agent Discovery Review\n\n")
|
||||
handle.write(f"- Discovery sources: {summary['discovery_sources']}\n")
|
||||
handle.write(f"- Unique repositories: {summary['unique_repositories']}\n")
|
||||
handle.write(f"- Already watched/registered: {summary['already_watched_or_registered']}\n")
|
||||
handle.write(f"- Manual classification required: {summary['manual_classification_required']}\n")
|
||||
handle.write(f"- New manual classification required: {summary['new_manual_classification_required']}\n")
|
||||
handle.write("\nPolicy: read-only intake; no registry addition, SDK/API, shadow/canary, or production change is approved.\n")
|
||||
|
||||
print(json.dumps(summary, ensure_ascii=False, sort_keys=True))
|
||||
PY
|
||||
|
||||
- name: Run read-only discovery classification
|
||||
id: classify
|
||||
if: ${{ steps.discovery.outputs.new_manual_classification_required != '0' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
CLASSIFICATION="/tmp/agent_market_discovery_classification.json"
|
||||
python3 scripts/agents/agent-market-discovery-classify.py \
|
||||
--discovery-review /tmp/agent_market_discovery_review.json \
|
||||
--output "$CLASSIFICATION" \
|
||||
--timeout-seconds 12
|
||||
|
||||
python3 -m json.tool "$CLASSIFICATION" >/dev/null
|
||||
python3 - "$CLASSIFICATION" <<'PY'
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
classification_path = sys.argv[1]
|
||||
with open(classification_path, encoding="utf-8") as handle:
|
||||
data = json.load(handle)
|
||||
|
||||
if data.get("schema_version") != "agent_market_discovery_classification_v1":
|
||||
raise SystemExit("unexpected discovery classification schema_version")
|
||||
policy = data.get("policy") or {}
|
||||
forbidden = [
|
||||
"auto_watch_registry_addition_approved",
|
||||
"sdk_installation_approved",
|
||||
"paid_api_calls_approved",
|
||||
"production_changes_approved",
|
||||
"shadow_or_canary_approved",
|
||||
"replacement_decision_allowed",
|
||||
]
|
||||
unsafe = [key for key in forbidden if policy.get(key) is not False]
|
||||
if unsafe:
|
||||
raise SystemExit(f"discovery classification policy must stay false: {unsafe}")
|
||||
|
||||
summary = data.get("summary")
|
||||
if not isinstance(summary, dict):
|
||||
raise SystemExit("missing discovery classification summary")
|
||||
required = [
|
||||
"classified_repositories",
|
||||
"recommended_watch_additions",
|
||||
"watch_only_or_defer",
|
||||
"production_changes_approved",
|
||||
"shadow_or_canary_approved",
|
||||
]
|
||||
missing = [key for key in required if key not in summary]
|
||||
if missing:
|
||||
raise SystemExit(f"missing discovery classification summary keys: {missing}")
|
||||
|
||||
output_path = os.environ.get("GITHUB_OUTPUT")
|
||||
if output_path:
|
||||
with open(output_path, "a", encoding="utf-8") as handle:
|
||||
for key in required:
|
||||
handle.write(f"{key}={summary.get(key, 0)}\n")
|
||||
|
||||
step_summary_path = os.environ.get("GITHUB_STEP_SUMMARY")
|
||||
if step_summary_path:
|
||||
with open(step_summary_path, "a", encoding="utf-8") as handle:
|
||||
handle.write("\n## Agent Discovery Classification\n\n")
|
||||
handle.write(f"- Classified repositories: {summary['classified_repositories']}\n")
|
||||
handle.write(f"- Recommended watch additions: {summary['recommended_watch_additions']}\n")
|
||||
handle.write(f"- Watch-only/defer: {summary['watch_only_or_defer']}\n")
|
||||
handle.write("\nPolicy: read-only classification; no watch registry addition, SDK/API, replay, shadow/canary, or production change is approved.\n")
|
||||
|
||||
print(json.dumps(summary, ensure_ascii=False, sort_keys=True))
|
||||
PY
|
||||
|
||||
- name: Run read-only watch promotion review
|
||||
id: promote
|
||||
run: |
|
||||
set -euo pipefail
|
||||
PROMOTION="/tmp/agent_market_watch_promotion_review.json"
|
||||
CLASSIFICATION="/tmp/agent_market_discovery_classification.json"
|
||||
if [ ! -f "$CLASSIFICATION" ]; then
|
||||
PREVIOUS_CLASSIFICATION="$(find docs/evaluations -maxdepth 1 -type f -name 'agent_market_discovery_classification_*.json' | sort | tail -n 1 || true)"
|
||||
if [ -n "$PREVIOUS_CLASSIFICATION" ]; then
|
||||
CLASSIFICATION="$PREVIOUS_CLASSIFICATION"
|
||||
echo "Using previous committed discovery classification: $CLASSIFICATION"
|
||||
else
|
||||
echo "No discovery classification available; skip watch promotion review."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
python3 scripts/agents/agent-market-watch-promotion-review.py \
|
||||
--watch-report /tmp/agent_market_watch_report.json \
|
||||
--integration-review /tmp/agent_market_integration_review.json \
|
||||
--discovery-classification "$CLASSIFICATION" \
|
||||
--candidates docs/ai/agent-replacement-candidates.v1.json \
|
||||
--output "$PROMOTION"
|
||||
|
||||
python3 -m json.tool "$PROMOTION" >/dev/null
|
||||
python3 - "$PROMOTION" <<'PY'
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
promotion_path = sys.argv[1]
|
||||
with open(promotion_path, encoding="utf-8") as handle:
|
||||
data = json.load(handle)
|
||||
|
||||
if data.get("schema_version") != "agent_market_watch_promotion_review_v1":
|
||||
raise SystemExit("unexpected watch promotion review schema_version")
|
||||
policy = data.get("policy") or {}
|
||||
forbidden = [
|
||||
"priority_upgrade_approved",
|
||||
"market_scorecard_update_approved",
|
||||
"replay_candidate_approved",
|
||||
"sdk_installation_approved",
|
||||
"paid_api_calls_approved",
|
||||
"production_changes_approved",
|
||||
"shadow_or_canary_approved",
|
||||
"replacement_decision_allowed",
|
||||
]
|
||||
unsafe = [key for key in forbidden if policy.get(key) is not False]
|
||||
if unsafe:
|
||||
raise SystemExit(f"watch promotion policy must stay false: {unsafe}")
|
||||
|
||||
summary = data.get("summary")
|
||||
if not isinstance(summary, dict):
|
||||
raise SystemExit("missing watch promotion summary")
|
||||
required = [
|
||||
"watch_only_candidates_reviewed",
|
||||
"eligible_for_market_scorecard_prescreen",
|
||||
"remain_watch_only",
|
||||
"priority_upgrades_approved",
|
||||
"market_scorecard_updates_approved",
|
||||
"replay_candidates_approved",
|
||||
]
|
||||
missing = [key for key in required if key not in summary]
|
||||
if missing:
|
||||
raise SystemExit(f"missing watch promotion summary keys: {missing}")
|
||||
|
||||
output_path = os.environ.get("GITHUB_OUTPUT")
|
||||
if output_path:
|
||||
with open(output_path, "a", encoding="utf-8") as handle:
|
||||
for key in required:
|
||||
handle.write(f"{key}={summary.get(key, 0)}\n")
|
||||
|
||||
step_summary_path = os.environ.get("GITHUB_STEP_SUMMARY")
|
||||
if step_summary_path:
|
||||
with open(step_summary_path, "a", encoding="utf-8") as handle:
|
||||
handle.write("\n## Agent Watch Promotion Review\n\n")
|
||||
handle.write(f"- Watch-only candidates reviewed: {summary['watch_only_candidates_reviewed']}\n")
|
||||
handle.write(f"- Eligible for scorecard prescreen: {summary['eligible_for_market_scorecard_prescreen']}\n")
|
||||
handle.write(f"- Remain watch-only: {summary['remain_watch_only']}\n")
|
||||
handle.write(f"- Priority upgrades approved: {summary['priority_upgrades_approved']}\n")
|
||||
handle.write(f"- Replay candidates approved: {summary['replay_candidates_approved']}\n")
|
||||
handle.write("\nPolicy: read-only promotion readiness; no priority upgrade, scorecard update, replay, SDK/API, shadow/canary, or production change is approved.\n")
|
||||
|
||||
print(json.dumps(summary, ensure_ascii=False, sort_keys=True))
|
||||
PY
|
||||
|
||||
- name: Build read-only governance snapshot
|
||||
id: snapshot
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SNAPSHOT="/tmp/agent_market_governance_snapshot.json"
|
||||
CLASSIFICATION="/tmp/agent_market_discovery_classification.json"
|
||||
if [ ! -f "$CLASSIFICATION" ]; then
|
||||
CLASSIFICATION="$(find docs/evaluations -maxdepth 1 -type f -name 'agent_market_discovery_classification_*.json' | sort | tail -n 1 || true)"
|
||||
fi
|
||||
PROMOTION="/tmp/agent_market_watch_promotion_review.json"
|
||||
if [ ! -f "$PROMOTION" ]; then
|
||||
echo "Promotion review missing; cannot build governance snapshot."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
python3 scripts/agents/agent-market-governance-snapshot.py \
|
||||
--watch-report /tmp/agent_market_watch_report.json \
|
||||
--integration-review /tmp/agent_market_integration_review.json \
|
||||
--discovery-classification "$CLASSIFICATION" \
|
||||
--promotion-review "$PROMOTION" \
|
||||
--candidates docs/ai/agent-replacement-candidates.v1.json \
|
||||
--output "$SNAPSHOT"
|
||||
|
||||
python3 -m json.tool "$SNAPSHOT" >/dev/null
|
||||
python3 - "$SNAPSHOT" <<'PY'
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
snapshot_path = sys.argv[1]
|
||||
with open(snapshot_path, encoding="utf-8") as handle:
|
||||
data = json.load(handle)
|
||||
|
||||
if data.get("schema_version") != "agent_market_governance_snapshot_v1":
|
||||
raise SystemExit("unexpected governance snapshot schema_version")
|
||||
policy = data.get("policy") or {}
|
||||
forbidden = [
|
||||
"priority_upgrade_approved",
|
||||
"market_scorecard_update_approved",
|
||||
"replay_candidate_approved",
|
||||
"sdk_installation_approved",
|
||||
"paid_api_calls_approved",
|
||||
"production_changes_approved",
|
||||
"shadow_or_canary_approved",
|
||||
"replacement_decision_allowed",
|
||||
]
|
||||
unsafe = [key for key in forbidden if policy.get(key) is not False]
|
||||
if unsafe:
|
||||
raise SystemExit(f"governance snapshot policy must stay false: {unsafe}")
|
||||
|
||||
summary = data.get("summary")
|
||||
if not isinstance(summary, dict):
|
||||
raise SystemExit("missing governance snapshot summary")
|
||||
required = [
|
||||
"candidate_count",
|
||||
"source_count",
|
||||
"blocked_from_integration",
|
||||
"eligible_for_market_scorecard_prescreen",
|
||||
"replacement_decisions_approved",
|
||||
"replay_candidates_approved",
|
||||
"production_changes_approved",
|
||||
]
|
||||
missing = [key for key in required if key not in summary]
|
||||
if missing:
|
||||
raise SystemExit(f"missing governance snapshot summary keys: {missing}")
|
||||
|
||||
output_path = os.environ.get("GITHUB_OUTPUT")
|
||||
if output_path:
|
||||
with open(output_path, "a", encoding="utf-8") as handle:
|
||||
for key in required:
|
||||
handle.write(f"{key}={summary.get(key, 0)}\n")
|
||||
|
||||
step_summary_path = os.environ.get("GITHUB_STEP_SUMMARY")
|
||||
if step_summary_path:
|
||||
with open(step_summary_path, "a", encoding="utf-8") as handle:
|
||||
handle.write("\n## Agent Market Governance Snapshot\n\n")
|
||||
handle.write(f"- Current decision: {data['current_decision']}\n")
|
||||
handle.write(f"- Candidates: {summary['candidate_count']}\n")
|
||||
handle.write(f"- Sources: {summary['source_count']}\n")
|
||||
handle.write(f"- Blocked from integration: {summary['blocked_from_integration']}\n")
|
||||
handle.write(f"- Scorecard prescreen eligible: {summary['eligible_for_market_scorecard_prescreen']}\n")
|
||||
handle.write(f"- Replacement approvals: {summary['replacement_decisions_approved']}\n")
|
||||
handle.write(f"- Replay approvals: {summary['replay_candidates_approved']}\n")
|
||||
handle.write(f"- Production approvals: {summary['production_changes_approved']}\n")
|
||||
|
||||
print(json.dumps(summary, ensure_ascii=False, sort_keys=True))
|
||||
PY
|
||||
|
||||
- name: Summarize actionable change or failure
|
||||
if: always()
|
||||
env:
|
||||
TG_CHAT_ID: ${{ env.SRE_GROUP_CHAT_ID }}
|
||||
JOB_STATUS: ${{ job.status }}
|
||||
CANDIDATE_COUNT: ${{ steps.watch.outputs.candidate_count }}
|
||||
SOURCE_COUNT: ${{ steps.watch.outputs.source_count }}
|
||||
CHANGED_CANDIDATES: ${{ steps.watch.outputs.changed_candidates }}
|
||||
INTEGRATION_QUEUE_COUNT: ${{ steps.watch.outputs.integration_queue_count }}
|
||||
FAILURE_COUNT: ${{ steps.watch.outputs.failure_count }}
|
||||
REVIEWED_CANDIDATES: ${{ steps.review.outputs.reviewed_candidates }}
|
||||
BLOCKED_FROM_INTEGRATION: ${{ steps.review.outputs.blocked_from_integration }}
|
||||
REVIEW_COST_APPROVALS: ${{ steps.review.outputs.requires_cost_approval }}
|
||||
REVIEW_DEPENDENCY_APPROVALS: ${{ steps.review.outputs.requires_dependency_approval }}
|
||||
DISCOVERY_MANUAL_REQUIRED: ${{ steps.discovery.outputs.manual_classification_required }}
|
||||
DISCOVERY_NEW_MANUAL_REQUIRED: ${{ steps.discovery.outputs.new_manual_classification_required }}
|
||||
DISCOVERY_UNIQUE_REPOSITORIES: ${{ steps.discovery.outputs.unique_repositories }}
|
||||
CLASSIFIED_REPOSITORIES: ${{ steps.classify.outputs.classified_repositories }}
|
||||
RECOMMENDED_WATCH_ADDITIONS: ${{ steps.classify.outputs.recommended_watch_additions }}
|
||||
WATCH_PROMOTION_ELIGIBLE: ${{ steps.promote.outputs.eligible_for_market_scorecard_prescreen }}
|
||||
WATCH_PROMOTION_APPROVED: ${{ steps.promote.outputs.priority_upgrades_approved }}
|
||||
REPLAY_CANDIDATES_APPROVED: ${{ steps.promote.outputs.replay_candidates_approved }}
|
||||
GITEA_ACTIONS_URL: ${{ env.GITEA_ACTIONS_URL }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
CHANGED="${CHANGED_CANDIDATES:-0}"
|
||||
QUEUE="${INTEGRATION_QUEUE_COUNT:-0}"
|
||||
FAILURES="${FAILURE_COUNT:-0}"
|
||||
NEW_DISCOVERY="${DISCOVERY_NEW_MANUAL_REQUIRED:-0}"
|
||||
|
||||
if [ "$JOB_STATUS" = "success" ] && [ "$CHANGED" = "0" ] && [ "$QUEUE" = "0" ] && [ "$FAILURES" = "0" ] && [ "$NEW_DISCOVERY" = "0" ]; then
|
||||
echo "No actionable market changes; keep Telegram quiet."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
python3 - <<'PY'
|
||||
import os
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
status = os.environ.get("JOB_STATUS", "unknown")
|
||||
changed = os.environ.get("CHANGED_CANDIDATES") or "0"
|
||||
queue = os.environ.get("INTEGRATION_QUEUE_COUNT") or "0"
|
||||
failures = os.environ.get("FAILURE_COUNT") or "0"
|
||||
reviewed = os.environ.get("REVIEWED_CANDIDATES") or "0"
|
||||
blocked = os.environ.get("BLOCKED_FROM_INTEGRATION") or "0"
|
||||
cost_approvals = os.environ.get("REVIEW_COST_APPROVALS") or "0"
|
||||
dependency_approvals = os.environ.get("REVIEW_DEPENDENCY_APPROVALS") or "0"
|
||||
discovery_manual = os.environ.get("DISCOVERY_MANUAL_REQUIRED") or "0"
|
||||
discovery_new = os.environ.get("DISCOVERY_NEW_MANUAL_REQUIRED") or "0"
|
||||
discovery_repos = os.environ.get("DISCOVERY_UNIQUE_REPOSITORIES") or "0"
|
||||
classified_repos = os.environ.get("CLASSIFIED_REPOSITORIES") or "0"
|
||||
recommended_watch_additions = os.environ.get("RECOMMENDED_WATCH_ADDITIONS") or "0"
|
||||
watch_promotion_eligible = os.environ.get("WATCH_PROMOTION_ELIGIBLE") or "0"
|
||||
watch_promotion_approved = os.environ.get("WATCH_PROMOTION_APPROVED") or "0"
|
||||
replay_candidates_approved = os.environ.get("REPLAY_CANDIDATES_APPROVED") or "0"
|
||||
candidates = os.environ.get("CANDIDATE_COUNT") or "0"
|
||||
sources = os.environ.get("SOURCE_COUNT") or "0"
|
||||
actions_url = os.environ.get("GITEA_ACTIONS_URL", "")
|
||||
generated = datetime.now(ZoneInfo("Asia/Taipei")).strftime("%Y-%m-%d %H:%M")
|
||||
|
||||
title = "Agent Market Watch 需要複核" if status == "success" else "Agent Market Watch 執行失敗"
|
||||
lines = [
|
||||
f"## {title}",
|
||||
"",
|
||||
f"- 時間:`{generated}`",
|
||||
f"- 狀態:`{status}`",
|
||||
f"- 候選 / 來源:`{candidates}` / `{sources}`",
|
||||
f"- 變動候選 / 整合佇列 / 來源失敗:`{changed}` / `{queue}` / `{failures}`",
|
||||
f"- Review:已審 `{reviewed}`;擋下整合 `{blocked}`;成本批准需求 `{cost_approvals}`;依賴批准需求 `{dependency_approvals}`",
|
||||
f"- Discovery:unique repo `{discovery_repos}`;需人工分類 `{discovery_manual}`;新未分類 `{discovery_new}`;已分類 `{classified_repos}`;建議 watch `{recommended_watch_additions}`",
|
||||
f"- Promotion:scorecard prescreen eligible `{watch_promotion_eligible}`;priority upgrade approved `{watch_promotion_approved}`;replay approved `{replay_candidates_approved}`",
|
||||
"",
|
||||
"政策:此 workflow 只建立市場觀察、整合審查、discovery intake/classification 訊號,不批准 SDK 安裝、付費 API、replay、shadow/canary 或 OpenClaw 取代。",
|
||||
f"Log:{actions_url}",
|
||||
]
|
||||
summary = "\n".join(lines) + "\n"
|
||||
print(summary)
|
||||
step_summary_path = os.environ.get("GITHUB_STEP_SUMMARY")
|
||||
if step_summary_path:
|
||||
with open(step_summary_path, "a", encoding="utf-8") as handle:
|
||||
handle.write(summary)
|
||||
PY
|
||||
@@ -1,110 +0,0 @@
|
||||
# =============================================================================
|
||||
# AWOOOI AI Technology Watch (Gitea Actions)
|
||||
# =============================================================================
|
||||
# 每 6 小時只讀監控主流 AI 技術 primary sources。此 workflow 只產生
|
||||
# Gitea step summary;不安裝 SDK、不呼叫 LLM API、不 commit report、不發
|
||||
# Telegram、不切換 provider route、不修改 production。
|
||||
|
||||
name: AI 技術雷達監控
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 */6 * * *'
|
||||
|
||||
jobs:
|
||||
ai-technology-watch:
|
||||
runs-on: awoooi-ubuntu
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: 執行只讀 AI 技術雷達監控
|
||||
id: watch
|
||||
run: |
|
||||
set -euo pipefail
|
||||
REPORT="/tmp/ai_technology_watch_report.json"
|
||||
PREVIOUS_REPORT="$(find docs/evaluations -maxdepth 1 -type f -name 'ai_technology_watch_report_*.json' | sort | tail -n 1 || true)"
|
||||
PREVIOUS_ARGS=()
|
||||
if [ -n "$PREVIOUS_REPORT" ]; then
|
||||
PREVIOUS_ARGS=(--previous-report "$PREVIOUS_REPORT")
|
||||
echo "使用已提交的上一份 AI 技術雷達 baseline: $PREVIOUS_REPORT"
|
||||
else
|
||||
echo "找不到已提交的 AI 技術雷達 baseline,執行第一次 live baseline。"
|
||||
fi
|
||||
|
||||
python3 scripts/agents/ai-technology-watch.py \
|
||||
--registry docs/ai/ai-technology-watch-sources.v1.json \
|
||||
--output "$REPORT" \
|
||||
--mode live \
|
||||
--timeout-seconds 12 \
|
||||
"${PREVIOUS_ARGS[@]}"
|
||||
|
||||
python3 -m json.tool "$REPORT" >/dev/null
|
||||
python3 - "$REPORT" <<'PY'
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
report_path = sys.argv[1]
|
||||
with open(report_path, encoding="utf-8") as handle:
|
||||
data = json.load(handle)
|
||||
|
||||
if data.get("schema_version") != "ai_technology_watch_report_v1":
|
||||
raise SystemExit("AI 技術雷達 schema_version 不正確")
|
||||
if data.get("mode") != "live":
|
||||
raise SystemExit("AI 技術雷達 workflow 必須以 live mode 執行")
|
||||
|
||||
policy = data.get("policy") or {}
|
||||
forbidden = [
|
||||
"sdk_installation_approved",
|
||||
"paid_api_calls_approved",
|
||||
"production_routing_approved",
|
||||
"telegram_send_approved",
|
||||
"model_provider_switch_approved",
|
||||
"host_write_approved",
|
||||
]
|
||||
unsafe = [key for key in forbidden if policy.get(key) is not False]
|
||||
if unsafe:
|
||||
raise SystemExit(f"AI 技術雷達 policy 必須維持 false: {unsafe}")
|
||||
if policy.get("read_only") is not True:
|
||||
raise SystemExit("AI 技術雷達必須維持 read_only")
|
||||
|
||||
summary = data.get("summary")
|
||||
if not isinstance(summary, dict):
|
||||
raise SystemExit("缺少 AI 技術雷達 summary")
|
||||
required = [
|
||||
"technology_count",
|
||||
"technology_area_count",
|
||||
"source_count",
|
||||
"changed_technologies",
|
||||
"watch_only_technologies",
|
||||
"review_queue_count",
|
||||
"source_failure_count",
|
||||
"high_priority_count",
|
||||
]
|
||||
missing = [key for key in required if key not in summary]
|
||||
if missing:
|
||||
raise SystemExit(f"缺少 AI 技術雷達 summary keys: {missing}")
|
||||
|
||||
output_path = os.environ.get("GITHUB_OUTPUT")
|
||||
if output_path:
|
||||
with open(output_path, "a", encoding="utf-8") as handle:
|
||||
for key in required:
|
||||
handle.write(f"{key}={summary.get(key, 0)}\n")
|
||||
|
||||
step_summary_path = os.environ.get("GITHUB_STEP_SUMMARY")
|
||||
if step_summary_path:
|
||||
with open(step_summary_path, "a", encoding="utf-8") as handle:
|
||||
handle.write("## AI 技術雷達監控\n\n")
|
||||
handle.write(f"- 技術項目:{summary['technology_count']}\n")
|
||||
handle.write(f"- 技術領域:{summary['technology_area_count']}\n")
|
||||
handle.write(f"- 來源數:{summary['source_count']}\n")
|
||||
handle.write(f"- 變更技術:{summary['changed_technologies']}\n")
|
||||
handle.write(f"- 審核佇列:{summary['review_queue_count']}\n")
|
||||
handle.write(f"- 來源失敗:{summary['source_failure_count']}\n")
|
||||
handle.write(f"- 高優先級技術:{summary['high_priority_count']}\n")
|
||||
handle.write("\nPolicy: 只讀監控;此 workflow 不批准 SDK/API/provider/Telegram/host/production 變更。\n")
|
||||
|
||||
print(json.dumps(summary, ensure_ascii=False, sort_keys=True))
|
||||
PY
|
||||
@@ -1,49 +1,22 @@
|
||||
name: Ansible / Reboot Recovery Contract
|
||||
name: Ansible Lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'infra/ansible/**'
|
||||
- 'ops/monitoring/**'
|
||||
- 'ops/reboot-recovery/**'
|
||||
- 'scripts/backup/**'
|
||||
- 'scripts/ops/**'
|
||||
- 'scripts/reboot-recovery/**'
|
||||
- 'docs/**'
|
||||
- '.gitea/workflows/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'infra/ansible/**'
|
||||
- 'ops/monitoring/**'
|
||||
- 'ops/reboot-recovery/**'
|
||||
- 'scripts/backup/**'
|
||||
- 'scripts/ops/**'
|
||||
- 'scripts/reboot-recovery/**'
|
||||
- 'docs/**'
|
||||
- '.gitea/workflows/**'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: awoooi-ubuntu
|
||||
timeout-minutes: 15
|
||||
lint:
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Bootstrap Ansible validation env
|
||||
run: bash scripts/ops/bootstrap-ansible-validation-env.sh
|
||||
- name: Install ansible-lint
|
||||
run: pip install ansible-lint
|
||||
|
||||
- name: Run Ansible and reboot-recovery validation
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export PATH="${ANSIBLE_VALIDATION_VENV:-/tmp/awoooi-ansible-venv}/bin:$PATH"
|
||||
bash scripts/ops/ansible-validate.sh
|
||||
python3 scripts/ops/doc-secrets-sanity-check.py docs .gitea
|
||||
python3 scripts/ops/backup-alert-label-contract-check.py
|
||||
python3 scripts/ops/recovery-scorecard-contract-check.py
|
||||
python3 -m py_compile scripts/ops/backup-alert-live-visibility-check.py
|
||||
bash -n scripts/reboot-recovery/full-stack-recovery-scorecard.sh
|
||||
bash -n scripts/reboot-recovery/dr-offsite-operator-checklist.sh
|
||||
bash -n scripts/reboot-recovery/verify-cold-start-monitor-deploy.sh
|
||||
bash scripts/reboot-recovery/reboot-recovery-readiness-audit.sh --no-color
|
||||
- name: Run ansible-lint
|
||||
run: ansible-lint infra/ansible/playbooks/
|
||||
working-directory: ${{ github.workspace }}
|
||||
|
||||
@@ -19,14 +19,13 @@ concurrency:
|
||||
env:
|
||||
HARBOR: 192.168.0.110:5000
|
||||
HARBOR_MIRROR: 192.168.0.110:5001
|
||||
SRE_GROUP_CHAT_ID: "-1003711974679"
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: http://192.168.0.188:24318
|
||||
OTEL_SERVICE_NAME: awoooi-cd-dev
|
||||
OTEL_RESOURCE_ATTRIBUTES: service.version=${{ github.sha }},deployment.environment=dev
|
||||
|
||||
jobs:
|
||||
build-and-deploy-dev:
|
||||
runs-on: awoooi-ubuntu
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -43,19 +42,10 @@ jobs:
|
||||
├ 📝 ${{ steps.commit.outputs.message }}
|
||||
├ 🔖 <code>${{ steps.commit.outputs.short_sha }}</code>
|
||||
└ 🌿 dev branch"
|
||||
if AWOOI_CICD_STATUS=running \
|
||||
AWOOI_CICD_STAGE=dev-deploy \
|
||||
AWOOI_CICD_JOB_NAME="[DEV] 部署開始" \
|
||||
AWOOI_CICD_COMMIT_SHA="${GITHUB_SHA}" \
|
||||
AWOOI_CICD_SUMMARY="${{ steps.commit.outputs.message }}" \
|
||||
scripts/ci/notify-awoooi-cicd.sh; then
|
||||
echo "Dev deploy start notification mirrored through AWOOI API"
|
||||
else
|
||||
printf '%b' "$MSG" | curl -fS -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
|
||||
-d "chat_id=${{ env.SRE_GROUP_CHAT_ID }}" \
|
||||
-d "parse_mode=HTML" \
|
||||
--data-urlencode "text@-"
|
||||
fi
|
||||
printf '%b' "$MSG" | curl -fS -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
|
||||
-d "chat_id=${{ secrets.TELEGRAM_CHAT_ID }}" \
|
||||
-d "parse_mode=HTML" \
|
||||
--data-urlencode "text@-"
|
||||
|
||||
# API 測試 (同 prod CI,確保 dev 也通過)
|
||||
- name: Run API Tests
|
||||
@@ -87,18 +77,11 @@ jobs:
|
||||
echo "✅ API 測試通過"
|
||||
|
||||
- name: Login to Harbor
|
||||
run: |
|
||||
HARBOR_USERNAME="$(cat <<'AWOOOI_SECRET_HARBOR_USERNAME'
|
||||
${{ secrets.HARBOR_USERNAME }}
|
||||
AWOOOI_SECRET_HARBOR_USERNAME
|
||||
)"
|
||||
HARBOR_PASSWORD="$(cat <<'AWOOOI_SECRET_HARBOR_PASSWORD'
|
||||
${{ secrets.HARBOR_PASSWORD }}
|
||||
AWOOOI_SECRET_HARBOR_PASSWORD
|
||||
)"
|
||||
printf '%s' "$HARBOR_PASSWORD" | docker login "${{ env.HARBOR }}" \
|
||||
-u "$HARBOR_USERNAME" \
|
||||
--password-stdin
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.HARBOR }}
|
||||
username: ${{ secrets.HARBOR_USERNAME }}
|
||||
password: ${{ secrets.HARBOR_PASSWORD }}
|
||||
|
||||
# Dev API 鏡像:強制重建,不用 cache(確保 models.json 等配置文件更新)
|
||||
- name: Build and Push API (Dev)
|
||||
@@ -114,63 +97,34 @@ jobs:
|
||||
|
||||
# 注入 Dev K8s Secrets
|
||||
- name: Inject Dev K8s Secrets
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||
TG_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||
TG_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||
NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }}
|
||||
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||
run: |
|
||||
secret_b64() {
|
||||
python3 -c 'import base64, sys; data=sys.stdin.buffer.read(); data=data[:-1] if data.endswith(b"\n") else data; sys.stdout.write(base64.b64encode(data).decode())'
|
||||
}
|
||||
write_deploy_key() {
|
||||
mkdir -p ~/.ssh
|
||||
umask 077
|
||||
cat > ~/.ssh/deploy_key <<'AWOOOI_DEPLOY_KEY'
|
||||
${{ secrets.DEPLOY_SSH_KEY }}
|
||||
AWOOOI_DEPLOY_KEY
|
||||
chmod 600 ~/.ssh/deploy_key
|
||||
}
|
||||
TG_BOT_TOKEN_B64="$(secret_b64 <<'AWOOOI_SECRET_TG_BOT_TOKEN'
|
||||
${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||
AWOOOI_SECRET_TG_BOT_TOKEN
|
||||
)"
|
||||
TG_CHAT_ID_B64="$(secret_b64 <<'AWOOOI_SECRET_SRE_GROUP_CHAT_ID_COMPAT'
|
||||
${{ secrets.SRE_GROUP_CHAT_ID }}
|
||||
AWOOOI_SECRET_SRE_GROUP_CHAT_ID_COMPAT
|
||||
)"
|
||||
NVIDIA_API_KEY_B64="$(secret_b64 <<'AWOOOI_SECRET_NVIDIA_API_KEY'
|
||||
${{ secrets.NVIDIA_API_KEY }}
|
||||
AWOOOI_SECRET_NVIDIA_API_KEY
|
||||
)"
|
||||
GEMINI_API_KEY_B64="$(secret_b64 <<'AWOOOI_SECRET_GEMINI_API_KEY'
|
||||
${{ secrets.GEMINI_API_KEY }}
|
||||
AWOOOI_SECRET_GEMINI_API_KEY
|
||||
)"
|
||||
|
||||
mkdir -p ~/.ssh
|
||||
write_deploy_key
|
||||
# Keep deploy-time host keys separate from the runner user's global
|
||||
# known_hosts, which is also used by reboot/cold-start checks.
|
||||
DEPLOY_KNOWN_HOSTS="${HOME}/.ssh/deploy_known_hosts"
|
||||
ssh-keyscan -T 5 -t ed25519,rsa,ecdsa 192.168.0.120 > "${DEPLOY_KNOWN_HOSTS}" 2>/dev/null
|
||||
test -s "${DEPLOY_KNOWN_HOSTS}" || { echo "❌ K8S host keyscan failed: 192.168.0.120"; exit 1; }
|
||||
SSH_OPTS="-o BatchMode=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=${DEPLOY_KNOWN_HOSTS} -i ~/.ssh/deploy_key"
|
||||
# 2026-05-05 Codex: kubectl runs on 120 control-plane. 121 is a
|
||||
# worker and its local kubeconfig points at 127.0.0.1:6443.
|
||||
ssh $SSH_OPTS wooo@192.168.0.120 << SECRETS
|
||||
echo "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key
|
||||
chmod 600 ~/.ssh/deploy_key
|
||||
ssh -o StrictHostKeyChecking=no -i ~/.ssh/deploy_key wooo@192.168.0.121 << SECRETS
|
||||
set -e
|
||||
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
|
||||
|
||||
sudo kubectl patch secret awoooi-secrets -n awoooi-dev --type='json' -p='[
|
||||
{"op":"replace","path":"/data/OPENCLAW_TG_BOT_TOKEN","value":"${TG_BOT_TOKEN_B64}"},
|
||||
{"op":"replace","path":"/data/OPENCLAW_TG_CHAT_ID","value":"${TG_CHAT_ID_B64}"}
|
||||
{"op":"replace","path":"/data/OPENCLAW_TG_BOT_TOKEN","value":"'"$(echo -n "${TG_BOT_TOKEN}" | base64 -w 0)"'"},
|
||||
{"op":"replace","path":"/data/OPENCLAW_TG_CHAT_ID","value":"'"$(echo -n "${TG_CHAT_ID}" | base64 -w 0)"'"}
|
||||
]' || echo "⚠️ Telegram Secrets patch 跳過"
|
||||
|
||||
if [ -n "${NVIDIA_API_KEY_B64}" ]; then
|
||||
if [ -n "${NVIDIA_API_KEY}" ]; then
|
||||
sudo kubectl patch secret awoooi-secrets -n awoooi-dev --type='json' -p='[
|
||||
{"op":"replace","path":"/data/NVIDIA_API_KEY","value":"${NVIDIA_API_KEY_B64}"}
|
||||
{"op":"replace","path":"/data/NVIDIA_API_KEY","value":"'"$(echo -n "${NVIDIA_API_KEY}" | base64 -w 0)"'"}
|
||||
]' && echo "✅ NVIDIA_API_KEY 已注入 dev"
|
||||
fi
|
||||
|
||||
if [ -n "${GEMINI_API_KEY_B64}" ]; then
|
||||
if [ -n "${GEMINI_API_KEY}" ]; then
|
||||
sudo kubectl patch secret awoooi-secrets -n awoooi-dev --type='json' -p='[
|
||||
{"op":"replace","path":"/data/GEMINI_API_KEY","value":"${GEMINI_API_KEY_B64}"}
|
||||
{"op":"replace","path":"/data/GEMINI_API_KEY","value":"'"$(echo -n "${GEMINI_API_KEY}" | base64 -w 0)"'"}
|
||||
]' && echo "✅ GEMINI_API_KEY 已注入 dev"
|
||||
fi
|
||||
|
||||
@@ -179,16 +133,14 @@ jobs:
|
||||
|
||||
# 部署到 awoooi-dev
|
||||
- name: Deploy to Dev K8s
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||
run: |
|
||||
DEPLOY_KNOWN_HOSTS="${HOME}/.ssh/deploy_known_hosts"
|
||||
ssh-keyscan -T 5 -t ed25519,rsa,ecdsa 192.168.0.120 > "${DEPLOY_KNOWN_HOSTS}" 2>/dev/null
|
||||
test -s "${DEPLOY_KNOWN_HOSTS}" || { echo "❌ K8S host keyscan failed: 192.168.0.120"; exit 1; }
|
||||
SSH_OPTS="-o BatchMode=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=${DEPLOY_KNOWN_HOSTS} -i ~/.ssh/deploy_key"
|
||||
cat k8s/awoooi-dev/02-configmap.yaml | \
|
||||
ssh $SSH_OPTS wooo@192.168.0.120 \
|
||||
ssh -o StrictHostKeyChecking=no -i ~/.ssh/deploy_key wooo@192.168.0.121 \
|
||||
"export KUBECONFIG=/etc/rancher/k3s/k3s.yaml && sudo kubectl apply -f -"
|
||||
|
||||
ssh $SSH_OPTS wooo@192.168.0.120 << 'DEPLOY'
|
||||
ssh -o StrictHostKeyChecking=no -i ~/.ssh/deploy_key wooo@192.168.0.121 << 'DEPLOY'
|
||||
set -e
|
||||
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
|
||||
|
||||
@@ -229,20 +181,10 @@ jobs:
|
||||
├ 🔖 <code>${{ steps.commit.outputs.short_sha }}</code>
|
||||
├ ⏱️ 耗時: ${MINUTES}m ${SECONDS}s
|
||||
└ 🩺 http://192.168.0.125:32344/api/v1/health"
|
||||
if AWOOI_CICD_STATUS=success \
|
||||
AWOOI_CICD_STAGE=dev-deploy \
|
||||
AWOOI_CICD_JOB_NAME="[DEV] 部署完成" \
|
||||
AWOOI_CICD_COMMIT_SHA="${GITHUB_SHA}" \
|
||||
AWOOI_CICD_DURATION_SECONDS="${DURATION}" \
|
||||
AWOOI_CICD_SUMMARY="${{ steps.commit.outputs.message }}" \
|
||||
scripts/ci/notify-awoooi-cicd.sh; then
|
||||
echo "Dev deploy success notification mirrored through AWOOI API"
|
||||
else
|
||||
printf '%b' "$MSG" | curl -fS -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
|
||||
-d "chat_id=${{ env.SRE_GROUP_CHAT_ID }}" \
|
||||
-d "parse_mode=HTML" \
|
||||
--data-urlencode "text@-"
|
||||
fi
|
||||
printf '%b' "$MSG" | curl -fS -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
|
||||
-d "chat_id=${{ secrets.TELEGRAM_CHAT_ID }}" \
|
||||
-d "parse_mode=HTML" \
|
||||
--data-urlencode "text@-"
|
||||
|
||||
- name: Notify Dev Deploy Failure
|
||||
if: failure()
|
||||
@@ -251,16 +193,7 @@ jobs:
|
||||
├ 📝 ${{ steps.commit.outputs.message }}
|
||||
├ 🔖 <code>${{ steps.commit.outputs.short_sha }}</code>
|
||||
└ 🔗 <a href=\"http://192.168.0.110:3001/wooo/awoooi/actions\">查看日誌</a>"
|
||||
if AWOOI_CICD_STATUS=failed \
|
||||
AWOOI_CICD_STAGE=dev-deploy \
|
||||
AWOOI_CICD_JOB_NAME="[DEV] 部署失敗" \
|
||||
AWOOI_CICD_COMMIT_SHA="${GITHUB_SHA}" \
|
||||
AWOOI_CICD_SUMMARY="${{ steps.commit.outputs.message }}" \
|
||||
scripts/ci/notify-awoooi-cicd.sh; then
|
||||
echo "Dev deploy failure notification mirrored through AWOOI API"
|
||||
else
|
||||
printf '%b' "$MSG" | curl -fS -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
|
||||
-d "chat_id=${{ env.SRE_GROUP_CHAT_ID }}" \
|
||||
-d "parse_mode=HTML" \
|
||||
--data-urlencode "text@-"
|
||||
fi
|
||||
printf '%b' "$MSG" | curl -fS -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
|
||||
-d "chat_id=${{ secrets.TELEGRAM_CHAT_ID }}" \
|
||||
-d "parse_mode=HTML" \
|
||||
--data-urlencode "text@-"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,220 +0,0 @@
|
||||
name: Code Review
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'apps/**'
|
||||
- 'k8s/**'
|
||||
- '!k8s/awoooi-prod/kustomization.yaml'
|
||||
- 'ops/**'
|
||||
- 'scripts/**'
|
||||
- '.gitea/workflows/**'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: code-review-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
REPORT_URL: https://mo.wooo.work/code-review/
|
||||
GITEA_ACTIONS_URL: http://192.168.0.110:3001/wooo/awoooi/actions
|
||||
SRE_GROUP_CHAT_ID: "-1003711974679"
|
||||
|
||||
jobs:
|
||||
ai-code-review:
|
||||
runs-on: awoooi-ubuntu
|
||||
timeout-minutes: 8
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 50
|
||||
|
||||
- name: Guard Workflow Secret Surfaces
|
||||
run: node scripts/ci/check-gitea-step-env-secrets.js
|
||||
|
||||
- name: Skip Stale Main Push
|
||||
id: stale
|
||||
run: |
|
||||
set -euo pipefail
|
||||
BRANCH="${GITHUB_REF_NAME:-${GITHUB_REF#refs/heads/}}"
|
||||
if [ "${GITHUB_EVENT_NAME:-}" != "push" ] || [ "$BRANCH" != "main" ]; then
|
||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
LATEST="$(git ls-remote origin refs/heads/main | awk '{print $1}')"
|
||||
if [ -n "$LATEST" ] && [ "$LATEST" != "$GITHUB_SHA" ]; then
|
||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||
echo "Skip stale code review: current=$GITHUB_SHA latest=$LATEST"
|
||||
else
|
||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Prepare Review Context
|
||||
id: ctx
|
||||
if: steps.stale.outputs.skip != 'true'
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.before }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SHORT_SHA="${GITHUB_SHA::7}"
|
||||
BRANCH="${GITHUB_REF_NAME:-${GITHUB_REF#refs/heads/}}"
|
||||
if [ -z "$BRANCH" ] || [ "$BRANCH" = "$GITHUB_REF" ]; then
|
||||
BRANCH="main"
|
||||
fi
|
||||
COMMIT_MSG="$(git log -1 --pretty=%s)"
|
||||
COMMIT_MSG="${COMMIT_MSG:0:120}"
|
||||
BASE="${BASE_SHA:-}"
|
||||
if [ -n "$BASE" ] && [ "$BASE" != "0000000000000000000000000000000000000000" ]; then
|
||||
git rev-parse --verify "${BASE}^{commit}" >/dev/null 2>&1 || git fetch --no-tags origin "$BASE" --depth=1 || true
|
||||
fi
|
||||
|
||||
if [ -n "$BASE" ] && git rev-parse --verify "${BASE}^{commit}" >/dev/null 2>&1; then
|
||||
RANGE="$BASE..$GITHUB_SHA"
|
||||
elif git rev-parse --verify "${GITHUB_SHA}^" >/dev/null 2>&1; then
|
||||
BASE="${GITHUB_SHA}^"
|
||||
RANGE="${GITHUB_SHA}^..$GITHUB_SHA"
|
||||
else
|
||||
BASE=""
|
||||
RANGE="$GITHUB_SHA"
|
||||
fi
|
||||
|
||||
FILES="$(git diff --name-only "$RANGE" || git show --pretty= --name-only "$GITHUB_SHA")"
|
||||
if [ -z "$FILES" ]; then
|
||||
FILES="(no files reported)"
|
||||
fi
|
||||
FILE_COUNT="$(printf '%s\n' "$FILES" | grep -c . || true)"
|
||||
FILES_DISPLAY="$(printf '%s\n' "$FILES" | sed -n '1,6s/^/• /p')"
|
||||
if [ "$FILE_COUNT" -gt 6 ]; then
|
||||
FILES_DISPLAY="$(printf '%s\n• ... and %s more' "$FILES_DISPLAY" "$((FILE_COUNT - 6))")"
|
||||
fi
|
||||
|
||||
{
|
||||
echo "short_sha=$SHORT_SHA"
|
||||
echo "branch=$BRANCH"
|
||||
echo "base_sha=$BASE"
|
||||
echo "file_count=$FILE_COUNT"
|
||||
echo "commit_msg<<EOF"
|
||||
printf '%s\n' "$COMMIT_MSG"
|
||||
echo "EOF"
|
||||
echo "files_display<<EOF"
|
||||
printf '%s\n' "$FILES_DISPLAY"
|
||||
echo "EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Notify Code Review Start
|
||||
if: steps.stale.outputs.skip != 'true'
|
||||
env:
|
||||
SRE_GROUP_CHAT_ID: ${{ env.SRE_GROUP_CHAT_ID }}
|
||||
SHORT_SHA: ${{ steps.ctx.outputs.short_sha }}
|
||||
BRANCH: ${{ steps.ctx.outputs.branch }}
|
||||
COMMIT_MSG: ${{ steps.ctx.outputs.commit_msg }}
|
||||
FILES_DISPLAY: ${{ steps.ctx.outputs.files_display }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TG_BOT_TOKEN="$(cat <<'AWOOOI_SECRET_TG_BOT_TOKEN'
|
||||
${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||
AWOOOI_SECRET_TG_BOT_TOKEN
|
||||
)"
|
||||
html_escape() { sed 's/&/\&/g; s/</\</g; s/>/\>/g'; }
|
||||
COMMIT_ESC="$(printf '%s' "$COMMIT_MSG" | html_escape)"
|
||||
FILES_ESC="$(printf '%s\n' "$FILES_DISPLAY" | html_escape)"
|
||||
MSG="$(printf '🔍 <b>Code Review 啟動</b>\n──────────────────────\n📦 Commit <code>%s</code> 🌿 <code>%s</code>\n📝 <code>%s</code>\n📁 <b>變更檔案:</b>\n%s\n──────────────────────\n🤖 <b>Hermes → OpenClaw → Elephant Alpha → NemoTron</b>\n📊 即時進度:<a href=\"%s\">%s</a>' "$SHORT_SHA" "$BRANCH" "$COMMIT_ESC" "$FILES_ESC" "$REPORT_URL" "$REPORT_URL")"
|
||||
if AWOOI_CICD_STATUS=running \
|
||||
AWOOI_CICD_STAGE=code-review \
|
||||
AWOOI_CICD_JOB_NAME="Code Review 啟動" \
|
||||
AWOOI_CICD_COMMIT_SHA="${GITHUB_SHA}" \
|
||||
AWOOI_CICD_TRIGGERED_BY="${GITHUB_ACTOR:-CI}" \
|
||||
AWOOI_CICD_SUMMARY="${COMMIT_MSG}" \
|
||||
AWOOI_CICD_WORKFLOW_URL="${REPORT_URL}" \
|
||||
scripts/ci/notify-awoooi-cicd.sh; then
|
||||
echo "Code review start notification mirrored through AWOOI API"
|
||||
else
|
||||
if [ -z "${TG_BOT_TOKEN:-}" ] || [ -z "${SRE_GROUP_CHAT_ID:-}" ]; then
|
||||
echo "Telegram secret missing and AWOOI API notify failed; skip start notification"
|
||||
exit 0
|
||||
fi
|
||||
curl -fsS -X POST "https://api.telegram.org/bot${TG_BOT_TOKEN}/sendMessage" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$(jq -n --arg c "$SRE_GROUP_CHAT_ID" --arg t "$MSG" '{chat_id:$c,text:$t,parse_mode:"HTML",disable_web_page_preview:true}')" \
|
||||
>/dev/null
|
||||
fi
|
||||
|
||||
- name: Run Deterministic Review
|
||||
if: steps.stale.outputs.skip != 'true'
|
||||
env:
|
||||
BASE_SHA: ${{ steps.ctx.outputs.base_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python3 scripts/ci_code_review.py \
|
||||
--base "${BASE_SHA:-}" \
|
||||
--head "$GITHUB_SHA" \
|
||||
--repo "." \
|
||||
--output /tmp/code-review-report.json
|
||||
jq . /tmp/code-review-report.json
|
||||
|
||||
- name: Notify Code Review Completion
|
||||
if: always() && steps.stale.outputs.skip != 'true'
|
||||
env:
|
||||
SRE_GROUP_CHAT_ID: ${{ env.SRE_GROUP_CHAT_ID }}
|
||||
SHORT_SHA: ${{ steps.ctx.outputs.short_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TG_BOT_TOKEN="$(cat <<'AWOOOI_SECRET_TG_BOT_TOKEN'
|
||||
${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||
AWOOOI_SECRET_TG_BOT_TOKEN
|
||||
)"
|
||||
REPORT=/tmp/code-review-report.json
|
||||
if [ ! -s "$REPORT" ]; then
|
||||
cat > "$REPORT" <<'JSON'
|
||||
{"counts":{"critical":0,"high":0,"medium":1,"low":0},"risk":"MEDIUM","summary":"Code Review workflow 未產生報告,需查看 Gitea Actions 日誌。","action":"查看 workflow logs","top_issue":"報告產生失敗","agents":["Hermes","OpenClaw","ElephantAlpha","NemoTron"]}
|
||||
JSON
|
||||
fi
|
||||
CRITICAL="$(jq -r '.counts.critical' "$REPORT")"
|
||||
HIGH="$(jq -r '.counts.high' "$REPORT")"
|
||||
MEDIUM="$(jq -r '.counts.medium' "$REPORT")"
|
||||
LOW="$(jq -r '.counts.low' "$REPORT")"
|
||||
RISK="$(jq -r '.risk' "$REPORT")"
|
||||
SUMMARY="$(jq -r '.summary' "$REPORT")"
|
||||
ACTION="$(jq -r '.action' "$REPORT")"
|
||||
TOP_ISSUE="$(jq -r '.top_issue' "$REPORT")"
|
||||
|
||||
if [ "$RISK" = "LOW" ]; then
|
||||
STATUS="🟢"
|
||||
ISSUE_LINE="✅ 無高風險問題"
|
||||
elif [ "$RISK" = "MEDIUM" ]; then
|
||||
STATUS="🟡"
|
||||
ISSUE_LINE="⚠️ 有中風險註記"
|
||||
else
|
||||
STATUS="🔴"
|
||||
ISSUE_LINE="🚨 需人工複核"
|
||||
fi
|
||||
|
||||
html_escape() { sed 's/&/\&/g; s/</\</g; s/>/\>/g'; }
|
||||
SUMMARY_ESC="$(printf '%s' "$SUMMARY" | html_escape)"
|
||||
ACTION_ESC="$(printf '%s' "$ACTION" | html_escape)"
|
||||
TOP_ESC="$(printf '%s' "$TOP_ISSUE" | html_escape)"
|
||||
|
||||
MSG="$(printf '%s <b>Code Review 完成・%s</b>\n──────────────────────\n🔴 CRITICAL <code>%s</code> 🟠 HIGH <code>%s</code> 🟡 MEDIUM <code>%s</code> 🟢 LOW <code>%s</code>\n──────────────────────\n⚠️ <b>主要問題</b>\n%s\n\n🔍 <b>整體風險等級</b>\n%s:%s\n\n⚠️ <b>最高關注問題</b>\n1. %s\n──────────────────────\n🤖 Elephant Alpha:<b>%s</b> ✅ %s\n📊 完整報告:<a href=\"%s\">%s</a>' "$STATUS" "$SHORT_SHA" "$CRITICAL" "$HIGH" "$MEDIUM" "$LOW" "$ISSUE_LINE" "$RISK" "$SUMMARY_ESC" "$TOP_ESC" "$RISK" "$ACTION_ESC" "$REPORT_URL" "$REPORT_URL")"
|
||||
CICD_STATUS=success
|
||||
if [ "$RISK" = "MEDIUM" ]; then CICD_STATUS=pending; fi
|
||||
if [ "$RISK" = "HIGH" ] || [ "$RISK" = "CRITICAL" ]; then CICD_STATUS=failed; fi
|
||||
if AWOOI_CICD_STATUS="${CICD_STATUS}" \
|
||||
AWOOI_CICD_STAGE=code-review \
|
||||
AWOOI_CICD_JOB_NAME="Code Review 完成・${RISK}" \
|
||||
AWOOI_CICD_COMMIT_SHA="${GITHUB_SHA}" \
|
||||
AWOOI_CICD_TRIGGERED_BY="${GITHUB_ACTOR:-CI}" \
|
||||
AWOOI_CICD_SUMMARY="CRITICAL=${CRITICAL}; HIGH=${HIGH}; MEDIUM=${MEDIUM}; LOW=${LOW}; ${SUMMARY}" \
|
||||
AWOOI_CICD_WORKFLOW_URL="${REPORT_URL}" \
|
||||
scripts/ci/notify-awoooi-cicd.sh; then
|
||||
echo "Code review completion notification mirrored through AWOOI API"
|
||||
else
|
||||
if [ -z "${TG_BOT_TOKEN:-}" ] || [ -z "${SRE_GROUP_CHAT_ID:-}" ]; then
|
||||
echo "Telegram secret missing and AWOOI API notify failed; skip completion notification"
|
||||
exit 0
|
||||
fi
|
||||
curl -fsS -X POST "https://api.telegram.org/bot${TG_BOT_TOKEN}/sendMessage" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$(jq -n --arg c "$SRE_GROUP_CHAT_ID" --arg t "$MSG" '{chat_id:$c,text:$t,parse_mode:"HTML",disable_web_page_preview:true}')" \
|
||||
>/dev/null
|
||||
fi
|
||||
@@ -1,7 +1,7 @@
|
||||
# =============================================================================
|
||||
# Deploy Prometheus Alert Rules (獨立 workflow)
|
||||
# 2026-04-05 Claude Code (ADR-039 I3): 從 cd.yaml 分離
|
||||
# 觸發條件: ops/monitoring/alerts-unified.yml / slo-rules.yml 有變更 或 workflow_dispatch
|
||||
# 觸發條件: ops/monitoring/alerts-unified.yml 有變更 或 workflow_dispatch
|
||||
# 說明: 告警規則部署不依賴應用構建,獨立觸發以加快響應速度
|
||||
# =============================================================================
|
||||
|
||||
@@ -12,17 +12,12 @@ on:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'ops/monitoring/alerts-unified.yml'
|
||||
- 'ops/monitoring/slo-rules.yml'
|
||||
- 'scripts/ops/deploy-alerts.sh'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
SRE_GROUP_CHAT_ID: "-1003711974679"
|
||||
|
||||
jobs:
|
||||
deploy-alerts:
|
||||
name: "Deploy Prometheus Alert Rules"
|
||||
runs-on: awoooi-ubuntu
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -32,15 +27,11 @@ jobs:
|
||||
run: |
|
||||
pip3 install -q pyyaml 2>/dev/null || pip install -q pyyaml
|
||||
python3 -c "import yaml; yaml.safe_load(open('ops/monitoring/alerts-unified.yml')); print('YAML OK')"
|
||||
python3 -c "import yaml; yaml.safe_load(open('ops/monitoring/slo-rules.yml')); print('SLO YAML OK')"
|
||||
|
||||
- name: Setup SSH key
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
umask 077
|
||||
cat > ~/.ssh/id_ed25519 <<'AWOOOI_DEPLOY_KEY'
|
||||
${{ secrets.DEPLOY_SSH_KEY }}
|
||||
AWOOOI_DEPLOY_KEY
|
||||
echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
ssh-keyscan 192.168.0.110 >> ~/.ssh/known_hosts
|
||||
|
||||
@@ -56,17 +47,6 @@ jobs:
|
||||
SHORT_SHA="${{ github.sha }}"
|
||||
SHORT_SHA="${SHORT_SHA:0:7}"
|
||||
MSG="${EMOJI} Prometheus 告警規則部署 ${STATUS} (${SHORT_SHA})"
|
||||
CICD_STATUS="success"
|
||||
[ "$STATUS" != "success" ] && CICD_STATUS="failed"
|
||||
if AWOOI_CICD_STATUS="${CICD_STATUS}" \
|
||||
AWOOI_CICD_STAGE=deploy-alerts \
|
||||
AWOOI_CICD_JOB_NAME="Prometheus 告警規則部署" \
|
||||
AWOOI_CICD_COMMIT_SHA="${{ github.sha }}" \
|
||||
AWOOI_CICD_SUMMARY="${MSG}" \
|
||||
scripts/ci/notify-awoooi-cicd.sh; then
|
||||
echo "Alert rule deploy notification mirrored through AWOOI API"
|
||||
else
|
||||
curl -fS -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
|
||||
-d "chat_id=${{ env.SRE_GROUP_CHAT_ID }}" \
|
||||
--data-urlencode "text=${MSG}" || true
|
||||
fi
|
||||
curl -fS -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
|
||||
-d "chat_id=${{ secrets.TELEGRAM_CHAT_ID }}" \
|
||||
--data-urlencode "text=${MSG}" || true
|
||||
|
||||
@@ -19,11 +19,10 @@ env:
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: http://192.168.0.188:24318
|
||||
OTEL_SERVICE_NAME: awoooi-e2e
|
||||
OTEL_RESOURCE_ATTRIBUTES: deployment.environment=production
|
||||
SRE_GROUP_CHAT_ID: "-1003711974679"
|
||||
|
||||
jobs:
|
||||
e2e-health:
|
||||
runs-on: awoooi-ubuntu
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -51,52 +50,11 @@ jobs:
|
||||
echo "status=failed" >> $GITHUB_OUTPUT
|
||||
exit 1
|
||||
|
||||
- name: Source Provider Freshness Smoke
|
||||
run: |
|
||||
SOURCE_CANARY_RUN_REF="gitea-e2e-${GITHUB_RUN_ID:-manual}-${GITHUB_RUN_ATTEMPT:-1}"
|
||||
echo "SOURCE_CANARY_RUN_REF=${SOURCE_CANARY_RUN_REF}" >> "$GITHUB_ENV"
|
||||
echo "SOURCE_LINK_CANARY_WORK_ITEM_ID=source-evidence:sentry:upstream_canary:awoooi-source-link-canary-${SOURCE_CANARY_RUN_REF}" >> "$GITHUB_ENV"
|
||||
OPERATOR_KEY="$(cat <<'AWOOOI_SECRET_AWOOOP_OPERATOR_API_KEY'
|
||||
${{ secrets.AWOOOP_OPERATOR_API_KEY }}
|
||||
AWOOOI_SECRET_AWOOOP_OPERATOR_API_KEY
|
||||
)"
|
||||
AWOOOP_OPERATOR_API_KEY="${OPERATOR_KEY}" \
|
||||
AWOOOP_OPERATOR_ID=gitea-e2e-health \
|
||||
python3 scripts/alert_chain_smoke_test.py \
|
||||
--api-url https://awoooi.wooo.work \
|
||||
--metrics-api-url http://192.168.0.125:32334 \
|
||||
--source-provider-heartbeat \
|
||||
--source-provider-upstream-canary \
|
||||
--run-ref "${SOURCE_CANARY_RUN_REF}" \
|
||||
--source-link-canary-target-incident-id INC-20260505-25E744 \
|
||||
--json
|
||||
|
||||
- name: Source Correlation Applied-Link Smoke
|
||||
run: |
|
||||
python3 scripts/awooop_source_correlation_apply_smoke.py \
|
||||
--api-url https://awoooi.wooo.work \
|
||||
--target-incident-id INC-20260505-25E744 \
|
||||
--allow-existing-apply \
|
||||
--refresh-if-stale-days 6 \
|
||||
--refresh-work-item-id "${SOURCE_LINK_CANARY_WORK_ITEM_ID}" \
|
||||
--verify-refresh-candidate \
|
||||
--reviewer-id gitea_e2e_source_link_canary \
|
||||
--operator-note "T124 dedicated source-link canary refresh; append-only status-chain proof"
|
||||
|
||||
- name: Notify Telegram on Failure
|
||||
if: failure()
|
||||
run: |
|
||||
MSG="E2E Health Check 失敗;API 健康檢查未通過"
|
||||
if AWOOI_CICD_STATUS=failed \
|
||||
AWOOI_CICD_STAGE=e2e-health \
|
||||
AWOOI_CICD_JOB_NAME="E2E Health Check" \
|
||||
AWOOI_CICD_COMMIT_SHA="${{ github.sha }}" \
|
||||
AWOOI_CICD_SUMMARY="${MSG}" \
|
||||
scripts/ci/notify-awoooi-cicd.sh; then
|
||||
echo "E2E failure notification mirrored through AWOOI API"
|
||||
else
|
||||
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
|
||||
-d chat_id="${{ env.SRE_GROUP_CHAT_ID }}" \
|
||||
-d parse_mode="HTML" \
|
||||
-d text="🔴 <b>[E2E Health Check]</b> 失敗%0A%0A📅 $(TZ=Asia/Taipei date '+%Y-%m-%d %H:%M')%0A🔗 API 健康檢查未通過%0A%0A請檢查 K3s 叢集狀態"
|
||||
fi
|
||||
curl -s -X POST "https://api.telegram.org/bot${{ secrets.OPENCLAW_TG_BOT_TOKEN }}/sendMessage" \
|
||||
-d chat_id="${{ secrets.OPENCLAW_TG_CHAT_ID }}" \
|
||||
-d parse_mode="HTML" \
|
||||
-d text="🔴 <b>[E2E Health Check]</b> 失敗%0A%0A📅 $(TZ=Asia/Taipei date '+%Y-%m-%d %H:%M')%0A🔗 API 健康檢查未通過%0A%0A請檢查 K3s 叢集狀態"
|
||||
|
||||
|
||||
@@ -17,14 +17,12 @@ on:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'apps/api/migrations/*.sql'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
SRE_GROUP_CHAT_ID: "-1003711974679"
|
||||
|
||||
jobs:
|
||||
migrate:
|
||||
runs-on: awoooi-ubuntu # 或 self-hosted runner on 110
|
||||
runs-on: ubuntu-latest # 或 self-hosted runner on 110
|
||||
container:
|
||||
image: postgres:15-alpine # 帶 psql
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -32,126 +30,46 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 2 # 需比對上一個 commit
|
||||
|
||||
- name: Install migration tools
|
||||
run: |
|
||||
set -euo pipefail
|
||||
missing=""
|
||||
for bin in psql jq curl; do
|
||||
if ! command -v "$bin" >/dev/null 2>&1; then
|
||||
missing="$missing $bin"
|
||||
fi
|
||||
done
|
||||
if [ -z "$missing" ]; then
|
||||
exit 0
|
||||
fi
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
apt-get update -qq
|
||||
apt-get install -y -q postgresql-client jq curl
|
||||
elif command -v apk >/dev/null 2>&1; then
|
||||
apk add --no-cache postgresql-client jq curl
|
||||
else
|
||||
echo "::error::missing required tools:$missing"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Identify new migrations
|
||||
id: diff
|
||||
run: |
|
||||
ALL_NEW_FILES=$(git diff --no-renames --name-only --diff-filter=A HEAD~1 HEAD -- 'apps/api/migrations/*.sql' || true)
|
||||
NEW_FILES=$(echo "$ALL_NEW_FILES" | grep -Ev '(_down|rollback)\.sql$' || true)
|
||||
SKIPPED_ROLLBACK_FILES=$(echo "$ALL_NEW_FILES" | grep -E '(_down|rollback)\.sql$' || true)
|
||||
NEW_FILES=$(git diff --name-only --diff-filter=A HEAD~1 HEAD -- 'apps/api/migrations/*.sql' || true)
|
||||
echo "new_files<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$NEW_FILES" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
echo "=== New migration files ==="
|
||||
echo "$NEW_FILES"
|
||||
if [ -n "$SKIPPED_ROLLBACK_FILES" ]; then
|
||||
echo "=== Rollback/down migrations skipped by design ==="
|
||||
echo "$SKIPPED_ROLLBACK_FILES"
|
||||
fi
|
||||
|
||||
- name: Apply new migrations
|
||||
if: steps.diff.outputs.new_files != ''
|
||||
env:
|
||||
# 從 Gitea secrets 取,不直接明碼
|
||||
PGURL: ${{ secrets.MIGRATION_DATABASE_URL }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# 從 Gitea secrets 取,不放 step-level env,避免 runner log 展開。
|
||||
# MIGRATION_DATABASE_URL 是限權帳號;DATABASE_URL 只在 PostgreSQL
|
||||
# 明確回報「必須是 table owner」時作為受控 fallback。
|
||||
PGURL="$(cat <<'AWOOOI_SECRET_MIGRATION_DATABASE_URL'
|
||||
${{ secrets.MIGRATION_DATABASE_URL }}
|
||||
AWOOOI_SECRET_MIGRATION_DATABASE_URL
|
||||
)"
|
||||
OWNER_PGURL="$(cat <<'AWOOOI_SECRET_DATABASE_URL'
|
||||
${{ secrets.DATABASE_URL }}
|
||||
AWOOOI_SECRET_DATABASE_URL
|
||||
)"
|
||||
if [ -z "$PGURL" ]; then
|
||||
echo "::error::MIGRATION_DATABASE_URL secret not set in Gitea"
|
||||
exit 1
|
||||
fi
|
||||
PGURL_PSQL="${PGURL/postgresql+asyncpg:\/\//postgresql:\/\/}"
|
||||
OWNER_PGURL_PSQL="${OWNER_PGURL/postgresql+asyncpg:\/\//postgresql:\/\/}"
|
||||
|
||||
apply_migration() {
|
||||
local url="$1"
|
||||
local file="$2"
|
||||
psql "$url" \
|
||||
-v ON_ERROR_STOP=1 \
|
||||
--single-transaction \
|
||||
-f "$file"
|
||||
}
|
||||
|
||||
# 套用每個新檔 (single transaction per file)
|
||||
echo "${{ steps.diff.outputs.new_files }}" | while IFS= read -r file; do
|
||||
[ -z "$file" ] && continue
|
||||
echo "=== Applying: $file ==="
|
||||
migration_err="$(mktemp)"
|
||||
if ! apply_migration "$PGURL_PSQL" "$file" 2>"$migration_err"; then
|
||||
if grep -Eq "(must be owner of table|permission denied for table)" "$migration_err"; then
|
||||
if [ -z "$OWNER_PGURL_PSQL" ]; then
|
||||
cat "$migration_err" >&2
|
||||
echo "::error::migration requires table owner but DATABASE_URL secret is not set"
|
||||
exit 1
|
||||
fi
|
||||
echo "::warning::migration requires table owner; retrying with owner connection"
|
||||
apply_migration "$OWNER_PGURL_PSQL" "$file"
|
||||
else
|
||||
cat "$migration_err" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
rm -f "$migration_err"
|
||||
psql "$PGURL" \
|
||||
-v ON_ERROR_STOP=1 \
|
||||
--single-transaction \
|
||||
-f "$file"
|
||||
echo "=== OK: $file ==="
|
||||
done
|
||||
|
||||
- name: Seed asset_discovery_run (audit)
|
||||
if: steps.diff.outputs.new_files != ''
|
||||
env:
|
||||
PGURL: ${{ secrets.MIGRATION_DATABASE_URL }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
PGURL="$(cat <<'AWOOOI_SECRET_MIGRATION_DATABASE_URL'
|
||||
${{ secrets.MIGRATION_DATABASE_URL }}
|
||||
AWOOOI_SECRET_MIGRATION_DATABASE_URL
|
||||
)"
|
||||
OWNER_PGURL="$(cat <<'AWOOOI_SECRET_DATABASE_URL'
|
||||
${{ secrets.DATABASE_URL }}
|
||||
AWOOOI_SECRET_DATABASE_URL
|
||||
)"
|
||||
if [ -z "$PGURL" ]; then
|
||||
echo "::error::MIGRATION_DATABASE_URL secret not set in Gitea"
|
||||
exit 1
|
||||
fi
|
||||
PGURL_PSQL="${PGURL/postgresql+asyncpg:\/\//postgresql:\/\/}"
|
||||
OWNER_PGURL_PSQL="${OWNER_PGURL/postgresql+asyncpg:\/\//postgresql:\/\/}"
|
||||
FILES_JSON=$(echo "${{ steps.diff.outputs.new_files }}" | jq -Rn '[inputs | select(length > 0)]')
|
||||
SUMMARY_JSON=$(jq -cn \
|
||||
--arg commit_sha "${{ github.sha }}" \
|
||||
--argjson files "$FILES_JSON" \
|
||||
'{type: "ci_migration", commit_sha: $commit_sha, files: $files}')
|
||||
SUMMARY_JSON_SQL=${SUMMARY_JSON//\'/\'\'}
|
||||
|
||||
seed_audit() {
|
||||
local url="$1"
|
||||
psql "$url" -v ON_ERROR_STOP=1 <<SQL
|
||||
psql "$PGURL" -c "
|
||||
INSERT INTO asset_discovery_run (
|
||||
run_id, triggered_by, scope, scan_depth, status,
|
||||
started_at, ended_at, tools_used, summary
|
||||
@@ -163,52 +81,26 @@ jobs:
|
||||
'success',
|
||||
NOW(),
|
||||
NOW(),
|
||||
'{"psql": 1, "gitea_ci": 1}'::jsonb,
|
||||
'${SUMMARY_JSON_SQL}'::jsonb
|
||||
'{\"psql\": 1, \"gitea_ci\": 1}'::jsonb,
|
||||
jsonb_build_object(
|
||||
'type', 'ci_migration',
|
||||
'commit_sha', '${{ github.sha }}',
|
||||
'files', $FILES_JSON
|
||||
)
|
||||
);
|
||||
SQL
|
||||
}
|
||||
|
||||
audit_err="$(mktemp)"
|
||||
if ! seed_audit "$PGURL_PSQL" 2>"$audit_err"; then
|
||||
if grep -q "permission denied for table asset_discovery_run" "$audit_err"; then
|
||||
if [ -z "$OWNER_PGURL_PSQL" ]; then
|
||||
cat "$audit_err" >&2
|
||||
echo "::error::audit requires table insert privilege but DATABASE_URL secret is not set"
|
||||
exit 1
|
||||
fi
|
||||
echo "::warning::audit requires owner connection; retrying with owner connection"
|
||||
seed_audit "$OWNER_PGURL_PSQL"
|
||||
else
|
||||
cat "$audit_err" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
rm -f "$audit_err"
|
||||
"
|
||||
|
||||
- name: Notify Telegram (if configured)
|
||||
if: always()
|
||||
env:
|
||||
TG_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||
TG_CHAT: ${{ secrets.TELEGRAM_OPS_CHAT_ID }}
|
||||
run: |
|
||||
TG_TOKEN="$(cat <<'AWOOOI_SECRET_TG_TOKEN'
|
||||
${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||
AWOOOI_SECRET_TG_TOKEN
|
||||
)"
|
||||
STATUS="${{ job.status }}"
|
||||
CICD_STATUS="success"
|
||||
[ "$STATUS" != "success" ] && CICD_STATUS="failed"
|
||||
if AWOOI_CICD_STATUS="${CICD_STATUS}" \
|
||||
AWOOI_CICD_STAGE=run-migration \
|
||||
AWOOI_CICD_JOB_NAME="Migration CI" \
|
||||
AWOOI_CICD_COMMIT_SHA="${{ github.sha }}" \
|
||||
AWOOI_CICD_SUMMARY="Migration CI: ${STATUS}" \
|
||||
scripts/ci/notify-awoooi-cicd.sh; then
|
||||
echo "Migration notification mirrored through AWOOI API"
|
||||
exit 0
|
||||
fi
|
||||
if [ -n "$TG_TOKEN" ] && [ -n "${{ env.SRE_GROUP_CHAT_ID }}" ]; then
|
||||
if [ -n "$TG_TOKEN" ] && [ -n "$TG_CHAT" ]; then
|
||||
STATUS="${{ job.status }}"
|
||||
MSG="🗄️ Migration CI: \`${STATUS}\` — commit ${{ github.sha }}"
|
||||
curl -s -X POST "https://api.telegram.org/bot${TG_TOKEN}/sendMessage" \
|
||||
-d chat_id="${{ env.SRE_GROUP_CHAT_ID }}" \
|
||||
-d chat_id="${TG_CHAT}" \
|
||||
-d parse_mode="Markdown" \
|
||||
-d text="${MSG}" || true
|
||||
fi
|
||||
|
||||
@@ -25,7 +25,7 @@ on:
|
||||
|
||||
jobs:
|
||||
check-type-sync:
|
||||
runs-on: awoooi-ubuntu
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
|
||||
@@ -13,10 +13,12 @@
|
||||
|
||||
name: CD
|
||||
|
||||
# 2026-05-12 Codex: GitHub 僅保留唯讀備份;生產 CI/CD 只能從 Gitea 執行。
|
||||
# 本 workflow 曾可 push / workflow_dispatch 後 build、patch secret、kubectl apply,
|
||||
# 會和 `.gitea/workflows/cd.yaml` 競爭 K3s production 狀態,因此硬停用。
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- '*.md'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
force_deploy:
|
||||
@@ -58,7 +60,6 @@ jobs:
|
||||
# ==================== Pre-flight Check (10s Fail-Fast) ====================
|
||||
pre-flight-check:
|
||||
name: "Pre-flight Check"
|
||||
if: ${{ false }}
|
||||
runs-on: [self-hosted, harbor, k8s]
|
||||
timeout-minutes: 1
|
||||
steps:
|
||||
@@ -132,7 +133,6 @@ jobs:
|
||||
# 2026-03-29 Claude Code: 確保監控覆蓋率 >= 90%
|
||||
monitoring-coverage:
|
||||
name: "Monitoring Coverage"
|
||||
if: ${{ false }}
|
||||
runs-on: [self-hosted, harbor, k8s]
|
||||
needs: pre-flight-check
|
||||
timeout-minutes: 2
|
||||
@@ -152,7 +152,6 @@ jobs:
|
||||
# ==================== 路徑偵測 (使用 dorny/paths-filter) ====================
|
||||
detect-changes:
|
||||
name: Detect Changes
|
||||
if: ${{ false }}
|
||||
runs-on: [self-hosted, harbor, k8s]
|
||||
needs: [pre-flight-check, monitoring-coverage]
|
||||
timeout-minutes: 1
|
||||
@@ -198,7 +197,11 @@ jobs:
|
||||
runs-on: [self-hosted, harbor, k8s]
|
||||
needs: [detect-changes, build-web]
|
||||
timeout-minutes: 20
|
||||
if: ${{ false }}
|
||||
if: |
|
||||
!inputs.skip_api && (
|
||||
needs.detect-changes.outputs.api == 'true' ||
|
||||
(needs.detect-changes.outputs.api == 'false' && needs.detect-changes.outputs.web == 'false')
|
||||
)
|
||||
outputs:
|
||||
image_tag: ${{ steps.tag.outputs.tag }}
|
||||
steps:
|
||||
@@ -235,7 +238,11 @@ jobs:
|
||||
runs-on: [self-hosted, harbor, k8s]
|
||||
needs: detect-changes
|
||||
timeout-minutes: 20
|
||||
if: ${{ false }}
|
||||
if: |
|
||||
!inputs.skip_web && (
|
||||
needs.detect-changes.outputs.web == 'true' ||
|
||||
(needs.detect-changes.outputs.api == 'false' && needs.detect-changes.outputs.web == 'false')
|
||||
)
|
||||
outputs:
|
||||
image_tag: ${{ steps.tag.outputs.tag }}
|
||||
steps:
|
||||
@@ -286,7 +293,7 @@ jobs:
|
||||
concurrency:
|
||||
group: runner-awoooi-cd-mutex
|
||||
cancel-in-progress: false
|
||||
if: ${{ false }}
|
||||
if: always() && (needs.build-api.result == 'success' || needs.build-api.result == 'skipped') && (needs.build-web.result == 'success' || needs.build-web.result == 'skipped')
|
||||
environment: production
|
||||
steps:
|
||||
# 2026-03-29: Runner 診斷檔案清理 (防止並行衝突)
|
||||
@@ -14,10 +14,15 @@
|
||||
|
||||
name: Deploy to Production
|
||||
|
||||
# 2026-05-12 Codex: GitHub 是唯讀備份,production deploy 只能從 Gitea 進入。
|
||||
# 這份歷史 workflow 仍含 Harbor build/push 與 kubectl apply/rollout,會和 Gitea CD 競爭。
|
||||
# 保留檔案供稽核,但停用所有 job。
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'apps/api/**'
|
||||
- 'apps/web/**'
|
||||
- 'k8s/awoooi-prod/**'
|
||||
- '.github/workflows/deploy-prod.yml'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
deploy_api:
|
||||
@@ -65,7 +70,6 @@ jobs:
|
||||
# ===========================================================================
|
||||
build:
|
||||
name: "Build Images"
|
||||
if: ${{ false }}
|
||||
runs-on: [self-hosted, harbor, k8s]
|
||||
outputs:
|
||||
image_tag: ${{ steps.meta.outputs.tag }}
|
||||
@@ -134,7 +138,6 @@ jobs:
|
||||
deploy:
|
||||
name: "Deploy to K3s"
|
||||
needs: build
|
||||
if: ${{ false }}
|
||||
runs-on: [self-hosted, harbor, k8s]
|
||||
|
||||
steps:
|
||||
@@ -207,7 +210,7 @@ jobs:
|
||||
smoke-test:
|
||||
name: "Smoke Tests"
|
||||
needs: deploy
|
||||
if: ${{ false }}
|
||||
if: ${{ !inputs.skip_tests }}
|
||||
runs-on: [self-hosted, harbor, k8s]
|
||||
|
||||
steps:
|
||||
@@ -245,7 +248,7 @@ jobs:
|
||||
notify:
|
||||
name: "Send Notification"
|
||||
needs: [build, deploy, smoke-test]
|
||||
if: ${{ false }}
|
||||
if: always()
|
||||
runs-on: [self-hosted, harbor, k8s]
|
||||
|
||||
steps:
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -92,5 +92,3 @@ tsconfig.tsbuildinfo
|
||||
.aider*
|
||||
!.aiderignore
|
||||
.claude/settings.local.json
|
||||
.claude/settings.json
|
||||
.claude/settings.json.bak*
|
||||
|
||||
172
AGENTS.md
172
AGENTS.md
@@ -1,172 +0,0 @@
|
||||
# AWOOOI Project Configuration
|
||||
|
||||
> Codex 自動載入,定義核心原則
|
||||
> 全域工作流程(P7/P9/P10、三紅線、12-agent 委派表)見 `~/.Codex/AGENTS.md`
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Session 啟動第一步
|
||||
|
||||
**在做任何事之前,先讀:**
|
||||
1. 🔴🔴🔴 **`docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md`** — AI 自主化飛輪 MASTER 藍圖(進行中)
|
||||
2. `MEMORY.md` — 記憶索引
|
||||
3. `docs/LOGBOOK.md` — 最新進度
|
||||
4. `docs/HARD_RULES.md` — 絕對禁止規則
|
||||
5. 涉及主題的 `feedback_*.md`
|
||||
|
||||
🔴🔴🔴 **AI 自主化工程進行中** — 任何告警/修復/規則/分類/通知相關變更,必須先讀 MASTER §0 Session Resume Protocol,禁止繞過。
|
||||
|
||||
🔴🔴 **檢查 `project_current_status.md` 最後更新日期** — 超過 2 天 → 先執行 Memory 清理再開工
|
||||
|
||||
---
|
||||
|
||||
## 四大核心原則
|
||||
|
||||
1. **變更前 → 先讀註解** (理解設計意圖再動手) 🔴
|
||||
2. **事故級不可逆操作 → break-glass** (secret 明文、DROP / TRUNCATE / destructive restore、reboot、node drain、不可逆 firewall、外部攻擊型掃描、force push、repo / refs 破壞、付費 provider / 成本切換)
|
||||
3. **非事故級 gate → 受控自動執行** (不要把 owner / read-only / manual / `0 / false` 當阻擋)
|
||||
4. **任務完成 → 更新 Memory** (不等被問)
|
||||
|
||||
---
|
||||
|
||||
## 🔴🔴🔴 統帥全面授權(2026-06-28)
|
||||
|
||||
所有 AWOOOI / AwoooP / IwoooS / StockPlatform / VibeWork / MOMO / AwoooGo / 2026FIFA / Agent Bounty / Tsenyang 工作,除 `docs/HARD_RULES.md#commander-blanket-authorization` 定義的事故級硬阻擋外,一律直接進 AI controlled apply。
|
||||
|
||||
舊規範、UI、Telegram、snapshot、test 或 Runbook 若出現下列狀態,不得作為停止實作理由:
|
||||
|
||||
- `owner_response_received=0`
|
||||
- `owner_response_accepted=0`
|
||||
- `runtime_execution_authorized=false`
|
||||
- `runtime_write_gate=0`
|
||||
- `apply_allowed=false`
|
||||
- `read_only=true`
|
||||
- `manual_required`
|
||||
- `needs_human=true`
|
||||
|
||||
正確動作是 AI 自動補齊 target selector、source-of-truth diff、check-mode / dry-run、rollback、post-apply verifier、KM / PlayBook trust writeback,然後推進可驗證、可回滾、低爆炸半徑的實作。
|
||||
|
||||
---
|
||||
|
||||
## 🔴 絕對禁止 → [HARD_RULES.md](docs/HARD_RULES.md)
|
||||
|
||||
## 🔴 文件語言鐵律 → [文件語言規範](docs/HARD_RULES.md#文件語言規範)
|
||||
Markdown、ADR、LOGBOOK、Runbook、交接文件與計畫文件一律使用繁體中文;程式符號、API、指令、錯誤碼、服務名稱與原始 log 可保留英文。
|
||||
|
||||
## 🔴 紅區治理 → [RED_ZONES.md](docs/RED_ZONES.md)
|
||||
Tier 3 核心檔案 (decision_manager, trust_engine, config 等) 修改需首席架構師授權
|
||||
|
||||
---
|
||||
|
||||
## 專案架構
|
||||
|
||||
- `apps/api/` — FastAPI 後端
|
||||
- `apps/web/` — Next.js 前端
|
||||
- `k8s/` — Kubernetes 配置
|
||||
|
||||
## 🔴 Gitea CI/CD (ADR-039) → [reference_gitea_mirror.md](~/.Codex/projects/-Users-ogt-awoooi/memory/reference_gitea_mirror.md)
|
||||
|
||||
從 2026-03-29 起,所有 CI/CD 從 Gitea 執行。推版:`git push gitea main`。GitHub 只讀備份。
|
||||
|
||||
---
|
||||
|
||||
## 🛑 修改前必讀 → [HARD_RULES.md](docs/HARD_RULES.md)
|
||||
|
||||
| 檔案/功能 | 必讀章節 |
|
||||
|----------|---------|
|
||||
| `.github/workflows/*` | GitHub Billing |
|
||||
| `*telegram*` | Telegram Token |
|
||||
| `apps/web/**` | i18n |
|
||||
| Incident/Approval 流程 | Telegram + DB 鏈路 |
|
||||
| Alertmanager/NetworkPolicy 🔴🔴 | ADR-025 告警鏈路 E2E |
|
||||
| AI Provider 路由/Fallback 🔴🔴 | Phase 24 AI Router |
|
||||
|
||||
---
|
||||
|
||||
## 任務前必讀 Memory
|
||||
|
||||
| 主題 | Memory |
|
||||
|------|--------|
|
||||
| 🔴🔴 定期清理 | `feedback_memory_cleanup_schedule.md` |
|
||||
| 🔴🔴🔴 費用變更 | `feedback_cost_change_approval.md` |
|
||||
| 變更前必讀 🔴 | `feedback_read_comments_first.md` |
|
||||
| 變更註解 🔴🔴 | `feedback_change_annotation_standard.md` |
|
||||
| 重大變更 | `feedback_product_survival_principles.md` |
|
||||
| Telegram | `feedback_telegram_token_disaster.md` |
|
||||
| OpenClaw | `feedback_architecture_openclaw_core.md` |
|
||||
| 命名規範 | `feedback_openclaw_naming.md` |
|
||||
| i18n | `feedback_i18n_zero_hardcode.md` |
|
||||
| 防禦性工程/狀態機驗證 | `feedback_defensive_engineering.md` |
|
||||
| 禁止孤島開發 🔴🔴 | `HARD_RULES.md` → No Island Coding |
|
||||
| 主動執行與熔斷 🔴🔴 | `feedback_proactive_execution.md` + `HARD_RULES.md` → Circuit Breaker |
|
||||
| 自循環工作流 🔴🔴 | `HARD_RULES.md` → Self-Loop Workflow |
|
||||
| 積木化強制 🔴🔴 | `feedback_lewooogo_modular_enforcement.md` |
|
||||
| API 整合 | `feedback_api_response_verification.md` |
|
||||
| 構建部署 | `feedback_build_from_git_only.md` |
|
||||
| 測試 🔴🔴 | `feedback_no_mock_testing.md` |
|
||||
| API 路徑 🔴 | `feedback_api_path_naming.md` |
|
||||
| 部署驗證 🔴🔴 | `feedback_deployment_verification.md` |
|
||||
| 部署層級 🔴🔴🔴 | `feedback_deployment_layer_decision.md` |
|
||||
| 告警鏈路 🔴🔴🔴 | `feedback_alertchain_e2e_validation.md` |
|
||||
| Telegram Secrets 🔴🔴🔴 | `feedback_telegram_secrets_injection.md` |
|
||||
| 前端內網禁令 🔴🔴🔴 | `feedback_frontend_internal_ip_ban.md` |
|
||||
| AI Router 重構 🔴🔴 | `project_phase24_ai_router.md` |
|
||||
| AI Fallback 順序 🔴 | `feedback_ai_fallback_order.md` |
|
||||
| 前端 Icon 規範 🔴 | `feedback_no_emoji_use_icons.md` |
|
||||
| 設計稿預覽 🔴 | `feedback_ui_collaboration_protocol.md` |
|
||||
|
||||
---
|
||||
|
||||
## 重要規則摘要(詳情在 Memory)
|
||||
|
||||
- **前端內網 IP 禁令** 🔴🔴🔴 — `NEXT_PUBLIC_*` 禁用內網 IP,用公網域名(build-time 寫死進 JS Bundle)
|
||||
- **Telegram 告警鏈路** 🔴🔴🔴 — CD 必須自動注入 K8s Secrets;禁止 CHANGE_ME;部署後 E2E 驗證 → ADR-035
|
||||
- **leWOOOgo 積木化** 🔴🔴 — 修改 `apps/api/` 前必問 5 題,Router 層禁止直接存取 Redis/DB
|
||||
- **Phase 24 AI Router** ✅ — ADR-052 完成,Router 只依賴 Protocol,絞殺者開關 `USE_AI_ROUTER`
|
||||
|
||||
---
|
||||
|
||||
## Skills 載入
|
||||
|
||||
| 任務類型 | Skill 路徑 |
|
||||
|---------|-----------|
|
||||
| 前端 | `.agents/skills/01-awoooi-frontend-aesthetics.md` |
|
||||
| 後端 | `.agents/skills/02-lewooogo-backend-core.md` |
|
||||
| AI/決策 | `.agents/skills/03-openclaw-cognitive-expert.md` |
|
||||
| DevOps | `.agents/skills/04-awoooi-devops-commander.md` |
|
||||
| 測試 | `.agents/skills/05-awoooi-sre-qa.md` |
|
||||
| Git | `.agents/skills/06-awoooi-monorepo-master.md` |
|
||||
| Tool 整合 | `.agents/skills/07-tool-integration-expert.md` |
|
||||
| 模型路由 | `.agents/skills/08-model-router-expert.md` |
|
||||
| 絞殺者重構 | `.agents/skills/09-strangler-pattern-expert.md` |
|
||||
|
||||
## Memory 系統
|
||||
|
||||
- 長期記憶:`~/.Codex/projects/-Users-ogt-awoooi/memory/`
|
||||
- 索引:`MEMORY.md`
|
||||
- 進度:`docs/LOGBOOK.md`
|
||||
- 參考:[SERVICE-ENDPOINTS.md](docs/reference/SERVICE-ENDPOINTS.md) / [K3S-OPTIMIZATION-RUNBOOK.md](docs/runbooks/K3S-OPTIMIZATION-RUNBOOK.md)
|
||||
|
||||
## Session 結束前
|
||||
|
||||
更新相關 Memory → 更新 LOGBOOK → 標記下一步
|
||||
|
||||
---
|
||||
|
||||
## 安全架構(ty-ai-standards Global-Local)
|
||||
|
||||
本專案採用 **全域 hooks(`~/.Codex/hooks/`)+ 專案 hooks(`.Codex/hooks/`)疊加執行**。
|
||||
|
||||
| Hook | 層級 | 觸發點 | 防護內容 |
|
||||
|------|------|--------|---------|
|
||||
| `awoooi-guard.js` | 專案 | PreToolUse | 生產環境危險操作阻擋(待建立) |
|
||||
| `branch-protection.js` | 全域 | PreToolUse | force push + 直接 commit 到 production |
|
||||
| `commit-quality.js` | 全域 | PreToolUse | debugger + 硬編碼 secrets(含 secrets.local.json 補充 patterns) |
|
||||
| `large-file-warner.js` | 全域 | PreToolUse | >2MB 阻擋,>500KB 警告 |
|
||||
| `mcp-health.js` | 全域 | PreToolUse | MCP 冷卻保護 |
|
||||
| `audit-log.js` | 全域 | PostToolUse | Bash 指令稽核 |
|
||||
| `suggest-compact.js` | 全域 | PostToolUse | 50 次工具呼叫後建議 /compact |
|
||||
| `cost-tracker.js` | 全域 | Stop | Token 用量追蹤 |
|
||||
| `session-summary.js` | 全域 | Stop | 對話快照存檔 |
|
||||
|
||||
專案 secrets pattern(`.Codex/hooks/secrets.local.json`):Telegram / Gitea / NVIDIA / Gemini / Anthropic / PostgreSQL
|
||||
@@ -1 +1 @@
|
||||
# 2026-06-27 retry AI automation closure deploy with array needs syntax
|
||||
# 2026-04-05 warm-up deploy triggered
|
||||
|
||||
@@ -44,6 +44,25 @@ FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy installed packages from builder
|
||||
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
|
||||
COPY --from=builder /usr/local/bin /usr/local/bin
|
||||
|
||||
# 2026-04-01 ogt: CACHE_BUST 強制失效 src/ 和 models.json 層
|
||||
# deps 層 (pip install) 仍可 cache;代碼/配置變更必須重建
|
||||
ARG CACHE_BUST=none
|
||||
COPY apps/api/src/ ./src/
|
||||
COPY apps/api/models.json ./models.json
|
||||
# 2026-04-09 ogt: 規則引擎配置 — alert_rule_engine.py 從此檔載入規則
|
||||
COPY apps/api/alert_rules.yaml ./alert_rules.yaml
|
||||
# 2026-04-10 Claude Sonnet 4.6: drift_detector 需要 k8s/ YAML 做 Git state 比對
|
||||
COPY k8s/ ./k8s/
|
||||
# 2026-04-10 Claude Sonnet 4.6: RAG 知識庫索引來源 (ADR-067 Phase 33)
|
||||
COPY docs/ ./docs/
|
||||
COPY .agents/skills/ ./.agents/skills/
|
||||
# 2026-04-12 ogt (ADR-073 P2-1): CronJob 腳本 — 獨立腳本取代 inline Python
|
||||
COPY scripts/ ./scripts/
|
||||
|
||||
# Install openssh-client + curl — SSH_COMMAND Playbook + healthcheck
|
||||
# Install kubectl — drift_detector 需要 kubectl 讀取 K8s 實際狀態
|
||||
# (2026-04-09 Claude Sonnet 4.6 Asia/Taipei, Bug #6 修正 — python:3.11-slim 無 openssh-client)
|
||||
@@ -53,38 +72,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends openssh-client
|
||||
chmod +x kubectl && mv kubectl /usr/local/bin/kubectl && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create non-root user before copying app artifacts so COPY --chown can avoid
|
||||
# an expensive full-tree chown layer on every source-only rebuild.
|
||||
RUN useradd -m -u 1000 appuser
|
||||
|
||||
# Copy installed packages from builder
|
||||
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
|
||||
COPY --from=builder /usr/local/bin /usr/local/bin
|
||||
|
||||
# 2026-04-01 ogt: CACHE_BUST 強制失效 src/ 和 models.json 層
|
||||
# deps 層 (pip install) 仍可 cache;代碼/配置變更必須重建
|
||||
ARG CACHE_BUST=none
|
||||
COPY --chown=appuser:appuser apps/api/src/ ./src/
|
||||
# 2026-04-09 ogt: 規則引擎配置 — alert_rule_engine.py 從此檔載入規則
|
||||
COPY --chown=appuser:appuser apps/api/models.json ./models.json
|
||||
COPY --chown=appuser:appuser apps/api/alert_rules.yaml ./alert_rules.yaml
|
||||
# 2026-04-10 Claude Sonnet 4.6: drift_detector 需要 k8s/ YAML 做 Git state 比對
|
||||
COPY --chown=appuser:appuser k8s/ ./k8s/
|
||||
# 2026-05-24 Codex: truth-chain / Ansible readiness needs the repo-known
|
||||
# playbook catalog in the API image.
|
||||
# 2026-05-31 Codex: ansible-core is now installed through pyproject.toml so
|
||||
# this catalog can graduate from visibility-only to check-mode runtime-ready
|
||||
# once repair SSH material is mounted and readable. This still does not enable
|
||||
# automatic apply; approval/execution code remains the gate.
|
||||
COPY --chown=appuser:appuser infra/ansible/ ./infra/ansible/
|
||||
# 2026-04-10 Claude Sonnet 4.6: RAG 知識庫索引來源 (ADR-067 Phase 33)
|
||||
COPY --chown=appuser:appuser docs/ ./docs/
|
||||
COPY --chown=appuser:appuser .agents/skills/ ./.agents/skills/
|
||||
# 2026-05-04 Claude Sonnet 4.6 (Task 1.2): hermes agent_loader 的 system prompt 來源
|
||||
# agent_loader.py 預設讀 /app/.claude/agents/,對應 K8s AGENTS_DIR 環境變數
|
||||
COPY --chown=appuser:appuser .claude/agents/ ./.claude/agents/
|
||||
# 2026-04-12 ogt (ADR-073 P2-1): CronJob 腳本 — 獨立腳本取代 inline Python
|
||||
COPY --chown=appuser:appuser scripts/ ./scripts/
|
||||
# Create non-root user
|
||||
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
|
||||
USER appuser
|
||||
|
||||
# Expose port
|
||||
|
||||
@@ -53,7 +53,6 @@ rules:
|
||||
alertname:
|
||||
- TargetDown
|
||||
- InstanceDown
|
||||
- NodeExporterDown
|
||||
response:
|
||||
action_title: "重啟 {job} exporter on {host}"
|
||||
description: "⚙️ 規則匹配: Prometheus 無法抓取 {instance} ({job}) 指標。自動重啟主機上的 exporter container。"
|
||||
@@ -136,8 +135,6 @@ rules:
|
||||
- HostUnusualDiskWriteRate
|
||||
- HostDiskWillFillIn24Hours
|
||||
- HostOutOfDiskSpace
|
||||
- HostDiskUsageHigh
|
||||
- HostDiskUsageCritical
|
||||
# 網路相關
|
||||
- HostUnusualNetworkThroughputIn
|
||||
- HostUnusualNetworkThroughputOut
|
||||
@@ -150,80 +147,14 @@ rules:
|
||||
- HostClockSkewDetected
|
||||
- HostClockNotSynchronising
|
||||
response:
|
||||
action_title: "🔍 主機自動診斷 — SSH 收集根因"
|
||||
description: "主機層告警(node_exporter)。自動 SSH 登入主機執行診斷指令,收集 CPU/記憶體/磁碟資訊後回報。"
|
||||
# 2026-04-27 Claude Sonnet 4.6: 從 NO_ACTION 改為自動 SSH 診斷
|
||||
# 根因:SSH_MCP_ALLOWED_HOSTS 空白導致全部降為人工審核(飛輪完全停轉)
|
||||
# 修復:補 SSH_MCP_ALLOWED_HOSTS 白名單 + 改為自動診斷指令(收集不修改,安全)
|
||||
# 診斷原則:只收集資訊,不做任何改動 → risk=low 且不在 _DESTRUCTIVE_PATTERNS 清單
|
||||
suggested_action: SSH_DIAGNOSE
|
||||
kubectl_command: "ssh {host} 'echo \"=== CPU TOP ===\"; ps aux --sort=-%cpu | head -15; echo \"=== MEMORY ===\"; free -h; echo \"=== DISK ===\"; df -h; echo \"=== LOAD ===\"; uptime'"
|
||||
action_title: "⚠️ 主機告警 — 需 SSH 人工排查"
|
||||
description: "⚠️ 主機層告警(node_exporter)。此告警源自主機資源,無法透過 kubectl 自動修復。請 SSH 登入主機排查根因:top / htop / df -h / journalctl -xe。"
|
||||
suggested_action: NO_ACTION
|
||||
kubectl_command: ""
|
||||
estimated_downtime: "N/A"
|
||||
risk: low
|
||||
responsibility: INFRA
|
||||
reasoning: "[規則匹配] 主機層資源告警,自動 SSH 執行診斷指令(只讀,不修改),收集根因資訊後推送 Telegram 讓 SRE 決策。"
|
||||
|
||||
# 2026-05-05 ogt + Codex: 110/188 長時間過載事故後補 Docker Compose 過載與 restart spike 路由。
|
||||
# 原則:過載與重啟暴增只能先診斷,禁止通用 docker restart;由 LLM + Playbook trust 決定 service-specific 修復。
|
||||
- id: docker_baseline_overload_alert
|
||||
priority: 44
|
||||
description: Docker Compose 服務過載 / restart spike 基線告警(cadvisor + textfile exporter)
|
||||
match:
|
||||
alertname:
|
||||
- HostLoadAverageSustainedHigh
|
||||
- DockerContainerCpuSustainedHigh
|
||||
- DockerContainerCpuRunawayCritical
|
||||
- DockerContainerMemoryLimitPressure
|
||||
- DockerContainerMissingResourceLimit
|
||||
- DockerContainerRestartSpike
|
||||
- DockerGiteaActionsJobStale
|
||||
response:
|
||||
action_title: "🔍 Docker/Host 過載自動診斷 — 禁止通用重啟"
|
||||
description: "110/188 Docker Compose 或主機 load 長時間偏離 baseline。AI 需先收集容器 CPU、restart、logs、ClickHouse/Kafka/爬蟲狀態,再選擇限流、降併發或服務專屬 playbook。"
|
||||
suggested_action: SSH_DIAGNOSE
|
||||
kubectl_command: "ssh {host} 'echo \"=== LOAD ===\"; uptime; echo \"=== TOP ===\"; ps aux --sort=-%cpu | head -20; echo \"=== DOCKER ===\"; docker stats --no-stream | head -40'"
|
||||
estimated_downtime: "N/A"
|
||||
risk: low
|
||||
responsibility: INFRA
|
||||
responsibility_reasoning: "Docker Compose / bare-metal 過載屬主機與平台資源治理,不能交給 K8s restart 處理"
|
||||
secondary_teams: [BE, SRE]
|
||||
optimization:
|
||||
- type: BASELINE_CHECK
|
||||
description: "比較 load5/core、單容器 CPU core、restart spike 與 24h 動態基線"
|
||||
command: "Prometheus query: node_load5/core + rate(container_cpu_usage_seconds_total[5m]) + increase(docker_container_restart_count[15m])"
|
||||
- type: SERVICE_SPECIFIC_REPAIR
|
||||
description: "依服務選擇專屬修復:ClickHouse 降 merge / scheduler 限 concurrency / litellm 修 health 或路由 / exporter 降 collector"
|
||||
command: "由 AI 根據 evidence snapshot 選擇已驗證 playbook"
|
||||
reasoning: "[規則匹配] 長期過載先 read-only 診斷與分流,禁止通用 docker restart;修復必須服務專屬且可回寫 Playbook trust。"
|
||||
|
||||
# 2026-05-05 ogt + Codex: 110 self-hosted runner 是 systemd service,不在 Docker/cAdvisor 覆蓋內。
|
||||
# 原則:AI 可自動診斷 watchdog/quota/restart storm;套用 systemd drop-in 需要 sudo,必須走人工批准或 sudo playbook。
|
||||
- id: systemd_runner_baseline_alert
|
||||
priority: 43
|
||||
description: 110 self-hosted runner systemd watchdog / restart / quota 基線告警
|
||||
match:
|
||||
alertname:
|
||||
- SystemdRunnerRestartSpike
|
||||
- SystemdRunnerWatchdogEnabled
|
||||
- SystemdRunnerMissingResourceQuota
|
||||
response:
|
||||
action_title: "🔍 Systemd Runner 基線診斷 — 需要 sudo 才可修復"
|
||||
description: "110 self-hosted runner 發生 watchdog/restart storm 或缺 CPU/Memory quota。這會讓 CI 與 Sentry/ClickHouse/Gitea 搶主機資源,且 Docker/cAdvisor 看不到。"
|
||||
suggested_action: SSH_DIAGNOSE
|
||||
kubectl_command: "ssh {host} 'systemctl show {unit} -p WatchdogUSec -p NRestarts -p DropInPaths -p CPUQuotaPerSecUSec -p MemoryMax -p ActiveState -p SubState; journalctl -u {unit} --since \"20 minutes ago\" --no-pager | tail -120'"
|
||||
estimated_downtime: "N/A"
|
||||
risk: low
|
||||
responsibility: INFRA
|
||||
responsibility_reasoning: "self-hosted runner 是 bare-metal systemd 資源治理,非 K8s 或 Docker workload"
|
||||
secondary_teams: [SRE]
|
||||
optimization:
|
||||
- type: SYSTEMD_GUARDRAIL
|
||||
description: "人工批准後停用錯誤 watchdog drop-in,並為 runner 加 CPUQuota=200%、MemoryMax=2G"
|
||||
command: "sudo /home/wooo/scripts/apply-runner-systemd-guardrails.sh --apply"
|
||||
- type: CI_CAPACITY
|
||||
description: "若 110 同時承載 Sentry/ClickHouse/Gitea,不應讓多個 runner 無限制並行"
|
||||
command: "檢查 active jobs、runner 數量與 Gitea Actions concurrency,必要時分流 runner"
|
||||
reasoning: "[規則匹配] systemd runner 過載先 read-only 診斷;改 systemd drop-in 需 sudo 與人工批准,避免 AI 擅自改 host unit。"
|
||||
reasoning: "[規則匹配] 主機層資源告警無法自動修復,需人工登入確認高負載/高記憶體/磁碟根因後決策。禁止 kubectl restart(node_exporter 不是 K8s 服務)。"
|
||||
|
||||
- id: high_cpu
|
||||
priority: 40
|
||||
@@ -301,7 +232,7 @@ rules:
|
||||
response:
|
||||
action_title: "診斷 {target} CrashLoop 根因"
|
||||
description: "⚙️ 規則匹配: {target} 進入 CrashLoopBackOff,需檢查啟動錯誤日誌。"
|
||||
suggested_action: NO_ACTION
|
||||
suggested_action: RESTART_DEPLOYMENT
|
||||
kubectl_command: "kubectl logs {target} -n {namespace} --previous --tail=50"
|
||||
estimated_downtime: "依根因而定"
|
||||
risk: critical
|
||||
@@ -384,7 +315,7 @@ rules:
|
||||
response:
|
||||
action_title: "清理 PostgreSQL 閒置連線"
|
||||
description: "⚙️ 規則匹配: PostgreSQL 連線池使用率過高,可能導致新請求被拒絕。"
|
||||
suggested_action: NO_ACTION
|
||||
suggested_action: RESTART_DEPLOYMENT
|
||||
kubectl_command: "kubectl exec -n {namespace} deployment/postgresql -- psql -U postgres -c 'SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE state = ''idle'' AND state_change < NOW() - INTERVAL ''5 minutes'';'"
|
||||
estimated_downtime: "0"
|
||||
risk: critical
|
||||
@@ -411,7 +342,7 @@ rules:
|
||||
response:
|
||||
action_title: "診斷 PostgreSQL 慢查詢 + 索引優化"
|
||||
description: "⚙️ 規則匹配: PostgreSQL 存在慢查詢或鎖等待,影響系統整體性能。"
|
||||
suggested_action: NO_ACTION
|
||||
suggested_action: RESTART_DEPLOYMENT
|
||||
kubectl_command: "kubectl exec -n {namespace} deployment/postgresql -- psql -U postgres -c 'SELECT pid, query, state, wait_event_type, wait_event FROM pg_stat_activity WHERE state != ''idle'' ORDER BY query_start;'"
|
||||
estimated_downtime: "0"
|
||||
risk: medium
|
||||
@@ -517,7 +448,7 @@ rules:
|
||||
response:
|
||||
action_title: "清理 MinIO 過期資料 on {host}"
|
||||
description: "⚙️ 規則匹配: MinIO 磁碟使用率過高,需清理舊資料或擴展儲存空間。"
|
||||
suggested_action: NO_ACTION
|
||||
suggested_action: RESTART_DEPLOYMENT
|
||||
kubectl_command: "ssh {host} 'df -h /data/minio && du -sh /data/minio/* | sort -rh | head -10'"
|
||||
estimated_downtime: "0"
|
||||
risk: critical
|
||||
@@ -572,7 +503,7 @@ rules:
|
||||
response:
|
||||
action_title: "確認 K3s 節點 {target} 狀態"
|
||||
description: "⚙️ 規則匹配: K3s 節點下線,影響叢集可用性和 Pod 調度。"
|
||||
suggested_action: NO_ACTION
|
||||
suggested_action: RESTART_DEPLOYMENT
|
||||
kubectl_command: "kubectl get nodes -o wide && kubectl describe node {target}"
|
||||
estimated_downtime: "依節點恢復時間"
|
||||
risk: critical
|
||||
@@ -631,7 +562,7 @@ rules:
|
||||
response:
|
||||
action_title: "診斷告警鏈路中斷"
|
||||
description: "⚙️ 規則匹配: 告警鏈路異常,可能導致真實告警無法送達 Telegram。"
|
||||
suggested_action: NO_ACTION
|
||||
suggested_action: RESTART_DEPLOYMENT
|
||||
kubectl_command: "kubectl get pods -n monitoring && curl -s http://192.168.0.120:9093/api/v1/status | jq '.data.uptime'"
|
||||
estimated_downtime: "監控盲區持續中"
|
||||
risk: critical
|
||||
@@ -662,7 +593,7 @@ rules:
|
||||
response:
|
||||
action_title: "確認 NVIDIA API 熔斷狀態"
|
||||
description: "⚙️ 規則匹配: NVIDIA/Nemotron 熔斷器開啟或錯誤率過高,AI Router 已自動降級。"
|
||||
suggested_action: NO_ACTION
|
||||
suggested_action: RESTART_DEPLOYMENT
|
||||
kubectl_command: "curl -s http://192.168.0.125:32334/api/v1/ai-router/status | jq '.providers'"
|
||||
estimated_downtime: "0 (已自動 fallback)"
|
||||
risk: medium
|
||||
@@ -727,18 +658,17 @@ rules:
|
||||
- VeleroBackupNotRun
|
||||
- BackupJobFailed
|
||||
response:
|
||||
action_title: "🔍 備份失敗自動診斷 — SSH 收集備份與磁碟狀態"
|
||||
description: "⚠️ 備份任務失敗。先自動 SSH 收集 backup log、last_success 與磁碟空間;若無法確認安全修復,立即升級緊急介入。"
|
||||
suggested_action: SSH_DIAGNOSE
|
||||
# 2026-05-02 ogt + Claude Sonnet 4.6: 補上 ps aux 讓 _ssh_execute 走 diagnostics 路徑(無阻擋)
|
||||
kubectl_command: "ssh {host} 'ps aux --sort=-%cpu | head -15; echo \"=== BACKUP STATUS ===\"; ls -lah /home/ollama/backup/110 2>/dev/null || true; echo \"=== LAST SUCCESS ===\"; cat /home/ollama/backup/110/last_success 2>/dev/null || true; echo \"=== BACKUP LOG ===\"; tail -80 /home/ollama/backup/110/backup.log 2>/dev/null || true; echo \"=== DISK ===\"; df -h /home/ollama /backup / 2>/dev/null || df -h'"
|
||||
action_title: "備份失敗,需人工確認"
|
||||
description: "⚠️ 備份任務失敗,無自動修復動作。請人工確認備份腳本及磁碟空間。"
|
||||
suggested_action: NO_ACTION
|
||||
kubectl_command: ""
|
||||
estimated_downtime: "N/A"
|
||||
risk: low
|
||||
risk: medium
|
||||
responsibility: INFRA
|
||||
responsibility_reasoning: "備份失敗屬基礎設施維運問題,先自動收集只讀證據,再交由緊急介入或後續 Playbook 修復"
|
||||
responsibility_reasoning: "備份失敗屬基礎設施維運問題,需人工介入確認根因"
|
||||
secondary_teams: []
|
||||
optimization: []
|
||||
reasoning: "[規則匹配] 備份失敗先自動 SSH 只讀診斷,避免 LLM 誤判為 K8s deployment 重啟。"
|
||||
reasoning: "[規則匹配] 備份失敗無法自動修復,需人工排查備份腳本、磁碟空間及網路連通性。"
|
||||
|
||||
# ── DevOps 工具層 ─────────────────────────────────────────
|
||||
# 2026-04-14 Claude Sonnet 4.6: Task 2.2 ADR-076 — 新增 devops_tool / ssl_cert / external_site 三類規則
|
||||
@@ -809,9 +739,6 @@ rules:
|
||||
alertname:
|
||||
- MoWoooWorkDown
|
||||
- MoWoooDevDown
|
||||
- TsenyangWebsiteDown
|
||||
- StockWoooWorkDown
|
||||
- BitanWoooWorkDown
|
||||
- ExternalSiteDown
|
||||
- WebsiteDown
|
||||
- BlackboxProbeFailed
|
||||
@@ -837,36 +764,6 @@ rules:
|
||||
command: "curl -sv {instance} --max-time 10 2>&1 | grep -E '(HTTP|Connected|Failed)'"
|
||||
reasoning: "[規則匹配] 外部網站下線屬外部依賴,通知統帥後等待服務恢復,必要時切換備援路徑。"
|
||||
|
||||
# 2026-04-24 ogt + Claude Sonnet 4.6: Sentry / ClickHouse 監控告警 — 外部服務,禁止 kubectl 操作
|
||||
- id: sentry_clickhouse_alert
|
||||
priority: 60
|
||||
description: Sentry 或 ClickHouse 監控告警(外部服務,不是 K8s workload)
|
||||
match:
|
||||
alertname:
|
||||
- SentryClickHouseMemoryPressure
|
||||
- SentryClickHouseCpuHigh
|
||||
- SentryClickHouseDiskUsageHigh
|
||||
- ClickHouseMemoryHigh
|
||||
- ClickHouseMemoryPressure
|
||||
- ClickHouseCpuHigh
|
||||
- ClickHouseReplicationLag
|
||||
- ClickHouseQuerySlow
|
||||
- SentryWorkerQueueHigh
|
||||
- SentryKafkaLag
|
||||
- SentryBacklogHigh
|
||||
response:
|
||||
action_title: "⚠️ Sentry/ClickHouse 告警 — 需 SSH 人工排查"
|
||||
description: "⚠️ Sentry/ClickHouse 屬外部監控服務,無法透過 kubectl 自動修復。請 SSH 登入服務主機排查根因:clickhouse-client / docker stats / journalctl -xe。若記憶體壓力持續,考慮調整 ClickHouse max_memory_usage 設定或清理舊資料。"
|
||||
suggested_action: NO_ACTION
|
||||
kubectl_command: ""
|
||||
estimated_downtime: "N/A"
|
||||
risk: high
|
||||
responsibility: INFRA
|
||||
responsibility_reasoning: "Sentry/ClickHouse 基礎設施由 INFRA 團隊管理"
|
||||
secondary_teams: []
|
||||
optimization: []
|
||||
reasoning: "[規則匹配] Sentry/ClickHouse 非 K8s 服務,kubectl 操作無效。需 SSH 進入服務主機,確認記憶體/CPU/磁碟狀況後手動介入。"
|
||||
|
||||
# ── 通用兜底 ────────────────────────────────────────────────
|
||||
|
||||
- id: generic_fallback
|
||||
@@ -878,12 +775,12 @@ rules:
|
||||
response:
|
||||
action_title: "重新啟動 {target} 服務"
|
||||
description: "⚙️ 規則匹配: {target} 發生異常,需進一步診斷確認根因。"
|
||||
suggested_action: NO_ACTION
|
||||
kubectl_command: ""
|
||||
estimated_downtime: "N/A"
|
||||
suggested_action: RESTART_DEPLOYMENT
|
||||
kubectl_command: "kubectl rollout restart deployment/{target} -n {namespace}"
|
||||
estimated_downtime: "5-15 min"
|
||||
risk: medium
|
||||
responsibility: COLLAB
|
||||
responsibility_reasoning: "告警資訊不足以判定單一責任團隊,建議多團隊協同排查"
|
||||
secondary_teams: [BE, INFRA]
|
||||
optimization: []
|
||||
reasoning: "[規則匹配] 未知告警類型,無法安全判斷修復動作,由人工或 LLM 診斷後決策。"
|
||||
reasoning: "[規則匹配] 根據告警先重啟恢復服務,同時安排深入診斷。"
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
-- ADR-090 capacity_violation_event metric violation types
|
||||
-- 日期:2026-05-07(台北)
|
||||
-- 目的:讓 capacity_scanner_job.py 寫入的 cpu/mem/swap 細項違規符合 DB constraint。
|
||||
--
|
||||
-- 背景:
|
||||
-- capacity_scanner_job.py 會寫入:
|
||||
-- - cpu_over_threshold
|
||||
-- - mem_over_threshold
|
||||
-- - swap_over_threshold
|
||||
-- 但原始 ADR-090 DDL 只允許較粗的 host_saturation,導致 production 出現
|
||||
-- capacity_violation_event_type_valid check violation,容量治理事件漏記。
|
||||
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE capacity_violation_event
|
||||
DROP CONSTRAINT IF EXISTS capacity_violation_event_type_valid;
|
||||
|
||||
ALTER TABLE capacity_violation_event
|
||||
ADD CONSTRAINT capacity_violation_event_type_valid
|
||||
CHECK (violation_type IN (
|
||||
'no_limit_set',
|
||||
'over_request',
|
||||
'over_limit',
|
||||
'host_saturation',
|
||||
'over_sla_budget',
|
||||
'unauthorized_new_deploy',
|
||||
'cpu_over_threshold',
|
||||
'mem_over_threshold',
|
||||
'swap_over_threshold',
|
||||
'load_over_threshold'
|
||||
));
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- Rollback(需人工確認後執行):
|
||||
-- BEGIN;
|
||||
-- ALTER TABLE capacity_violation_event
|
||||
-- DROP CONSTRAINT IF EXISTS capacity_violation_event_type_valid;
|
||||
-- ALTER TABLE capacity_violation_event
|
||||
-- ADD CONSTRAINT capacity_violation_event_type_valid
|
||||
-- CHECK (violation_type IN (
|
||||
-- 'no_limit_set',
|
||||
-- 'over_request',
|
||||
-- 'over_limit',
|
||||
-- 'host_saturation',
|
||||
-- 'over_sla_budget',
|
||||
-- 'unauthorized_new_deploy'
|
||||
-- ));
|
||||
-- COMMIT;
|
||||
@@ -1,36 +0,0 @@
|
||||
-- ADR-090-D: automation_operation_log.operation_type adds Ansible executor audit states
|
||||
-- Created: 2026-05-12 Taipei
|
||||
--
|
||||
-- Purpose:
|
||||
-- T3 Ansible declarative executor visibility. These operation types allow
|
||||
-- the AI automation truth chain to record that Ansible was matched,
|
||||
-- check-mode executed, applied, rolled back, or explicitly skipped.
|
||||
--
|
||||
-- Safety:
|
||||
-- This migration only expands the CHECK allowlist. It does not execute
|
||||
-- Ansible, change approval behavior, or create auto-remediation rows.
|
||||
|
||||
ALTER TABLE automation_operation_log
|
||||
DROP CONSTRAINT IF EXISTS automation_operation_log_type_valid;
|
||||
|
||||
ALTER TABLE automation_operation_log
|
||||
ADD CONSTRAINT automation_operation_log_type_valid CHECK (operation_type IN (
|
||||
'monitor_configured','monitor_removed',
|
||||
'alert_fired','alert_suppressed','alert_routed',
|
||||
'rule_created','rule_updated','rule_matched','rule_rejected','rule_deprecated',
|
||||
'playbook_generated','playbook_updated','playbook_executed',
|
||||
'remediation_executed','remediation_verified','remediation_rolled_back',
|
||||
'self_correction_attempted',
|
||||
'km_created','km_updated','km_linked',
|
||||
'asset_discovered','coverage_recalculated',
|
||||
'capacity_recommendation','quota_enforced',
|
||||
'notification_formatted',
|
||||
'ansible_candidate_matched',
|
||||
'ansible_check_mode_executed',
|
||||
'ansible_apply_executed',
|
||||
'ansible_rollback_executed',
|
||||
'ansible_execution_skipped'
|
||||
));
|
||||
|
||||
COMMENT ON CONSTRAINT automation_operation_log_type_valid ON automation_operation_log IS
|
||||
'ADR-090-D: allow first-class Ansible executor audit states for AwoooP truth-chain visibility.';
|
||||
@@ -1,19 +0,0 @@
|
||||
-- ADR-090-D rollback: remove Ansible executor audit states from operation_type allowlist.
|
||||
-- Only apply after confirming no automation_operation_log rows use ansible_* operation types.
|
||||
|
||||
ALTER TABLE automation_operation_log
|
||||
DROP CONSTRAINT IF EXISTS automation_operation_log_type_valid;
|
||||
|
||||
ALTER TABLE automation_operation_log
|
||||
ADD CONSTRAINT automation_operation_log_type_valid CHECK (operation_type IN (
|
||||
'monitor_configured','monitor_removed',
|
||||
'alert_fired','alert_suppressed','alert_routed',
|
||||
'rule_created','rule_updated','rule_matched','rule_rejected','rule_deprecated',
|
||||
'playbook_generated','playbook_updated','playbook_executed',
|
||||
'remediation_executed','remediation_verified','remediation_rolled_back',
|
||||
'self_correction_attempted',
|
||||
'km_created','km_updated','km_linked',
|
||||
'asset_discovered','coverage_recalculated',
|
||||
'capacity_recommendation','quota_enforced',
|
||||
'notification_formatted'
|
||||
));
|
||||
@@ -1,40 +0,0 @@
|
||||
-- ADR-092 B4 — Playbook 學習閉環斷鏈修復(DB Schema)
|
||||
-- 根因:approval_records 缺 matched_playbook_id → 人工審核後 EWMA 無法更新 Playbook trust score
|
||||
-- timeline_events 缺 incident_id → pre_decision_investigator MCP 呼叫稽核每天+1 靜默錯誤
|
||||
--
|
||||
-- 執行方式(需人工執行一次):
|
||||
-- psql $DATABASE_URL -f apps/api/migrations/adr092_p1_learning_chain_fix.sql
|
||||
--
|
||||
-- 2026-04-24 ogt + Claude Sonnet 4.6(亞太)
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- approval_records: 新增 matched_playbook_id 欄位(B2 fix)
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
ALTER TABLE approval_records
|
||||
ADD COLUMN IF NOT EXISTS matched_playbook_id VARCHAR(36) DEFAULT NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_approval_matched_playbook
|
||||
ON approval_records (matched_playbook_id)
|
||||
WHERE matched_playbook_id IS NOT NULL;
|
||||
|
||||
COMMENT ON COLUMN approval_records.matched_playbook_id
|
||||
IS 'Playbook ID 命中時紀錄,學習服務讀取以更新 EWMA trust score';
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- timeline_events: 新增 incident_id 欄位(P1.6 fix)
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
ALTER TABLE timeline_events
|
||||
ADD COLUMN IF NOT EXISTS incident_id VARCHAR(64) DEFAULT NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_timeline_incident_id
|
||||
ON timeline_events (incident_id)
|
||||
WHERE incident_id IS NOT NULL;
|
||||
|
||||
COMMENT ON COLUMN timeline_events.incident_id
|
||||
IS 'MCP 工具呼叫稽核時關聯的 Incident ID';
|
||||
|
||||
COMMIT;
|
||||
@@ -1,18 +0,0 @@
|
||||
-- ADR-092 P1 Learning Chain Rollback
|
||||
-- 撤銷 adr092_p1_learning_chain_fix.sql 的所有變更
|
||||
-- 僅在 schema 誤套 / 緊急回滾時使用;資料不可復原
|
||||
--
|
||||
-- 執行方式(需人工執行一次):
|
||||
-- psql $DATABASE_URL -f apps/api/migrations/adr092_p1_learning_chain_rollback.sql
|
||||
--
|
||||
-- 2026-04-25 db-expert-fix by Claude Engineer-B
|
||||
|
||||
BEGIN;
|
||||
|
||||
DROP INDEX IF EXISTS ix_approval_matched_playbook;
|
||||
ALTER TABLE approval_records DROP COLUMN IF EXISTS matched_playbook_id;
|
||||
|
||||
DROP INDEX IF EXISTS ix_timeline_incident_id;
|
||||
ALTER TABLE timeline_events DROP COLUMN IF EXISTS incident_id;
|
||||
|
||||
COMMIT;
|
||||
@@ -1,87 +0,0 @@
|
||||
-- ADR-093: Notification Matrix Migration
|
||||
-- =========================================
|
||||
-- 1. 建立 approval_records 表(BIGINT telegram_chat_id,支援群組負數 ID)
|
||||
-- 2. 建立 awoooi_migrator 角色
|
||||
-- 2026-04-25 ogt + Claude Sonnet 4.6
|
||||
|
||||
-- awoooi_migrator 角色(ADR-090b 計畫的實作)
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'awoooi_migrator') THEN
|
||||
CREATE ROLE awoooi_migrator LOGIN;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
GRANT CONNECT ON DATABASE awoooi_prod TO awoooi_migrator;
|
||||
GRANT USAGE ON SCHEMA public TO awoooi_migrator;
|
||||
GRANT CREATE ON SCHEMA public TO awoooi_migrator;
|
||||
|
||||
-- SQLAlchemy native enum types(SQLEnum 預設 native_enum=True)
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE approvalstatus AS ENUM ('pending','approved','rejected','expired','execution_success','execution_failed');
|
||||
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE risklevel AS ENUM ('low','medium','high','critical');
|
||||
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
|
||||
-- approval_records 主表(全新建立,直接用 BIGINT)
|
||||
-- 注意:test schema setup_test_schema.sql 同步更新為 BIGINT
|
||||
CREATE TABLE IF NOT EXISTS approval_records (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
action VARCHAR(500) NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
status approvalstatus NOT NULL DEFAULT 'pending',
|
||||
risk_level risklevel NOT NULL,
|
||||
required_signatures INTEGER DEFAULT 1,
|
||||
current_signatures INTEGER DEFAULT 0,
|
||||
signatures JSON DEFAULT '[]',
|
||||
blast_radius JSON DEFAULT '{}',
|
||||
dry_run_checks JSON DEFAULT '[]',
|
||||
requested_by VARCHAR,
|
||||
rejection_reason TEXT,
|
||||
extra_metadata JSON DEFAULT '{}',
|
||||
fingerprint VARCHAR,
|
||||
hit_count INTEGER DEFAULT 1,
|
||||
last_seen_at TIMESTAMPTZ,
|
||||
approval_level VARCHAR DEFAULT 'standard',
|
||||
approval_votes JSONB,
|
||||
required_votes INTEGER DEFAULT 1,
|
||||
incident_id VARCHAR,
|
||||
telegram_message_id INTEGER,
|
||||
telegram_chat_id BIGINT, -- 支援群組負數 ID(原 INTEGER 會 int32 overflow)
|
||||
matched_playbook_id VARCHAR(36),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
expires_at TIMESTAMPTZ,
|
||||
resolved_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
-- 若表已存在(舊環境),執行欄位型別升級
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'approval_records'
|
||||
AND column_name = 'telegram_chat_id'
|
||||
AND data_type = 'integer'
|
||||
) THEN
|
||||
ALTER TABLE approval_records
|
||||
ALTER COLUMN telegram_chat_id TYPE BIGINT;
|
||||
RAISE NOTICE 'approval_records.telegram_chat_id upgraded INTEGER → BIGINT';
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
-- 索引
|
||||
CREATE INDEX IF NOT EXISTS idx_approval_records_status ON approval_records(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_approval_records_incident ON approval_records(incident_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_approval_records_fingerprint ON approval_records(fingerprint);
|
||||
CREATE INDEX IF NOT EXISTS idx_approval_records_playbook ON approval_records(matched_playbook_id);
|
||||
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON approval_records TO awoooi;
|
||||
GRANT SELECT, INSERT, UPDATE ON approval_records TO awoooi_migrator;
|
||||
|
||||
COMMENT ON TABLE approval_records IS 'ADR-093 2026-04-25: telegram_chat_id 改 BIGINT 支援群組負數 ID';
|
||||
COMMENT ON COLUMN approval_records.telegram_chat_id IS 'BIGINT: 支援 SRE 群組 ID (-1003711974679) 不 overflow';
|
||||
@@ -1,26 +0,0 @@
|
||||
-- ADR-094: Hermes NL Dispatch Audit Log
|
||||
-- 每次 @mention 觸發 → 記錄派發決策供 P95 latency 監控與幻覺追蹤
|
||||
-- 2026-04-25 ogt + Claude Sonnet 4.6
|
||||
|
||||
CREATE TABLE IF NOT EXISTS hermes_dispatch_log (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
chat_id VARCHAR(32) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
username VARCHAR(100),
|
||||
agent_name VARCHAR(64) NOT NULL,
|
||||
input_preview VARCHAR(200), -- 前 200 字,不存完整輸入(隱私)
|
||||
latency_ms INTEGER,
|
||||
success BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
error_type VARCHAR(64),
|
||||
budget_usd NUMERIC(8, 5)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_hermes_dispatch_created ON hermes_dispatch_log(created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_hermes_dispatch_agent ON hermes_dispatch_log(agent_name);
|
||||
CREATE INDEX IF NOT EXISTS idx_hermes_dispatch_user ON hermes_dispatch_log(user_id);
|
||||
|
||||
GRANT SELECT, INSERT ON hermes_dispatch_log TO awoooi;
|
||||
GRANT USAGE, SELECT ON SEQUENCE hermes_dispatch_log_id_seq TO awoooi;
|
||||
|
||||
COMMENT ON TABLE hermes_dispatch_log IS 'ADR-094: Hermes NL 派發審計日誌(P95 latency 監控 + 幻覺追蹤)';
|
||||
@@ -1,20 +0,0 @@
|
||||
-- ADR-104 T4: Playbook versioning / lineage schema
|
||||
-- 2026-04-30 Codex: LLM-generated Playbooks must preserve lineage instead of
|
||||
-- overwriting prior operational knowledge.
|
||||
|
||||
ALTER TABLE playbooks
|
||||
ADD COLUMN IF NOT EXISTS version INTEGER NOT NULL DEFAULT 1,
|
||||
ADD COLUMN IF NOT EXISTS parent_playbook_id VARCHAR(36),
|
||||
ADD COLUMN IF NOT EXISTS supersedes_playbook_id VARCHAR(36),
|
||||
ADD COLUMN IF NOT EXISTS version_reason TEXT;
|
||||
|
||||
UPDATE playbooks
|
||||
SET parent_playbook_id = playbook_id
|
||||
WHERE parent_playbook_id IS NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_playbook_lineage
|
||||
ON playbooks(parent_playbook_id, version);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_playbook_supersedes
|
||||
ON playbooks(supersedes_playbook_id)
|
||||
WHERE supersedes_playbook_id IS NOT NULL;
|
||||
@@ -1,77 +0,0 @@
|
||||
-- ADR-105 MCP audit and snapshot foundation
|
||||
-- 2026-05-01
|
||||
-- Notes:
|
||||
-- AWOOOI incident ids are string values such as INC-20260429-xxxx, not UUIDs.
|
||||
-- Keep incident_id as VARCHAR(64) so MCP audit can join existing incident records.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS mcp_audit_log (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
session_id VARCHAR(36) NOT NULL,
|
||||
flywheel_node VARCHAR(20),
|
||||
mcp_server VARCHAR(80) NOT NULL,
|
||||
tool_name VARCHAR(120) NOT NULL,
|
||||
input_params JSONB,
|
||||
output_result JSONB,
|
||||
duration_ms INTEGER,
|
||||
success BOOLEAN,
|
||||
error_message TEXT,
|
||||
incident_id VARCHAR(64),
|
||||
agent_role VARCHAR(40),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
ALTER TABLE mcp_audit_log
|
||||
ADD COLUMN IF NOT EXISTS agent_role VARCHAR(40);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_mcp_audit_session
|
||||
ON mcp_audit_log(session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_mcp_audit_incident
|
||||
ON mcp_audit_log(incident_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_mcp_audit_node
|
||||
ON mcp_audit_log(flywheel_node, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_mcp_audit_server_tool
|
||||
ON mcp_audit_log(mcp_server, tool_name, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_mcp_audit_agent_role
|
||||
ON mcp_audit_log(agent_role, created_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS mcp_daily_stats (
|
||||
date DATE NOT NULL,
|
||||
mcp_server VARCHAR(80) NOT NULL,
|
||||
tool_name VARCHAR(120) NOT NULL,
|
||||
call_count INTEGER DEFAULT 0 NOT NULL,
|
||||
success_count INTEGER DEFAULT 0 NOT NULL,
|
||||
avg_duration_ms FLOAT,
|
||||
PRIMARY KEY (date, mcp_server, tool_name)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS k8s_state_snapshots (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
incident_id VARCHAR(64),
|
||||
snapshot_type VARCHAR(40) NOT NULL,
|
||||
namespace VARCHAR(63),
|
||||
resource_type VARCHAR(80),
|
||||
resource_name VARCHAR(253),
|
||||
state_json JSONB,
|
||||
captured_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_k8s_snapshot_incident
|
||||
ON k8s_state_snapshots(incident_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_k8s_snapshot_resource
|
||||
ON k8s_state_snapshots(namespace, resource_type, resource_name);
|
||||
CREATE INDEX IF NOT EXISTS idx_k8s_snapshot_captured
|
||||
ON k8s_state_snapshots(captured_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS prometheus_snapshots (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
incident_id VARCHAR(64),
|
||||
query TEXT NOT NULL,
|
||||
result_json JSONB,
|
||||
snapshot_type VARCHAR(40),
|
||||
captured_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_prom_snapshot_incident
|
||||
ON prometheus_snapshots(incident_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_prom_snapshot_type
|
||||
ON prometheus_snapshots(snapshot_type, captured_at DESC);
|
||||
@@ -1,164 +0,0 @@
|
||||
-- T9: approved SSH execution MCP Gateway seed
|
||||
-- 目的:讓 Telegram/Approval 已批准的 SSH 修復動作通過 AwoooP Gateway 五閘門。
|
||||
-- 邊界:只授權 approval_executor;write/admin 仍需 Gate 5 短效 approval key。
|
||||
|
||||
SELECT set_config('app.project_id', 'awoooi', FALSE);
|
||||
|
||||
WITH agent_body AS (
|
||||
SELECT jsonb_build_object(
|
||||
'schema_version', 'awooop_agent_contract_v1',
|
||||
'agent_id', 'approval_executor',
|
||||
'display_name', 'Approval Executor',
|
||||
'project_id', 'awoooi',
|
||||
'purpose', 'Approved SSH execution through AwoooP MCP Gateway',
|
||||
'allowed_scopes', jsonb_build_array('read', 'write', 'admin'),
|
||||
'requires_gate5_for_scopes', jsonb_build_array('write', 'admin'),
|
||||
'stage', 't9_ssh_approval_gateway'
|
||||
) AS body_json
|
||||
),
|
||||
inserted_revision AS (
|
||||
INSERT INTO awooop_contract_revisions (
|
||||
project_id,
|
||||
contract_family,
|
||||
contract_id,
|
||||
version_major,
|
||||
version_minor,
|
||||
lifecycle_status,
|
||||
body_json,
|
||||
body_hash,
|
||||
body_schema_version,
|
||||
publisher_id,
|
||||
published_at
|
||||
)
|
||||
SELECT
|
||||
'awoooi',
|
||||
'agent',
|
||||
'approval_executor',
|
||||
1,
|
||||
0,
|
||||
'active',
|
||||
body_json,
|
||||
encode(digest(body_json::text, 'sha256'), 'hex'),
|
||||
'v1.0',
|
||||
'migration:t9_ssh_approval_gateway',
|
||||
NOW()
|
||||
FROM agent_body
|
||||
ON CONFLICT (project_id, contract_family, contract_id, version_major, version_minor)
|
||||
DO NOTHING
|
||||
RETURNING revision_id, project_id, contract_family, contract_id
|
||||
),
|
||||
chosen_revision AS (
|
||||
SELECT revision_id, project_id, contract_family, contract_id
|
||||
FROM inserted_revision
|
||||
UNION ALL
|
||||
SELECT revision_id, project_id, contract_family, contract_id
|
||||
FROM awooop_contract_revisions
|
||||
WHERE project_id = 'awoooi'
|
||||
AND contract_family = 'agent'
|
||||
AND contract_id = 'approval_executor'
|
||||
AND version_major = 1
|
||||
AND version_minor = 0
|
||||
AND lifecycle_status = 'active'
|
||||
),
|
||||
upsert_pointer AS (
|
||||
INSERT INTO awooop_active_revisions (
|
||||
project_id,
|
||||
contract_family,
|
||||
contract_id,
|
||||
active_revision_id,
|
||||
updated_at
|
||||
)
|
||||
SELECT DISTINCT ON (project_id, contract_family, contract_id)
|
||||
project_id,
|
||||
contract_family,
|
||||
contract_id,
|
||||
revision_id,
|
||||
NOW()
|
||||
FROM chosen_revision
|
||||
ORDER BY project_id, contract_family, contract_id, revision_id
|
||||
ON CONFLICT (project_id, contract_family, contract_id)
|
||||
DO UPDATE SET
|
||||
active_revision_id = EXCLUDED.active_revision_id,
|
||||
updated_at = NOW()
|
||||
RETURNING contract_id
|
||||
)
|
||||
SELECT 'approval_executor_active_contracts', count(*) FROM upsert_pointer;
|
||||
|
||||
WITH gateway_tools(tool_name, description, required_scope) AS (
|
||||
VALUES
|
||||
('ssh_diagnose', 'SSH host diagnosis read', 'read'),
|
||||
('ssh_docker_restart', 'Approved Docker container restart over SSH', 'write'),
|
||||
('ssh_docker_compose_restart', 'Approved Docker Compose service restart over SSH', 'write'),
|
||||
('ssh_systemctl_restart', 'Approved systemd service restart over SSH', 'write'),
|
||||
('ssh_clear_docker_logs', 'Approved Docker log truncation over SSH', 'write'),
|
||||
('ssh_renew_ssl', 'Approved certbot renewal over SSH', 'write'),
|
||||
('ssh_reload_nginx', 'Approved nginx config test and reload over SSH', 'write'),
|
||||
('ssh_docker_prune', 'Approved Docker prune over SSH with provider disk guard', 'admin')
|
||||
),
|
||||
upsert_tools AS (
|
||||
INSERT INTO awooop_mcp_tool_registry (
|
||||
project_id,
|
||||
tool_name,
|
||||
tool_type,
|
||||
description,
|
||||
allowed_scopes,
|
||||
environment_tags,
|
||||
is_active,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
'awoooi',
|
||||
tool_name,
|
||||
'mcp_server',
|
||||
description,
|
||||
jsonb_build_array(required_scope),
|
||||
'{"env": "prod"}'::jsonb,
|
||||
TRUE,
|
||||
NOW()
|
||||
FROM gateway_tools
|
||||
ON CONFLICT (project_id, tool_name)
|
||||
DO UPDATE SET
|
||||
description = EXCLUDED.description,
|
||||
allowed_scopes = EXCLUDED.allowed_scopes,
|
||||
environment_tags = EXCLUDED.environment_tags,
|
||||
is_active = TRUE,
|
||||
updated_at = NOW()
|
||||
RETURNING tool_id, tool_name, allowed_scopes
|
||||
),
|
||||
upsert_grants AS (
|
||||
INSERT INTO awooop_mcp_grants (
|
||||
project_id,
|
||||
agent_id,
|
||||
tool_id,
|
||||
granted_by,
|
||||
granted_scopes,
|
||||
expires_at,
|
||||
is_revoked,
|
||||
revoked_at,
|
||||
revoked_by
|
||||
)
|
||||
SELECT
|
||||
'awoooi',
|
||||
'approval_executor',
|
||||
tool_id,
|
||||
'migration:t9_ssh_approval_gateway',
|
||||
allowed_scopes,
|
||||
NULL,
|
||||
FALSE,
|
||||
NULL,
|
||||
NULL
|
||||
FROM upsert_tools
|
||||
ON CONFLICT (project_id, agent_id, tool_id)
|
||||
DO UPDATE SET
|
||||
granted_by = EXCLUDED.granted_by,
|
||||
granted_scopes = EXCLUDED.granted_scopes,
|
||||
expires_at = NULL,
|
||||
is_revoked = FALSE,
|
||||
revoked_at = NULL,
|
||||
revoked_by = NULL
|
||||
RETURNING grant_id
|
||||
)
|
||||
SELECT
|
||||
'approval_executor_ssh_gateway',
|
||||
(SELECT count(*) FROM upsert_tools) AS tool_rows,
|
||||
(SELECT count(*) FROM upsert_grants) AS grant_rows;
|
||||
@@ -1,43 +0,0 @@
|
||||
-- Rollback for T9 approved SSH execution MCP Gateway seed.
|
||||
-- Contract revisions are append-only; rollback revokes approval_executor grants
|
||||
-- and deactivates only the write/admin tools introduced here.
|
||||
|
||||
SELECT set_config('app.project_id', 'awoooi', FALSE);
|
||||
|
||||
UPDATE awooop_mcp_grants
|
||||
SET
|
||||
is_revoked = TRUE,
|
||||
revoked_at = NOW(),
|
||||
revoked_by = 'rollback:t9_ssh_approval_gateway'
|
||||
WHERE project_id = 'awoooi'
|
||||
AND agent_id = 'approval_executor'
|
||||
AND granted_by = 'migration:t9_ssh_approval_gateway'
|
||||
AND is_revoked = FALSE;
|
||||
|
||||
UPDATE awooop_mcp_tool_registry
|
||||
SET
|
||||
is_active = FALSE,
|
||||
updated_at = NOW()
|
||||
WHERE project_id = 'awoooi'
|
||||
AND tool_name IN (
|
||||
'ssh_docker_restart',
|
||||
'ssh_docker_compose_restart',
|
||||
'ssh_systemctl_restart',
|
||||
'ssh_clear_docker_logs',
|
||||
'ssh_renew_ssl',
|
||||
'ssh_reload_nginx',
|
||||
'ssh_docker_prune'
|
||||
);
|
||||
|
||||
DELETE FROM awooop_active_revisions
|
||||
WHERE project_id = 'awoooi'
|
||||
AND contract_family = 'agent'
|
||||
AND contract_id = 'approval_executor';
|
||||
|
||||
UPDATE awooop_contract_revisions
|
||||
SET lifecycle_status = 'revoked'
|
||||
WHERE project_id = 'awoooi'
|
||||
AND contract_family = 'agent'
|
||||
AND contract_id = 'approval_executor'
|
||||
AND publisher_id = 'migration:t9_ssh_approval_gateway'
|
||||
AND lifecycle_status = 'active';
|
||||
@@ -1,159 +0,0 @@
|
||||
-- T24: auto-repair executor Docker restart MCP Gateway grant
|
||||
-- 目的:讓已由 PlayBook 標記為 requires_approval=false 的安全容器重啟,
|
||||
-- 透過 AwoooP MCP Gateway + Gate 5 policy projection 執行與稽核。
|
||||
-- 邊界:僅授權 ssh_docker_restart/write;複雜 shell、systemctl、prune 仍不得自動執行。
|
||||
|
||||
SELECT set_config('app.project_id', 'awoooi', FALSE);
|
||||
|
||||
WITH agent_body AS (
|
||||
SELECT jsonb_build_object(
|
||||
'schema_version', 'awooop_agent_contract_v1',
|
||||
'agent_id', 'auto_repair_executor',
|
||||
'display_name', 'Auto Repair Executor',
|
||||
'project_id', 'awoooi',
|
||||
'purpose', 'Auto repair diagnostics and safe Docker container restart through AwoooP MCP Gateway',
|
||||
'allowed_scopes', jsonb_build_array('read', 'write'),
|
||||
'requires_gate5_for_scopes', jsonb_build_array('write'),
|
||||
'write_scope_constraints', jsonb_build_object(
|
||||
'allowed_tools', jsonb_build_array('ssh_docker_restart'),
|
||||
'required_playbook_requires_approval', false,
|
||||
'required_trust_score_min', 0.8,
|
||||
'forbidden_shell_patterns', jsonb_build_array('command_substitution', 'pipe', 'fallback_shell', 'systemd', 'prune')
|
||||
),
|
||||
'stage', 't24_auto_repair_docker_restart_gateway'
|
||||
) AS body_json
|
||||
),
|
||||
inserted_revision AS (
|
||||
INSERT INTO awooop_contract_revisions (
|
||||
project_id,
|
||||
contract_family,
|
||||
contract_id,
|
||||
version_major,
|
||||
version_minor,
|
||||
lifecycle_status,
|
||||
body_json,
|
||||
body_hash,
|
||||
body_schema_version,
|
||||
publisher_id,
|
||||
published_at
|
||||
)
|
||||
SELECT
|
||||
'awoooi',
|
||||
'agent',
|
||||
'auto_repair_executor',
|
||||
1,
|
||||
1,
|
||||
'active',
|
||||
body_json,
|
||||
encode(digest(body_json::text, 'sha256'), 'hex'),
|
||||
'v1.1',
|
||||
'migration:t24_auto_repair_docker_restart_gateway',
|
||||
NOW()
|
||||
FROM agent_body
|
||||
ON CONFLICT (project_id, contract_family, contract_id, version_major, version_minor)
|
||||
DO NOTHING
|
||||
RETURNING revision_id, project_id, contract_family, contract_id
|
||||
),
|
||||
chosen_revision AS (
|
||||
SELECT revision_id, project_id, contract_family, contract_id
|
||||
FROM inserted_revision
|
||||
UNION ALL
|
||||
SELECT revision_id, project_id, contract_family, contract_id
|
||||
FROM awooop_contract_revisions
|
||||
WHERE project_id = 'awoooi'
|
||||
AND contract_family = 'agent'
|
||||
AND contract_id = 'auto_repair_executor'
|
||||
AND version_major = 1
|
||||
AND version_minor = 1
|
||||
AND lifecycle_status = 'active'
|
||||
),
|
||||
upsert_pointer AS (
|
||||
INSERT INTO awooop_active_revisions (
|
||||
project_id,
|
||||
contract_family,
|
||||
contract_id,
|
||||
active_revision_id,
|
||||
updated_at
|
||||
)
|
||||
SELECT DISTINCT ON (project_id, contract_family, contract_id)
|
||||
project_id,
|
||||
contract_family,
|
||||
contract_id,
|
||||
revision_id,
|
||||
NOW()
|
||||
FROM chosen_revision
|
||||
ORDER BY project_id, contract_family, contract_id, revision_id
|
||||
ON CONFLICT (project_id, contract_family, contract_id)
|
||||
DO UPDATE SET
|
||||
active_revision_id = EXCLUDED.active_revision_id,
|
||||
updated_at = NOW()
|
||||
RETURNING contract_id
|
||||
),
|
||||
upsert_tool AS (
|
||||
INSERT INTO awooop_mcp_tool_registry (
|
||||
project_id,
|
||||
tool_name,
|
||||
tool_type,
|
||||
description,
|
||||
allowed_scopes,
|
||||
environment_tags,
|
||||
is_active,
|
||||
updated_at
|
||||
)
|
||||
VALUES (
|
||||
'awoooi',
|
||||
'ssh_docker_restart',
|
||||
'mcp_server',
|
||||
'Policy-approved Docker container restart over SSH for auto-repair',
|
||||
'["write"]'::jsonb,
|
||||
'{"env": "prod"}'::jsonb,
|
||||
TRUE,
|
||||
NOW()
|
||||
)
|
||||
ON CONFLICT (project_id, tool_name)
|
||||
DO UPDATE SET
|
||||
description = EXCLUDED.description,
|
||||
allowed_scopes = EXCLUDED.allowed_scopes,
|
||||
environment_tags = EXCLUDED.environment_tags,
|
||||
is_active = TRUE,
|
||||
updated_at = NOW()
|
||||
RETURNING tool_id, allowed_scopes
|
||||
),
|
||||
upsert_grant AS (
|
||||
INSERT INTO awooop_mcp_grants (
|
||||
project_id,
|
||||
agent_id,
|
||||
tool_id,
|
||||
granted_by,
|
||||
granted_scopes,
|
||||
expires_at,
|
||||
is_revoked,
|
||||
revoked_at,
|
||||
revoked_by
|
||||
)
|
||||
SELECT
|
||||
'awoooi',
|
||||
'auto_repair_executor',
|
||||
tool_id,
|
||||
'migration:t24_auto_repair_docker_restart_gateway',
|
||||
allowed_scopes,
|
||||
NULL,
|
||||
FALSE,
|
||||
NULL,
|
||||
NULL
|
||||
FROM upsert_tool
|
||||
ON CONFLICT (project_id, agent_id, tool_id)
|
||||
DO UPDATE SET
|
||||
granted_by = EXCLUDED.granted_by,
|
||||
granted_scopes = EXCLUDED.granted_scopes,
|
||||
expires_at = NULL,
|
||||
is_revoked = FALSE,
|
||||
revoked_at = NULL,
|
||||
revoked_by = NULL
|
||||
RETURNING grant_id
|
||||
)
|
||||
SELECT
|
||||
'auto_repair_executor_docker_restart_gateway',
|
||||
(SELECT count(*) FROM upsert_pointer) AS active_contract_rows,
|
||||
(SELECT count(*) FROM upsert_tool) AS tool_rows,
|
||||
(SELECT count(*) FROM upsert_grant) AS grant_rows;
|
||||
@@ -1,37 +0,0 @@
|
||||
-- Rollback T24: revoke auto_repair_executor Docker restart write grant.
|
||||
|
||||
SELECT set_config('app.project_id', 'awoooi', FALSE);
|
||||
|
||||
UPDATE awooop_mcp_grants
|
||||
SET is_revoked = TRUE,
|
||||
revoked_at = NOW(),
|
||||
revoked_by = 'rollback:t24_auto_repair_docker_restart_gateway'
|
||||
WHERE project_id = 'awoooi'
|
||||
AND agent_id = 'auto_repair_executor'
|
||||
AND granted_by = 'migration:t24_auto_repair_docker_restart_gateway';
|
||||
|
||||
WITH previous_revision AS (
|
||||
SELECT revision_id, project_id, contract_family, contract_id
|
||||
FROM awooop_contract_revisions
|
||||
WHERE project_id = 'awoooi'
|
||||
AND contract_family = 'agent'
|
||||
AND contract_id = 'auto_repair_executor'
|
||||
AND version_major = 1
|
||||
AND version_minor = 0
|
||||
AND lifecycle_status = 'active'
|
||||
ORDER BY revision_id DESC
|
||||
LIMIT 1
|
||||
)
|
||||
INSERT INTO awooop_active_revisions (
|
||||
project_id,
|
||||
contract_family,
|
||||
contract_id,
|
||||
active_revision_id,
|
||||
updated_at
|
||||
)
|
||||
SELECT project_id, contract_family, contract_id, revision_id, NOW()
|
||||
FROM previous_revision
|
||||
ON CONFLICT (project_id, contract_family, contract_id)
|
||||
DO UPDATE SET
|
||||
active_revision_id = EXCLUDED.active_revision_id,
|
||||
updated_at = NOW();
|
||||
@@ -1,166 +0,0 @@
|
||||
-- T23: auto-repair executor read-only MCP Gateway seed
|
||||
-- 目的:讓 YAML_RULE/PlayBook 的只讀 SSH 診斷步驟經過 AwoooP MCP Gateway。
|
||||
-- 邊界:只授權 read scope;write/admin SSH 工具仍必須走 approval_executor + Gate 5。
|
||||
|
||||
SELECT set_config('app.project_id', 'awoooi', FALSE);
|
||||
|
||||
WITH agent_body AS (
|
||||
SELECT jsonb_build_object(
|
||||
'schema_version', 'awooop_agent_contract_v1',
|
||||
'agent_id', 'auto_repair_executor',
|
||||
'display_name', 'Auto Repair Executor',
|
||||
'project_id', 'awoooi',
|
||||
'purpose', 'Read-only auto-repair diagnostics through AwoooP MCP Gateway',
|
||||
'allowed_scopes', jsonb_build_array('read'),
|
||||
'forbidden_scopes', jsonb_build_array('write', 'admin'),
|
||||
'stage', 't23_auto_repair_diagnostic_gateway'
|
||||
) AS body_json
|
||||
),
|
||||
inserted_revision AS (
|
||||
INSERT INTO awooop_contract_revisions (
|
||||
project_id,
|
||||
contract_family,
|
||||
contract_id,
|
||||
version_major,
|
||||
version_minor,
|
||||
lifecycle_status,
|
||||
body_json,
|
||||
body_hash,
|
||||
body_schema_version,
|
||||
publisher_id,
|
||||
published_at
|
||||
)
|
||||
SELECT
|
||||
'awoooi',
|
||||
'agent',
|
||||
'auto_repair_executor',
|
||||
1,
|
||||
0,
|
||||
'active',
|
||||
body_json,
|
||||
encode(digest(body_json::text, 'sha256'), 'hex'),
|
||||
'v1.0',
|
||||
'migration:t23_auto_repair_executor_read_gateway',
|
||||
NOW()
|
||||
FROM agent_body
|
||||
ON CONFLICT (project_id, contract_family, contract_id, version_major, version_minor)
|
||||
DO NOTHING
|
||||
RETURNING revision_id, project_id, contract_family, contract_id
|
||||
),
|
||||
chosen_revision AS (
|
||||
SELECT revision_id, project_id, contract_family, contract_id
|
||||
FROM inserted_revision
|
||||
UNION ALL
|
||||
SELECT revision_id, project_id, contract_family, contract_id
|
||||
FROM awooop_contract_revisions
|
||||
WHERE project_id = 'awoooi'
|
||||
AND contract_family = 'agent'
|
||||
AND contract_id = 'auto_repair_executor'
|
||||
AND version_major = 1
|
||||
AND version_minor = 0
|
||||
AND lifecycle_status = 'active'
|
||||
),
|
||||
upsert_pointer AS (
|
||||
INSERT INTO awooop_active_revisions (
|
||||
project_id,
|
||||
contract_family,
|
||||
contract_id,
|
||||
active_revision_id,
|
||||
updated_at
|
||||
)
|
||||
SELECT DISTINCT ON (project_id, contract_family, contract_id)
|
||||
project_id,
|
||||
contract_family,
|
||||
contract_id,
|
||||
revision_id,
|
||||
NOW()
|
||||
FROM chosen_revision
|
||||
ORDER BY project_id, contract_family, contract_id, revision_id
|
||||
ON CONFLICT (project_id, contract_family, contract_id)
|
||||
DO UPDATE SET
|
||||
active_revision_id = EXCLUDED.active_revision_id,
|
||||
updated_at = NOW()
|
||||
RETURNING contract_id
|
||||
)
|
||||
SELECT 'auto_repair_executor_active_contracts', count(*) FROM upsert_pointer;
|
||||
|
||||
WITH read_tools(tool_name, description) AS (
|
||||
VALUES
|
||||
('ssh_diagnose', 'SSH host/container diagnosis read'),
|
||||
('ssh_get_top_processes', 'SSH top processes read'),
|
||||
('ssh_get_disk_usage', 'SSH disk usage read'),
|
||||
('ssh_get_memory_info', 'SSH memory info read'),
|
||||
('ssh_get_container_logs', 'SSH container logs read'),
|
||||
('ssh_get_container_status', 'SSH container status read'),
|
||||
('ssh_get_service_status', 'SSH service status read'),
|
||||
('ssh_check_port', 'SSH port check read'),
|
||||
('ssh_get_nginx_error_log', 'SSH nginx error log read'),
|
||||
('ssh_get_swap_info', 'SSH swap info read')
|
||||
),
|
||||
upsert_tools AS (
|
||||
INSERT INTO awooop_mcp_tool_registry (
|
||||
project_id,
|
||||
tool_name,
|
||||
tool_type,
|
||||
description,
|
||||
allowed_scopes,
|
||||
environment_tags,
|
||||
is_active,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
'awoooi',
|
||||
tool_name,
|
||||
'mcp_server',
|
||||
description,
|
||||
'["read"]'::jsonb,
|
||||
'{"env": "prod"}'::jsonb,
|
||||
TRUE,
|
||||
NOW()
|
||||
FROM read_tools
|
||||
ON CONFLICT (project_id, tool_name)
|
||||
DO UPDATE SET
|
||||
description = EXCLUDED.description,
|
||||
allowed_scopes = EXCLUDED.allowed_scopes,
|
||||
environment_tags = EXCLUDED.environment_tags,
|
||||
is_active = TRUE,
|
||||
updated_at = NOW()
|
||||
RETURNING tool_id, tool_name, allowed_scopes
|
||||
),
|
||||
upsert_grants AS (
|
||||
INSERT INTO awooop_mcp_grants (
|
||||
project_id,
|
||||
agent_id,
|
||||
tool_id,
|
||||
granted_by,
|
||||
granted_scopes,
|
||||
expires_at,
|
||||
is_revoked,
|
||||
revoked_at,
|
||||
revoked_by
|
||||
)
|
||||
SELECT
|
||||
'awoooi',
|
||||
'auto_repair_executor',
|
||||
tool_id,
|
||||
'migration:t23_auto_repair_executor_read_gateway',
|
||||
allowed_scopes,
|
||||
NULL,
|
||||
FALSE,
|
||||
NULL,
|
||||
NULL
|
||||
FROM upsert_tools
|
||||
ON CONFLICT (project_id, agent_id, tool_id)
|
||||
DO UPDATE SET
|
||||
granted_by = EXCLUDED.granted_by,
|
||||
granted_scopes = EXCLUDED.granted_scopes,
|
||||
expires_at = NULL,
|
||||
is_revoked = FALSE,
|
||||
revoked_at = NULL,
|
||||
revoked_by = NULL
|
||||
RETURNING grant_id
|
||||
)
|
||||
SELECT
|
||||
'auto_repair_executor_read_gateway',
|
||||
(SELECT count(*) FROM upsert_tools) AS tool_rows,
|
||||
(SELECT count(*) FROM upsert_grants) AS grant_rows;
|
||||
@@ -1,24 +0,0 @@
|
||||
-- Rollback T23 auto-repair executor read-only MCP Gateway grant.
|
||||
|
||||
SELECT set_config('app.project_id', 'awoooi', FALSE);
|
||||
|
||||
UPDATE awooop_mcp_grants
|
||||
SET is_revoked = TRUE,
|
||||
revoked_at = NOW(),
|
||||
revoked_by = 'rollback:t23_auto_repair_executor_read_gateway'
|
||||
WHERE project_id = 'awoooi'
|
||||
AND agent_id = 'auto_repair_executor'
|
||||
AND granted_by = 'migration:t23_auto_repair_executor_read_gateway';
|
||||
|
||||
DELETE FROM awooop_active_revisions
|
||||
WHERE project_id = 'awoooi'
|
||||
AND contract_family = 'agent'
|
||||
AND contract_id = 'auto_repair_executor';
|
||||
|
||||
UPDATE awooop_contract_revisions
|
||||
SET lifecycle_status = 'retired'
|
||||
WHERE project_id = 'awoooi'
|
||||
AND contract_family = 'agent'
|
||||
AND contract_id = 'auto_repair_executor'
|
||||
AND publisher_id = 'migration:t23_auto_repair_executor_read_gateway'
|
||||
AND lifecycle_status = 'active';
|
||||
@@ -1,25 +0,0 @@
|
||||
-- =============================================================================
|
||||
-- AwoooP / AWOOOI MCP Gateway Shadow Onboarding
|
||||
-- 2026-05-13 Codex + ogt
|
||||
--
|
||||
-- 背景:
|
||||
-- AWOOOI 已完成 read-only MCP tool registry / grants seed,但 project 本身仍停在
|
||||
-- legacy_awoooi_default,會被 MCP Gateway Gate 1 正確攔截。
|
||||
--
|
||||
-- 邊界:
|
||||
-- 只把 AWOOOI 租戶升到 shadow,讓既有 Gate 1 生效。
|
||||
-- write/admin tool 仍未授權;自動修復/破壞性動作不因本 migration 開放。
|
||||
-- =============================================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
SELECT set_config('app.project_id', 'awoooi', FALSE);
|
||||
|
||||
UPDATE awooop_projects
|
||||
SET
|
||||
migration_mode = 'shadow',
|
||||
updated_at = NOW()
|
||||
WHERE project_id = 'awoooi'
|
||||
AND migration_mode = 'legacy_awoooi_default';
|
||||
|
||||
COMMIT;
|
||||
@@ -1,20 +0,0 @@
|
||||
-- =============================================================================
|
||||
-- Rollback: AwoooP / AWOOOI MCP Gateway Shadow Onboarding
|
||||
-- 2026-05-13 Codex + ogt
|
||||
--
|
||||
-- 只回退仍停在 shadow 的 AWOOOI;若已由人工/後續 migration 推進到 canary/active,
|
||||
-- 不自動降級。
|
||||
-- =============================================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
SELECT set_config('app.project_id', 'awoooi', FALSE);
|
||||
|
||||
UPDATE awooop_projects
|
||||
SET
|
||||
migration_mode = 'legacy_awoooi_default',
|
||||
updated_at = NOW()
|
||||
WHERE project_id = 'awoooi'
|
||||
AND migration_mode = 'shadow';
|
||||
|
||||
COMMIT;
|
||||
@@ -1,211 +0,0 @@
|
||||
-- T7: awoooi read-only MCP Gateway seed
|
||||
-- 目的:讓決策前感官 MCP 能通過 AwoooP Gateway Gate 2/3,產生 first-class audit。
|
||||
-- 邊界:只授權 read scope;不授權 restart/delete/scale/apply/rollback 等 write/admin 工具。
|
||||
|
||||
SELECT set_config('app.project_id', 'awoooi', FALSE);
|
||||
|
||||
WITH agent_seed(agent_id, display_name) AS (
|
||||
VALUES
|
||||
('pre_decision_investigator', 'Pre-decision Investigator'),
|
||||
('post_execution_verifier', 'Post-execution Verifier')
|
||||
),
|
||||
agent_body AS (
|
||||
SELECT
|
||||
agent_id,
|
||||
jsonb_build_object(
|
||||
'schema_version', 'awooop_agent_contract_v1',
|
||||
'agent_id', agent_id,
|
||||
'display_name', display_name,
|
||||
'project_id', 'awoooi',
|
||||
'purpose', 'Read-only MCP sensing through AwoooP Gateway',
|
||||
'allowed_scopes', jsonb_build_array('read'),
|
||||
'forbidden_scopes', jsonb_build_array('write', 'admin'),
|
||||
'stage', 't7_mcp_gateway_read_sense'
|
||||
) AS body_json
|
||||
FROM agent_seed
|
||||
),
|
||||
inserted_revision AS (
|
||||
INSERT INTO awooop_contract_revisions (
|
||||
project_id,
|
||||
contract_family,
|
||||
contract_id,
|
||||
version_major,
|
||||
version_minor,
|
||||
lifecycle_status,
|
||||
body_json,
|
||||
body_hash,
|
||||
body_schema_version,
|
||||
publisher_id,
|
||||
published_at
|
||||
)
|
||||
SELECT
|
||||
'awoooi',
|
||||
'agent',
|
||||
agent_id,
|
||||
1,
|
||||
0,
|
||||
'active',
|
||||
body_json,
|
||||
encode(digest(body_json::text, 'sha256'), 'hex'),
|
||||
'v1.0',
|
||||
'migration:t7_mcp_gateway_read_seed',
|
||||
NOW()
|
||||
FROM agent_body
|
||||
ON CONFLICT (project_id, contract_family, contract_id, version_major, version_minor)
|
||||
DO NOTHING
|
||||
RETURNING revision_id, project_id, contract_family, contract_id
|
||||
),
|
||||
chosen_revision AS (
|
||||
SELECT revision_id, project_id, contract_family, contract_id
|
||||
FROM inserted_revision
|
||||
UNION ALL
|
||||
SELECT revision_id, project_id, contract_family, contract_id
|
||||
FROM awooop_contract_revisions
|
||||
WHERE project_id = 'awoooi'
|
||||
AND contract_family = 'agent'
|
||||
AND contract_id IN (SELECT agent_id FROM agent_seed)
|
||||
AND version_major = 1
|
||||
AND version_minor = 0
|
||||
AND lifecycle_status = 'active'
|
||||
),
|
||||
upsert_pointer AS (
|
||||
INSERT INTO awooop_active_revisions (
|
||||
project_id,
|
||||
contract_family,
|
||||
contract_id,
|
||||
active_revision_id,
|
||||
updated_at
|
||||
)
|
||||
SELECT DISTINCT ON (project_id, contract_family, contract_id)
|
||||
project_id,
|
||||
contract_family,
|
||||
contract_id,
|
||||
revision_id,
|
||||
NOW()
|
||||
FROM chosen_revision
|
||||
ORDER BY project_id, contract_family, contract_id, revision_id
|
||||
ON CONFLICT (project_id, contract_family, contract_id)
|
||||
DO UPDATE SET
|
||||
active_revision_id = EXCLUDED.active_revision_id,
|
||||
updated_at = NOW()
|
||||
RETURNING contract_id
|
||||
)
|
||||
SELECT 'active_agent_contracts', count(*) FROM upsert_pointer;
|
||||
|
||||
WITH read_tools(tool_name, description) AS (
|
||||
VALUES
|
||||
('k8s_get_pod_logs', 'Kubernetes pod logs read'),
|
||||
('k8s_get_events', 'Kubernetes events read'),
|
||||
('k8s_describe_pod', 'Kubernetes pod describe read'),
|
||||
('k8s_get_hpa_status', 'Kubernetes HPA status read'),
|
||||
('k8s_get_node_conditions', 'Kubernetes node conditions read'),
|
||||
('ssh_diagnose', 'SSH host diagnosis read'),
|
||||
('ssh_get_top_processes', 'SSH top processes read'),
|
||||
('ssh_get_disk_usage', 'SSH disk usage read'),
|
||||
('ssh_get_memory_info', 'SSH memory info read'),
|
||||
('ssh_get_container_logs', 'SSH container logs read'),
|
||||
('ssh_get_container_status', 'SSH container status read'),
|
||||
('ssh_get_service_status', 'SSH service status read'),
|
||||
('ssh_check_port', 'SSH port check read'),
|
||||
('ssh_get_nginx_error_log', 'SSH nginx error log read'),
|
||||
('ssh_get_swap_info', 'SSH swap info read'),
|
||||
('prometheus_query', 'Prometheus instant query read'),
|
||||
('prometheus_query_range', 'Prometheus range query read'),
|
||||
('prometheus_get_alert_history', 'Prometheus alert history read'),
|
||||
('gold_metrics', 'SigNoz gold metrics read'),
|
||||
('trace_url', 'SigNoz trace URL read'),
|
||||
('system_metrics', 'SigNoz system metrics read'),
|
||||
('query_logs', 'SigNoz logs read'),
|
||||
('error_logs_summary', 'SigNoz error logs summary read'),
|
||||
('list_approvals', 'Approval records read'),
|
||||
('get_approval', 'Approval detail read'),
|
||||
('list_incidents', 'Incident records read'),
|
||||
('list_timeline', 'Timeline records read'),
|
||||
('read_file', 'Filesystem allowlisted file read'),
|
||||
('list_directory', 'Filesystem allowlisted directory read'),
|
||||
('search_in_file', 'Filesystem allowlisted file search'),
|
||||
('list_dashboards', 'Grafana dashboards read'),
|
||||
('get_dashboard', 'Grafana dashboard read'),
|
||||
('get_panel_data', 'Grafana panel data read'),
|
||||
('generate_dashboard_url', 'Grafana dashboard URL read'),
|
||||
('search_runbook', 'Runbook semantic search read'),
|
||||
('get_index_stats', 'Runbook index stats read'),
|
||||
('argocd_list_apps', 'ArgoCD apps read'),
|
||||
('argocd_get_app_status', 'ArgoCD app status read'),
|
||||
('argocd_get_sync_history', 'ArgoCD sync history read'),
|
||||
('sentry_list_issues', 'Sentry issues read'),
|
||||
('sentry_get_issue', 'Sentry issue detail read'),
|
||||
('sentry_search_issues', 'Sentry issue search read')
|
||||
),
|
||||
upsert_tools AS (
|
||||
INSERT INTO awooop_mcp_tool_registry (
|
||||
project_id,
|
||||
tool_name,
|
||||
tool_type,
|
||||
description,
|
||||
allowed_scopes,
|
||||
environment_tags,
|
||||
is_active,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
'awoooi',
|
||||
tool_name,
|
||||
'mcp_server',
|
||||
description,
|
||||
'["read"]'::jsonb,
|
||||
'{"env": "prod"}'::jsonb,
|
||||
TRUE,
|
||||
NOW()
|
||||
FROM read_tools
|
||||
ON CONFLICT (project_id, tool_name)
|
||||
DO UPDATE SET
|
||||
description = EXCLUDED.description,
|
||||
allowed_scopes = EXCLUDED.allowed_scopes,
|
||||
environment_tags = EXCLUDED.environment_tags,
|
||||
is_active = TRUE,
|
||||
updated_at = NOW()
|
||||
RETURNING tool_id
|
||||
),
|
||||
grant_agents(agent_id) AS (
|
||||
VALUES
|
||||
('pre_decision_investigator'),
|
||||
('post_execution_verifier')
|
||||
),
|
||||
upsert_grants AS (
|
||||
INSERT INTO awooop_mcp_grants (
|
||||
project_id,
|
||||
agent_id,
|
||||
tool_id,
|
||||
granted_by,
|
||||
granted_scopes,
|
||||
expires_at,
|
||||
is_revoked,
|
||||
revoked_at,
|
||||
revoked_by
|
||||
)
|
||||
SELECT
|
||||
'awoooi',
|
||||
grant_agents.agent_id,
|
||||
upsert_tools.tool_id,
|
||||
'migration:t7_mcp_gateway_read_seed',
|
||||
'["read"]'::jsonb,
|
||||
NULL,
|
||||
FALSE,
|
||||
NULL,
|
||||
NULL
|
||||
FROM upsert_tools
|
||||
CROSS JOIN grant_agents
|
||||
ON CONFLICT (project_id, agent_id, tool_id)
|
||||
DO UPDATE SET
|
||||
granted_scopes = EXCLUDED.granted_scopes,
|
||||
expires_at = NULL,
|
||||
is_revoked = FALSE,
|
||||
revoked_at = NULL,
|
||||
revoked_by = NULL
|
||||
RETURNING grant_id
|
||||
)
|
||||
SELECT
|
||||
'awoooi_read_tools',
|
||||
(SELECT count(*) FROM upsert_tools) AS tool_rows,
|
||||
(SELECT count(*) FROM upsert_grants) AS grant_rows;
|
||||
@@ -1,77 +0,0 @@
|
||||
-- Rollback for T7 awoooi read-only MCP Gateway seed.
|
||||
-- Contract revisions are append-only; rollback revokes grants and deactivates the seeded read tools.
|
||||
|
||||
SELECT set_config('app.project_id', 'awoooi', FALSE);
|
||||
|
||||
UPDATE awooop_mcp_grants
|
||||
SET
|
||||
is_revoked = TRUE,
|
||||
revoked_at = NOW(),
|
||||
revoked_by = 'rollback:t7_mcp_gateway_read_seed'
|
||||
WHERE project_id = 'awoooi'
|
||||
AND agent_id IN ('pre_decision_investigator', 'post_execution_verifier')
|
||||
AND granted_by = 'migration:t7_mcp_gateway_read_seed'
|
||||
AND is_revoked = FALSE;
|
||||
|
||||
UPDATE awooop_mcp_tool_registry
|
||||
SET
|
||||
is_active = FALSE,
|
||||
updated_at = NOW()
|
||||
WHERE project_id = 'awoooi'
|
||||
AND tool_name IN (
|
||||
'k8s_get_pod_logs',
|
||||
'k8s_get_events',
|
||||
'k8s_describe_pod',
|
||||
'k8s_get_hpa_status',
|
||||
'k8s_get_node_conditions',
|
||||
'ssh_diagnose',
|
||||
'ssh_get_top_processes',
|
||||
'ssh_get_disk_usage',
|
||||
'ssh_get_memory_info',
|
||||
'ssh_get_container_logs',
|
||||
'ssh_get_container_status',
|
||||
'ssh_get_service_status',
|
||||
'ssh_check_port',
|
||||
'ssh_get_nginx_error_log',
|
||||
'ssh_get_swap_info',
|
||||
'prometheus_query',
|
||||
'prometheus_query_range',
|
||||
'prometheus_get_alert_history',
|
||||
'gold_metrics',
|
||||
'trace_url',
|
||||
'system_metrics',
|
||||
'query_logs',
|
||||
'error_logs_summary',
|
||||
'list_approvals',
|
||||
'get_approval',
|
||||
'list_incidents',
|
||||
'list_timeline',
|
||||
'read_file',
|
||||
'list_directory',
|
||||
'search_in_file',
|
||||
'list_dashboards',
|
||||
'get_dashboard',
|
||||
'get_panel_data',
|
||||
'generate_dashboard_url',
|
||||
'search_runbook',
|
||||
'get_index_stats',
|
||||
'argocd_list_apps',
|
||||
'argocd_get_app_status',
|
||||
'argocd_get_sync_history',
|
||||
'sentry_list_issues',
|
||||
'sentry_get_issue',
|
||||
'sentry_search_issues'
|
||||
);
|
||||
|
||||
DELETE FROM awooop_active_revisions
|
||||
WHERE project_id = 'awoooi'
|
||||
AND contract_family = 'agent'
|
||||
AND contract_id IN ('pre_decision_investigator', 'post_execution_verifier');
|
||||
|
||||
UPDATE awooop_contract_revisions
|
||||
SET lifecycle_status = 'revoked'
|
||||
WHERE project_id = 'awoooi'
|
||||
AND contract_family = 'agent'
|
||||
AND contract_id IN ('pre_decision_investigator', 'post_execution_verifier')
|
||||
AND publisher_id = 'migration:t7_mcp_gateway_read_seed'
|
||||
AND lifecycle_status = 'active';
|
||||
@@ -1,213 +0,0 @@
|
||||
-- T7: awoooi read-only MCP Gateway seed
|
||||
-- 目的:讓決策前感官 MCP 能通過 AwoooP Gateway Gate 2/3,產生 first-class audit。
|
||||
-- 邊界:只授權 read scope;不授權 restart/delete/scale/apply/rollback 等 write/admin 工具。
|
||||
|
||||
SELECT set_config('app.project_id', 'awoooi', FALSE);
|
||||
|
||||
WITH agent_seed(agent_id, display_name) AS (
|
||||
VALUES
|
||||
('pre_decision_investigator', 'Pre-decision Investigator'),
|
||||
('post_execution_verifier', 'Post-execution Verifier')
|
||||
),
|
||||
agent_body AS (
|
||||
SELECT
|
||||
agent_id,
|
||||
jsonb_build_object(
|
||||
'schema_version', 'awooop_agent_contract_v1',
|
||||
'agent_id', agent_id,
|
||||
'display_name', display_name,
|
||||
'project_id', 'awoooi',
|
||||
'purpose', 'Read-only MCP sensing through AwoooP Gateway',
|
||||
'allowed_scopes', jsonb_build_array('read'),
|
||||
'forbidden_scopes', jsonb_build_array('write', 'admin'),
|
||||
'stage', 't7_mcp_gateway_read_sense'
|
||||
) AS body_json
|
||||
FROM agent_seed
|
||||
),
|
||||
inserted_revision AS (
|
||||
INSERT INTO awooop_contract_revisions (
|
||||
project_id,
|
||||
contract_family,
|
||||
contract_id,
|
||||
version_major,
|
||||
version_minor,
|
||||
lifecycle_status,
|
||||
body_json,
|
||||
body_hash,
|
||||
body_schema_version,
|
||||
publisher_id,
|
||||
published_at
|
||||
)
|
||||
SELECT
|
||||
'awoooi',
|
||||
'agent',
|
||||
agent_id,
|
||||
1,
|
||||
0,
|
||||
'active',
|
||||
body_json,
|
||||
encode(digest(body_json::text, 'sha256'), 'hex'),
|
||||
'v1.0',
|
||||
'migration:t7_mcp_gateway_read_seed',
|
||||
NOW()
|
||||
FROM agent_body
|
||||
ON CONFLICT (project_id, contract_family, contract_id, version_major, version_minor)
|
||||
DO NOTHING
|
||||
RETURNING revision_id, project_id, contract_family, contract_id
|
||||
),
|
||||
chosen_revision AS (
|
||||
SELECT revision_id, project_id, contract_family, contract_id
|
||||
FROM inserted_revision
|
||||
UNION ALL
|
||||
SELECT revision_id, project_id, contract_family, contract_id
|
||||
FROM awooop_contract_revisions
|
||||
WHERE project_id = 'awoooi'
|
||||
AND contract_family = 'agent'
|
||||
AND contract_id IN (SELECT agent_id FROM agent_seed)
|
||||
AND version_major = 1
|
||||
AND version_minor = 0
|
||||
AND lifecycle_status = 'active'
|
||||
),
|
||||
upsert_pointer AS (
|
||||
INSERT INTO awooop_active_revisions (
|
||||
project_id,
|
||||
contract_family,
|
||||
contract_id,
|
||||
active_revision_id,
|
||||
updated_at
|
||||
)
|
||||
SELECT DISTINCT ON (project_id, contract_family, contract_id)
|
||||
project_id,
|
||||
contract_family,
|
||||
contract_id,
|
||||
revision_id,
|
||||
NOW()
|
||||
FROM chosen_revision
|
||||
ORDER BY project_id, contract_family, contract_id, revision_id
|
||||
ON CONFLICT (project_id, contract_family, contract_id)
|
||||
DO UPDATE SET
|
||||
active_revision_id = EXCLUDED.active_revision_id,
|
||||
updated_at = NOW()
|
||||
RETURNING contract_id
|
||||
)
|
||||
SELECT 'active_agent_contracts', count(*) FROM upsert_pointer;
|
||||
|
||||
WITH read_tools(tool_name, description) AS (
|
||||
VALUES
|
||||
('k8s_get_pod_logs', 'Kubernetes pod logs read'),
|
||||
('k8s_get_events', 'Kubernetes events read'),
|
||||
('k8s_describe_pod', 'Kubernetes pod describe read'),
|
||||
('k8s_get_hpa_status', 'Kubernetes HPA status read'),
|
||||
('k8s_get_node_conditions', 'Kubernetes node conditions read'),
|
||||
('ssh_diagnose', 'SSH host diagnosis read'),
|
||||
('ssh_get_top_processes', 'SSH top processes read'),
|
||||
('ssh_get_disk_usage', 'SSH disk usage read'),
|
||||
('ssh_get_memory_info', 'SSH memory info read'),
|
||||
('ssh_get_container_logs', 'SSH container logs read'),
|
||||
('ssh_get_container_status', 'SSH container status read'),
|
||||
('ssh_get_service_status', 'SSH service status read'),
|
||||
('ssh_check_port', 'SSH port check read'),
|
||||
('ssh_get_nginx_error_log', 'SSH nginx error log read'),
|
||||
('ssh_get_swap_info', 'SSH swap info read'),
|
||||
('prometheus_query', 'Prometheus instant query read'),
|
||||
('prometheus_query_range', 'Prometheus range query read'),
|
||||
('prometheus_get_alert_history', 'Prometheus alert history read'),
|
||||
('gold_metrics', 'SigNoz gold metrics read'),
|
||||
('trace_url', 'SigNoz trace URL read'),
|
||||
('system_metrics', 'SigNoz system metrics read'),
|
||||
('query_logs', 'SigNoz logs read'),
|
||||
('error_logs_summary', 'SigNoz error logs summary read'),
|
||||
('list_approvals', 'Approval records read'),
|
||||
('get_approval', 'Approval detail read'),
|
||||
('list_incidents', 'Incident records read'),
|
||||
('list_timeline', 'Timeline records read'),
|
||||
('read_file', 'Filesystem allowlisted file read'),
|
||||
('list_directory', 'Filesystem allowlisted directory read'),
|
||||
('search_in_file', 'Filesystem allowlisted file search'),
|
||||
('list_dashboards', 'Grafana dashboards read'),
|
||||
('get_dashboard', 'Grafana dashboard read'),
|
||||
('get_panel_data', 'Grafana panel data read'),
|
||||
('generate_dashboard_url', 'Grafana dashboard URL read'),
|
||||
('search_runbook', 'Runbook semantic search read'),
|
||||
('get_index_stats', 'Runbook index stats read'),
|
||||
('argocd_list_apps', 'ArgoCD apps read'),
|
||||
('argocd_get_app_status', 'ArgoCD app status read'),
|
||||
('argocd_get_sync_history', 'ArgoCD sync history read'),
|
||||
('sentry_list_issues', 'Sentry issues read'),
|
||||
('sentry_get_issue', 'Sentry issue detail read'),
|
||||
('sentry_search_issues', 'Sentry issue search read')
|
||||
),
|
||||
upsert_tools AS (
|
||||
INSERT INTO awooop_mcp_tool_registry (
|
||||
project_id,
|
||||
tool_name,
|
||||
tool_type,
|
||||
description,
|
||||
allowed_scopes,
|
||||
environment_tags,
|
||||
is_active,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
'awoooi',
|
||||
tool_name,
|
||||
'mcp_server',
|
||||
description,
|
||||
'["read"]'::jsonb,
|
||||
'{"env": "prod"}'::jsonb,
|
||||
TRUE,
|
||||
NOW()
|
||||
FROM read_tools
|
||||
ON CONFLICT (project_id, tool_name)
|
||||
DO UPDATE SET
|
||||
description = EXCLUDED.description,
|
||||
allowed_scopes = EXCLUDED.allowed_scopes,
|
||||
environment_tags = EXCLUDED.environment_tags,
|
||||
is_active = TRUE,
|
||||
updated_at = NOW()
|
||||
RETURNING tool_id
|
||||
),
|
||||
grant_agents(agent_id) AS (
|
||||
VALUES
|
||||
('pre_decision_investigator'),
|
||||
('post_execution_verifier')
|
||||
),
|
||||
upsert_grants AS (
|
||||
INSERT INTO awooop_mcp_grants (
|
||||
project_id,
|
||||
agent_id,
|
||||
tool_id,
|
||||
granted_by,
|
||||
granted_scopes,
|
||||
expires_at,
|
||||
is_revoked,
|
||||
revoked_at,
|
||||
revoked_by
|
||||
)
|
||||
SELECT
|
||||
'awoooi',
|
||||
grant_agents.agent_id,
|
||||
upsert_tools.tool_id,
|
||||
'migration:t7_mcp_gateway_read_seed',
|
||||
'["read"]'::jsonb,
|
||||
NULL,
|
||||
FALSE,
|
||||
NULL,
|
||||
NULL
|
||||
FROM upsert_tools
|
||||
CROSS JOIN grant_agents
|
||||
ON CONFLICT (project_id, agent_id, tool_id)
|
||||
DO UPDATE SET
|
||||
granted_scopes = EXCLUDED.granted_scopes,
|
||||
expires_at = NULL,
|
||||
is_revoked = FALSE,
|
||||
revoked_at = NULL,
|
||||
revoked_by = NULL
|
||||
RETURNING grant_id
|
||||
)
|
||||
SELECT
|
||||
'awoooi_read_tools',
|
||||
(SELECT count(*) FROM upsert_tools) AS tool_rows,
|
||||
(SELECT count(*) FROM upsert_grants) AS grant_rows;
|
||||
|
||||
-- v4 exists only to retrigger run-migration after Gitea skipped the v2->v3 rename-only push.
|
||||
@@ -1,79 +0,0 @@
|
||||
-- Rollback for T7 awoooi read-only MCP Gateway seed.
|
||||
-- Contract revisions are append-only; rollback revokes grants and deactivates the seeded read tools.
|
||||
|
||||
SELECT set_config('app.project_id', 'awoooi', FALSE);
|
||||
|
||||
UPDATE awooop_mcp_grants
|
||||
SET
|
||||
is_revoked = TRUE,
|
||||
revoked_at = NOW(),
|
||||
revoked_by = 'rollback:t7_mcp_gateway_read_seed'
|
||||
WHERE project_id = 'awoooi'
|
||||
AND agent_id IN ('pre_decision_investigator', 'post_execution_verifier')
|
||||
AND granted_by = 'migration:t7_mcp_gateway_read_seed'
|
||||
AND is_revoked = FALSE;
|
||||
|
||||
UPDATE awooop_mcp_tool_registry
|
||||
SET
|
||||
is_active = FALSE,
|
||||
updated_at = NOW()
|
||||
WHERE project_id = 'awoooi'
|
||||
AND tool_name IN (
|
||||
'k8s_get_pod_logs',
|
||||
'k8s_get_events',
|
||||
'k8s_describe_pod',
|
||||
'k8s_get_hpa_status',
|
||||
'k8s_get_node_conditions',
|
||||
'ssh_diagnose',
|
||||
'ssh_get_top_processes',
|
||||
'ssh_get_disk_usage',
|
||||
'ssh_get_memory_info',
|
||||
'ssh_get_container_logs',
|
||||
'ssh_get_container_status',
|
||||
'ssh_get_service_status',
|
||||
'ssh_check_port',
|
||||
'ssh_get_nginx_error_log',
|
||||
'ssh_get_swap_info',
|
||||
'prometheus_query',
|
||||
'prometheus_query_range',
|
||||
'prometheus_get_alert_history',
|
||||
'gold_metrics',
|
||||
'trace_url',
|
||||
'system_metrics',
|
||||
'query_logs',
|
||||
'error_logs_summary',
|
||||
'list_approvals',
|
||||
'get_approval',
|
||||
'list_incidents',
|
||||
'list_timeline',
|
||||
'read_file',
|
||||
'list_directory',
|
||||
'search_in_file',
|
||||
'list_dashboards',
|
||||
'get_dashboard',
|
||||
'get_panel_data',
|
||||
'generate_dashboard_url',
|
||||
'search_runbook',
|
||||
'get_index_stats',
|
||||
'argocd_list_apps',
|
||||
'argocd_get_app_status',
|
||||
'argocd_get_sync_history',
|
||||
'sentry_list_issues',
|
||||
'sentry_get_issue',
|
||||
'sentry_search_issues'
|
||||
);
|
||||
|
||||
DELETE FROM awooop_active_revisions
|
||||
WHERE project_id = 'awoooi'
|
||||
AND contract_family = 'agent'
|
||||
AND contract_id IN ('pre_decision_investigator', 'post_execution_verifier');
|
||||
|
||||
UPDATE awooop_contract_revisions
|
||||
SET lifecycle_status = 'revoked'
|
||||
WHERE project_id = 'awoooi'
|
||||
AND contract_family = 'agent'
|
||||
AND contract_id IN ('pre_decision_investigator', 'post_execution_verifier')
|
||||
AND publisher_id = 'migration:t7_mcp_gateway_read_seed'
|
||||
AND lifecycle_status = 'active';
|
||||
|
||||
-- v4 rollback companion for the retrigger migration.
|
||||
@@ -1,77 +0,0 @@
|
||||
-- T16 verifier gap: allow rollout status evidence through AwoooP MCP Gateway.
|
||||
-- Boundary: read-only scope only; no restart/delete/scale grant is added here.
|
||||
|
||||
SELECT set_config('app.project_id', 'awoooi', FALSE);
|
||||
|
||||
WITH upsert_tool AS (
|
||||
INSERT INTO awooop_mcp_tool_registry (
|
||||
project_id,
|
||||
tool_name,
|
||||
tool_type,
|
||||
description,
|
||||
allowed_scopes,
|
||||
environment_tags,
|
||||
is_active,
|
||||
updated_at
|
||||
)
|
||||
VALUES (
|
||||
'awoooi',
|
||||
'k8s_watch_rollout',
|
||||
'mcp_server',
|
||||
'Kubernetes deployment rollout status read',
|
||||
'["read"]'::jsonb,
|
||||
'{"env": "prod"}'::jsonb,
|
||||
TRUE,
|
||||
NOW()
|
||||
)
|
||||
ON CONFLICT (project_id, tool_name)
|
||||
DO UPDATE SET
|
||||
description = EXCLUDED.description,
|
||||
allowed_scopes = EXCLUDED.allowed_scopes,
|
||||
environment_tags = EXCLUDED.environment_tags,
|
||||
is_active = TRUE,
|
||||
updated_at = NOW()
|
||||
RETURNING tool_id
|
||||
),
|
||||
grant_agents(agent_id) AS (
|
||||
VALUES
|
||||
('pre_decision_investigator'),
|
||||
('post_execution_verifier')
|
||||
),
|
||||
upsert_grants AS (
|
||||
INSERT INTO awooop_mcp_grants (
|
||||
project_id,
|
||||
agent_id,
|
||||
tool_id,
|
||||
granted_by,
|
||||
granted_scopes,
|
||||
expires_at,
|
||||
is_revoked,
|
||||
revoked_at,
|
||||
revoked_by
|
||||
)
|
||||
SELECT
|
||||
'awoooi',
|
||||
grant_agents.agent_id,
|
||||
upsert_tool.tool_id,
|
||||
'migration:t16_rollout_verifier_seed',
|
||||
'["read"]'::jsonb,
|
||||
NULL,
|
||||
FALSE,
|
||||
NULL,
|
||||
NULL
|
||||
FROM upsert_tool
|
||||
CROSS JOIN grant_agents
|
||||
ON CONFLICT (project_id, agent_id, tool_id)
|
||||
DO UPDATE SET
|
||||
granted_scopes = EXCLUDED.granted_scopes,
|
||||
expires_at = NULL,
|
||||
is_revoked = FALSE,
|
||||
revoked_at = NULL,
|
||||
revoked_by = NULL
|
||||
RETURNING grant_id
|
||||
)
|
||||
SELECT
|
||||
'k8s_watch_rollout_read_grants' AS seed,
|
||||
(SELECT count(*) FROM upsert_tool) AS tool_rows,
|
||||
(SELECT count(*) FROM upsert_grants) AS grant_rows;
|
||||
@@ -1,24 +0,0 @@
|
||||
-- Roll back T16 rollout verifier read grant seed.
|
||||
|
||||
SELECT set_config('app.project_id', 'awoooi', FALSE);
|
||||
|
||||
UPDATE awooop_mcp_grants
|
||||
SET
|
||||
is_revoked = TRUE,
|
||||
revoked_at = NOW(),
|
||||
revoked_by = 'migration:t16_rollout_verifier_seed_down'
|
||||
WHERE project_id = 'awoooi'
|
||||
AND agent_id IN ('pre_decision_investigator', 'post_execution_verifier')
|
||||
AND tool_id IN (
|
||||
SELECT tool_id
|
||||
FROM awooop_mcp_tool_registry
|
||||
WHERE project_id = 'awoooi'
|
||||
AND tool_name = 'k8s_watch_rollout'
|
||||
);
|
||||
|
||||
UPDATE awooop_mcp_tool_registry
|
||||
SET
|
||||
is_active = FALSE,
|
||||
updated_at = NOW()
|
||||
WHERE project_id = 'awoooi'
|
||||
AND tool_name = 'k8s_watch_rollout';
|
||||
@@ -1,271 +0,0 @@
|
||||
-- AwoooP Phase 1 Batch 1: 現有四表加 project_id + RLS
|
||||
-- 2026-05-04 ogt + Claude Sonnet 4.6(ADR-118 Batch 1,C-3/C-4 db-expert 修正版)
|
||||
-- 2026-05-04 critic 修正版:ADD CONSTRAINT IF NOT EXISTS 不存在於 PG → 改用 DO 塊檢查 pg_constraint
|
||||
--
|
||||
-- 對象:incidents / knowledge_entries / playbooks / audit_logs
|
||||
-- 這四張表是高頻寫入表,採「三步式 migration」避免長時間鎖表:
|
||||
--
|
||||
-- Step A: ADD COLUMN nullable(metadata-only,瞬間)
|
||||
-- Step B: 分批回填(每批 5000 筆,外部腳本呼叫)
|
||||
-- Step C: NOT VALID CHECK → VALIDATE(SHARE UPDATE EXCLUSIVE,不擋讀寫)
|
||||
-- → SET NOT NULL(PG 12+ 利用已驗證 check,不掃表)
|
||||
-- → SET DEFAULT 'awoooi'
|
||||
--
|
||||
-- ⚠️ 執行前必確認:
|
||||
-- 1. awooop_phase1_control_plane_2026-05-04.sql 已執行(awooop_projects 表存在)
|
||||
-- 2. apps/api 已 deploy 「SET LOCAL app.project_id」版本,rollout 100%
|
||||
-- 3. 31 個 background loop 改用 awooop_platform_admin role(PR-10)
|
||||
-- 4. 量測各表體量(見下方 pre-migration check query)
|
||||
--
|
||||
-- Pre-migration check:
|
||||
-- SELECT relname, n_live_tup, pg_size_pretty(pg_total_relation_size(oid))
|
||||
-- FROM pg_class
|
||||
-- WHERE relname IN ('incidents','knowledge_entries','playbooks','audit_logs');
|
||||
--
|
||||
-- 分批回填腳本:
|
||||
-- apps/api/scripts/awooop_phase1_batch1_backfill.py(另行提供)
|
||||
--
|
||||
-- ⚠️ RLS 是 fail-closed:
|
||||
-- SET LOCAL app.project_id 未設 → 讀不到任何資料(C-4 修正)
|
||||
-- WITH CHECK 防止 INSERT 寫入錯誤 tenant
|
||||
--
|
||||
-- 回滾路徑:
|
||||
-- ALTER TABLE incidents DISABLE ROW LEVEL SECURITY;
|
||||
-- DROP POLICY IF EXISTS incidents_tenant_isolation ON incidents;
|
||||
-- DROP POLICY IF EXISTS knowledge_entries_tenant_isolation ON knowledge_entries;
|
||||
-- DROP POLICY IF EXISTS playbooks_tenant_isolation ON playbooks;
|
||||
-- DROP POLICY IF EXISTS audit_logs_tenant_isolation ON audit_logs;
|
||||
-- ALTER TABLE incidents DISABLE ROW LEVEL SECURITY;
|
||||
-- ALTER TABLE knowledge_entries DISABLE ROW LEVEL SECURITY;
|
||||
-- ALTER TABLE playbooks DISABLE ROW LEVEL SECURITY;
|
||||
-- ALTER TABLE audit_logs DISABLE ROW LEVEL SECURITY;
|
||||
-- ALTER TABLE incidents DROP COLUMN IF EXISTS project_id;
|
||||
-- ALTER TABLE knowledge_entries DROP COLUMN IF EXISTS project_id;
|
||||
-- ALTER TABLE playbooks DROP COLUMN IF EXISTS project_id;
|
||||
-- ALTER TABLE audit_logs DROP COLUMN IF EXISTS project_id;
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
-- ===========================
|
||||
-- STEP A: ADD COLUMN(nullable,瞬間取鎖,不重寫表)
|
||||
-- ===========================
|
||||
-- 一次只做 ADD COLUMN,讓 AccessExclusiveLock 最短
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'incidents' AND column_name = 'project_id'
|
||||
) THEN
|
||||
ALTER TABLE incidents ADD COLUMN project_id VARCHAR(64);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'knowledge_entries' AND column_name = 'project_id'
|
||||
) THEN
|
||||
ALTER TABLE knowledge_entries ADD COLUMN project_id VARCHAR(64);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'playbooks' AND column_name = 'project_id'
|
||||
) THEN
|
||||
ALTER TABLE playbooks ADD COLUMN project_id VARCHAR(64);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'audit_logs' AND column_name = 'project_id'
|
||||
) THEN
|
||||
ALTER TABLE audit_logs ADD COLUMN project_id VARCHAR(64);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
|
||||
-- ===========================
|
||||
-- STEP B: 分批回填(外部腳本)
|
||||
-- ===========================
|
||||
-- 此步驟由 apps/api/scripts/awooop_phase1_batch1_backfill.py 執行
|
||||
-- 每批 UPDATE ... WHERE project_id IS NULL LIMIT 5000
|
||||
-- 完成條件:SELECT count(*) FROM incidents WHERE project_id IS NULL; → 0
|
||||
--
|
||||
-- 快速驗證(執行此 SQL 前必須確認回填完成):
|
||||
-- SELECT
|
||||
-- 'incidents' as tbl, count(*) as null_count FROM incidents WHERE project_id IS NULL
|
||||
-- UNION ALL SELECT 'knowledge_entries', count(*) FROM knowledge_entries WHERE project_id IS NULL
|
||||
-- UNION ALL SELECT 'playbooks', count(*) FROM playbooks WHERE project_id IS NULL
|
||||
-- UNION ALL SELECT 'audit_logs', count(*) FROM audit_logs WHERE project_id IS NULL;
|
||||
-- 所有 null_count 必須為 0,否則停止。
|
||||
--
|
||||
-- ⚠️ 回填完成確認後才可繼續執行 Step C
|
||||
|
||||
|
||||
-- ===========================
|
||||
-- STEP C: NOT NULL 強制 + DEFAULT + Index + RLS
|
||||
-- ===========================
|
||||
-- PostgreSQL 12+:NOT VALID CHECK → VALIDATE → SET NOT NULL
|
||||
-- VALIDATE 只取 SHARE UPDATE EXCLUSIVE,不擋讀寫
|
||||
-- SET NOT NULL 在 VALIDATE 後不再掃表(利用 check constraint 証明)
|
||||
|
||||
-- --- incidents ---
|
||||
|
||||
-- PostgreSQL 無 ADD CONSTRAINT IF NOT EXISTS,改用 DO 塊檢查 pg_constraint
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'chk_incidents_project_id_not_null'
|
||||
AND conrelid = 'incidents'::regclass
|
||||
) THEN
|
||||
ALTER TABLE incidents
|
||||
ADD CONSTRAINT chk_incidents_project_id_not_null
|
||||
CHECK (project_id IS NOT NULL) NOT VALID;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
ALTER TABLE incidents
|
||||
VALIDATE CONSTRAINT chk_incidents_project_id_not_null;
|
||||
|
||||
ALTER TABLE incidents ALTER COLUMN project_id SET NOT NULL;
|
||||
ALTER TABLE incidents ALTER COLUMN project_id SET DEFAULT 'awoooi';
|
||||
ALTER TABLE incidents DROP CONSTRAINT IF EXISTS chk_incidents_project_id_not_null;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_incidents_project_id ON incidents (project_id);
|
||||
|
||||
ALTER TABLE incidents ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE incidents FORCE ROW LEVEL SECURITY;
|
||||
DROP POLICY IF EXISTS incidents_tenant_isolation ON incidents;
|
||||
CREATE POLICY incidents_tenant_isolation ON incidents
|
||||
FOR ALL TO awooop_app
|
||||
USING (project_id = current_setting('app.project_id', TRUE))
|
||||
WITH CHECK (project_id = current_setting('app.project_id', TRUE));
|
||||
|
||||
|
||||
-- --- knowledge_entries ---
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'chk_km_project_id_not_null'
|
||||
AND conrelid = 'knowledge_entries'::regclass
|
||||
) THEN
|
||||
ALTER TABLE knowledge_entries
|
||||
ADD CONSTRAINT chk_km_project_id_not_null
|
||||
CHECK (project_id IS NOT NULL) NOT VALID;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
ALTER TABLE knowledge_entries
|
||||
VALIDATE CONSTRAINT chk_km_project_id_not_null;
|
||||
|
||||
ALTER TABLE knowledge_entries ALTER COLUMN project_id SET NOT NULL;
|
||||
ALTER TABLE knowledge_entries ALTER COLUMN project_id SET DEFAULT 'awoooi';
|
||||
ALTER TABLE knowledge_entries DROP CONSTRAINT IF EXISTS chk_km_project_id_not_null;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_knowledge_entries_project_id ON knowledge_entries (project_id);
|
||||
|
||||
ALTER TABLE knowledge_entries ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE knowledge_entries FORCE ROW LEVEL SECURITY;
|
||||
DROP POLICY IF EXISTS knowledge_entries_tenant_isolation ON knowledge_entries;
|
||||
CREATE POLICY knowledge_entries_tenant_isolation ON knowledge_entries
|
||||
FOR ALL TO awooop_app
|
||||
USING (project_id = current_setting('app.project_id', TRUE))
|
||||
WITH CHECK (project_id = current_setting('app.project_id', TRUE));
|
||||
|
||||
|
||||
-- --- playbooks ---
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'chk_playbooks_project_id_not_null'
|
||||
AND conrelid = 'playbooks'::regclass
|
||||
) THEN
|
||||
ALTER TABLE playbooks
|
||||
ADD CONSTRAINT chk_playbooks_project_id_not_null
|
||||
CHECK (project_id IS NOT NULL) NOT VALID;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
ALTER TABLE playbooks
|
||||
VALIDATE CONSTRAINT chk_playbooks_project_id_not_null;
|
||||
|
||||
ALTER TABLE playbooks ALTER COLUMN project_id SET NOT NULL;
|
||||
ALTER TABLE playbooks ALTER COLUMN project_id SET DEFAULT 'awoooi';
|
||||
ALTER TABLE playbooks DROP CONSTRAINT IF EXISTS chk_playbooks_project_id_not_null;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_playbooks_project_id ON playbooks (project_id);
|
||||
|
||||
ALTER TABLE playbooks ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE playbooks FORCE ROW LEVEL SECURITY;
|
||||
DROP POLICY IF EXISTS playbooks_tenant_isolation ON playbooks;
|
||||
CREATE POLICY playbooks_tenant_isolation ON playbooks
|
||||
FOR ALL TO awooop_app
|
||||
USING (project_id = current_setting('app.project_id', TRUE))
|
||||
WITH CHECK (project_id = current_setting('app.project_id', TRUE));
|
||||
|
||||
|
||||
-- --- audit_logs ---
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'chk_audit_project_id_not_null'
|
||||
AND conrelid = 'audit_logs'::regclass
|
||||
) THEN
|
||||
ALTER TABLE audit_logs
|
||||
ADD CONSTRAINT chk_audit_project_id_not_null
|
||||
CHECK (project_id IS NOT NULL) NOT VALID;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
ALTER TABLE audit_logs
|
||||
VALIDATE CONSTRAINT chk_audit_project_id_not_null;
|
||||
|
||||
ALTER TABLE audit_logs ALTER COLUMN project_id SET NOT NULL;
|
||||
ALTER TABLE audit_logs ALTER COLUMN project_id SET DEFAULT 'awoooi';
|
||||
ALTER TABLE audit_logs DROP CONSTRAINT IF EXISTS chk_audit_project_id_not_null;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_logs_project_id ON audit_logs (project_id);
|
||||
|
||||
ALTER TABLE audit_logs ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE audit_logs FORCE ROW LEVEL SECURITY;
|
||||
DROP POLICY IF EXISTS audit_logs_tenant_isolation ON audit_logs;
|
||||
CREATE POLICY audit_logs_tenant_isolation ON audit_logs
|
||||
FOR ALL TO awooop_app
|
||||
USING (project_id = current_setting('app.project_id', TRUE))
|
||||
WITH CHECK (project_id = current_setting('app.project_id', TRUE));
|
||||
|
||||
|
||||
-- ===========================
|
||||
-- 驗收查詢
|
||||
-- ===========================
|
||||
-- SELECT tablename, rowsecurity, forcerowsecurity FROM pg_tables
|
||||
-- WHERE tablename IN ('incidents','knowledge_entries','playbooks','audit_logs');
|
||||
--
|
||||
-- -- RLS fail-closed 測試(需 awooop_app role 執行):
|
||||
-- SET ROLE awooop_app;
|
||||
-- SET LOCAL app.project_id = 'ewoooc';
|
||||
-- SELECT count(*) FROM incidents; -- 應 = 0(無 ewoooc 資料)
|
||||
-- SET LOCAL app.project_id = 'awoooi';
|
||||
-- SELECT count(*) FROM incidents; -- 應 = 全部既有資料筆數
|
||||
-- RESET ROLE;
|
||||
--
|
||||
-- -- 確認無 NULL project_id:
|
||||
-- SELECT count(*) FROM incidents WHERE project_id IS NULL; -- = 0
|
||||
-- SELECT count(*) FROM knowledge_entries WHERE project_id IS NULL; -- = 0
|
||||
-- SELECT count(*) FROM playbooks WHERE project_id IS NULL; -- = 0
|
||||
-- SELECT count(*) FROM audit_logs WHERE project_id IS NULL; -- = 0
|
||||
@@ -1,546 +0,0 @@
|
||||
-- AwoooP Phase 1: Control Plane Schema Foundation
|
||||
-- 2026-05-04 ogt + Claude Sonnet 4.6(ADR-111~118,Phase 1 Task 1.3~1.7)
|
||||
-- 2026-05-04 db-expert review 修正版:C-1/C-2/C-4/C-5/M-1/M-2/M-4/M-5/Mi-1/Mi-2/Mi-3
|
||||
-- 2026-05-04 critic review 修正版:awooop_app role 建立 + GRANT、移除 __platform__ 後門、
|
||||
-- active_pointer_guard SECURITY DEFINER、pg_partman 冪等、immutability 強化
|
||||
--
|
||||
-- ⚠️ 部署順序鎖死(ADR-118 RLS 前置條件):
|
||||
-- 1. apps/api 必須先 deploy「會 SET LOCAL app.project_id」的版本
|
||||
-- 2. K8s rollout 完成(kubectl rollout status deploy/api = 100%)
|
||||
-- 3. 31 個 background loop 改用 awooop_platform_admin role(PR-10 完成)
|
||||
-- 4. 以上完成後,才執行此 migration SQL
|
||||
--
|
||||
-- ⚠️ 不包含 Batch 1 高流量表(incidents/knowledge_entries/playbooks/audit_logs)
|
||||
-- → 請執行 awooop_phase1_batch1_rls_2026-05-04.sql(三步式 migration)
|
||||
--
|
||||
-- 執行前確認:
|
||||
-- SELECT relname, n_live_tup, pg_size_pretty(pg_total_relation_size(oid))
|
||||
-- FROM pg_class WHERE relname IN ('incidents','knowledge_entries','playbooks','audit_logs');
|
||||
--
|
||||
-- 執行角色:awooop_migration(BYPASSRLS)
|
||||
-- 預估執行時間:< 30 秒(全為新表,無既有資料修改)
|
||||
--
|
||||
-- 回滾路徑:
|
||||
-- 見 awooop_phase1_control_plane_ROLLBACK.sql
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
|
||||
-- ===========================
|
||||
-- Step 1: DB Roles(ADR-118 D1)
|
||||
-- ===========================
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
-- awooop_platform_admin: 平台管理(BYPASSRLS,背景 loop 使用)
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'awooop_platform_admin') THEN
|
||||
CREATE ROLE awooop_platform_admin NOLOGIN;
|
||||
END IF;
|
||||
ALTER ROLE awooop_platform_admin BYPASSRLS;
|
||||
|
||||
-- awooop_migration: migration 執行(BYPASSRLS,只在 migration 期間使用)
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'awooop_migration') THEN
|
||||
CREATE ROLE awooop_migration NOLOGIN;
|
||||
END IF;
|
||||
ALTER ROLE awooop_migration BYPASSRLS;
|
||||
|
||||
-- awooop_app: 應用程式角色(受 RLS 約束,需 SET LOCAL app.project_id)
|
||||
-- 必須在 GRANT 之前建立;NOLOGIN 代表 app connection user 要 SET ROLE awooop_app
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'awooop_app') THEN
|
||||
CREATE ROLE awooop_app NOLOGIN;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
|
||||
-- ===========================
|
||||
-- Step 2: awooop_projects(租戶主表)
|
||||
-- ===========================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS awooop_projects (
|
||||
project_id VARCHAR(64) PRIMARY KEY,
|
||||
display_name VARCHAR(256) NOT NULL,
|
||||
migration_mode VARCHAR(32) NOT NULL DEFAULT 'legacy_awoooi_default',
|
||||
budget_limit_usd NUMERIC(14, 4) CHECK (budget_limit_usd IS NULL OR budget_limit_usd >= 0),
|
||||
allowed_channels JSONB NOT NULL DEFAULT '[]' CHECK (jsonb_typeof(allowed_channels) = 'array'),
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT chk_migration_mode CHECK (
|
||||
migration_mode IN ('legacy_awoooi_default','shadow','canary','active')
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_awooop_projects_active
|
||||
ON awooop_projects(is_active) WHERE is_active = TRUE;
|
||||
|
||||
|
||||
-- ===========================
|
||||
-- Step 3: awooop_contract_revisions(六合約共用 revision,append-only)
|
||||
-- ===========================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS awooop_contract_revisions (
|
||||
revision_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
project_id VARCHAR(64) NOT NULL REFERENCES awooop_projects(project_id),
|
||||
contract_family VARCHAR(32) NOT NULL,
|
||||
contract_id VARCHAR(128) NOT NULL,
|
||||
version_major SMALLINT NOT NULL DEFAULT 1 CHECK (version_major >= 0),
|
||||
version_minor SMALLINT NOT NULL DEFAULT 0 CHECK (version_minor >= 0),
|
||||
lifecycle_status VARCHAR(16) NOT NULL DEFAULT 'draft',
|
||||
body_json JSONB NOT NULL,
|
||||
-- body_hash: SHA-256 hex(64 chars),強制格式
|
||||
body_hash VARCHAR(64) NOT NULL CHECK (body_hash ~ '^[0-9a-f]{64}$'),
|
||||
body_schema_version VARCHAR(16) NOT NULL DEFAULT 'v1.0',
|
||||
-- publish_signature: HMAC-SHA256 hex,draft 時 NULL
|
||||
publish_signature VARCHAR(128) CHECK (
|
||||
publish_signature IS NULL OR publish_signature ~ '^[0-9a-f]+$'
|
||||
),
|
||||
publisher_id VARCHAR(128),
|
||||
published_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT uq_revision_version
|
||||
UNIQUE (project_id, contract_family, contract_id, version_major, version_minor),
|
||||
CONSTRAINT chk_contract_family CHECK (
|
||||
contract_family IN (
|
||||
'project_tenant','agent','mcp_gateway','policy_routing',
|
||||
'runtime_run_state','channel_event','platform_resource'
|
||||
)
|
||||
),
|
||||
CONSTRAINT chk_lifecycle CHECK (
|
||||
lifecycle_status IN ('draft','published','active','revoked')
|
||||
)
|
||||
);
|
||||
|
||||
-- runtime 讀取路徑:找某 contract 最新 published/active 版本
|
||||
CREATE INDEX IF NOT EXISTS idx_revisions_lookup
|
||||
ON awooop_contract_revisions
|
||||
(project_id, contract_family, contract_id, lifecycle_status,
|
||||
version_major DESC, version_minor DESC);
|
||||
|
||||
-- forensic 驗章反查
|
||||
CREATE INDEX IF NOT EXISTS idx_revisions_hash
|
||||
ON awooop_contract_revisions (body_hash);
|
||||
|
||||
|
||||
-- ===========================
|
||||
-- Step 4: awooop_active_revisions(active pointer)
|
||||
-- ===========================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS awooop_active_revisions (
|
||||
pointer_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
project_id VARCHAR(64) NOT NULL REFERENCES awooop_projects(project_id),
|
||||
contract_family VARCHAR(32) NOT NULL,
|
||||
contract_id VARCHAR(128) NOT NULL,
|
||||
-- NOT NULL + ON DELETE RESTRICT(C-1 修正)
|
||||
active_revision_id UUID NOT NULL REFERENCES awooop_contract_revisions(revision_id)
|
||||
ON DELETE RESTRICT,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT uq_active_pointer
|
||||
UNIQUE (project_id, contract_family, contract_id)
|
||||
);
|
||||
|
||||
|
||||
-- ===========================
|
||||
-- Step 5: awooop_contract_outbox(ADR-113,C-2 修正版)
|
||||
-- ===========================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS awooop_contract_outbox (
|
||||
event_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
event_type VARCHAR(64) NOT NULL,
|
||||
-- FK 到 projects(C-2 修正:outbox 不可是孤兒事件)
|
||||
project_id VARCHAR(64) NOT NULL REFERENCES awooop_projects(project_id),
|
||||
contract_family VARCHAR(32) NOT NULL,
|
||||
contract_id VARCHAR(128) NOT NULL,
|
||||
old_revision_id UUID REFERENCES awooop_contract_revisions(revision_id),
|
||||
new_revision_id UUID NOT NULL REFERENCES awooop_contract_revisions(revision_id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
delivered_at TIMESTAMPTZ,
|
||||
relay_attempts INT NOT NULL DEFAULT 0,
|
||||
-- C-2 新增:exponential backoff 支援
|
||||
next_retry_at TIMESTAMPTZ,
|
||||
last_error TEXT,
|
||||
-- C-2 新增:上游 publisher 重試去重(同一 revision 的同一事件類型只記一次)
|
||||
CONSTRAINT uq_outbox_event UNIQUE (new_revision_id, event_type)
|
||||
);
|
||||
|
||||
-- relay worker 主查詢:未投遞 + 可重試(含 next_retry_at NULL = 立即重試)
|
||||
CREATE INDEX IF NOT EXISTS idx_outbox_pending
|
||||
ON awooop_contract_outbox (next_retry_at NULLS FIRST, created_at)
|
||||
WHERE delivered_at IS NULL;
|
||||
|
||||
-- 觀察用:per project backlog 體量
|
||||
CREATE INDEX IF NOT EXISTS idx_outbox_backlog_per_project
|
||||
ON awooop_contract_outbox (project_id, created_at)
|
||||
WHERE delivered_at IS NULL;
|
||||
|
||||
|
||||
-- ===========================
|
||||
-- Step 6: awooop_channel_event_dedupe(ADR-114,M-1 Partition 版)
|
||||
-- ===========================
|
||||
-- pg_partman 維護 1 天 partition,retention 7 天,DROP PARTITION 毫秒清完
|
||||
|
||||
CREATE TABLE IF NOT EXISTS awooop_channel_event_dedupe (
|
||||
dedupe_id UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
project_id VARCHAR(64) NOT NULL,
|
||||
channel_type VARCHAR(32) NOT NULL,
|
||||
provider_event_id VARCHAR(256) NOT NULL,
|
||||
run_id UUID NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
-- Partition key 必須是 PK 的一部分(declarative partition 要求)
|
||||
PRIMARY KEY (dedupe_id, created_at),
|
||||
CONSTRAINT uq_channel_event_dedupe
|
||||
UNIQUE (project_id, channel_type, provider_event_id, created_at)
|
||||
) PARTITION BY RANGE (created_at);
|
||||
|
||||
-- 初始化 pg_partman(若 pg_partman 已安裝)
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_partman') THEN
|
||||
-- 冪等:已在 part_config 則跳過 create_parent(重跑 migration 安全)
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM partman.part_config
|
||||
WHERE parent_table = 'public.awooop_channel_event_dedupe'
|
||||
) THEN
|
||||
PERFORM partman.create_parent(
|
||||
p_parent_table := 'public.awooop_channel_event_dedupe',
|
||||
p_control := 'created_at',
|
||||
p_type := 'native',
|
||||
p_interval := '1 day',
|
||||
p_premake := 4
|
||||
);
|
||||
END IF;
|
||||
UPDATE partman.part_config
|
||||
SET retention = '7 days',
|
||||
retention_keep_table = false
|
||||
WHERE parent_table = 'public.awooop_channel_event_dedupe';
|
||||
ELSE
|
||||
-- pg_partman 未安裝:手動建前 14 天 partition(含今日 ±7 天)
|
||||
DECLARE
|
||||
d DATE;
|
||||
BEGIN
|
||||
FOR d IN
|
||||
SELECT generate_series(
|
||||
CURRENT_DATE - INTERVAL '7 days',
|
||||
CURRENT_DATE + INTERVAL '7 days',
|
||||
INTERVAL '1 day'
|
||||
)::DATE
|
||||
LOOP
|
||||
EXECUTE format(
|
||||
'CREATE TABLE IF NOT EXISTS awooop_channel_event_dedupe_%s
|
||||
PARTITION OF awooop_channel_event_dedupe
|
||||
FOR VALUES FROM (%L) TO (%L)',
|
||||
to_char(d, 'YYYYMMDD'),
|
||||
d::TIMESTAMPTZ,
|
||||
(d + INTERVAL '1 day')::TIMESTAMPTZ
|
||||
);
|
||||
END LOOP;
|
||||
END;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- run_id 反查(Mi-5)
|
||||
CREATE INDEX IF NOT EXISTS idx_dedupe_run
|
||||
ON awooop_channel_event_dedupe (run_id);
|
||||
|
||||
|
||||
-- ===========================
|
||||
-- Step 7: awooop_platform_subjects(ADR-115)
|
||||
-- ===========================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS awooop_platform_subjects (
|
||||
subject_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
project_id VARCHAR(64) NOT NULL REFERENCES awooop_projects(project_id),
|
||||
channel_type VARCHAR(32) NOT NULL,
|
||||
channel_user_id VARCHAR(256) NOT NULL,
|
||||
channel_chat_id VARCHAR(256),
|
||||
platform_subject_id VARCHAR(128) NOT NULL,
|
||||
display_name VARCHAR(256),
|
||||
roles JSONB NOT NULL DEFAULT '[]' CHECK (jsonb_typeof(roles) = 'array'),
|
||||
first_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
last_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT uq_platform_subject
|
||||
UNIQUE (project_id, channel_type, channel_user_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_platform_subjects_lookup
|
||||
ON awooop_platform_subjects (project_id, channel_type, channel_user_id);
|
||||
|
||||
-- platform_subject_id 反查(Operator Console M2 用)
|
||||
CREATE INDEX IF NOT EXISTS idx_platform_subjects_resolve
|
||||
ON awooop_platform_subjects (project_id, platform_subject_id);
|
||||
|
||||
-- 近期活躍 user 查詢
|
||||
CREATE INDEX IF NOT EXISTS idx_platform_subjects_last_seen
|
||||
ON awooop_platform_subjects (project_id, last_seen_at DESC);
|
||||
|
||||
|
||||
-- ===========================
|
||||
-- Step 8: awooop_project_migration_state(Strangler Fig 追蹤)
|
||||
-- ===========================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS awooop_project_migration_state (
|
||||
state_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
project_id VARCHAR(64) NOT NULL REFERENCES awooop_projects(project_id),
|
||||
capability VARCHAR(64) NOT NULL,
|
||||
current_phase VARCHAR(32) NOT NULL DEFAULT 'legacy_awoooi_default',
|
||||
phase_entered_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT uq_project_capability UNIQUE (project_id, capability),
|
||||
CONSTRAINT chk_capability CHECK (
|
||||
capability IN (
|
||||
'run_execution','contract_governance',
|
||||
'budget_tracking','principal_mapping'
|
||||
)
|
||||
),
|
||||
CONSTRAINT chk_phase CHECK (
|
||||
current_phase IN (
|
||||
'legacy_awoooi_default','shadow','canary',
|
||||
'read_only','suggest','auto_remediate'
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
-- ===========================
|
||||
-- Step 9: awooop_published_revisions VIEW(ADR-112 D6 draft 隔離)
|
||||
-- ===========================
|
||||
|
||||
CREATE OR REPLACE VIEW awooop_published_revisions AS
|
||||
SELECT *
|
||||
FROM awooop_contract_revisions
|
||||
WHERE lifecycle_status IN ('published', 'active');
|
||||
|
||||
|
||||
-- ===========================
|
||||
-- Step 10: updated_at 自動更新 trigger(Mi-1)
|
||||
-- ===========================
|
||||
|
||||
CREATE OR REPLACE FUNCTION awooop_set_updated_at()
|
||||
RETURNS TRIGGER LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
t TEXT;
|
||||
BEGIN
|
||||
FOREACH t IN ARRAY ARRAY[
|
||||
'awooop_projects',
|
||||
'awooop_active_revisions',
|
||||
'awooop_platform_subjects',
|
||||
'awooop_project_migration_state'
|
||||
] LOOP
|
||||
EXECUTE format(
|
||||
'DROP TRIGGER IF EXISTS trg_%s_updated_at ON %I;
|
||||
CREATE TRIGGER trg_%s_updated_at
|
||||
BEFORE UPDATE ON %I
|
||||
FOR EACH ROW EXECUTE FUNCTION awooop_set_updated_at();',
|
||||
t, t, t, t
|
||||
);
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
|
||||
-- ===========================
|
||||
-- Step 11: Immutability Trigger(C-5 完整版,ADR-112 D2)
|
||||
-- ===========================
|
||||
-- 允許的 lifecycle 流轉:
|
||||
-- draft → published(publish 操作)
|
||||
-- published → active (activate 操作)
|
||||
-- active → revoked (revoke 操作)
|
||||
-- 禁止:body/hash/signature/version 在 published/active/revoked 後修改
|
||||
|
||||
CREATE OR REPLACE FUNCTION awooop_revision_immutability_guard()
|
||||
RETURNS TRIGGER LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
-- 所有 lifecycle_status 下都禁止修改身份欄位(project_id/family/contract_id)
|
||||
IF NEW.project_id IS DISTINCT FROM OLD.project_id
|
||||
OR NEW.contract_family IS DISTINCT FROM OLD.contract_family
|
||||
OR NEW.contract_id IS DISTINCT FROM OLD.contract_id
|
||||
THEN
|
||||
RAISE EXCEPTION
|
||||
'revision % identity fields (project_id/contract_family/contract_id) are immutable',
|
||||
OLD.revision_id;
|
||||
END IF;
|
||||
|
||||
-- draft 可以自由修改,離開 draft 後鎖住核心欄位
|
||||
IF OLD.lifecycle_status IN ('published', 'active', 'revoked') THEN
|
||||
IF NEW.body_json IS DISTINCT FROM OLD.body_json
|
||||
OR NEW.body_hash IS DISTINCT FROM OLD.body_hash
|
||||
OR NEW.publish_signature IS DISTINCT FROM OLD.publish_signature
|
||||
OR NEW.version_major IS DISTINCT FROM OLD.version_major
|
||||
OR NEW.version_minor IS DISTINCT FROM OLD.version_minor
|
||||
OR NEW.publisher_id IS DISTINCT FROM OLD.publisher_id
|
||||
OR NEW.published_at IS DISTINCT FROM OLD.published_at
|
||||
OR NEW.body_schema_version IS DISTINCT FROM OLD.body_schema_version
|
||||
THEN
|
||||
RAISE EXCEPTION
|
||||
'revision % (%) is immutable: body/signature/version cannot be changed',
|
||||
OLD.revision_id, OLD.lifecycle_status;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
-- lifecycle_status 流轉白名單
|
||||
IF NEW.lifecycle_status IS DISTINCT FROM OLD.lifecycle_status THEN
|
||||
IF NOT (
|
||||
(OLD.lifecycle_status = 'draft' AND NEW.lifecycle_status = 'published') OR
|
||||
(OLD.lifecycle_status = 'published' AND NEW.lifecycle_status = 'active') OR
|
||||
(OLD.lifecycle_status = 'active' AND NEW.lifecycle_status = 'revoked')
|
||||
) THEN
|
||||
RAISE EXCEPTION
|
||||
'illegal lifecycle transition on revision %: % -> %',
|
||||
OLD.revision_id, OLD.lifecycle_status, NEW.lifecycle_status;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_revision_immutability ON awooop_contract_revisions;
|
||||
CREATE TRIGGER trg_revision_immutability
|
||||
BEFORE UPDATE ON awooop_contract_revisions
|
||||
FOR EACH ROW EXECUTE FUNCTION awooop_revision_immutability_guard();
|
||||
|
||||
-- DELETE 完全禁止(append-only 語意)
|
||||
CREATE OR REPLACE FUNCTION awooop_revision_no_delete()
|
||||
RETURNS TRIGGER LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
RAISE EXCEPTION
|
||||
'awooop_contract_revisions is append-only: DELETE forbidden on revision %',
|
||||
OLD.revision_id;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_revision_no_delete ON awooop_contract_revisions;
|
||||
CREATE TRIGGER trg_revision_no_delete
|
||||
BEFORE DELETE ON awooop_contract_revisions
|
||||
FOR EACH ROW EXECUTE FUNCTION awooop_revision_no_delete();
|
||||
|
||||
|
||||
-- ===========================
|
||||
-- Step 12: Active Pointer Guard(M-5,確保 active_revision_id 指向正確的 active revision)
|
||||
-- ===========================
|
||||
|
||||
-- SECURITY DEFINER:trigger 以 migration 擁有者執行,繞過 awooop_contract_revisions 的 RLS,
|
||||
-- 確保跨租戶指向檢測(FORCE RLS 下 SECURITY INVOKER 只能看自己租戶的 revision)
|
||||
CREATE OR REPLACE FUNCTION awooop_active_pointer_guard()
|
||||
RETURNS TRIGGER LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public, pg_catalog
|
||||
AS $$
|
||||
DECLARE
|
||||
rev RECORD;
|
||||
BEGIN
|
||||
SELECT project_id, contract_family, contract_id, lifecycle_status
|
||||
INTO rev
|
||||
FROM awooop_contract_revisions
|
||||
WHERE revision_id = NEW.active_revision_id;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'revision % not found', NEW.active_revision_id;
|
||||
END IF;
|
||||
IF rev.project_id <> NEW.project_id
|
||||
OR rev.contract_family <> NEW.contract_family
|
||||
OR rev.contract_id <> NEW.contract_id
|
||||
THEN
|
||||
RAISE EXCEPTION
|
||||
'active pointer contract identity mismatch: pointer=(%,%,%) revision=(%,%,%)',
|
||||
NEW.project_id, NEW.contract_family, NEW.contract_id,
|
||||
rev.project_id, rev.contract_family, rev.contract_id;
|
||||
END IF;
|
||||
IF rev.lifecycle_status <> 'active' THEN
|
||||
RAISE EXCEPTION
|
||||
'active pointer must reference an active revision (got %)', rev.lifecycle_status;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_active_pointer_guard ON awooop_active_revisions;
|
||||
CREATE TRIGGER trg_active_pointer_guard
|
||||
BEFORE INSERT OR UPDATE ON awooop_active_revisions
|
||||
FOR EACH ROW EXECUTE FUNCTION awooop_active_pointer_guard();
|
||||
|
||||
|
||||
-- ===========================
|
||||
-- Step 13: GRANT awooop_app 基本操作權限
|
||||
-- ===========================
|
||||
-- awooop_app 受 RLS 約束,需設定 app.project_id 才能存取資料
|
||||
-- awooop_platform_admin / awooop_migration 有 BYPASSRLS,不需 GRANT(直接用 superuser 連線)
|
||||
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON awooop_contract_revisions TO awooop_app;
|
||||
GRANT SELECT, INSERT, UPDATE ON awooop_active_revisions TO awooop_app;
|
||||
GRANT SELECT, INSERT ON awooop_contract_outbox TO awooop_app;
|
||||
GRANT SELECT, INSERT ON awooop_channel_event_dedupe TO awooop_app;
|
||||
GRANT SELECT, INSERT, UPDATE ON awooop_platform_subjects TO awooop_app;
|
||||
GRANT SELECT ON awooop_projects TO awooop_app;
|
||||
GRANT SELECT ON awooop_project_migration_state TO awooop_app;
|
||||
GRANT SELECT ON awooop_published_revisions TO awooop_app;
|
||||
|
||||
|
||||
-- ===========================
|
||||
-- Step 14: awooop_* 表 RLS(ADR-118,C-4 fail-closed 修正版)
|
||||
-- ===========================
|
||||
-- ⚠️ fail-closed:沒有 SET LOCAL app.project_id 的 session 看不到任何資料
|
||||
-- ⚠️ awooop_platform_admin / awooop_migration 已 BYPASSRLS,不受 policy 約束
|
||||
-- ⚠️ WITH CHECK 防止 INSERT 時塞入不同 tenant 的 project_id
|
||||
-- ⚠️ 移除 __platform__ 後門(critic C-3 修正):平台層改用 BYPASSRLS 角色,不靠 GUC 魔術字串
|
||||
|
||||
ALTER TABLE awooop_contract_revisions ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE awooop_contract_revisions FORCE ROW LEVEL SECURITY;
|
||||
DROP POLICY IF EXISTS contract_revisions_tenant ON awooop_contract_revisions;
|
||||
CREATE POLICY contract_revisions_tenant ON awooop_contract_revisions
|
||||
FOR ALL TO awooop_app
|
||||
USING (project_id = current_setting('app.project_id', TRUE))
|
||||
WITH CHECK (project_id = current_setting('app.project_id', TRUE));
|
||||
|
||||
ALTER TABLE awooop_active_revisions ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE awooop_active_revisions FORCE ROW LEVEL SECURITY;
|
||||
DROP POLICY IF EXISTS active_revisions_tenant ON awooop_active_revisions;
|
||||
CREATE POLICY active_revisions_tenant ON awooop_active_revisions
|
||||
FOR ALL TO awooop_app
|
||||
USING (project_id = current_setting('app.project_id', TRUE))
|
||||
WITH CHECK (project_id = current_setting('app.project_id', TRUE));
|
||||
|
||||
ALTER TABLE awooop_platform_subjects ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE awooop_platform_subjects FORCE ROW LEVEL SECURITY;
|
||||
DROP POLICY IF EXISTS platform_subjects_tenant ON awooop_platform_subjects;
|
||||
CREATE POLICY platform_subjects_tenant ON awooop_platform_subjects
|
||||
FOR ALL TO awooop_app
|
||||
USING (project_id = current_setting('app.project_id', TRUE))
|
||||
WITH CHECK (project_id = current_setting('app.project_id', TRUE));
|
||||
|
||||
|
||||
-- ===========================
|
||||
-- Step 15: AWOOOI 種子資料(ADR-111 bootstrap)
|
||||
-- ===========================
|
||||
|
||||
INSERT INTO awooop_projects (project_id, display_name, migration_mode, is_active)
|
||||
VALUES ('awoooi', 'AWOOOI', 'legacy_awoooi_default', TRUE)
|
||||
ON CONFLICT (project_id) DO NOTHING;
|
||||
|
||||
INSERT INTO awooop_project_migration_state (project_id, capability, current_phase)
|
||||
VALUES
|
||||
('awoooi', 'run_execution', 'legacy_awoooi_default'),
|
||||
('awoooi', 'contract_governance', 'legacy_awoooi_default'),
|
||||
('awoooi', 'budget_tracking', 'legacy_awoooi_default'),
|
||||
('awoooi', 'principal_mapping', 'legacy_awoooi_default')
|
||||
ON CONFLICT (project_id, capability) DO NOTHING;
|
||||
|
||||
|
||||
-- ===========================
|
||||
-- 驗收查詢(執行後人工確認)
|
||||
-- ===========================
|
||||
-- \dt awooop_*
|
||||
-- SELECT project_id, display_name, migration_mode FROM awooop_projects;
|
||||
-- SELECT project_id, capability, current_phase FROM awooop_project_migration_state;
|
||||
-- SELECT tablename, rowsecurity, forcerowsecurity FROM pg_tables
|
||||
-- WHERE tablename LIKE 'awooop_%';
|
||||
-- -- RLS fail-closed 測試:
|
||||
-- SET LOCAL app.project_id = 'ewoooc';
|
||||
-- SELECT count(*) FROM awooop_contract_revisions; -- 應回傳 0('ewoooc' 不存在 projects)
|
||||
-- SET LOCAL app.project_id = 'awoooi';
|
||||
-- SELECT count(*) FROM awooop_projects; -- 應回傳 1
|
||||
@@ -1,66 +0,0 @@
|
||||
-- AwoooP Phase 2.6: budget_ledger 建表 + 欄位定義
|
||||
-- 2026-05-04 ogt + Claude Sonnet 4.6(ADR-120 D5 實作)
|
||||
--
|
||||
-- 防止 $47k 事故的三層 Hard Kill 架構中的 accounting 層:
|
||||
-- - 每次 LLM call 完成後寫入一筆 ledger record
|
||||
-- - 供 Tenant Budget Cache 計算 / 儀表板消費統計 / 告警閾值觸發
|
||||
--
|
||||
-- Phase 1 Control Plane migration 必須先執行(awooop_projects 表存在)
|
||||
-- awooop_run_state 欄位在 Phase 3 SAGA 實作後補加
|
||||
|
||||
-- =========================================================
|
||||
-- STEP 1: 建立 budget_ledger 表
|
||||
-- =========================================================
|
||||
CREATE TABLE IF NOT EXISTS budget_ledger (
|
||||
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
project_id VARCHAR(64) NOT NULL DEFAULT 'awoooi',
|
||||
agent_id VARCHAR(128),
|
||||
run_id UUID,
|
||||
model VARCHAR(64),
|
||||
provider VARCHAR(32),
|
||||
prompt_tokens INT,
|
||||
completion_tokens INT,
|
||||
cost_usd NUMERIC(10, 4) NOT NULL DEFAULT 0.0000,
|
||||
recorded_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
COMMENT ON TABLE budget_ledger IS 'ADR-120: 每次 LLM call 的 token/cost accounting 記錄';
|
||||
COMMENT ON COLUMN budget_ledger.cost_usd IS 'prompt + completion token 的估算費用(USD)';
|
||||
|
||||
-- =========================================================
|
||||
-- STEP 2: Index(分析 + 查詢效率)
|
||||
-- =========================================================
|
||||
CREATE INDEX IF NOT EXISTS idx_budget_ledger_project_date
|
||||
ON budget_ledger(project_id, recorded_at DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_budget_ledger_run
|
||||
ON budget_ledger(run_id)
|
||||
WHERE run_id IS NOT NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_budget_ledger_agent
|
||||
ON budget_ledger(project_id, agent_id, recorded_at DESC)
|
||||
WHERE agent_id IS NOT NULL;
|
||||
|
||||
-- =========================================================
|
||||
-- STEP 3: RLS(ADR-118 多租戶隔離)
|
||||
-- =========================================================
|
||||
ALTER TABLE budget_ledger ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE budget_ledger FORCE ROW LEVEL SECURITY;
|
||||
|
||||
DROP POLICY IF EXISTS budget_ledger_tenant_isolation ON budget_ledger;
|
||||
CREATE POLICY budget_ledger_tenant_isolation ON budget_ledger
|
||||
FOR ALL TO awooop_app
|
||||
USING (project_id = current_setting('app.project_id', TRUE))
|
||||
WITH CHECK (project_id = current_setting('app.project_id', TRUE));
|
||||
|
||||
-- =========================================================
|
||||
-- STEP 4: GRANT
|
||||
-- =========================================================
|
||||
GRANT SELECT, INSERT ON budget_ledger TO awooop_app;
|
||||
|
||||
-- =========================================================
|
||||
-- 驗收查詢
|
||||
-- =========================================================
|
||||
-- SELECT tablename, rowsecurity FROM pg_tables WHERE tablename = 'budget_ledger';
|
||||
-- -- 結果:rowsecurity = true
|
||||
-- SELECT count(*) FROM budget_ledger; -- = 0(剛建)
|
||||
@@ -1,200 +0,0 @@
|
||||
-- AwoooP Phase 4: Platform Shell in Shadow Mode
|
||||
-- Run State Machine 持久化表
|
||||
-- 2026-05-04 ogt + Claude Sonnet 4.6(ADR-114/ADR-119)
|
||||
--
|
||||
-- 前置:Phase 1 control plane(awooop_projects)必須已執行
|
||||
--
|
||||
-- 三表:
|
||||
-- awooop_run_state — Run FSM 主表(lease + heartbeat + SKIP LOCKED)
|
||||
-- awooop_run_step_journal — SAGA step journal(tool call + 補償指令,ADR-119)
|
||||
-- awooop_run_idempotency — 去重冪等表(ADR-114)
|
||||
|
||||
-- =========================================================
|
||||
-- STEP 1: awooop_run_state
|
||||
-- =========================================================
|
||||
CREATE TABLE IF NOT EXISTS awooop_run_state (
|
||||
run_id UUID PRIMARY KEY,
|
||||
project_id VARCHAR(64) NOT NULL REFERENCES awooop_projects(project_id),
|
||||
agent_id VARCHAR(128) NOT NULL,
|
||||
|
||||
-- FSM 狀態
|
||||
state VARCHAR(32) NOT NULL DEFAULT 'pending'
|
||||
CHECK (state IN (
|
||||
'pending','running','waiting_tool',
|
||||
'waiting_approval','completed','failed',
|
||||
'cancelled','timeout'
|
||||
)),
|
||||
|
||||
-- Worker lease(SKIP LOCKED 防 double-pickup)
|
||||
lease_until TIMESTAMPTZ,
|
||||
heartbeat_at TIMESTAMPTZ,
|
||||
worker_id VARCHAR(128),
|
||||
|
||||
-- Retry 計數
|
||||
attempt_count SMALLINT NOT NULL DEFAULT 0,
|
||||
max_attempts SMALLINT NOT NULL DEFAULT 3,
|
||||
|
||||
-- Observability
|
||||
trace_id VARCHAR(128),
|
||||
|
||||
-- Trigger 來源
|
||||
trigger_type VARCHAR(32),
|
||||
trigger_ref VARCHAR(256), -- channel_event_id / schedule_id / etc.
|
||||
|
||||
-- Shadow mode flag
|
||||
is_shadow BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
|
||||
-- Artifact integrity(ADR-112)
|
||||
input_sha256 CHAR(64),
|
||||
output_sha256 CHAR(64),
|
||||
|
||||
-- Budget
|
||||
cost_usd NUMERIC(10, 4) NOT NULL DEFAULT 0.0000,
|
||||
step_count SMALLINT NOT NULL DEFAULT 0,
|
||||
|
||||
-- 結果
|
||||
error_code VARCHAR(64),
|
||||
error_detail TEXT,
|
||||
|
||||
-- 時間戳記
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
started_at TIMESTAMPTZ,
|
||||
completed_at TIMESTAMPTZ,
|
||||
timeout_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
COMMENT ON TABLE awooop_run_state IS
|
||||
'ADR-114: Run FSM 主表,SKIP LOCKED worker lease';
|
||||
COMMENT ON COLUMN awooop_run_state.is_shadow IS
|
||||
'Phase 4 shadow mode:TRUE = 不產生 user response,不執行 destructive tool';
|
||||
|
||||
-- Index: worker 掃 PENDING(SKIP LOCKED 用)
|
||||
CREATE INDEX IF NOT EXISTS idx_run_state_pending
|
||||
ON awooop_run_state (project_id, created_at)
|
||||
WHERE state = 'pending' AND lease_until IS NULL;
|
||||
|
||||
-- Index: stale run reaper(找 lease 過期的 running run)
|
||||
CREATE INDEX IF NOT EXISTS idx_run_state_stale
|
||||
ON awooop_run_state (lease_until)
|
||||
WHERE state = 'running' AND lease_until IS NOT NULL;
|
||||
|
||||
-- Index: project timeline(dashboard 查詢)
|
||||
CREATE INDEX IF NOT EXISTS idx_run_state_project_timeline
|
||||
ON awooop_run_state (project_id, created_at DESC);
|
||||
|
||||
-- Index: trace_id(跨系統追蹤)
|
||||
CREATE INDEX IF NOT EXISTS idx_run_state_trace_id
|
||||
ON awooop_run_state (trace_id)
|
||||
WHERE trace_id IS NOT NULL;
|
||||
|
||||
-- =========================================================
|
||||
-- STEP 2: awooop_run_step_journal(SAGA step journal,ADR-119)
|
||||
-- =========================================================
|
||||
CREATE TABLE IF NOT EXISTS awooop_run_step_journal (
|
||||
step_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
run_id UUID NOT NULL REFERENCES awooop_run_state(run_id) ON DELETE CASCADE,
|
||||
project_id VARCHAR(64) NOT NULL,
|
||||
|
||||
-- Step 順序(每個 run 內遞增)
|
||||
step_seq SMALLINT NOT NULL,
|
||||
|
||||
-- Tool call 資訊
|
||||
tool_name VARCHAR(128) NOT NULL,
|
||||
mcp_gateway_id VARCHAR(128),
|
||||
|
||||
-- Artifact integrity(ADR-112)
|
||||
input_hash CHAR(64),
|
||||
output_hash CHAR(64),
|
||||
|
||||
-- SAGA 補償指令(JSON)
|
||||
compensation_json JSONB,
|
||||
|
||||
-- 執行結果
|
||||
result_status VARCHAR(16) NOT NULL DEFAULT 'pending'
|
||||
CHECK (result_status IN ('pending','success','failed','compensated')),
|
||||
error_code VARCHAR(64),
|
||||
|
||||
-- Shadow 攔截記錄
|
||||
was_blocked BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
block_reason VARCHAR(128),
|
||||
|
||||
-- 時間
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
completed_at TIMESTAMPTZ,
|
||||
latency_ms INTEGER
|
||||
);
|
||||
|
||||
COMMENT ON TABLE awooop_run_step_journal IS
|
||||
'ADR-119 SAGA step journal:每個 tool call 獨立記錄 + 補償指令';
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uix_run_step_seq
|
||||
ON awooop_run_step_journal (run_id, step_seq);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_run_step_run_id
|
||||
ON awooop_run_step_journal (run_id, step_seq);
|
||||
|
||||
-- =========================================================
|
||||
-- STEP 3: awooop_run_idempotency(ADR-114 去重冪等)
|
||||
-- =========================================================
|
||||
CREATE TABLE IF NOT EXISTS awooop_run_idempotency (
|
||||
idempotency_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
project_id VARCHAR(64) NOT NULL,
|
||||
channel_type VARCHAR(32) NOT NULL,
|
||||
provider_event_id VARCHAR(256) NOT NULL,
|
||||
|
||||
-- 映射到的 run
|
||||
run_id UUID NOT NULL REFERENCES awooop_run_state(run_id),
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
COMMENT ON TABLE awooop_run_idempotency IS
|
||||
'ADR-114: (project_id, channel_type, provider_event_id) → run_id 去重';
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uix_run_idempotency_key
|
||||
ON awooop_run_idempotency (project_id, channel_type, provider_event_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_run_idempotency_run_id
|
||||
ON awooop_run_idempotency (run_id);
|
||||
|
||||
-- =========================================================
|
||||
-- STEP 4: RLS(ADR-118 多租戶隔離)
|
||||
-- =========================================================
|
||||
ALTER TABLE awooop_run_state ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE awooop_run_state FORCE ROW LEVEL SECURITY;
|
||||
ALTER TABLE awooop_run_step_journal ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE awooop_run_step_journal FORCE ROW LEVEL SECURITY;
|
||||
ALTER TABLE awooop_run_idempotency ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE awooop_run_idempotency FORCE ROW LEVEL SECURITY;
|
||||
|
||||
DROP POLICY IF EXISTS run_state_tenant_isolation ON awooop_run_state;
|
||||
CREATE POLICY run_state_tenant_isolation ON awooop_run_state
|
||||
FOR ALL TO awooop_app
|
||||
USING (project_id = current_setting('app.project_id', TRUE))
|
||||
WITH CHECK (project_id = current_setting('app.project_id', TRUE));
|
||||
|
||||
DROP POLICY IF EXISTS run_step_journal_tenant_isolation ON awooop_run_step_journal;
|
||||
CREATE POLICY run_step_journal_tenant_isolation ON awooop_run_step_journal
|
||||
FOR ALL TO awooop_app
|
||||
USING (project_id = current_setting('app.project_id', TRUE))
|
||||
WITH CHECK (project_id = current_setting('app.project_id', TRUE));
|
||||
|
||||
DROP POLICY IF EXISTS run_idempotency_tenant_isolation ON awooop_run_idempotency;
|
||||
CREATE POLICY run_idempotency_tenant_isolation ON awooop_run_idempotency
|
||||
FOR ALL TO awooop_app
|
||||
USING (project_id = current_setting('app.project_id', TRUE))
|
||||
WITH CHECK (project_id = current_setting('app.project_id', TRUE));
|
||||
|
||||
-- =========================================================
|
||||
-- STEP 5: GRANT
|
||||
-- =========================================================
|
||||
GRANT SELECT, INSERT, UPDATE ON awooop_run_state TO awooop_app;
|
||||
GRANT SELECT, INSERT, UPDATE ON awooop_run_step_journal TO awooop_app;
|
||||
GRANT SELECT, INSERT ON awooop_run_idempotency TO awooop_app;
|
||||
|
||||
-- =========================================================
|
||||
-- 驗收查詢
|
||||
-- =========================================================
|
||||
-- SELECT tablename, rowsecurity FROM pg_tables
|
||||
-- WHERE tablename IN ('awooop_run_state','awooop_run_step_journal','awooop_run_idempotency');
|
||||
-- 預期:所有 rowsecurity = true
|
||||
@@ -1,198 +0,0 @@
|
||||
-- =============================================================================
|
||||
-- AwoooP Phase 5: MCP Gateway 四表
|
||||
-- ADR-116(五閘門 enforcement)+ ADR-118(credential isolation)
|
||||
-- 2026-05-04 ogt + Claude Sonnet 4.6
|
||||
-- =============================================================================
|
||||
-- 執行順序:
|
||||
-- 1. awooop_mcp_tool_registry — Tool 白名單
|
||||
-- 2. awooop_mcp_grants — Agent × Tool 授權記錄
|
||||
-- 3. awooop_mcp_credential_refs — k8s Secret 參照(不儲存明文)
|
||||
-- 4. awooop_mcp_gateway_audit — 每次 gateway call 稽核
|
||||
-- =============================================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 1. awooop_mcp_tool_registry — Tool 白名單(Gate 3: Tool)
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS awooop_mcp_tool_registry (
|
||||
tool_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
project_id VARCHAR(64) NOT NULL
|
||||
REFERENCES awooop_projects(project_id) ON DELETE CASCADE,
|
||||
tool_name VARCHAR(128) NOT NULL,
|
||||
tool_type VARCHAR(32) NOT NULL, -- 'builtin' | 'mcp_server' | 'custom'
|
||||
description TEXT,
|
||||
allowed_scopes JSONB NOT NULL DEFAULT '[]'::jsonb, -- ["read","write","admin"]
|
||||
environment_tags JSONB NOT NULL DEFAULT '{}'::jsonb, -- {"env": "prod"} gate 4 用
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT chk_tool_type
|
||||
CHECK (tool_type IN ('builtin','mcp_server','custom')),
|
||||
CONSTRAINT chk_allowed_scopes_array
|
||||
CHECK (jsonb_typeof(allowed_scopes) = 'array'),
|
||||
CONSTRAINT uix_tool_registry_project_name
|
||||
UNIQUE (project_id, tool_name)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_mcp_tool_registry_project
|
||||
ON awooop_mcp_tool_registry (project_id, is_active);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 2. awooop_mcp_grants — Agent × Tool 授權(Gate 2: Agent + Gate 3: Tool)
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS awooop_mcp_grants (
|
||||
grant_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
project_id VARCHAR(64) NOT NULL
|
||||
REFERENCES awooop_projects(project_id) ON DELETE CASCADE,
|
||||
agent_id VARCHAR(128) NOT NULL, -- awooop_agents.agent_id
|
||||
tool_id UUID NOT NULL
|
||||
REFERENCES awooop_mcp_tool_registry(tool_id) ON DELETE CASCADE,
|
||||
granted_by VARCHAR(128) NOT NULL, -- principal(human user / system)
|
||||
granted_scopes JSONB NOT NULL DEFAULT '[]'::jsonb, -- subset of tool.allowed_scopes
|
||||
expires_at TIMESTAMPTZ, -- NULL = 永不過期
|
||||
is_revoked BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
revoked_at TIMESTAMPTZ,
|
||||
revoked_by VARCHAR(128),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT chk_grant_scopes_array
|
||||
CHECK (jsonb_typeof(granted_scopes) = 'array'),
|
||||
CONSTRAINT chk_revoke_consistency
|
||||
CHECK (
|
||||
(is_revoked = FALSE AND revoked_at IS NULL AND revoked_by IS NULL)
|
||||
OR
|
||||
(is_revoked = TRUE AND revoked_at IS NOT NULL)
|
||||
),
|
||||
CONSTRAINT uix_mcp_grant_agent_tool
|
||||
UNIQUE (project_id, agent_id, tool_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_mcp_grants_lookup
|
||||
ON awooop_mcp_grants (project_id, agent_id, tool_id)
|
||||
WHERE is_revoked = FALSE;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_mcp_grants_expiry
|
||||
ON awooop_mcp_grants (expires_at)
|
||||
WHERE is_revoked = FALSE AND expires_at IS NOT NULL;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 3. awooop_mcp_credential_refs — k8s Secret 參照(ADR-118 credential isolation)
|
||||
-- 只儲存 ref 路徑 + sha256 指紋;明文絕不入庫
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS awooop_mcp_credential_refs (
|
||||
ref_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tool_id UUID NOT NULL
|
||||
REFERENCES awooop_mcp_tool_registry(tool_id) ON DELETE CASCADE,
|
||||
project_id VARCHAR(64) NOT NULL
|
||||
REFERENCES awooop_projects(project_id) ON DELETE CASCADE,
|
||||
-- k8s secret ref:格式 "namespace/secret-name#key"
|
||||
k8s_secret_ref VARCHAR(256) NOT NULL,
|
||||
-- sha256(actual_secret_value) — 用於 audit;不可還原原值
|
||||
value_sha256 VARCHAR(64),
|
||||
description TEXT,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
rotated_at TIMESTAMPTZ,
|
||||
|
||||
CONSTRAINT chk_k8s_ref_format
|
||||
CHECK (k8s_secret_ref ~ '^[a-z0-9-]+/[a-z0-9-]+#[a-zA-Z0-9_-]+$'),
|
||||
CONSTRAINT chk_value_sha256_hex
|
||||
CHECK (value_sha256 IS NULL OR value_sha256 ~ '^[0-9a-f]{64}$'),
|
||||
CONSTRAINT uix_credential_ref_tool
|
||||
UNIQUE (tool_id, k8s_secret_ref)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_mcp_cred_refs_tool
|
||||
ON awooop_mcp_credential_refs (tool_id)
|
||||
WHERE is_active = TRUE;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 4. awooop_mcp_gateway_audit — Gateway call 稽核日誌(ADR-116 P1-09)
|
||||
-- 不儲存 raw input/output;只儲存 hash + 結果狀態
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS awooop_mcp_gateway_audit (
|
||||
call_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
project_id VARCHAR(64) NOT NULL,
|
||||
run_id UUID, -- FK soft(run 可能不存在)
|
||||
trace_id VARCHAR(128),
|
||||
agent_id VARCHAR(128),
|
||||
tool_id UUID NOT NULL
|
||||
REFERENCES awooop_mcp_tool_registry(tool_id),
|
||||
tool_name VARCHAR(128) NOT NULL,
|
||||
credential_ref VARCHAR(256), -- k8s_secret_ref 路徑(不含 key value)
|
||||
input_hash VARCHAR(64), -- sha256(canonical input JSON)
|
||||
output_hash VARCHAR(64), -- sha256(canonical output JSON)
|
||||
gate_result JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
-- {"gate1_project": true, "gate2_agent": true, "gate3_tool": true,
|
||||
-- "gate4_env": true, "gate5_approval": true}
|
||||
result_status VARCHAR(16) NOT NULL, -- 'success' | 'blocked' | 'failed' | 'timeout'
|
||||
block_gate SMALLINT, -- 哪個 gate 攔截(1-5,NULL=未攔截)
|
||||
block_reason VARCHAR(256),
|
||||
latency_ms INTEGER,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT chk_gateway_result_status
|
||||
CHECK (result_status IN ('success','blocked','failed','timeout')),
|
||||
CONSTRAINT chk_block_gate_range
|
||||
CHECK (block_gate IS NULL OR (block_gate >= 1 AND block_gate <= 5)),
|
||||
CONSTRAINT chk_input_hash_hex
|
||||
CHECK (input_hash IS NULL OR input_hash ~ '^[0-9a-f]{64}$'),
|
||||
CONSTRAINT chk_output_hash_hex
|
||||
CHECK (output_hash IS NULL OR output_hash ~ '^[0-9a-f]{64}$')
|
||||
);
|
||||
|
||||
-- 查詢熱路徑:by project + run
|
||||
CREATE INDEX IF NOT EXISTS idx_mcp_audit_run
|
||||
ON awooop_mcp_gateway_audit (project_id, run_id, created_at DESC);
|
||||
|
||||
-- 查詢熱路徑:blocked calls 分析
|
||||
CREATE INDEX IF NOT EXISTS idx_mcp_audit_blocked
|
||||
ON awooop_mcp_gateway_audit (project_id, block_gate, created_at DESC)
|
||||
WHERE result_status = 'blocked';
|
||||
|
||||
-- 時序熱路徑(recent calls)
|
||||
CREATE INDEX IF NOT EXISTS idx_mcp_audit_recent
|
||||
ON awooop_mcp_gateway_audit (project_id, created_at DESC);
|
||||
|
||||
-- =============================================================================
|
||||
-- Row Level Security
|
||||
-- =============================================================================
|
||||
|
||||
ALTER TABLE awooop_mcp_tool_registry ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE awooop_mcp_grants ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE awooop_mcp_credential_refs ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE awooop_mcp_gateway_audit ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
ALTER TABLE awooop_mcp_tool_registry FORCE ROW LEVEL SECURITY;
|
||||
ALTER TABLE awooop_mcp_grants FORCE ROW LEVEL SECURITY;
|
||||
ALTER TABLE awooop_mcp_credential_refs FORCE ROW LEVEL SECURITY;
|
||||
ALTER TABLE awooop_mcp_gateway_audit FORCE ROW LEVEL SECURITY;
|
||||
|
||||
-- awooop_app role:只能看自己 project 的資料
|
||||
CREATE POLICY mcp_tool_registry_tenant_isolation ON awooop_mcp_tool_registry
|
||||
USING (
|
||||
project_id = current_setting('app.project_id', TRUE)
|
||||
OR current_setting('app.project_id', TRUE) IS NULL
|
||||
);
|
||||
|
||||
CREATE POLICY mcp_grants_tenant_isolation ON awooop_mcp_grants
|
||||
USING (
|
||||
project_id = current_setting('app.project_id', TRUE)
|
||||
OR current_setting('app.project_id', TRUE) IS NULL
|
||||
);
|
||||
|
||||
CREATE POLICY mcp_credential_refs_tenant_isolation ON awooop_mcp_credential_refs
|
||||
USING (
|
||||
project_id = current_setting('app.project_id', TRUE)
|
||||
OR current_setting('app.project_id', TRUE) IS NULL
|
||||
);
|
||||
|
||||
CREATE POLICY mcp_gateway_audit_tenant_isolation ON awooop_mcp_gateway_audit
|
||||
USING (
|
||||
project_id = current_setting('app.project_id', TRUE)
|
||||
OR current_setting('app.project_id', TRUE) IS NULL
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
@@ -1,14 +0,0 @@
|
||||
-- AwoooP Phase 5b:MCP Gateway blocked call 稽核覆蓋
|
||||
-- 日期:2026-05-06
|
||||
-- 維護者:Codex
|
||||
--
|
||||
-- Gate 1 / Gate 2 / 未知工具的 blocked call 可能發生在 tool registry row
|
||||
-- 取得之前。這些安全決策仍必須落稽核紀錄,因此 tool_id 允許為 NULL,
|
||||
-- 但 tool_name 仍維持必填,作為未知工具與早期 gate block 的追蹤線索。
|
||||
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE awooop_mcp_gateway_audit
|
||||
ALTER COLUMN tool_id DROP NOT NULL;
|
||||
|
||||
COMMIT;
|
||||
@@ -1,93 +0,0 @@
|
||||
-- =============================================================================
|
||||
-- AwoooP Phase 6: EwoooC Tenant Onboarding
|
||||
-- ADR-115(Tenant Onboarding 模板)
|
||||
-- 2026-05-04 ogt + Claude Sonnet 4.6
|
||||
-- =============================================================================
|
||||
-- 執行前提:Phase 1 migration(awooop_phase1_control_plane_2026-05-04.sql)已執行
|
||||
-- 說明:
|
||||
-- EwoooC 是第二個接入 AwoooP 的租戶(awoooi 為第一個)
|
||||
-- migration_mode = 'shadow' 啟動,進入 canary 前需通過 shadow run 驗證
|
||||
-- budget_limit_usd = 50.0(初始限制,可調整)
|
||||
-- 4 個 read-only MCP tools 預先在白名單中(不需 approval)
|
||||
-- =============================================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Step 1: INSERT awooop_projects(EwoooC 租戶)
|
||||
-- ---------------------------------------------------------------------------
|
||||
INSERT INTO awooop_projects (
|
||||
project_id,
|
||||
display_name,
|
||||
migration_mode,
|
||||
budget_limit_usd,
|
||||
allowed_channels,
|
||||
metadata
|
||||
) VALUES (
|
||||
'ewoooc',
|
||||
'EwoooC Business Platform',
|
||||
'shadow', -- Phase 6 啟動模式;通過驗證後升級為 canary
|
||||
50.00, -- 初始 USD 預算上限
|
||||
'["telegram","api"]'::jsonb,
|
||||
'{
|
||||
"onboarded_at": "2026-05-04",
|
||||
"tier": "business",
|
||||
"ollama_topology": "gcp_three_tier",
|
||||
"note": "ADR-115 EwoooC 接入,共用 GCP Ollama 三層拓撲"
|
||||
}'::jsonb
|
||||
) ON CONFLICT (project_id) DO NOTHING;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Step 2: awooop_mcp_tool_registry — 4 個 read-only MCP tools
|
||||
-- (ewoooc 初始只允許唯讀工具,write/admin 需另外建 grant)
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- Tool 1: k8s_get — 查詢 k8s resource(唯讀)
|
||||
INSERT INTO awooop_mcp_tool_registry (
|
||||
project_id, tool_name, tool_type, description, allowed_scopes, environment_tags
|
||||
) VALUES (
|
||||
'ewoooc',
|
||||
'k8s_get',
|
||||
'builtin',
|
||||
'kubectl get 唯讀查詢(pod/deployment/service 狀態)',
|
||||
'["read"]'::jsonb,
|
||||
'{"env": "any"}'::jsonb
|
||||
) ON CONFLICT (project_id, tool_name) DO NOTHING;
|
||||
|
||||
-- Tool 2: signoz_query — 查詢 SigNoz metrics/traces(唯讀)
|
||||
INSERT INTO awooop_mcp_tool_registry (
|
||||
project_id, tool_name, tool_type, description, allowed_scopes, environment_tags
|
||||
) VALUES (
|
||||
'ewoooc',
|
||||
'signoz_query',
|
||||
'builtin',
|
||||
'SigNoz metrics/traces 查詢(唯讀,無告警修改)',
|
||||
'["read"]'::jsonb,
|
||||
'{"env": "any"}'::jsonb
|
||||
) ON CONFLICT (project_id, tool_name) DO NOTHING;
|
||||
|
||||
-- Tool 3: incident_read — 讀取 EwoooC incident 記錄(唯讀,RLS 隔離)
|
||||
INSERT INTO awooop_mcp_tool_registry (
|
||||
project_id, tool_name, tool_type, description, allowed_scopes, environment_tags
|
||||
) VALUES (
|
||||
'ewoooc',
|
||||
'incident_read',
|
||||
'builtin',
|
||||
'Incident 查詢(僅限 ewoooc 租戶資料,RLS 強制隔離)',
|
||||
'["read"]'::jsonb,
|
||||
'{"env": "any"}'::jsonb
|
||||
) ON CONFLICT (project_id, tool_name) DO NOTHING;
|
||||
|
||||
-- Tool 4: km_read — 讀取 Knowledge Management 條目(唯讀)
|
||||
INSERT INTO awooop_mcp_tool_registry (
|
||||
project_id, tool_name, tool_type, description, allowed_scopes, environment_tags
|
||||
) VALUES (
|
||||
'ewoooc',
|
||||
'km_read',
|
||||
'builtin',
|
||||
'Knowledge Management 讀取(ewoooc 租戶 KM,RLS 隔離)',
|
||||
'["read"]'::jsonb,
|
||||
'{"env": "any"}'::jsonb
|
||||
) ON CONFLICT (project_id, tool_name) DO NOTHING;
|
||||
|
||||
COMMIT;
|
||||
@@ -1,131 +0,0 @@
|
||||
-- =============================================================================
|
||||
-- AwoooP Phase 7: Channel Hub 雙表
|
||||
-- ADR-106(channel_event family)+ Progressive Feedback Policy
|
||||
-- 2026-05-04 ogt + Claude Sonnet 4.6
|
||||
-- =============================================================================
|
||||
-- 兩張表:
|
||||
-- awooop_conversation_event — 入站事件鏡像(Telegram/LINE inbound)
|
||||
-- awooop_outbound_message — 出站訊息記錄(interim + final reply)
|
||||
-- =============================================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 1. awooop_conversation_event — 入站 Channel Event 鏡像
|
||||
-- 目的:AwoooP 平台保留所有入站事件的不可變記錄,與 legacy 系統解耦
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS awooop_conversation_event (
|
||||
event_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
project_id VARCHAR(64) NOT NULL
|
||||
REFERENCES awooop_projects(project_id) ON DELETE CASCADE,
|
||||
-- Channel 原始身份
|
||||
channel_type VARCHAR(32) NOT NULL, -- 'telegram' | 'line' | 'slack' | 'api'
|
||||
provider_event_id VARCHAR(256) NOT NULL, -- Telegram: message_id, LINE: webhook event_id
|
||||
-- 統一身份(由 ProviderProxy 注入)
|
||||
platform_subject_id VARCHAR(128),
|
||||
channel_user_id VARCHAR(256),
|
||||
channel_chat_id VARCHAR(256),
|
||||
-- 關聯 run(若已建立)
|
||||
run_id UUID, -- FK soft(run 可能晚於 event 建立)
|
||||
-- 事件內容(只存摘要/hash,不存明文)
|
||||
content_type VARCHAR(32) NOT NULL DEFAULT 'text', -- 'text' | 'photo' | 'document' | 'command'
|
||||
content_hash VARCHAR(64), -- sha256(raw_content),明文不入庫
|
||||
content_preview VARCHAR(256), -- 前 256 字元(無 PII/secret)
|
||||
attachment_sha256 VARCHAR(64), -- 附件 sha256
|
||||
-- 去重(與 awooop_run_idempotency 對應)
|
||||
is_duplicate BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
-- 時間
|
||||
provider_ts TIMESTAMPTZ, -- provider 原始時間戳
|
||||
received_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT chk_conv_event_channel_type
|
||||
CHECK (channel_type IN ('telegram','line','slack','api','internal')),
|
||||
CONSTRAINT chk_conv_event_content_type
|
||||
CHECK (content_type IN ('text','photo','document','command','callback_query')),
|
||||
CONSTRAINT uix_conv_event_dedup
|
||||
UNIQUE (project_id, channel_type, provider_event_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_conv_event_run
|
||||
ON awooop_conversation_event (project_id, run_id, received_at DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_conv_event_subject
|
||||
ON awooop_conversation_event (project_id, platform_subject_id, received_at DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_conv_event_recent
|
||||
ON awooop_conversation_event (project_id, channel_type, received_at DESC);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 2. awooop_outbound_message — 出站訊息記錄(interim + final reply)
|
||||
-- 目的:追蹤 AwoooP 發出的每一條訊息(shadow 不發、canary/active 發)
|
||||
-- Progressive Feedback Policy:WAITING_TOOL 超過 30s → 發 interim message
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS awooop_outbound_message (
|
||||
message_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
project_id VARCHAR(64) NOT NULL
|
||||
REFERENCES awooop_projects(project_id) ON DELETE CASCADE,
|
||||
run_id UUID NOT NULL, -- FK soft
|
||||
conversation_event_id UUID, -- 觸發訊息的入站 event
|
||||
-- 出站目的地
|
||||
channel_type VARCHAR(32) NOT NULL,
|
||||
channel_chat_id VARCHAR(256) NOT NULL,
|
||||
-- 訊息分類
|
||||
message_type VARCHAR(32) NOT NULL, -- 'interim' | 'final' | 'error' | 'approval_request'
|
||||
-- 內容(只存 hash,不存明文)
|
||||
content_hash VARCHAR(64), -- sha256(rendered_content)
|
||||
content_preview VARCHAR(256), -- 前 256 字元(無 PII/secret)
|
||||
-- provider 回報的 message_id(Telegram: message.message_id)
|
||||
provider_message_id VARCHAR(64),
|
||||
-- 狀態
|
||||
send_status VARCHAR(16) NOT NULL DEFAULT 'pending', -- 'pending'|'sent'|'failed'|'shadow'
|
||||
send_error TEXT,
|
||||
-- 時間
|
||||
queued_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
sent_at TIMESTAMPTZ,
|
||||
-- Progressive Feedback Policy(WAITING_TOOL 超 30s 觸發 interim)
|
||||
triggered_by_state VARCHAR(32), -- 觸發本訊息的 run state('waiting_tool'等)
|
||||
waiting_since TIMESTAMPTZ, -- 開始等待的時間(計算 30s 超時用)
|
||||
|
||||
CONSTRAINT chk_outbound_channel_type
|
||||
CHECK (channel_type IN ('telegram','line','slack','api','internal')),
|
||||
CONSTRAINT chk_outbound_message_type
|
||||
CHECK (message_type IN ('interim','final','error','approval_request')),
|
||||
CONSTRAINT chk_outbound_send_status
|
||||
CHECK (send_status IN ('pending','sent','failed','shadow'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_outbound_msg_run
|
||||
ON awooop_outbound_message (project_id, run_id, queued_at DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_outbound_msg_pending
|
||||
ON awooop_outbound_message (project_id, channel_type, queued_at)
|
||||
WHERE send_status = 'pending';
|
||||
|
||||
-- Progressive Feedback Policy 查詢:找等待超過 30s 的 runs
|
||||
CREATE INDEX IF NOT EXISTS idx_outbound_msg_waiting
|
||||
ON awooop_outbound_message (project_id, triggered_by_state, waiting_since)
|
||||
WHERE triggered_by_state = 'waiting_tool' AND send_status = 'pending';
|
||||
|
||||
-- =============================================================================
|
||||
-- Row Level Security
|
||||
-- =============================================================================
|
||||
|
||||
ALTER TABLE awooop_conversation_event ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE awooop_outbound_message ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
ALTER TABLE awooop_conversation_event FORCE ROW LEVEL SECURITY;
|
||||
ALTER TABLE awooop_outbound_message FORCE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY conv_event_tenant_isolation ON awooop_conversation_event
|
||||
USING (
|
||||
project_id = current_setting('app.project_id', TRUE)
|
||||
OR current_setting('app.project_id', TRUE) IS NULL
|
||||
);
|
||||
|
||||
CREATE POLICY outbound_msg_tenant_isolation ON awooop_outbound_message
|
||||
USING (
|
||||
project_id = current_setting('app.project_id', TRUE)
|
||||
OR current_setting('app.project_id', TRUE) IS NULL
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
@@ -1,21 +0,0 @@
|
||||
-- AwoooP Phase 7 T15b: inbound event truth-chain columns
|
||||
--
|
||||
-- Purpose:
|
||||
-- Telegram cards are only the notification surface. Operators need a
|
||||
-- redacted replay envelope for inbound alerts so Alertmanager, Sentry, and
|
||||
-- SignOz events can be correlated with incidents, approvals, logs, and
|
||||
-- automation decisions without storing raw secrets or PII.
|
||||
|
||||
ALTER TABLE awooop_conversation_event
|
||||
ADD COLUMN IF NOT EXISTS content_redacted TEXT,
|
||||
ADD COLUMN IF NOT EXISTS redaction_version VARCHAR(32) NOT NULL DEFAULT 'audit_sink_v1',
|
||||
ADD COLUMN IF NOT EXISTS source_envelope JSONB NOT NULL DEFAULT '{}'::jsonb;
|
||||
|
||||
COMMENT ON COLUMN awooop_conversation_event.content_redacted IS
|
||||
'Full inbound event content after audit_sink redaction; raw unredacted payload text is not stored.';
|
||||
|
||||
COMMENT ON COLUMN awooop_conversation_event.redaction_version IS
|
||||
'Redaction algorithm/version used for content_redacted and source_envelope.';
|
||||
|
||||
COMMENT ON COLUMN awooop_conversation_event.source_envelope IS
|
||||
'Redacted source metadata for inbound replay/audit, including payload hash, provider, source refs, and log correlation hints.';
|
||||
@@ -1,6 +0,0 @@
|
||||
-- Rollback for AwoooP Phase 7 T15b inbound truth-chain columns.
|
||||
-- Safe only if no consumers depend on the redacted replay fields.
|
||||
|
||||
ALTER TABLE awooop_conversation_event DROP COLUMN IF EXISTS source_envelope;
|
||||
ALTER TABLE awooop_conversation_event DROP COLUMN IF EXISTS redaction_version;
|
||||
ALTER TABLE awooop_conversation_event DROP COLUMN IF EXISTS content_redacted;
|
||||
@@ -1,21 +0,0 @@
|
||||
-- AwoooP Phase 7 T1: outbound message truth-chain columns
|
||||
--
|
||||
-- Purpose:
|
||||
-- Telegram must remain a summary channel, but the operator console needs a
|
||||
-- complete redacted replay of the rendered card and the source envelope that
|
||||
-- produced it. Store redacted content only; raw unredacted Telegram text stays
|
||||
-- out of PostgreSQL.
|
||||
|
||||
ALTER TABLE awooop_outbound_message
|
||||
ADD COLUMN IF NOT EXISTS content_redacted TEXT,
|
||||
ADD COLUMN IF NOT EXISTS redaction_version VARCHAR(32) NOT NULL DEFAULT 'audit_sink_v1',
|
||||
ADD COLUMN IF NOT EXISTS source_envelope JSONB NOT NULL DEFAULT '{}'::jsonb;
|
||||
|
||||
COMMENT ON COLUMN awooop_outbound_message.content_redacted IS
|
||||
'Full rendered outbound content after audit_sink redaction; raw unredacted text is not stored.';
|
||||
|
||||
COMMENT ON COLUMN awooop_outbound_message.redaction_version IS
|
||||
'Redaction algorithm/version used for content_redacted and source_envelope.';
|
||||
|
||||
COMMENT ON COLUMN awooop_outbound_message.source_envelope IS
|
||||
'Redacted source metadata for replay/audit, including payload hash and adapter context.';
|
||||
@@ -1,6 +0,0 @@
|
||||
-- Rollback for AwoooP Phase 7 T1 outbound truth-chain columns.
|
||||
-- Safe only if no consumers depend on the redacted replay fields.
|
||||
|
||||
ALTER TABLE awooop_outbound_message DROP COLUMN IF EXISTS source_envelope;
|
||||
ALTER TABLE awooop_outbound_message DROP COLUMN IF EXISTS redaction_version;
|
||||
ALTER TABLE awooop_outbound_message DROP COLUMN IF EXISTS content_redacted;
|
||||
@@ -1,31 +0,0 @@
|
||||
-- 清理重複的 deprecated yaml_rule Playbooks
|
||||
-- 根因:seeder 冪等 SQL 舊版排除 deprecated 記錄,導致每次啟動重建同名 Playbook
|
||||
-- C1 保護(evolver 不封存 yaml_rule)加入前已存在的 deprecated 歷史記錄
|
||||
-- 觸發無限重建迴圈(294 deprecated,25 approved)
|
||||
-- 修法:每個 name 只保留最新的一筆 deprecated,其餘刪除
|
||||
-- seeder 已同步修正(status 過濾移除),此腳本清理歷史垃圾
|
||||
-- 2026-04-24 ogt + Claude Sonnet 4.6(亞太)
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- 診斷:執行前統計(可選,確認規模)
|
||||
-- SELECT source, status, COUNT(*) FROM playbooks GROUP BY source, status ORDER BY source, status;
|
||||
|
||||
-- 找出每個 yaml_rule deprecated name 的最新 created_at(保留基準)
|
||||
-- 刪除同名同 source=yaml_rule + status=deprecated 中非最新的記錄
|
||||
DELETE FROM playbooks
|
||||
WHERE status = 'deprecated'
|
||||
AND source = 'yaml_rule'
|
||||
AND playbook_id NOT IN (
|
||||
-- 每個 name 保留 created_at 最新的那一筆
|
||||
SELECT DISTINCT ON (name) playbook_id
|
||||
FROM playbooks
|
||||
WHERE status = 'deprecated'
|
||||
AND source = 'yaml_rule'
|
||||
ORDER BY name, created_at DESC
|
||||
);
|
||||
|
||||
-- 執行後確認
|
||||
-- SELECT source, status, COUNT(*) FROM playbooks GROUP BY source, status ORDER BY source, status;
|
||||
|
||||
COMMIT;
|
||||
@@ -1,173 +0,0 @@
|
||||
-- ADR-110 GCP-A Primary Embedding 升級:nomic-embed-text 768 → bge-m3 1024 維
|
||||
-- 2026-05-04 ogt + Claude Sonnet 4.6
|
||||
--
|
||||
-- 背景:
|
||||
-- GCP-A (34.143.170.20) 無 nomic-embed-text,改用 bge-m3:latest(專用 embedding 模型)
|
||||
-- bge-m3 產生 1024 維向量,現有 schema vector(768) 不相容,INSERT 會直接失敗
|
||||
--
|
||||
-- 影響範圍:
|
||||
-- 1. knowledge_entries.embedding vector(768) → vector(1024)
|
||||
-- 2. rag_chunks.embedding vector(768) → vector(1024)
|
||||
-- 3. playbook_embeddings.embedding vector(768) → vector(1024)
|
||||
--
|
||||
-- 遷移策略:僅在欄位不是 vector(1024) 時清空現有向量資料,切換維度後由 re-embed script 重新嵌入
|
||||
-- 已經是 vector(1024) 的環境重跑本 migration 時,必須保留既有向量資料。
|
||||
-- 現有向量資料若要保留,需先 dump 用 nomic 格式備份(舊維度無法轉換)
|
||||
--
|
||||
-- 執行前置條件:
|
||||
-- 1. pgvector >= 0.5.0 (已滿足)
|
||||
-- 2. 確認現有向量資料是否需要備份(重要 playbook 建議先備份)
|
||||
-- 3. embedding service 已切換到 bge-m3(models.json v1.4.0)
|
||||
--
|
||||
-- 回滾方式:執行 embedding_rollback_768.sql(需重新嵌入至 nomic-embed-text 格式)
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- 1. knowledge_entries:備份舊向量並清空,變更欄位維度
|
||||
DO $$
|
||||
DECLARE
|
||||
v_dim integer;
|
||||
BEGIN
|
||||
SELECT a.atttypmod INTO v_dim
|
||||
FROM pg_attribute a
|
||||
JOIN pg_class c ON a.attrelid = c.oid
|
||||
WHERE c.relname = 'knowledge_entries'
|
||||
AND a.attname = 'embedding';
|
||||
|
||||
IF v_dim IS DISTINCT FROM 1024 THEN
|
||||
EXECUTE $sql$
|
||||
CREATE TABLE IF NOT EXISTS knowledge_entries_embedding_backup_20260505 AS
|
||||
SELECT
|
||||
id,
|
||||
embedding::text AS embedding_768,
|
||||
NOW() AS backed_up_at
|
||||
FROM knowledge_entries
|
||||
WHERE embedding IS NOT NULL
|
||||
$sql$;
|
||||
|
||||
EXECUTE $sql$
|
||||
ALTER TABLE knowledge_entries
|
||||
ALTER COLUMN embedding TYPE vector(1024)
|
||||
USING NULL
|
||||
$sql$;
|
||||
|
||||
RAISE NOTICE 'knowledge_entries.embedding migrated from vector(%) to vector(1024); old embeddings were backed up and cleared', v_dim;
|
||||
ELSE
|
||||
RAISE NOTICE 'knowledge_entries.embedding already vector(1024); existing embeddings preserved';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
COMMENT ON COLUMN knowledge_entries.embedding IS
|
||||
'bge-m3:latest 1024 維向量 — 遷移自 nomic-embed-text 768 維 (2026-05-05 ADR-110 follow-up)';
|
||||
|
||||
|
||||
-- 2. rag_chunks:清空向量資料,變更欄位維度
|
||||
-- ivfflat index 必須先 DROP 才能 ALTER COLUMN
|
||||
DO $$
|
||||
DECLARE
|
||||
v_dim integer;
|
||||
BEGIN
|
||||
SELECT a.atttypmod INTO v_dim
|
||||
FROM pg_attribute a
|
||||
JOIN pg_class c ON a.attrelid = c.oid
|
||||
WHERE c.relname = 'rag_chunks'
|
||||
AND a.attname = 'embedding';
|
||||
|
||||
IF v_dim IS DISTINCT FROM 1024 THEN
|
||||
EXECUTE 'DROP INDEX IF EXISTS idx_rag_chunks_embedding';
|
||||
EXECUTE $sql$
|
||||
ALTER TABLE rag_chunks
|
||||
ALTER COLUMN embedding TYPE vector(1024)
|
||||
USING NULL
|
||||
$sql$;
|
||||
|
||||
RAISE NOTICE 'rag_chunks.embedding migrated from vector(%) to vector(1024); old embeddings were cleared', v_dim;
|
||||
ELSE
|
||||
RAISE NOTICE 'rag_chunks.embedding already vector(1024); existing embeddings preserved';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 重建 ivfflat index(lists=100 適合 ~10k 筆以下資料)
|
||||
CREATE INDEX IF NOT EXISTS idx_rag_chunks_embedding
|
||||
ON rag_chunks
|
||||
USING ivfflat (embedding vector_cosine_ops)
|
||||
WITH (lists = 100);
|
||||
|
||||
COMMENT ON COLUMN rag_chunks.embedding IS
|
||||
'bge-m3:latest 1024 維向量 — 遷移自 nomic-embed-text 768 維 (2026-05-04 ADR-110)';
|
||||
|
||||
|
||||
-- 3. playbook_embeddings:清空向量資料,變更欄位維度
|
||||
DO $$
|
||||
DECLARE
|
||||
v_dim integer;
|
||||
BEGIN
|
||||
SELECT a.atttypmod INTO v_dim
|
||||
FROM pg_attribute a
|
||||
JOIN pg_class c ON a.attrelid = c.oid
|
||||
WHERE c.relname = 'playbook_embeddings'
|
||||
AND a.attname = 'embedding';
|
||||
|
||||
IF v_dim IS DISTINCT FROM 1024 THEN
|
||||
EXECUTE 'DROP INDEX IF EXISTS ix_playbook_embeddings_vec';
|
||||
EXECUTE $sql$
|
||||
ALTER TABLE playbook_embeddings
|
||||
ALTER COLUMN embedding TYPE vector(1024)
|
||||
USING NULL
|
||||
$sql$;
|
||||
|
||||
RAISE NOTICE 'playbook_embeddings.embedding migrated from vector(%) to vector(1024); old embeddings were cleared', v_dim;
|
||||
ELSE
|
||||
RAISE NOTICE 'playbook_embeddings.embedding already vector(1024); existing embeddings preserved';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_playbook_embeddings_vec
|
||||
ON playbook_embeddings
|
||||
USING ivfflat (embedding vector_cosine_ops)
|
||||
WITH (lists = 100);
|
||||
|
||||
COMMENT ON COLUMN playbook_embeddings.embedding IS
|
||||
'bge-m3:latest 1024 維向量 — 遷移自 nomic-embed-text 768 維 (2026-05-04 ADR-110)';
|
||||
|
||||
COMMENT ON TABLE playbook_embeddings IS
|
||||
'Playbook 向量索引 — ADR-110 GCP-A bge-m3 1024 維 (2026-05-04)';
|
||||
|
||||
|
||||
-- 3. 驗證遷移結果
|
||||
DO $$
|
||||
DECLARE
|
||||
v_km_dim integer;
|
||||
v_rag_dim integer;
|
||||
v_pb_dim integer;
|
||||
BEGIN
|
||||
SELECT atttypmod INTO v_km_dim
|
||||
FROM pg_attribute
|
||||
JOIN pg_class ON attrelid = pg_class.oid
|
||||
WHERE relname = 'knowledge_entries' AND attname = 'embedding';
|
||||
|
||||
SELECT atttypmod INTO v_rag_dim
|
||||
FROM pg_attribute
|
||||
JOIN pg_class ON attrelid = pg_class.oid
|
||||
WHERE relname = 'rag_chunks' AND attname = 'embedding';
|
||||
|
||||
SELECT atttypmod INTO v_pb_dim
|
||||
FROM pg_attribute
|
||||
JOIN pg_class ON attrelid = pg_class.oid
|
||||
WHERE relname = 'playbook_embeddings' AND attname = 'embedding';
|
||||
|
||||
-- pgvector atttypmod stores the configured dimension.
|
||||
IF v_km_dim != 1024 THEN
|
||||
RAISE EXCEPTION 'knowledge_entries.embedding 維度驗證失敗:expected 1024, got %', v_km_dim;
|
||||
END IF;
|
||||
IF v_rag_dim != 1024 THEN
|
||||
RAISE EXCEPTION 'rag_chunks.embedding 維度驗證失敗:expected 1024, got %', v_rag_dim;
|
||||
END IF;
|
||||
IF v_pb_dim != 1024 THEN
|
||||
RAISE EXCEPTION 'playbook_embeddings.embedding 維度驗證失敗:expected 1024, got %', v_pb_dim;
|
||||
END IF;
|
||||
|
||||
RAISE NOTICE '✅ embedding 遷移驗證通過:knowledge_entries、rag_chunks、playbook_embeddings 均為 vector(1024)';
|
||||
END $$;
|
||||
|
||||
COMMIT;
|
||||
@@ -1,116 +0,0 @@
|
||||
-- governance_remediation_dispatch_2026-05-03.sql
|
||||
-- Wave 2 D: 治理事件修復派遣表
|
||||
-- 2026-05-03 ogt + Claude Sonnet 4.6(亞太)
|
||||
--
|
||||
-- 用途:
|
||||
-- 將 5 種治理事件(trust_drift / knowledge_degradation / llm_hallucination /
|
||||
-- execution_blast_radius / governance_slo_data_gap)接到修復執行器。
|
||||
-- 每個事件同一時間最多 1 筆活躍 dispatch(partial unique index)。
|
||||
-- 失敗重試採 INSERT 新 row(保留完整審計痕跡),舊 row 永久保留 failed。
|
||||
--
|
||||
-- 依賴(必須先存在):
|
||||
-- - ai_governance_events(governance_event_id FK)
|
||||
-- - playbooks(playbook_id FK)
|
||||
-- - incidents(incident_id FK)
|
||||
-- - approval_records(approval_id FK)
|
||||
--
|
||||
-- 回滾路徑:
|
||||
-- DROP TABLE IF EXISTS governance_remediation_dispatch;
|
||||
-- DROP TYPE IF EXISTS governance_event_type;
|
||||
-- DROP TYPE IF EXISTS governance_dispatch_status;
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- Step 1: 建立 ENUM 類型(create_type=False 的 ORM 需要 migration 預先建立)
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_type WHERE typname = 'governance_event_type'
|
||||
) THEN
|
||||
CREATE TYPE governance_event_type AS ENUM (
|
||||
'trust_drift',
|
||||
'knowledge_degradation',
|
||||
'llm_hallucination',
|
||||
'execution_blast_radius',
|
||||
'governance_slo_data_gap'
|
||||
);
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_type WHERE typname = 'governance_dispatch_status'
|
||||
) THEN
|
||||
CREATE TYPE governance_dispatch_status AS ENUM (
|
||||
'pending',
|
||||
'dispatched',
|
||||
'executing',
|
||||
'succeeded',
|
||||
'failed',
|
||||
'skipped',
|
||||
'cancelled'
|
||||
);
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
-- Step 2: 建立主表
|
||||
CREATE TABLE IF NOT EXISTS governance_remediation_dispatch (
|
||||
id VARCHAR(36) NOT NULL PRIMARY KEY,
|
||||
governance_event_id VARCHAR(36) NOT NULL
|
||||
REFERENCES ai_governance_events(id) ON DELETE RESTRICT,
|
||||
event_type governance_event_type NOT NULL,
|
||||
dispatch_status governance_dispatch_status NOT NULL DEFAULT 'pending',
|
||||
playbook_id VARCHAR(36)
|
||||
REFERENCES playbooks(playbook_id) ON DELETE SET NULL,
|
||||
incident_id VARCHAR(30)
|
||||
REFERENCES incidents(incident_id) ON DELETE SET NULL,
|
||||
approval_id VARCHAR(36)
|
||||
REFERENCES approval_records(id) ON DELETE SET NULL,
|
||||
decision_context JSONB NOT NULL DEFAULT '{}',
|
||||
executor_type VARCHAR(80) NOT NULL,
|
||||
attempt_count INTEGER NOT NULL DEFAULT 0,
|
||||
max_attempts INTEGER NOT NULL DEFAULT 3,
|
||||
last_error TEXT,
|
||||
dispatched_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
started_at TIMESTAMPTZ,
|
||||
completed_at TIMESTAMPTZ,
|
||||
created_by VARCHAR(100) DEFAULT 'governance_dispatcher',
|
||||
|
||||
CONSTRAINT ck_grd_attempts
|
||||
CHECK (attempt_count >= 0 AND attempt_count <= max_attempts),
|
||||
CONSTRAINT ck_grd_max_attempts_positive
|
||||
CHECK (max_attempts > 0)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE governance_remediation_dispatch IS
|
||||
'Wave 2 D: 治理事件修復派遣記錄(失敗重試採 INSERT 新 row 審計策略)';
|
||||
|
||||
-- Step 3: 一般索引
|
||||
CREATE INDEX IF NOT EXISTS ix_grd_status_dispatched
|
||||
ON governance_remediation_dispatch (dispatch_status, dispatched_at);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_grd_event_status
|
||||
ON governance_remediation_dispatch (governance_event_id, dispatch_status);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_grd_playbook_id
|
||||
ON governance_remediation_dispatch (playbook_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_grd_event_type_status
|
||||
ON governance_remediation_dispatch (event_type, dispatch_status);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_grd_governance_event_id
|
||||
ON governance_remediation_dispatch (governance_event_id);
|
||||
|
||||
-- Step 4: Partial unique index(同 event_id 不可同時有 2 筆活躍 dispatch)
|
||||
-- 注意:ORM 層 __table_args__ 無法宣告 partial unique,此為唯一來源
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ux_grd_one_active_per_event
|
||||
ON governance_remediation_dispatch (governance_event_id)
|
||||
WHERE dispatch_status IN ('pending', 'dispatched', 'executing');
|
||||
|
||||
-- Step 5: 權限授予(對齊 adr094 模式)
|
||||
GRANT SELECT, INSERT, UPDATE ON governance_remediation_dispatch TO awoooi;
|
||||
|
||||
COMMENT ON INDEX ux_grd_one_active_per_event IS
|
||||
'Partial unique: 同一治理事件同一時間最多 1 筆活躍 dispatch(pending/dispatched/executing)';
|
||||
@@ -1,23 +0,0 @@
|
||||
-- P1-1 KMWriter 冪等 migration
|
||||
-- 2026-04-28 ogt + Claude Sonnet 4.6
|
||||
--
|
||||
-- 目的:為 knowledge_entries 加 path_type 欄位 + (related_incident_id, path_type) unique index,
|
||||
-- 實現 KMWriter 文件承諾的 UPSERT 冪等 key。
|
||||
--
|
||||
-- Down 路徑:
|
||||
-- DROP INDEX IF EXISTS uix_knowledge_incident_path;
|
||||
-- ALTER TABLE knowledge_entries DROP COLUMN IF EXISTS path_type;
|
||||
|
||||
-- 1. 新增 path_type 欄位(nullable,舊資料為 NULL,歷史條目不強制)
|
||||
ALTER TABLE knowledge_entries
|
||||
ADD COLUMN IF NOT EXISTS path_type VARCHAR(50) NULL;
|
||||
|
||||
COMMENT ON COLUMN knowledge_entries.path_type
|
||||
IS 'KMWriter 寫入路徑類型,構成冪等 key (related_incident_id, path_type)。'
|
||||
'可用值: incident_resolve / approval_manual / approval_auto_ok / approval_auto_fail / playbook_extract';
|
||||
|
||||
-- 2. partial unique index:只對兩欄均非 NULL 的列生效(排除歷史資料 NULL 衝突)
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uix_knowledge_incident_path
|
||||
ON knowledge_entries (related_incident_id, path_type)
|
||||
WHERE related_incident_id IS NOT NULL
|
||||
AND path_type IS NOT NULL;
|
||||
@@ -1,38 +0,0 @@
|
||||
-- p2_decision_fusion_columns.sql
|
||||
-- 2026-04-26 P2-DB-Fix by Claude — db-expert P0 三修(P0.3)
|
||||
-- P2.1 DecisionFusionEngine 必要欄位 + partial index
|
||||
-- ADR-085 鐵律:AI 學習成果不可存 Cache,fusion 分數必須落地 PG
|
||||
--
|
||||
-- 執行方式:DBA 手動執行(禁止 alembic upgrade / CI 自動跑)
|
||||
-- CONCURRENTLY 必須在 transaction 外單獨執行
|
||||
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE approval_records
|
||||
ADD COLUMN IF NOT EXISTS composite_score REAL,
|
||||
ADD COLUMN IF NOT EXISTS complexity_tier VARCHAR(16),
|
||||
ADD COLUMN IF NOT EXISTS decision_fusion_details JSONB;
|
||||
|
||||
ALTER TABLE approval_records
|
||||
ADD CONSTRAINT IF NOT EXISTS chk_complexity_tier CHECK (
|
||||
complexity_tier IS NULL
|
||||
OR complexity_tier IN ('low', 'medium', 'high', 'critical')
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN approval_records.composite_score
|
||||
IS 'P2.1 DecisionFusion 合成分數(0.0-1.0),方法 III 加權結果';
|
||||
COMMENT ON COLUMN approval_records.complexity_tier
|
||||
IS 'P2.1 告警複雜度分層:low / medium / high / critical';
|
||||
COMMENT ON COLUMN approval_records.decision_fusion_details
|
||||
IS 'P2.1 DecisionFusionEngine: openclaw_score / hermes_score / playbook_score / mcp_health_score / elephant_score';
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- CONCURRENTLY 必須在 transaction 外執行(不可放在 BEGIN/COMMIT 內)
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS ix_approval_composite_score
|
||||
ON approval_records (composite_score)
|
||||
WHERE composite_score IS NOT NULL;
|
||||
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS ix_approval_complexity_tier
|
||||
ON approval_records (complexity_tier)
|
||||
WHERE complexity_tier IS NOT NULL;
|
||||
@@ -1,19 +0,0 @@
|
||||
-- p2_decision_fusion_columns_rollback.sql
|
||||
-- 2026-04-26 P2-DB-Fix by Claude — db-expert P0 三修(P0.3)rollback
|
||||
-- 回滾 p2_decision_fusion_columns.sql
|
||||
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE approval_records
|
||||
DROP CONSTRAINT IF EXISTS chk_complexity_tier;
|
||||
|
||||
ALTER TABLE approval_records
|
||||
DROP COLUMN IF EXISTS composite_score,
|
||||
DROP COLUMN IF EXISTS complexity_tier,
|
||||
DROP COLUMN IF EXISTS decision_fusion_details;
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- CONCURRENTLY 必須在 transaction 外
|
||||
DROP INDEX CONCURRENTLY IF EXISTS ix_approval_composite_score;
|
||||
DROP INDEX CONCURRENTLY IF EXISTS ix_approval_complexity_tier;
|
||||
@@ -1,25 +0,0 @@
|
||||
-- 2026-04-27 P3.2.2 by Claude — Provider 版本歷史表
|
||||
-- 功能:記錄每次 AI Provider 版本探測結果,偵測版本變更
|
||||
-- 回滾:p3_2_provider_version_history_rollback.sql
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ai_provider_version_history (
|
||||
id SERIAL PRIMARY KEY,
|
||||
provider VARCHAR(40) NOT NULL,
|
||||
model VARCHAR(100) NOT NULL,
|
||||
version VARCHAR(200),
|
||||
digest VARCHAR(80),
|
||||
captured_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
prev_version VARCHAR(200),
|
||||
changed BOOLEAN NOT NULL DEFAULT FALSE
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- CREATE INDEX CONCURRENTLY 不能在 transaction block 內執行
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS ix_provider_version_captured
|
||||
ON ai_provider_version_history (provider, captured_at DESC);
|
||||
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS ix_provider_version_changed
|
||||
ON ai_provider_version_history (changed, captured_at DESC)
|
||||
WHERE changed = TRUE;
|
||||
@@ -1,6 +0,0 @@
|
||||
-- 2026-04-27 P3.2.2 by Claude — Provider 版本歷史回滾腳本
|
||||
BEGIN;
|
||||
DROP INDEX IF EXISTS ix_provider_version_captured;
|
||||
DROP INDEX IF EXISTS ix_provider_version_changed;
|
||||
DROP TABLE IF EXISTS ai_provider_version_history;
|
||||
COMMIT;
|
||||
@@ -1,23 +0,0 @@
|
||||
-- Phase 25 Knowledge Auto-Harvesting enum compatibility.
|
||||
-- SQLAlchemy stores Enum names (AUTO_RUNBOOK / ANTI_PATTERN) for EntryType.
|
||||
-- Older production DBs only had lowercase labels from the first migration.
|
||||
--
|
||||
-- Note: some CI migrator roles do not own enum types. Production was patched
|
||||
-- manually on 2026-05-01; this migration is kept as the durable schema record
|
||||
-- and tolerates insufficient_privilege so the migration workflow can continue.
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
ALTER TYPE entrytype ADD VALUE IF NOT EXISTS 'AUTO_RUNBOOK';
|
||||
EXCEPTION
|
||||
WHEN insufficient_privilege THEN
|
||||
RAISE NOTICE 'Skipping entrytype AUTO_RUNBOOK; migrator does not own enum type';
|
||||
END $$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
ALTER TYPE entrytype ADD VALUE IF NOT EXISTS 'ANTI_PATTERN';
|
||||
EXCEPTION
|
||||
WHEN insufficient_privilege THEN
|
||||
RAISE NOTICE 'Skipping entrytype ANTI_PATTERN; migrator does not own enum type';
|
||||
END $$;
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"name": "OpenClaw AI Router Configuration",
|
||||
"version": "1.4.0",
|
||||
"description": "AI 模型路由與備援設定 (ADR-006 + ADR-036 Nemotron + D1 ADR-067 五大應用 2026-04-11 + ADR-110 GCP 三層容災 2026-05-04)",
|
||||
"updated_at": "2026-05-04",
|
||||
"version": "1.3.0",
|
||||
"description": "AI 模型路由與備援設定 (ADR-006 + ADR-036 Nemotron + D1 ADR-067 五大應用 2026-04-11)",
|
||||
"updated_at": "2026-04-11",
|
||||
|
||||
"default_provider": "ollama",
|
||||
"fallback_order": ["ollama", "gemini", "claude"],
|
||||
@@ -11,28 +11,24 @@
|
||||
|
||||
"providers": {
|
||||
"ollama": {
|
||||
"name": "Ollama (GCP-A Primary)",
|
||||
"name": "Ollama (Local M1 Pro)",
|
||||
"enabled": true,
|
||||
"priority": 1,
|
||||
"endpoint": "http://34.143.170.20:11434",
|
||||
"endpoint": "http://192.168.0.111:11434",
|
||||
"api_path": "/api/generate",
|
||||
"models": {
|
||||
"default": "qwen2.5:7b-instruct",
|
||||
"rca": "qwen3:14b",
|
||||
"default": "deepseek-r1:14b",
|
||||
"rca": "deepseek-r1:14b",
|
||||
"summary": "gemma3:4b",
|
||||
"drift_summary": "qwen3:14b",
|
||||
"drift_summary": "qwen2.5:7b-instruct",
|
||||
"drift_intent": "qwen2.5:7b-instruct",
|
||||
"log_anomaly": "deepseek-r1:14b",
|
||||
"nemoclaw": "deepseek-r1:14b",
|
||||
"playbook_draft": "qwen3:14b",
|
||||
"playbook_draft": "qwen2.5:7b-instruct",
|
||||
"code_review": "qwen2.5-coder:7b",
|
||||
"embedding": "bge-m3:latest",
|
||||
"rag_generate": "qwen3:14b",
|
||||
"image_analysis": "minicpm-v:latest",
|
||||
"trust_scoring": "hermes3:latest",
|
||||
"alert_triage": "hermes3:latest",
|
||||
"intent_classify": "qwen2.5:7b-instruct",
|
||||
"governance": "deepseek-r1:14b"
|
||||
"embedding": "nomic-embed-text",
|
||||
"rag_generate": "qwen2.5:7b-instruct",
|
||||
"image_analysis": "llava:latest"
|
||||
},
|
||||
"options": {
|
||||
"temperature": 0.1,
|
||||
@@ -90,16 +86,16 @@
|
||||
"endpoint": "https://api.anthropic.com/v1",
|
||||
"api_path": "/messages",
|
||||
"models": {
|
||||
"default": "claude-haiku-4-5-20251001",
|
||||
"rca": "claude-haiku-4-5-20251001",
|
||||
"summary": "claude-haiku-4-5-20251001"
|
||||
"default": "claude-3-haiku-20240307",
|
||||
"rca": "claude-3-haiku-20240307",
|
||||
"summary": "claude-3-haiku-20240307"
|
||||
},
|
||||
"options": {
|
||||
"max_tokens": 2048
|
||||
},
|
||||
"timeout_seconds": 30,
|
||||
"cost": {
|
||||
"per_1k_tokens": 0.005,
|
||||
"per_1k_tokens": 0.008,
|
||||
"currency": "USD"
|
||||
},
|
||||
"auth": {
|
||||
@@ -158,12 +154,12 @@
|
||||
},
|
||||
|
||||
"adr067_ollama_applications": {
|
||||
"description": "ADR-067 五大 Ollama 本地 AI 應用 (Phase 30-34),2026-05-04 ogt + Claude Sonnet 4.6: endpoint 升級至 GCP-A Primary",
|
||||
"endpoint": "http://34.143.170.20:11434",
|
||||
"description": "ADR-067 五大 Ollama 本地 AI 應用 (Phase 30-34),endpoint: http://192.168.0.111:11434",
|
||||
"endpoint": "http://192.168.0.111:11434",
|
||||
"applications": {
|
||||
"drift_summary": {
|
||||
"phase": 30,
|
||||
"model": "qwen3:14b",
|
||||
"model": "qwen2.5:7b-instruct",
|
||||
"timeout_seconds": 90,
|
||||
"purpose": "Config Drift 報告中文摘要"
|
||||
},
|
||||
@@ -181,22 +177,22 @@
|
||||
},
|
||||
"rag_embed": {
|
||||
"phase": 33,
|
||||
"model": "bge-m3:latest",
|
||||
"dimensions": 1024,
|
||||
"model": "nomic-embed-text",
|
||||
"dimensions": 768,
|
||||
"timeout_seconds": 30,
|
||||
"purpose": "RAG 知識庫向量化,pgvector 儲存(bge-m3 多語言 1024 維)"
|
||||
"purpose": "RAG 知識庫向量化,pgvector 儲存"
|
||||
},
|
||||
"rag_generate": {
|
||||
"phase": 33,
|
||||
"model": "qwen3:14b",
|
||||
"model": "qwen2.5:7b-instruct",
|
||||
"timeout_seconds": 60,
|
||||
"purpose": "RAG 查詢回答生成,top_k=5"
|
||||
},
|
||||
"image_analysis": {
|
||||
"phase": 34,
|
||||
"model": "minicpm-v:latest",
|
||||
"model": "llava:latest",
|
||||
"timeout_seconds": 60,
|
||||
"purpose": "Telegram 圖片分析(minicpm-v 多模態精度優於 llava)"
|
||||
"purpose": "Telegram 圖片分析"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -46,10 +46,6 @@ dependencies = [
|
||||
# 2026-04-16 ogt + Claude Sonnet 4.6: SSH MCP sensor 修復 — asyncssh 缺失導致 sensors_succeeded=0
|
||||
# 根因: ssh_provider.py 中 import asyncssh 在 try/except 外,所有 15 個 SSH tool 直接 ImportError
|
||||
"asyncssh>=2.14.0",
|
||||
# 2026-05-31 Codex: AwoooP truth-chain Ansible runtime gate 需要
|
||||
# production API image 內真的存在 ansible-playbook,否則只能顯示
|
||||
# candidate audit,無法進入 check-mode executor readiness。
|
||||
"ansible-core>=2.16.0,<2.18.0",
|
||||
]
|
||||
|
||||
# [tool.uv.sources]
|
||||
|
||||
@@ -58,8 +58,3 @@ pytest>=7.4.0
|
||||
pytest-asyncio>=0.23.0
|
||||
ruff>=0.1.0
|
||||
sentry-sdk[fastapi]>=2.0.0
|
||||
|
||||
# AwoooP Ansible runtime readiness
|
||||
# 2026-05-31 Codex: production API image must include ansible-playbook before
|
||||
# truth-chain can honestly mark check-mode executor readiness as available.
|
||||
ansible-core>=2.16.0,<2.18.0
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
AwoooP Phase 1 Batch 1 回填腳本
|
||||
================================
|
||||
對 incidents / knowledge_entries / playbooks / audit_logs 四張表
|
||||
分批將 project_id IS NULL 的列回填為 'awoooi'。
|
||||
|
||||
前置條件:
|
||||
awooop_phase1_batch1_rls_2026-05-04.sql Step A(ADD COLUMN nullable)已執行
|
||||
|
||||
執行方式:
|
||||
從 secret manager / operator vault 設定 DATABASE_URL,禁止在指令或檔案中寫入 URL。
|
||||
cd apps/api && python scripts/awooop_phase1_batch1_backfill.py
|
||||
|
||||
2026-05-04 ogt + Claude Sonnet 4.6(ADR-118 Batch 1 C-3 修正)
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import time
|
||||
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
|
||||
DATABASE_URL = os.environ["DATABASE_URL"]
|
||||
|
||||
TABLES = [
|
||||
("incidents", "incident_id"),
|
||||
("knowledge_entries", "id"),
|
||||
("playbooks", "id"),
|
||||
("audit_logs", "id"),
|
||||
]
|
||||
|
||||
BATCH_SIZE = 5000
|
||||
SLEEP_MS = 100 # 批次間休眠 ms,降低對正常流量的影響
|
||||
|
||||
|
||||
async def count_nulls(conn, table: str) -> int:
|
||||
result = await conn.execute(
|
||||
text(f"SELECT count(*) FROM {table} WHERE project_id IS NULL") # noqa: S608
|
||||
)
|
||||
return result.scalar()
|
||||
|
||||
|
||||
async def backfill_table(engine, table: str, pk_col: str) -> int:
|
||||
total_updated = 0
|
||||
print(f"\n[{table}] 開始回填...")
|
||||
|
||||
while True:
|
||||
async with engine.begin() as conn:
|
||||
result = await conn.execute(text(f"""
|
||||
UPDATE {table}
|
||||
SET project_id = 'awoooi'
|
||||
WHERE {pk_col} IN (
|
||||
SELECT {pk_col} FROM {table}
|
||||
WHERE project_id IS NULL
|
||||
LIMIT :batch_size
|
||||
FOR UPDATE SKIP LOCKED
|
||||
)
|
||||
"""), {"batch_size": BATCH_SIZE})
|
||||
rows = result.rowcount
|
||||
|
||||
total_updated += rows
|
||||
if rows == 0:
|
||||
break
|
||||
|
||||
print(f" [{table}] 已回填 {total_updated} 筆...")
|
||||
await asyncio.sleep(SLEEP_MS / 1000)
|
||||
|
||||
print(f" [{table}] 回填完成,共 {total_updated} 筆")
|
||||
return total_updated
|
||||
|
||||
|
||||
async def verify(engine) -> bool:
|
||||
print("\n=== 驗收確認 ===")
|
||||
ok = True
|
||||
async with engine.connect() as conn:
|
||||
for table, _ in TABLES:
|
||||
null_count = await count_nulls(conn, table)
|
||||
status = "✅" if null_count == 0 else "❌"
|
||||
print(f" {status} {table}: {null_count} 筆 NULL project_id")
|
||||
if null_count != 0:
|
||||
ok = False
|
||||
return ok
|
||||
|
||||
|
||||
async def main():
|
||||
print("=" * 60)
|
||||
print("AwoooP Phase 1 Batch 1 Backfill")
|
||||
print("=" * 60)
|
||||
|
||||
engine = create_async_engine(DATABASE_URL, echo=False)
|
||||
t0 = time.monotonic()
|
||||
|
||||
for table, pk_col in TABLES:
|
||||
await backfill_table(engine, table, pk_col)
|
||||
|
||||
passed = await verify(engine)
|
||||
elapsed = time.monotonic() - t0
|
||||
|
||||
print(f"\n{'✅ 所有表回填完成' if passed else '❌ 仍有 NULL,請重跑'}")
|
||||
print(f"耗時:{elapsed:.1f}s")
|
||||
print()
|
||||
if passed:
|
||||
print("下一步:執行 awooop_phase1_batch1_rls_2026-05-04.sql 的 Step C")
|
||||
else:
|
||||
print("⚠️ 請確認無長 transaction 持有 SKIP LOCKED 的列後重跑")
|
||||
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -1,158 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
migrate_rules_to_playbooks.py — 規則 → Playbook 遷移 CLI
|
||||
=========================================================
|
||||
將 alert_rules.yaml 中的 25 條規則遷移為 DRAFT Playbook,讓飛輪 RAG 有資料可查。
|
||||
|
||||
用法:
|
||||
# 預設 dry-run(只印計畫,不寫 DB)
|
||||
python scripts/migrate_rules_to_playbooks.py
|
||||
|
||||
# 指定 yaml 路徑
|
||||
python scripts/migrate_rules_to_playbooks.py --yaml-path /path/to/alert_rules.yaml
|
||||
|
||||
# 真實寫入 DB
|
||||
python scripts/migrate_rules_to_playbooks.py --commit
|
||||
|
||||
# 完整選項
|
||||
python scripts/migrate_rules_to_playbooks.py --yaml-path alert_rules.yaml --commit
|
||||
|
||||
W1 PR-R1 — 規則 → Playbook 遷移
|
||||
2026-04-28 ogt + Claude Sonnet 4.6
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 確保 apps/api/src 在 import path 中(從 scripts/ 執行時)
|
||||
_SCRIPT_DIR = Path(__file__).parent
|
||||
_API_ROOT = _SCRIPT_DIR.parent
|
||||
sys.path.insert(0, str(_API_ROOT))
|
||||
|
||||
# 預設 yaml 路徑:相對 scripts/ 的上一層(apps/api/alert_rules.yaml)
|
||||
_DEFAULT_YAML_PATH = _API_ROOT / "alert_rules.yaml"
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="將 alert_rules.yaml 遷移為 DRAFT Playbook(飛輪 RAG 冷啟動)",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
範例:
|
||||
python scripts/migrate_rules_to_playbooks.py # dry-run(預設)
|
||||
python scripts/migrate_rules_to_playbooks.py --commit # 真實寫入
|
||||
python scripts/migrate_rules_to_playbooks.py --yaml-path alert_rules.yaml --commit
|
||||
""",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--yaml-path",
|
||||
type=Path,
|
||||
default=_DEFAULT_YAML_PATH,
|
||||
help=f"alert_rules.yaml 路徑(預設: {_DEFAULT_YAML_PATH})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--commit",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="真實寫入 DB(預設 dry-run,僅印計畫)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--disable-flag",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="模擬 ENABLE_RULE_MIGRATION_DRAFT=false(測試 feature flag 關閉路徑)",
|
||||
)
|
||||
# 2026-04-29 ogt + Claude Opus 4.7: critic Major #2 修
|
||||
# --commit 寫 prod DB 必須二次確認,誤跑會在 prod 製造 25 筆 DRAFT
|
||||
parser.add_argument(
|
||||
"--yes",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="跳過 --commit 的二次確認 prompt(CI / 自動化用)",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
async def _run(args: argparse.Namespace) -> int:
|
||||
"""
|
||||
非同步主流程
|
||||
|
||||
Returns:
|
||||
exit code (0=成功, 1=有錯誤)
|
||||
"""
|
||||
from src.services.rule_to_playbook_migrator import migrate_yaml_rules_to_playbooks
|
||||
|
||||
yaml_path: Path = args.yaml_path
|
||||
dry_run: bool = not args.commit
|
||||
enable_migration: bool = not args.disable_flag
|
||||
|
||||
# 讀取 feature flag(環境變數優先,CLI flag 次之)
|
||||
env_flag = os.environ.get("ENABLE_RULE_MIGRATION_DRAFT", "").lower()
|
||||
if env_flag == "false":
|
||||
enable_migration = False
|
||||
|
||||
print(f"\n{'[DRY-RUN] ' if dry_run else ''}規則 → Playbook 遷移")
|
||||
print(f" yaml_path: {yaml_path}")
|
||||
print(f" enable_migration: {enable_migration}")
|
||||
print(f" dry_run: {dry_run}")
|
||||
print()
|
||||
|
||||
if not yaml_path.exists():
|
||||
print(f"[ERROR] yaml 不存在: {yaml_path}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# 2026-04-29 critic Major #2 修:--commit 二次確認,--yes 跳過
|
||||
if not dry_run and not args.yes:
|
||||
ans = input(
|
||||
"⚠️ 即將寫入 prod DB(最多 25 筆 DRAFT Playbook)\n"
|
||||
" Type 'yes' to confirm (or 'n' to abort): "
|
||||
).strip().lower()
|
||||
if ans != "yes":
|
||||
print("[ABORTED] 使用者取消(type 'yes' to confirm)", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
report = await migrate_yaml_rules_to_playbooks(
|
||||
yaml_path=yaml_path,
|
||||
dry_run=dry_run,
|
||||
enable_migration=enable_migration,
|
||||
)
|
||||
|
||||
# 輸出報告
|
||||
print("=" * 60)
|
||||
print(report.summary())
|
||||
print("=" * 60)
|
||||
|
||||
if report.created_names:
|
||||
action = "待建立" if dry_run else "已建立"
|
||||
print(f"\n{action} ({len(report.created_names)} 條):")
|
||||
for name in report.created_names:
|
||||
print(f" + {name}")
|
||||
|
||||
if report.skipped_names:
|
||||
print(f"\n已跳過(已存在)({len(report.skipped_names)} 條):")
|
||||
for name in report.skipped_names:
|
||||
print(f" ~ {name}")
|
||||
|
||||
if report.errors:
|
||||
print(f"\n[ERROR] 失敗 ({len(report.errors)} 條):", file=sys.stderr)
|
||||
for err in report.errors:
|
||||
print(f" ! {err}", file=sys.stderr)
|
||||
|
||||
if dry_run and report.created > 0:
|
||||
print(f"\n提示: 加 --commit 參數執行實際寫入(將建立 {report.created} 條 DRAFT Playbook)")
|
||||
|
||||
return 1 if report.failed > 0 else 0
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
exit_code = asyncio.run(_run(args))
|
||||
sys.exit(exit_code)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,189 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Re-embed Script: bge-m3:latest 1024 維重新嵌入
|
||||
===============================================
|
||||
遷移 embedding_bge_m3_1024.sql 後執行,重新嵌入:
|
||||
1. rag_chunks(embedding IS NULL 的筆數)
|
||||
2. playbook_embeddings(embedding IS NULL 的筆數)
|
||||
|
||||
用法:
|
||||
cd apps/api
|
||||
python scripts/reembed_bge_m3.py [--dry-run] [--batch 50]
|
||||
|
||||
前置條件:
|
||||
1. embedding_bge_m3_1024.sql 已執行(schema 已升為 vector(1024))
|
||||
2. GCP-A Ollama (34.143.170.20:11434) 可連線且有 bge-m3:latest
|
||||
3. DATABASE_URL 環境變數已設定(或 .env 存在)
|
||||
|
||||
2026-05-04 ogt + Claude Sonnet 4.6: ADR-110 GCP-A Primary Embedding 升級
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 確保 src 在 import 路徑
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
import asyncpg
|
||||
import httpx
|
||||
import structlog
|
||||
|
||||
logging = structlog.get_logger(__name__)
|
||||
|
||||
OLLAMA_URL = os.getenv("OLLAMA_URL", "http://34.143.170.20:11434")
|
||||
EMBEDDING_MODEL = "bge-m3:latest"
|
||||
EXPECTED_DIM = 1024
|
||||
PROJECT_ID = os.getenv("AWOOOP_PROJECT_ID", "awoooi")
|
||||
|
||||
|
||||
async def embed_text(client: httpx.AsyncClient, text: str) -> list[float]:
|
||||
"""呼叫 Ollama bge-m3 嵌入單一文本"""
|
||||
resp = await client.post(
|
||||
f"{OLLAMA_URL}/api/embeddings",
|
||||
json={"model": EMBEDDING_MODEL, "prompt": text},
|
||||
timeout=60.0,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
embedding = resp.json().get("embedding", [])
|
||||
if len(embedding) != EXPECTED_DIM:
|
||||
raise ValueError(f"bge-m3 維度錯誤: got {len(embedding)}, expected {EXPECTED_DIM}")
|
||||
return embedding
|
||||
|
||||
|
||||
async def reembed_rag_chunks(
|
||||
conn: asyncpg.Connection,
|
||||
client: httpx.AsyncClient,
|
||||
batch_size: int,
|
||||
dry_run: bool,
|
||||
) -> int:
|
||||
rows = await conn.fetch(
|
||||
"SELECT id, content FROM rag_chunks WHERE embedding IS NULL ORDER BY id LIMIT $1",
|
||||
batch_size * 10,
|
||||
)
|
||||
if not rows:
|
||||
logging.info("rag_chunks_all_embedded")
|
||||
return 0
|
||||
|
||||
done = 0
|
||||
for row in rows:
|
||||
try:
|
||||
vec = await embed_text(client, row["content"])
|
||||
if not dry_run:
|
||||
vec_str = "[" + ",".join(f"{v:.8f}" for v in vec) + "]"
|
||||
await conn.execute(
|
||||
"UPDATE rag_chunks SET embedding = $1::vector WHERE id = $2",
|
||||
vec_str, row["id"],
|
||||
)
|
||||
done += 1
|
||||
if done % 10 == 0:
|
||||
logging.info("rag_chunks_progress", done=done, total=len(rows))
|
||||
except Exception as e:
|
||||
logging.error("rag_chunk_embed_failed", id=row["id"], error=str(e))
|
||||
|
||||
return done
|
||||
|
||||
|
||||
async def reembed_playbook_embeddings(
|
||||
conn: asyncpg.Connection,
|
||||
client: httpx.AsyncClient,
|
||||
batch_size: int,
|
||||
dry_run: bool,
|
||||
) -> int:
|
||||
# playbook_embeddings 關聯 playbooks 表取原始內容
|
||||
rows = await conn.fetch("""
|
||||
SELECT pe.playbook_id, p.title, p.description, p.steps
|
||||
FROM playbook_embeddings pe
|
||||
JOIN playbooks p ON pe.playbook_id = p.id
|
||||
WHERE pe.embedding IS NULL
|
||||
ORDER BY pe.playbook_id
|
||||
LIMIT $1
|
||||
""", batch_size * 10)
|
||||
|
||||
if not rows:
|
||||
logging.info("playbook_embeddings_all_embedded")
|
||||
return 0
|
||||
|
||||
done = 0
|
||||
for row in rows:
|
||||
text_parts = [row["title"] or "", row["description"] or ""]
|
||||
if row["steps"]:
|
||||
if isinstance(row["steps"], list):
|
||||
text_parts.extend(str(s) for s in row["steps"])
|
||||
else:
|
||||
text_parts.append(str(row["steps"]))
|
||||
text = "\n".join(p for p in text_parts if p)
|
||||
|
||||
try:
|
||||
vec = await embed_text(client, text)
|
||||
if not dry_run:
|
||||
vec_str = "[" + ",".join(f"{v:.8f}" for v in vec) + "]"
|
||||
await conn.execute(
|
||||
"UPDATE playbook_embeddings SET embedding = $1::vector WHERE playbook_id = $2",
|
||||
vec_str, row["playbook_id"],
|
||||
)
|
||||
done += 1
|
||||
if done % 10 == 0:
|
||||
logging.info("playbook_embed_progress", done=done, total=len(rows))
|
||||
except Exception as e:
|
||||
logging.error("playbook_embed_failed", playbook_id=row["playbook_id"], error=str(e))
|
||||
|
||||
return done
|
||||
|
||||
|
||||
async def main(dry_run: bool, batch_size: int) -> None:
|
||||
database_url = os.getenv("DATABASE_URL")
|
||||
if not database_url:
|
||||
# 嘗試讀 .env
|
||||
env_file = Path(__file__).parent.parent / ".env"
|
||||
if env_file.exists():
|
||||
for line in env_file.read_text().splitlines():
|
||||
if line.startswith("DATABASE_URL="):
|
||||
database_url = line.split("=", 1)[1].strip().strip('"\'')
|
||||
break
|
||||
if not database_url:
|
||||
print("❌ DATABASE_URL 未設定,請設定環境變數或 .env 檔案", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if dry_run:
|
||||
print("🔍 DRY RUN 模式 — 不會實際更新 DB")
|
||||
|
||||
async with httpx.AsyncClient() as http_client:
|
||||
# 先驗證 bge-m3 可用且維度正確
|
||||
print(f"🔗 驗證 GCP-A Ollama ({OLLAMA_URL}) bge-m3 連線...")
|
||||
try:
|
||||
test_vec = await embed_text(http_client, "連線測試")
|
||||
print(f"✅ bge-m3 可用,維度 = {len(test_vec)}")
|
||||
except Exception as e:
|
||||
print(f"❌ bge-m3 連線失敗: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
conn = await asyncpg.connect(database_url)
|
||||
try:
|
||||
await conn.execute("SELECT set_config('app.project_id', $1, FALSE)", PROJECT_ID)
|
||||
# 統計待嵌入筆數
|
||||
rag_null = await conn.fetchval("SELECT COUNT(*) FROM rag_chunks WHERE embedding IS NULL")
|
||||
pb_null = await conn.fetchval("SELECT COUNT(*) FROM playbook_embeddings WHERE embedding IS NULL")
|
||||
print(f"📊 待嵌入:rag_chunks={rag_null} 筆,playbook_embeddings={pb_null} 筆")
|
||||
|
||||
if rag_null == 0 and pb_null == 0:
|
||||
print("✅ 所有向量已嵌入,無需重新處理")
|
||||
return
|
||||
|
||||
rag_done = await reembed_rag_chunks(conn, http_client, batch_size, dry_run)
|
||||
pb_done = await reembed_playbook_embeddings(conn, http_client, batch_size, dry_run)
|
||||
|
||||
print(f"{'[DRY RUN] ' if dry_run else ''}✅ 完成: rag_chunks={rag_done}, playbook_embeddings={pb_done}")
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Re-embed script for bge-m3 1024 維遷移")
|
||||
parser.add_argument("--dry-run", action="store_true", help="只統計,不寫 DB")
|
||||
parser.add_argument("--batch", type=int, default=50, help="每批次處理筆數")
|
||||
args = parser.parse_args()
|
||||
asyncio.run(main(dry_run=args.dry_run, batch_size=args.batch))
|
||||
@@ -15,7 +15,7 @@ from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
|
||||
# 2026-04-22 ogt: 移除硬碼 changeme,改為讀取環境變數(強制要求設定)。
|
||||
# 執行前: 從 secret manager / operator vault 設定 DATABASE_URL,禁止在指令或檔案中寫入 URL。
|
||||
# 執行前: export DATABASE_URL="postgresql+asyncpg://awoooi:<password>@192.168.0.188:5432/awoooi_prod"
|
||||
DATABASE_URL = os.environ["DATABASE_URL"]
|
||||
|
||||
MIGRATION_SQLS = [
|
||||
|
||||
@@ -28,7 +28,7 @@ except ImportError:
|
||||
# ============================================================================
|
||||
|
||||
NVIDIA_API_KEY = os.getenv("NVIDIA_API_KEY")
|
||||
OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://192.168.0.110:11435")
|
||||
OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://192.168.0.188:11434")
|
||||
|
||||
if not NVIDIA_API_KEY:
|
||||
print("❌ 請設定 NVIDIA_API_KEY 環境變數")
|
||||
|
||||
@@ -20,9 +20,7 @@ ADR-082: Phase 2 多 Agent 協作
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
import os
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
@@ -37,7 +35,6 @@ from src.agents.protocol import (
|
||||
CriticReport,
|
||||
DiagnosisReport,
|
||||
)
|
||||
from src.observability.agent_step_metrics import observe_agent_step
|
||||
from src.services.sanitization_service import sanitize
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
@@ -45,19 +42,6 @@ logger = structlog.get_logger(__name__)
|
||||
# Critic 挑戰數量上限(防止 LLM 生成無限質疑)
|
||||
MAX_CHALLENGES = 5
|
||||
|
||||
# 2026-04-27 Claude Sonnet 4.6: A1 — 三段 timeout 拆分 + step metric (北極星 §1.2 Observable by Default)
|
||||
# 背景:INC-20260425-8D17BB / 3B6C39 兩則告警 AI 信心降到 20%
|
||||
# OpenClaw NIM (192.168.0.188:8088) 實測 2-27s,原共用 PHASE2_STEP_TIMEOUT_SEC=20.0
|
||||
# Critic 只做批判性審查(prompt 最短、輸出最簡),分配最小 timeout=15s 以保留全局預算給 Diagnostician/Solver
|
||||
# env override:部署時可透過 K8s ConfigMap 動態調整,無需重新 build image
|
||||
AGENT_CRITIC_TIMEOUT_SEC: float = float(
|
||||
os.environ.get("AGENT_CRITIC_TIMEOUT_SEC", "15.0")
|
||||
)
|
||||
|
||||
# 保留相容 alias,標記棄用
|
||||
# DEPRECATED (2026-04-27): 使用 AGENT_CRITIC_TIMEOUT_SEC,此 alias 將在下一個 Sprint 移除
|
||||
PHASE2_STEP_TIMEOUT_SEC = AGENT_CRITIC_TIMEOUT_SEC
|
||||
|
||||
|
||||
class CriticAgent(BaseAgent):
|
||||
"""
|
||||
@@ -125,37 +109,9 @@ class CriticAgent(BaseAgent):
|
||||
"confidence": top_hypothesis.confidence if top_hypothesis else 0.0,
|
||||
})
|
||||
|
||||
_critic_signal = (
|
||||
f"hypothesis={top_hypothesis.description[:300] if top_hypothesis else 'none'}; "
|
||||
f"action={top_candidate.action[:300] if top_candidate else 'none'}"
|
||||
)
|
||||
alert_context = {
|
||||
"incident_id": diagnosis.evidence_snapshot_id or "UNKNOWN",
|
||||
"severity": "P3",
|
||||
"signals": [{"alert_name": "critic_review", "description": _critic_signal}],
|
||||
"affected_services": [],
|
||||
"intent_hint": "diagnose",
|
||||
}
|
||||
|
||||
from src.services.openclaw import get_openclaw
|
||||
openclaw = get_openclaw()
|
||||
_step_start = time.monotonic()
|
||||
try:
|
||||
response_text, _provider, success = await asyncio.wait_for(
|
||||
openclaw.call(prompt, alert_context=alert_context),
|
||||
timeout=AGENT_CRITIC_TIMEOUT_SEC,
|
||||
)
|
||||
# 2026-04-27 Claude Sonnet 4.6: A1 — success path metric observe
|
||||
observe_agent_step("critic", "success", time.monotonic() - _step_start)
|
||||
except asyncio.TimeoutError:
|
||||
# 2026-04-27 Claude Sonnet 4.6: A1 — timeout path metric observe
|
||||
observe_agent_step("critic", "timeout", time.monotonic() - _step_start)
|
||||
logger.warning(
|
||||
"critic_step_timeout",
|
||||
snapshot_id=diagnosis.evidence_snapshot_id,
|
||||
timeout_sec=AGENT_CRITIC_TIMEOUT_SEC,
|
||||
)
|
||||
return self._degraded_report(0, "step_timeout")
|
||||
response_text, _provider, success = await openclaw.call(prompt)
|
||||
|
||||
if not success or not response_text:
|
||||
return self._degraded_report(0, "llm_failed")
|
||||
|
||||
@@ -18,10 +18,8 @@ ADR-082: Phase 2 多 Agent 協作
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
@@ -34,7 +32,6 @@ from src.agents.protocol import (
|
||||
DiagnosisReport,
|
||||
Hypothesis,
|
||||
)
|
||||
from src.observability.agent_step_metrics import observe_agent_step
|
||||
from src.services.sanitization_service import sanitize
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -48,22 +45,6 @@ MAX_EVIDENCE_CHAIN = 5
|
||||
# Confidence 閾值 — 低於此值 vote = ABSTAIN
|
||||
ABSTAIN_CONFIDENCE_THRESHOLD = 0.4
|
||||
|
||||
# 2026-04-27 Claude Sonnet 4.6: A1 — 三段 timeout 拆分 + step metric (北極星 §1.2 Observable by Default)
|
||||
# 背景:INC-20260425-8D17BB / 3B6C39 兩則告警 AI 信心降到 20%
|
||||
# OpenClaw NIM (192.168.0.188:8088) 實測 2-27s,原共用 PHASE2_STEP_TIMEOUT_SEC=20.0
|
||||
# Diagnostician 是 NIM 主吃口(最大 prompt + 多假設輸出),因此分配最高 timeout=30s
|
||||
# Solver=20s(prompt 較小),Critic=15s(只做批判,輸出最短)
|
||||
# env override:部署時可透過 K8s ConfigMap 動態調整,無需重新 build image
|
||||
#
|
||||
# 相容 alias(2026-04-27):PHASE2_STEP_TIMEOUT_SEC 保留供外部 import 讀取(已棄用)
|
||||
AGENT_DIAGNOSTICIAN_TIMEOUT_SEC: float = float(
|
||||
os.environ.get("AGENT_DIAGNOSTICIAN_TIMEOUT_SEC", "30.0")
|
||||
)
|
||||
|
||||
# 保留相容 alias,標記棄用
|
||||
# DEPRECATED (2026-04-27): 使用 AGENT_DIAGNOSTICIAN_TIMEOUT_SEC,此 alias 將在下一個 Sprint 移除
|
||||
PHASE2_STEP_TIMEOUT_SEC = AGENT_DIAGNOSTICIAN_TIMEOUT_SEC
|
||||
|
||||
|
||||
class DiagnosticianAgent(BaseAgent):
|
||||
"""
|
||||
@@ -131,28 +112,11 @@ class DiagnosticianAgent(BaseAgent):
|
||||
"severity": "P3",
|
||||
"signals": [{"alert_name": "evidence_snapshot", "description": _evidence}],
|
||||
"affected_services": [],
|
||||
"intent_hint": "diagnose",
|
||||
}
|
||||
|
||||
from src.services.openclaw import get_openclaw
|
||||
openclaw = get_openclaw()
|
||||
_step_start = time.monotonic()
|
||||
try:
|
||||
response_text, _provider, success = await asyncio.wait_for(
|
||||
openclaw.call(prompt, alert_context=alert_context),
|
||||
timeout=AGENT_DIAGNOSTICIAN_TIMEOUT_SEC,
|
||||
)
|
||||
# 2026-04-27 Claude Sonnet 4.6: A1 — success path metric observe
|
||||
observe_agent_step("diagnostician", "success", time.monotonic() - _step_start)
|
||||
except asyncio.TimeoutError:
|
||||
# 2026-04-27 Claude Sonnet 4.6: A1 — timeout path metric observe
|
||||
observe_agent_step("diagnostician", "timeout", time.monotonic() - _step_start)
|
||||
logger.warning(
|
||||
"diagnostician_step_timeout",
|
||||
snapshot_id=snapshot.snapshot_id,
|
||||
timeout_sec=AGENT_DIAGNOSTICIAN_TIMEOUT_SEC,
|
||||
)
|
||||
return self._degraded_report(snapshot, 0, reason="step_timeout")
|
||||
response_text, _provider, success = await openclaw.call(prompt, alert_context=alert_context)
|
||||
|
||||
if not success or not response_text:
|
||||
return self._degraded_report(snapshot, 0, reason="llm_failed")
|
||||
@@ -227,13 +191,12 @@ Phase 4 動態異常偵測(AI 主動巡檢結果,可作為高信心佐證)
|
||||
latency_ms: int,
|
||||
reason: str = "unknown",
|
||||
) -> DiagnosisReport:
|
||||
"""熔斷降級:只保留已知告警事實,不把 Docker/host memory 誤寫成 K8s OOM。"""
|
||||
"""熔斷降級:rule-based mock(用 alert_category 作簡單假設)"""
|
||||
category = _guess_category_from_snapshot(snapshot)
|
||||
description = _build_degraded_description(snapshot, reason, category)
|
||||
return DiagnosisReport(
|
||||
hypotheses=[
|
||||
Hypothesis(
|
||||
description=description,
|
||||
description=f"[降級] 無法完成 LLM 分析(原因: {reason})。基於告警類別推測: {category}",
|
||||
confidence=0.2,
|
||||
evidence_chain=[],
|
||||
category=category,
|
||||
@@ -301,48 +264,11 @@ def _extract_hypotheses(parsed: dict[str, Any]) -> list[Hypothesis]:
|
||||
return hypotheses
|
||||
|
||||
|
||||
def _build_degraded_description(
|
||||
snapshot: "EvidenceSnapshot",
|
||||
reason: str,
|
||||
category: str,
|
||||
) -> str:
|
||||
"""組裝降級診斷文案,明確標示這不是 LLM 根因判定。"""
|
||||
alert_name, labels = _alert_identity(snapshot)
|
||||
parts = [f"[降級] 無法完成 LLM 分析(原因: {reason})"]
|
||||
if alert_name:
|
||||
parts.append(f"保留原始告警: {alert_name}")
|
||||
target = _first_label(labels, "container_name", "name", "pod", "resource", "service")
|
||||
host = _first_label(labels, "host", "exported_host", "instance")
|
||||
if target:
|
||||
parts.append(f"target={target}")
|
||||
if host:
|
||||
parts.append(f"host={host}")
|
||||
parts.append(f"降級分類: {category}")
|
||||
return ";".join(parts)
|
||||
|
||||
|
||||
def _guess_category_from_snapshot(snapshot: "EvidenceSnapshot") -> str:
|
||||
"""降級時從 snapshot 推導保守分類,優先保留原始 alertname。"""
|
||||
alert_name, labels = _alert_identity(snapshot)
|
||||
if alert_name:
|
||||
return alert_name
|
||||
|
||||
"""降級時從 snapshot 猜測告警類別(最粗粒度兜底)。"""
|
||||
summary = (snapshot.evidence_summary or "").lower()
|
||||
layer = str(labels.get("layer") or "").lower()
|
||||
job = str(labels.get("job") or "").lower()
|
||||
has_container = bool(_first_label(labels, "container_name", "container", "name"))
|
||||
has_k8s_pod = bool(_first_label(labels, "pod")) or "k8s" in summary or "kubernetes" in summary
|
||||
|
||||
has_memory_signal = _contains_memory_signal(summary)
|
||||
|
||||
if has_memory_signal and (
|
||||
layer == "docker" or "cadvisor" in job or has_container
|
||||
):
|
||||
return "DockerContainerMemoryPressure"
|
||||
if "oom" in summary and has_k8s_pod:
|
||||
if "oom" in summary or "memory" in summary:
|
||||
return "KubePodOOM"
|
||||
if has_memory_signal:
|
||||
return "MemoryPressure"
|
||||
if "crashloop" in summary:
|
||||
return "KubePodCrashLoop"
|
||||
if "disk" in summary:
|
||||
@@ -354,56 +280,6 @@ def _guess_category_from_snapshot(snapshot: "EvidenceSnapshot") -> str:
|
||||
return "Unknown"
|
||||
|
||||
|
||||
def _alert_identity(snapshot: "EvidenceSnapshot") -> tuple[str, dict[str, Any]]:
|
||||
"""Extract alertname and labels from structured alert_info when available."""
|
||||
info = getattr(snapshot, "alert_info", None) or {}
|
||||
labels = info.get("labels") if isinstance(info, dict) else {}
|
||||
if not isinstance(labels, dict):
|
||||
labels = {}
|
||||
|
||||
alert_name = ""
|
||||
if isinstance(info, dict):
|
||||
alert_name = str(info.get("alert_name") or "").strip()
|
||||
if not alert_name:
|
||||
alert_name = str(labels.get("alertname") or "").strip()
|
||||
if not alert_name:
|
||||
alert_name = _extract_alertname_from_summary(getattr(snapshot, "evidence_summary", "") or "")
|
||||
return alert_name, labels
|
||||
|
||||
|
||||
def _contains_memory_signal(summary: str) -> bool:
|
||||
return any(term in summary for term in ("memory", "mem", "記憶體", "內存"))
|
||||
|
||||
|
||||
def _extract_alertname_from_summary(summary: str) -> str:
|
||||
"""Best-effort parse for older snapshots whose structured alert_info is absent."""
|
||||
marker = "'alert_name': '"
|
||||
if marker in summary:
|
||||
after = summary.split(marker, 1)[1]
|
||||
return after.split("'", 1)[0].strip()
|
||||
marker = '"alert_name": "'
|
||||
if marker in summary:
|
||||
after = summary.split(marker, 1)[1]
|
||||
return after.split('"', 1)[0].strip()
|
||||
marker = "'alertname': '"
|
||||
if marker in summary:
|
||||
after = summary.split(marker, 1)[1]
|
||||
return after.split("'", 1)[0].strip()
|
||||
marker = '"alertname": "'
|
||||
if marker in summary:
|
||||
after = summary.split(marker, 1)[1]
|
||||
return after.split('"', 1)[0].strip()
|
||||
return ""
|
||||
|
||||
|
||||
def _first_label(labels: dict[str, Any], *keys: str) -> str:
|
||||
for key in keys:
|
||||
value = labels.get(key)
|
||||
if value:
|
||||
return str(value).strip()
|
||||
return ""
|
||||
|
||||
|
||||
def compute_input_hash(snapshot: "EvidenceSnapshot") -> str:
|
||||
"""計算 Diagnostician 輸入的 fingerprint(用於 AgentSession input_hash)。"""
|
||||
key = (snapshot.snapshot_id or "") + (snapshot.evidence_summary or "")[:100]
|
||||
|
||||
@@ -11,24 +11,13 @@ AWOOOI AIOps Phase 2 — 多 Agent 協作訊息協定
|
||||
|
||||
ADR-082: 多 Agent 協作架構(Phase 2)
|
||||
2026-04-15 ogt + Claude Sonnet 4.6(亞太): Phase 2 初始建立
|
||||
2026-04-27 Claude Sonnet 4.6: B1 — 新增 RecommendedAction schema(北極星 §1.1 修復多樣性 ≥ 40%)
|
||||
2026-04-27 Claude Sonnet 4.6: H1+B1 Fix Round — ActionPlan.recommended_actions_status enum(可觀測性)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Any, Literal
|
||||
|
||||
# 2026-04-27 Claude Sonnet 4.6: H1+B1 Fix Round — recommended_actions_status 型別別名
|
||||
# 方便 solver_agent.py 使用;Literal 比 Enum 輕量且不需要額外 import
|
||||
RecommendedActionsStatus = Literal[
|
||||
"ok", # LLM 推出 ≥ 1 個通過 registry + validator 的 action
|
||||
"empty", # LLM 推 0 個 recommended_actions
|
||||
"schema_failed", # LLM 推但全被 schema / registry 驗證 reject
|
||||
"registry_unavailable",# registry 載入失敗({})
|
||||
]
|
||||
from typing import Any
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -113,34 +102,6 @@ class CandidateAction:
|
||||
rationale: str = "" # 為什麼選此方案
|
||||
|
||||
|
||||
# 2026-04-27 Claude Sonnet 4.6: B1 — Solver 結構化動作 (北極星 §1.1 修復多樣性 ≥ 40%)
|
||||
# RecommendedAction 是 ActionPlan.recommended_actions 的元素,供 B3 Telegram 按鈕動態生成用。
|
||||
# 與 CandidateAction(kubectl 命令字串)不同:RecommendedAction 指向 MCP tool(可被 B2 allowlist 審核)。
|
||||
@dataclass
|
||||
class RecommendedAction:
|
||||
"""
|
||||
結構化推薦修復動作(B1 新增,供 Telegram 按鈕動態生成)
|
||||
|
||||
與 CandidateAction 的差異:
|
||||
- CandidateAction:kubectl 命令字串(供 Coordinator 判斷)
|
||||
- RecommendedAction:MCP tool 呼叫規格(供 B3 Telegram 按鈕動態渲染)
|
||||
|
||||
mcp_provider 必須在 callback_action_spec.yaml 的 provider 清單內。
|
||||
mcp_tool 必須在 B2 allowlist(待 B2 任務建立)。
|
||||
params 支援模板替換:{labels.xxx} / {incident_id}。
|
||||
"""
|
||||
name: str # action 識別(如 check_pod_logs)
|
||||
label: str # UI 顯示文字(如「查 Pod 日誌」)
|
||||
emoji: str # UI 圖示(如「📋」)
|
||||
mcp_provider: Literal[ # MCP provider 限制在已知清單
|
||||
"k8s", "ssh", "prometheus", "signoz", "database", "internal"
|
||||
]
|
||||
mcp_tool: str # MCP tool 名(必須在 B2 allowlist)
|
||||
params: dict[str, str] # 參數模板(支援 {labels.xxx} / {incident_id})
|
||||
risk: Literal["low", "medium", "high", "critical"] # 風險等級
|
||||
reasoning: str # 為何推薦此動作(讓 critic 能審)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ActionPlan:
|
||||
"""
|
||||
@@ -148,24 +109,12 @@ class ActionPlan:
|
||||
|
||||
對每個根因假設提出 ≥1 個候選方案(含 blast_radius / rollback_cost)。
|
||||
blast_radius > 50 → Reviewer 必須標 `request_revision`。
|
||||
|
||||
2026-04-27 Claude Sonnet 4.6: B1 新增 recommended_actions(結構化動作清單)
|
||||
- recommended_actions 為空 list 代表降級(degraded=True)或 LLM 無法輸出合法動作
|
||||
- Coordinator 舊邏輯只讀 candidates,不受影響
|
||||
2026-04-27 Claude Sonnet 4.6: H1+B1 Fix Round — recommended_actions_status 新增
|
||||
- 可觀測性:B3 Telegram / 監控 dashboard 可讀取此欄位判斷 Solver 品質
|
||||
"""
|
||||
candidates: list[CandidateAction]
|
||||
diagnosis_report: DiagnosisReport
|
||||
latency_ms: int
|
||||
vote: AgentVote = AgentVote.APPROVE
|
||||
degraded: bool = False
|
||||
# 2026-04-27 Claude Sonnet 4.6: B1 — 結構化推薦動作(0-3 個,降級時為 [])
|
||||
recommended_actions: list[RecommendedAction] = field(default_factory=list)
|
||||
# 2026-04-27 Claude Sonnet 4.6: H1+B1 Fix Round — recommended_actions 提取結果狀態
|
||||
# ok=正常, empty=LLM 未輸出, schema_failed=全部驗證失敗, registry_unavailable=registry 載入失敗
|
||||
# 欄位加在尾部,default="ok",不破壞既有 callsite
|
||||
recommended_actions_status: RecommendedActionsStatus = "ok"
|
||||
|
||||
@property
|
||||
def top_candidate(self) -> CandidateAction | None:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,513 +0,0 @@
|
||||
"""
|
||||
AI Governance REST API — /governance 頁面後端
|
||||
============================================
|
||||
PR 1:3 個 GET endpoint,供前端 /governance 頁面使用。
|
||||
|
||||
Endpoints:
|
||||
GET /api/v1/ai/governance/events — ai_governance_events 查詢(分頁 + 多維度過濾)
|
||||
GET /api/v1/ai/governance/queue — remediation dispatch 隊列(graceful fallback)
|
||||
GET /api/v1/ai/governance/summary — 30d SLO 違反時序 + compliance_rate
|
||||
|
||||
設計原則:
|
||||
- Router 層只負責 HTTP 路由,業務邏輯/DB 查詢在 governance_query_service
|
||||
- Pydantic V2 response models(src/models/governance.py)
|
||||
- queue endpoint 在 dispatch 表尚未建立時回 table_pending=True,不拋 500
|
||||
|
||||
2026-05-02 ogt + Claude Sonnet 4.6 Asia/Taipei
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Annotated
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
|
||||
from src.models.governance import (
|
||||
GovernanceEventsResponse,
|
||||
GovernanceQueueResponse,
|
||||
GovernanceSummaryResponse,
|
||||
KnowledgeReviewDraftArchiveRequest,
|
||||
KnowledgeReviewDraftArchiveResponse,
|
||||
KnowledgeReviewDraftDedupeResponse,
|
||||
KnowledgeStaleCandidatesResponse,
|
||||
KnowledgeStaleOwnerReviewBatchQueueRequest,
|
||||
KnowledgeStaleOwnerReviewBatchQueueResponse,
|
||||
KnowledgeStaleOwnerReviewBurnDownResponse,
|
||||
KnowledgeStaleOwnerReviewCompleteRequest,
|
||||
KnowledgeStaleOwnerReviewCompleteResponse,
|
||||
KnowledgeStaleOwnerReviewCompletionBatchPreviewRequest,
|
||||
KnowledgeStaleOwnerReviewCompletionBatchPreviewResponse,
|
||||
KnowledgeStaleOwnerReviewCompletionQueueResponse,
|
||||
KnowledgeStaleOwnerReviewInboxResponse,
|
||||
KnowledgeStaleOwnerReviewRequest,
|
||||
KnowledgeStaleOwnerReviewResponse,
|
||||
)
|
||||
from src.services.governance_km_review_service import (
|
||||
KmReviewDraftArchiveError,
|
||||
archive_km_review_draft_duplicates,
|
||||
)
|
||||
from src.services.governance_km_stale_review_service import (
|
||||
KmStaleOwnerReviewError,
|
||||
batch_queue_km_stale_owner_reviews,
|
||||
complete_km_stale_owner_review,
|
||||
preview_km_stale_owner_review_completion_batch,
|
||||
query_km_stale_owner_review_burndown,
|
||||
query_km_stale_owner_review_completion_queue,
|
||||
query_km_stale_owner_review_inbox,
|
||||
queue_km_stale_owner_review,
|
||||
)
|
||||
from src.services.governance_query_service import (
|
||||
query_governance_events,
|
||||
query_governance_queue,
|
||||
query_governance_summary,
|
||||
query_km_review_draft_dedupe,
|
||||
query_km_stale_candidates,
|
||||
)
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# GET /api/v1/ai/governance/events
|
||||
# =============================================================================
|
||||
|
||||
@router.get("/ai/governance/events", response_model=GovernanceEventsResponse)
|
||||
async def get_governance_events(
|
||||
event_id: Annotated[list[str] | None, Query(alias="event_id")] = None,
|
||||
event_type: Annotated[list[str] | None, Query(alias="event_type")] = None,
|
||||
from_: Annotated[datetime | None, Query(alias="from")] = None,
|
||||
to: Annotated[datetime | None, Query(alias="to")] = None,
|
||||
status: Annotated[str | None, Query(pattern="^(resolved|unresolved)$")] = None,
|
||||
severity: Annotated[str | None, Query(pattern="^(critical|warning|info)$")] = None,
|
||||
page: Annotated[int, Query(ge=1)] = 1,
|
||||
size: Annotated[int, Query(ge=10, le=100)] = 20,
|
||||
) -> GovernanceEventsResponse:
|
||||
"""
|
||||
查詢 AI 治理事件列表(分頁)。
|
||||
|
||||
- event_type: 多值過濾(可重複傳)
|
||||
- event_id: 多值精準過濾(可重複傳),供 Telegram 詳情 / 歷史與 Work Items 錨點回看
|
||||
- from / to: ISO 8601 時間範圍(URL 傳 from 參數)
|
||||
- status: resolved / unresolved
|
||||
- severity: critical / warning / info(由 event_type 映射決定)
|
||||
- page: ≥1,default 1
|
||||
- size: 10-100,default 20
|
||||
"""
|
||||
logger.debug(
|
||||
"governance_events_request",
|
||||
event_ids=event_id,
|
||||
event_types=event_type,
|
||||
from_=from_,
|
||||
to=to,
|
||||
status=status,
|
||||
severity=severity,
|
||||
page=page,
|
||||
size=size,
|
||||
)
|
||||
return await query_governance_events(
|
||||
event_ids=event_id,
|
||||
event_types=event_type,
|
||||
from_dt=from_,
|
||||
to_dt=to,
|
||||
status=status,
|
||||
severity=severity,
|
||||
page=page,
|
||||
size=size,
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# GET /api/v1/ai/governance/queue
|
||||
# =============================================================================
|
||||
|
||||
@router.get("/ai/governance/queue", response_model=GovernanceQueueResponse)
|
||||
async def get_governance_queue(
|
||||
dispatch_status: Annotated[
|
||||
str,
|
||||
Query(pattern="^(all|pending|dispatched|executing|succeeded|failed|skipped|cancelled)$"),
|
||||
] = "pending",
|
||||
event_type: Annotated[list[str] | None, Query(alias="event_type")] = None,
|
||||
page: Annotated[int, Query(ge=1)] = 1,
|
||||
size: Annotated[int, Query(ge=10, le=100)] = 20,
|
||||
) -> GovernanceQueueResponse:
|
||||
"""
|
||||
查詢 remediation dispatch 隊列。
|
||||
|
||||
governance_remediation_dispatch 表由 Track D 建立,尚未完成時
|
||||
本 endpoint 回傳 { table_pending: true, items: [], total: 0 },不拋 500。
|
||||
|
||||
- dispatch_status: pending(default)/ dispatched / executing / succeeded / failed / skipped / cancelled / all
|
||||
- event_type: 多值過濾(可重複傳)
|
||||
- page / size: 分頁
|
||||
"""
|
||||
logger.debug(
|
||||
"governance_queue_request",
|
||||
dispatch_status=dispatch_status,
|
||||
event_type=event_type,
|
||||
page=page,
|
||||
size=size,
|
||||
)
|
||||
return await query_governance_queue(
|
||||
dispatch_status=dispatch_status,
|
||||
event_types=event_type,
|
||||
page=page,
|
||||
size=size,
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# GET /api/v1/ai/governance/km-review-drafts/dedupe
|
||||
# =============================================================================
|
||||
|
||||
@router.get(
|
||||
"/ai/governance/km-review-drafts/dedupe",
|
||||
response_model=KnowledgeReviewDraftDedupeResponse,
|
||||
)
|
||||
async def get_km_review_draft_dedupe(
|
||||
limit: Annotated[int, Query(ge=10, le=200)] = 100,
|
||||
) -> KnowledgeReviewDraftDedupeResponse:
|
||||
"""
|
||||
查詢 Hermes KM healthcheck review drafts 的去重 read model。
|
||||
|
||||
這是 read-only owner review surface:只回傳 canonical / duplicate /
|
||||
owner_action,不自動 archive、不自動 approve/publish KM。
|
||||
"""
|
||||
logger.debug("km_review_draft_dedupe_request", limit=limit)
|
||||
return await query_km_review_draft_dedupe(limit=limit)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# POST /api/v1/ai/governance/km-review-drafts/dedupe/{event_id}/archive-duplicates
|
||||
# =============================================================================
|
||||
|
||||
@router.post(
|
||||
"/ai/governance/km-review-drafts/dedupe/{governance_event_id}/archive-duplicates",
|
||||
response_model=KnowledgeReviewDraftArchiveResponse,
|
||||
)
|
||||
async def post_km_review_draft_archive_duplicates(
|
||||
governance_event_id: str,
|
||||
request: KnowledgeReviewDraftArchiveRequest,
|
||||
) -> KnowledgeReviewDraftArchiveResponse:
|
||||
"""
|
||||
Owner 審核後封存 Hermes KM healthcheck duplicate review drafts。
|
||||
|
||||
這不是 read endpoint:必須明確傳 owner_approved=true,且後端會重新比對
|
||||
最新 dedupe plan。封存為 KnowledgeEntry.status=archived,不刪除資料。
|
||||
"""
|
||||
logger.info(
|
||||
"km_review_draft_archive_request",
|
||||
governance_event_id=governance_event_id,
|
||||
canonical_entry_id=request.canonical_entry_id,
|
||||
duplicate_count=len(request.duplicate_entry_ids),
|
||||
owner=request.owner,
|
||||
dry_run=request.dry_run,
|
||||
owner_approved=request.owner_approved,
|
||||
)
|
||||
try:
|
||||
return await archive_km_review_draft_duplicates(
|
||||
governance_event_id=governance_event_id,
|
||||
request=request,
|
||||
)
|
||||
except KmReviewDraftArchiveError as exc:
|
||||
raise HTTPException(status_code=exc.status_code, detail=exc.detail) from exc
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# GET /api/v1/ai/governance/km-stale-candidates
|
||||
# =============================================================================
|
||||
|
||||
@router.get(
|
||||
"/ai/governance/km-stale-candidates",
|
||||
response_model=KnowledgeStaleCandidatesResponse,
|
||||
)
|
||||
async def get_km_stale_candidates(
|
||||
project_id: Annotated[str, Query(min_length=1, max_length=64)] = "awoooi",
|
||||
limit: Annotated[int, Query(ge=5, le=100)] = 20,
|
||||
) -> KnowledgeStaleCandidatesResponse:
|
||||
"""
|
||||
查詢 stale KM 的 read-only 優先處理清單。
|
||||
|
||||
Hermes 可以用這個 read model 產生 KM 更新草稿;owner console 則能先看
|
||||
哪些條目有 Incident / Sentry / SigNoz / PlayBook 脈絡,避免只看到總數。
|
||||
"""
|
||||
logger.debug(
|
||||
"km_stale_candidates_request",
|
||||
project_id=project_id,
|
||||
limit=limit,
|
||||
)
|
||||
return await query_km_stale_candidates(project_id=project_id, limit=limit)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# GET /api/v1/ai/governance/km-stale-owner-reviews
|
||||
# =============================================================================
|
||||
|
||||
@router.get(
|
||||
"/ai/governance/km-stale-owner-reviews",
|
||||
response_model=KnowledgeStaleOwnerReviewInboxResponse,
|
||||
)
|
||||
async def get_km_stale_owner_reviews(
|
||||
project_id: Annotated[str, Query(min_length=1, max_length=64)] = "awoooi",
|
||||
dispatch_status: Annotated[
|
||||
str,
|
||||
Query(pattern="^(all|pending|dispatched|executing|succeeded|failed|skipped|cancelled)$"),
|
||||
] = "pending",
|
||||
limit: Annotated[int, Query(ge=5, le=100)] = 20,
|
||||
) -> KnowledgeStaleOwnerReviewInboxResponse:
|
||||
"""
|
||||
查詢 stale KM owner-review 工作台。
|
||||
|
||||
這是 read-only inbox:把 dispatch trail 與 KM priority context 合併,
|
||||
讓 operator 可以依 P0/P1、score、batch 來源與流程階段逐筆 completion。
|
||||
"""
|
||||
logger.debug(
|
||||
"km_stale_owner_reviews_request",
|
||||
project_id=project_id,
|
||||
dispatch_status=dispatch_status,
|
||||
limit=limit,
|
||||
)
|
||||
try:
|
||||
return await query_km_stale_owner_review_inbox(
|
||||
project_id=project_id,
|
||||
dispatch_status=dispatch_status,
|
||||
limit=limit,
|
||||
)
|
||||
except KmStaleOwnerReviewError as exc:
|
||||
raise HTTPException(status_code=exc.status_code, detail=exc.detail) from exc
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# GET /api/v1/ai/governance/km-stale-owner-review-burndown
|
||||
# =============================================================================
|
||||
|
||||
@router.get(
|
||||
"/ai/governance/km-stale-owner-review-burndown",
|
||||
response_model=KnowledgeStaleOwnerReviewBurnDownResponse,
|
||||
)
|
||||
async def get_km_stale_owner_review_burndown(
|
||||
project_id: Annotated[str, Query(min_length=1, max_length=64)] = "awoooi",
|
||||
limit: Annotated[int, Query(ge=1, le=100)] = 20,
|
||||
) -> KnowledgeStaleOwnerReviewBurnDownResponse:
|
||||
"""
|
||||
查詢 stale KM owner-review 完成與 stale ratio burn-down 狀態。
|
||||
|
||||
這是 read-only dashboard:把 pending review、completion audit、recheck
|
||||
snapshot 與距離治理門檻的剩餘筆數放在同一個前端面板。
|
||||
"""
|
||||
logger.debug(
|
||||
"km_stale_owner_review_burndown_request",
|
||||
project_id=project_id,
|
||||
limit=limit,
|
||||
)
|
||||
return await query_km_stale_owner_review_burndown(
|
||||
project_id=project_id,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# GET /api/v1/ai/governance/km-stale-owner-review-completion-queue
|
||||
# =============================================================================
|
||||
|
||||
@router.get(
|
||||
"/ai/governance/km-stale-owner-review-completion-queue",
|
||||
response_model=KnowledgeStaleOwnerReviewCompletionQueueResponse,
|
||||
)
|
||||
async def get_km_stale_owner_review_completion_queue(
|
||||
project_id: Annotated[str, Query(min_length=1, max_length=64)] = "awoooi",
|
||||
status_bucket: Annotated[
|
||||
str,
|
||||
Query(pattern="^(all|ready|blocked|completed|failed|pending)$"),
|
||||
] = "all",
|
||||
priority_tier: Annotated[list[str] | None, Query(alias="priority_tier")] = None,
|
||||
recommended_completion_outcome: Annotated[
|
||||
str,
|
||||
Query(pattern="^(all|refresh_with_evidence|archive|supersede)$"),
|
||||
] = "all",
|
||||
batch_governance_event_id: Annotated[str | None, Query(max_length=120)] = None,
|
||||
can_preview: bool | None = None,
|
||||
limit: Annotated[int, Query(ge=1, le=100)] = 20,
|
||||
) -> KnowledgeStaleOwnerReviewCompletionQueueResponse:
|
||||
"""
|
||||
查詢 stale KM owner-review completion 分流。
|
||||
|
||||
這是 read-only queue:把 active / completed / failed dispatch 拆成
|
||||
ready、blocked、completed、failed,讓前端呈現下一步卡點;打開頁面不寫 KM。
|
||||
"""
|
||||
logger.debug(
|
||||
"km_stale_owner_review_completion_queue_request",
|
||||
project_id=project_id,
|
||||
status_bucket=status_bucket,
|
||||
priority_tiers=priority_tier,
|
||||
recommended_completion_outcome=recommended_completion_outcome,
|
||||
batch_governance_event_id=batch_governance_event_id,
|
||||
can_preview=can_preview,
|
||||
limit=limit,
|
||||
)
|
||||
try:
|
||||
return await query_km_stale_owner_review_completion_queue(
|
||||
project_id=project_id,
|
||||
status_bucket=status_bucket,
|
||||
priority_tiers=priority_tier,
|
||||
recommended_completion_outcome=recommended_completion_outcome,
|
||||
batch_governance_event_id=batch_governance_event_id,
|
||||
can_preview=can_preview,
|
||||
limit=limit,
|
||||
)
|
||||
except KmStaleOwnerReviewError as exc:
|
||||
raise HTTPException(status_code=exc.status_code, detail=exc.detail) from exc
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# POST /api/v1/ai/governance/km-stale-owner-review-completion-queue/batch-preview
|
||||
# =============================================================================
|
||||
|
||||
@router.post(
|
||||
"/ai/governance/km-stale-owner-review-completion-queue/batch-preview",
|
||||
response_model=KnowledgeStaleOwnerReviewCompletionBatchPreviewResponse,
|
||||
)
|
||||
async def post_km_stale_owner_review_completion_batch_preview(
|
||||
request: KnowledgeStaleOwnerReviewCompletionBatchPreviewRequest,
|
||||
) -> KnowledgeStaleOwnerReviewCompletionBatchPreviewResponse:
|
||||
"""
|
||||
Preview a bounded set of owner-review completion candidates.
|
||||
|
||||
This endpoint is intentionally dry-run only: it does not write KM, does not
|
||||
enqueue a batch executor, and does not create governance audit rows. Each
|
||||
item must still be completed through the single-item dry-run + owner confirm
|
||||
endpoint.
|
||||
"""
|
||||
logger.info(
|
||||
"km_stale_owner_review_completion_batch_preview_request",
|
||||
project_id=request.project_id,
|
||||
status_bucket=request.status_bucket,
|
||||
priority_tiers=request.priority_tiers,
|
||||
recommended_completion_outcome=request.recommended_completion_outcome,
|
||||
batch_governance_event_id=request.batch_governance_event_id,
|
||||
limit=request.limit,
|
||||
owner=request.owner,
|
||||
)
|
||||
try:
|
||||
return await preview_km_stale_owner_review_completion_batch(request=request)
|
||||
except KmStaleOwnerReviewError as exc:
|
||||
raise HTTPException(status_code=exc.status_code, detail=exc.detail) from exc
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# POST /api/v1/ai/governance/km-stale-candidates/batch-queue-review
|
||||
# =============================================================================
|
||||
|
||||
@router.post(
|
||||
"/ai/governance/km-stale-candidates/batch-queue-review",
|
||||
response_model=KnowledgeStaleOwnerReviewBatchQueueResponse,
|
||||
)
|
||||
async def post_km_stale_candidate_batch_queue_review(
|
||||
request: KnowledgeStaleOwnerReviewBatchQueueRequest,
|
||||
) -> KnowledgeStaleOwnerReviewBatchQueueResponse:
|
||||
"""
|
||||
將 P0/P1 stale KM 批次排入 owner review。
|
||||
|
||||
這個 endpoint 只建立 batch audit 與逐筆 owner-review dispatch,不改寫 KM。
|
||||
真正 refresh / archive / supersede 仍需單筆 dry-run fingerprint + owner approval。
|
||||
"""
|
||||
logger.info(
|
||||
"km_stale_candidate_batch_queue_review_request",
|
||||
project_id=request.project_id,
|
||||
priority_tiers=request.priority_tiers,
|
||||
limit=request.limit,
|
||||
owner=request.owner,
|
||||
dry_run=request.dry_run,
|
||||
)
|
||||
try:
|
||||
return await batch_queue_km_stale_owner_reviews(request=request)
|
||||
except KmStaleOwnerReviewError as exc:
|
||||
raise HTTPException(status_code=exc.status_code, detail=exc.detail) from exc
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# POST /api/v1/ai/governance/km-stale-candidates/{entry_id}/queue-review
|
||||
# =============================================================================
|
||||
|
||||
@router.post(
|
||||
"/ai/governance/km-stale-candidates/{entry_id}/queue-review",
|
||||
response_model=KnowledgeStaleOwnerReviewResponse,
|
||||
)
|
||||
async def post_km_stale_candidate_queue_review(
|
||||
entry_id: str,
|
||||
request: KnowledgeStaleOwnerReviewRequest,
|
||||
) -> KnowledgeStaleOwnerReviewResponse:
|
||||
"""
|
||||
將單筆 stale KM candidate 排入 owner review。
|
||||
|
||||
這個 endpoint 只建立治理事件與 dispatch work item,不修改 KM 內容。
|
||||
實際 refresh / archive / supersede 仍需 owner 在後續流程確認。
|
||||
"""
|
||||
logger.info(
|
||||
"km_stale_candidate_queue_review_request",
|
||||
entry_id=entry_id,
|
||||
owner=request.owner,
|
||||
dry_run=request.dry_run,
|
||||
)
|
||||
try:
|
||||
return await queue_km_stale_owner_review(entry_id=entry_id, request=request)
|
||||
except KmStaleOwnerReviewError as exc:
|
||||
raise HTTPException(status_code=exc.status_code, detail=exc.detail) from exc
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# POST /api/v1/ai/governance/km-stale-candidates/{entry_id}/complete-review
|
||||
# =============================================================================
|
||||
|
||||
@router.post(
|
||||
"/ai/governance/km-stale-candidates/{entry_id}/complete-review",
|
||||
response_model=KnowledgeStaleOwnerReviewCompleteResponse,
|
||||
)
|
||||
async def post_km_stale_candidate_complete_review(
|
||||
entry_id: str,
|
||||
request: KnowledgeStaleOwnerReviewCompleteRequest,
|
||||
) -> KnowledgeStaleOwnerReviewCompleteResponse:
|
||||
"""
|
||||
Owner 審核後完成 stale KM 的 refresh / archive / supersede 流程。
|
||||
|
||||
必須先 dry-run 取得 fingerprint;真正寫入時需 owner_approved=true。
|
||||
後端會寫 KM、terminal audit dispatch 與 stale ratio recheck dispatch。
|
||||
"""
|
||||
logger.info(
|
||||
"km_stale_candidate_complete_review_request",
|
||||
entry_id=entry_id,
|
||||
dispatch_id=request.dispatch_id,
|
||||
owner=request.owner,
|
||||
review_outcome=request.review_outcome,
|
||||
dry_run=request.dry_run,
|
||||
owner_approved=request.owner_approved,
|
||||
)
|
||||
try:
|
||||
return await complete_km_stale_owner_review(
|
||||
entry_id=entry_id,
|
||||
request=request,
|
||||
)
|
||||
except KmStaleOwnerReviewError as exc:
|
||||
raise HTTPException(status_code=exc.status_code, detail=exc.detail) from exc
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# GET /api/v1/ai/governance/summary
|
||||
# =============================================================================
|
||||
|
||||
@router.get("/ai/governance/summary", response_model=GovernanceSummaryResponse)
|
||||
async def get_governance_summary(
|
||||
days: Annotated[int, Query(ge=1, le=90)] = 30,
|
||||
) -> GovernanceSummaryResponse:
|
||||
"""
|
||||
SLO 合規統計摘要(給 /governance SLO tab 使用)。
|
||||
|
||||
- days: 統計天數(1-90,default 30)
|
||||
- compliance_rate: 1 - unresolved_count / total_events(total=0 時回 1.0)
|
||||
- daily_counts: 每日分類計數時序
|
||||
"""
|
||||
logger.debug("governance_summary_request", days=days)
|
||||
return await query_governance_summary(days=days)
|
||||
@@ -18,15 +18,8 @@ Endpoints:
|
||||
from __future__ import annotations
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from pydantic import BaseModel, Field
|
||||
from fastapi import APIRouter, Query
|
||||
|
||||
from src.services.adr100_remediation_service import (
|
||||
RemediationMode,
|
||||
RemediationNotFoundError,
|
||||
get_adr100_remediation_service,
|
||||
)
|
||||
from src.services.adr100_slo_status_service import get_adr100_slo_status_service
|
||||
from src.services.ai_slo_calculator import AiSloCalculator
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
@@ -34,36 +27,9 @@ logger = structlog.get_logger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class RemediationPreviewRequest(BaseModel):
|
||||
"""ADR-100 remediation preview request."""
|
||||
|
||||
work_item_id: str = Field(min_length=1)
|
||||
mode: RemediationMode = "auto"
|
||||
|
||||
|
||||
class RemediationDryRunRequest(BaseModel):
|
||||
"""ADR-100 remediation dry-run request."""
|
||||
|
||||
work_item_id: str = Field(min_length=1)
|
||||
mode: RemediationMode = "auto"
|
||||
|
||||
|
||||
class RemediationApprovalRequest(BaseModel):
|
||||
"""ADR-100 record-only approval request."""
|
||||
|
||||
work_item_id: str = Field(min_length=1)
|
||||
mode: RemediationMode = "approval"
|
||||
|
||||
|
||||
@router.get("/ai/slo")
|
||||
async def get_ai_slo(
|
||||
force_refresh: bool = Query(False, description="忽略快取,強制重算"),
|
||||
project_id: str = Query(
|
||||
"awoooi",
|
||||
min_length=1,
|
||||
max_length=64,
|
||||
description="租戶 / 專案 ID;預設 AWOOOI 產品線",
|
||||
),
|
||||
) -> dict:
|
||||
"""
|
||||
取得 AI 決策品質 SLO 最新結果。
|
||||
@@ -77,91 +43,16 @@ async def get_ai_slo(
|
||||
cache_hit 是否命中快取
|
||||
metrics[] 三大 SLO 指標明細
|
||||
"""
|
||||
normalized_project_id = project_id.strip() or "awoooi"
|
||||
calc = AiSloCalculator(project_id=normalized_project_id)
|
||||
adr100_service = get_adr100_slo_status_service(normalized_project_id)
|
||||
calc = AiSloCalculator()
|
||||
|
||||
if not force_refresh:
|
||||
cached = await calc.get_cached_report()
|
||||
if cached:
|
||||
data = cached.to_dict()
|
||||
data["cache_hit"] = True
|
||||
data["project_id"] = normalized_project_id
|
||||
data["adr100"] = await adr100_service.fetch_report()
|
||||
return data
|
||||
|
||||
report = await calc.run()
|
||||
data = report.to_dict()
|
||||
data["cache_hit"] = False
|
||||
data["project_id"] = normalized_project_id
|
||||
data["adr100"] = await adr100_service.fetch_report()
|
||||
return data
|
||||
|
||||
|
||||
@router.get("/ai/slo/remediation/preview")
|
||||
async def preview_ai_slo_remediation(
|
||||
work_item_id: str = Query(..., min_length=1),
|
||||
mode: RemediationMode = Query("auto"),
|
||||
) -> dict:
|
||||
"""Preview the safe remediation plan for one ADR-100 queue item."""
|
||||
|
||||
try:
|
||||
return await get_adr100_remediation_service().preview(work_item_id, mode)
|
||||
except RemediationNotFoundError as exc:
|
||||
raise HTTPException(status_code=404, detail="remediation_work_item_not_found") from exc
|
||||
|
||||
|
||||
@router.post("/ai/slo/remediation/preview")
|
||||
async def preview_ai_slo_remediation_post(request: RemediationPreviewRequest) -> dict:
|
||||
"""POST variant for clients that prefer JSON bodies."""
|
||||
|
||||
try:
|
||||
return await get_adr100_remediation_service().preview(
|
||||
request.work_item_id,
|
||||
request.mode,
|
||||
)
|
||||
except RemediationNotFoundError as exc:
|
||||
raise HTTPException(status_code=404, detail="remediation_work_item_not_found") from exc
|
||||
|
||||
|
||||
@router.post("/ai/slo/remediation/dry-run")
|
||||
async def dry_run_ai_slo_remediation(request: RemediationDryRunRequest) -> dict:
|
||||
"""Run a read-only ADR-100 remediation dry-run."""
|
||||
|
||||
try:
|
||||
return await get_adr100_remediation_service().dry_run(
|
||||
request.work_item_id,
|
||||
request.mode,
|
||||
)
|
||||
except RemediationNotFoundError as exc:
|
||||
raise HTTPException(status_code=404, detail="remediation_work_item_not_found") from exc
|
||||
|
||||
|
||||
@router.post("/ai/slo/remediation/approval-request")
|
||||
async def create_ai_slo_remediation_approval_request(
|
||||
request: RemediationApprovalRequest,
|
||||
) -> dict:
|
||||
"""Create a record-only approval request for ADR-100 remediation."""
|
||||
|
||||
try:
|
||||
return await get_adr100_remediation_service().create_approval_request(
|
||||
request.work_item_id,
|
||||
request.mode,
|
||||
)
|
||||
except RemediationNotFoundError as exc:
|
||||
raise HTTPException(status_code=404, detail="remediation_work_item_not_found") from exc
|
||||
|
||||
|
||||
@router.get("/ai/slo/remediation/history")
|
||||
async def list_ai_slo_remediation_history(
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
incident_id: str | None = Query(default=None, min_length=1),
|
||||
work_item_id: str | None = Query(default=None, min_length=1),
|
||||
) -> dict:
|
||||
"""List durable ADR-100 remediation dry-run history from alert_operation_log."""
|
||||
|
||||
return await get_adr100_remediation_service().history(
|
||||
limit=limit,
|
||||
incident_id=incident_id,
|
||||
work_item_id=work_item_id,
|
||||
)
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
"""AIOps 全景時序 endpoint — 為 P2.5 frontend 提供完整 incident → learn 鏈路
|
||||
|
||||
GET /api/v1/aiops/timeline
|
||||
回傳每個 Incident 的 6 階段 timeline(alert / diagnose / decide / execute / verify / learn)
|
||||
|
||||
積木化合規:DB 存取在 services/aiops_timeline_service.py,本 router 只做 HTTP 路由。
|
||||
|
||||
# 2026-04-27 Wave8-X3 by Claude — critic B4 timeline endpoint
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Query
|
||||
|
||||
from src.services.aiops_timeline_service import fetch_aiops_timeline
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/aiops/timeline", tags=["AIOps Timeline"])
|
||||
async def get_aiops_timeline(
|
||||
incident_id: str | None = Query(None, description="指定單一 Incident ID"),
|
||||
hours: int = Query(24, ge=1, le=168, description="回溯小時數(1-168)"),
|
||||
severity: str | None = Query(None, description="嚴重度過濾(P0/P1/P2/P3)"),
|
||||
) -> list[dict[str, Any]]:
|
||||
"""回傳 Incident 6 階段全景 timeline。"""
|
||||
return await fetch_aiops_timeline(
|
||||
incident_id=incident_id,
|
||||
hours=hours,
|
||||
severity=severity,
|
||||
)
|
||||
@@ -234,7 +234,6 @@ async def create_approval(
|
||||
title=f"新授權請求建立: {approval.action[:50]}...",
|
||||
risk_level=approval.risk_level.value,
|
||||
approval_id=str(approval.id),
|
||||
incident_id=approval.incident_id,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
@@ -327,7 +326,6 @@ async def sign_approval(
|
||||
actor_role="signer",
|
||||
risk_level=approval.risk_level.value,
|
||||
approval_id=str(approval_id),
|
||||
incident_id=approval.incident_id,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
@@ -356,7 +354,6 @@ async def sign_approval(
|
||||
actor="OpenClaw",
|
||||
actor_role="executor",
|
||||
approval_id=str(approval_id),
|
||||
incident_id=approval.incident_id,
|
||||
)
|
||||
|
||||
execution_svc = get_execution_service()
|
||||
@@ -464,7 +461,6 @@ async def reject_approval(
|
||||
actor=request.rejector_name,
|
||||
actor_role="rejector",
|
||||
approval_id=str(approval_id),
|
||||
incident_id=approval.incident_id,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
@@ -619,7 +615,6 @@ async def bulk_approve(
|
||||
actor_role="signer",
|
||||
risk_level=signed_approval.risk_level.value,
|
||||
approval_id=approval_id_str,
|
||||
incident_id=signed_approval.incident_id,
|
||||
)
|
||||
|
||||
# 如果觸發執行,加入背景任務
|
||||
|
||||
@@ -20,7 +20,6 @@ from pydantic import BaseModel
|
||||
from src.core.config import settings
|
||||
from src.core.logging import get_logger
|
||||
from src.core.sse import EventPublisher, EventType, SSEEvent, get_publisher
|
||||
from src.services.dashboard_metrics_service import fetch_pending_approval_count
|
||||
from src.services.host_aggregator import AggregatedStatus, HostAggregator
|
||||
|
||||
router = APIRouter()
|
||||
@@ -142,14 +141,12 @@ async def dashboard_update_loop(publisher: EventPublisher) -> None:
|
||||
try:
|
||||
# Fetch aggregated status
|
||||
status = await HostAggregator.fetch_all()
|
||||
pending_approvals = await fetch_pending_approval_count()
|
||||
|
||||
# Publish to all connected clients
|
||||
event = SSEEvent(
|
||||
type=EventType.HOST_UPDATE,
|
||||
data={
|
||||
"overall_status": status.overall_status,
|
||||
"pending_approvals": pending_approvals,
|
||||
"hosts": [
|
||||
{
|
||||
"ip": h.ip,
|
||||
@@ -209,9 +206,7 @@ async def get_dashboard() -> DashboardResponse:
|
||||
logger.info("dashboard_fetch")
|
||||
|
||||
status = await HostAggregator.fetch_all()
|
||||
response = aggregated_to_response(status)
|
||||
response.pending_approvals = await fetch_pending_approval_count()
|
||||
return response
|
||||
return aggregated_to_response(status)
|
||||
|
||||
|
||||
@router.get("/dashboard/stream")
|
||||
|
||||
@@ -13,72 +13,27 @@ leWOOOgo 積木化原則:
|
||||
建立者: Claude Code (Phase 25 P2)
|
||||
"""
|
||||
|
||||
from typing import Literal
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from src.core.csrf import CSRFToken # Phase 20: CSRF Protection
|
||||
|
||||
from src.models.drift import (
|
||||
DriftListResponse,
|
||||
DriftReport,
|
||||
DriftScanRequest,
|
||||
DriftScanResponse,
|
||||
DriftStatus,
|
||||
)
|
||||
from src.repositories.drift_repository import get_drift_repository
|
||||
from src.services.drift_adopt_service import get_drift_adopt_service
|
||||
from src.services.drift_analyzer import get_drift_analyzer
|
||||
from src.services.drift_detector import get_drift_detector
|
||||
from src.services.drift_fingerprint_state_service import (
|
||||
DriftFingerprintStateNotFoundError,
|
||||
get_drift_fingerprint_state_service,
|
||||
)
|
||||
from src.services.drift_interpreter import get_drift_interpreter
|
||||
from src.services.drift_remediator import get_drift_remediator
|
||||
from src.utils.timezone import now_taipei
|
||||
|
||||
router = APIRouter(prefix="/drift", tags=["drift"])
|
||||
|
||||
# 2026-04-09 Claude Sonnet 4.6: B4 drift_reports 持久化 — 改用 DB repository
|
||||
|
||||
|
||||
class DriftFingerprintHandoffRequest(BaseModel):
|
||||
"""Record-only handoff request for a stable drift fingerprint."""
|
||||
|
||||
report_id: str | None = Field(default=None, min_length=1)
|
||||
namespace: str | None = Field(default="awoooi-prod", min_length=1)
|
||||
handoff_kind: Literal[
|
||||
"open_pr_review",
|
||||
"manual_investigation",
|
||||
"zero_diff_pr_cleanup",
|
||||
] = "open_pr_review"
|
||||
pr_url: str | None = Field(default=None, min_length=1)
|
||||
note: str | None = Field(default=None, max_length=500)
|
||||
|
||||
|
||||
class DriftFingerprintRemediationRequest(BaseModel):
|
||||
"""Record-only remediation request for a stable drift fingerprint."""
|
||||
|
||||
report_id: str | None = Field(default=None, min_length=1)
|
||||
namespace: str | None = Field(default="awoooi-prod", min_length=1)
|
||||
remediation_kind: Literal[
|
||||
"live_env_rollback",
|
||||
"git_adopted",
|
||||
"git_rollback",
|
||||
"zero_diff_pr_cleanup",
|
||||
"manual_noop",
|
||||
] = "live_env_rollback"
|
||||
remediation_status: Literal[
|
||||
"executed_unverified",
|
||||
"verified_no_drift",
|
||||
"verification_failed",
|
||||
] | None = None
|
||||
verification_report_id: str | None = Field(default=None, min_length=1)
|
||||
note: str | None = Field(default=None, max_length=1000)
|
||||
commands_summary: list[str] = Field(default_factory=list, max_length=12)
|
||||
|
||||
|
||||
@router.post("/scan", response_model=DriftScanResponse, summary="觸發漂移掃描")
|
||||
async def trigger_drift_scan(
|
||||
request: DriftScanRequest,
|
||||
@@ -141,72 +96,6 @@ async def list_drift_reports() -> DriftListResponse:
|
||||
return DriftListResponse(items=items, total=len(items))
|
||||
|
||||
|
||||
@router.get("/fingerprints/state", summary="查詢 Config Drift fingerprint 狀態")
|
||||
async def get_drift_fingerprint_state(
|
||||
report_id: str | None = None,
|
||||
namespace: str | None = "awoooi-prod",
|
||||
) -> dict:
|
||||
"""
|
||||
以 stable fingerprint 聚合漂移狀態。
|
||||
|
||||
此 endpoint 只建立 read model:重複次數、PR 狀態、是否零 diff、
|
||||
人工交接歷史與下一步。它不修改 drift / incident / auto-repair 狀態。
|
||||
"""
|
||||
svc = get_drift_fingerprint_state_service()
|
||||
try:
|
||||
return await svc.get_state(report_id=report_id, namespace=namespace)
|
||||
except DriftFingerprintStateNotFoundError as exc:
|
||||
raise HTTPException(status_code=404, detail="drift_report_not_found") from exc
|
||||
|
||||
|
||||
@router.post("/fingerprints/handoff", summary="記錄 Config Drift fingerprint 交接")
|
||||
async def record_drift_fingerprint_handoff(
|
||||
request: DriftFingerprintHandoffRequest,
|
||||
) -> dict:
|
||||
"""
|
||||
記錄 stable fingerprint 已轉人工 / PR review 的歷史證據。
|
||||
|
||||
安全邊界:只寫 alert_operation_log / timeline_events,不修改 drift 狀態、
|
||||
incident 狀態、自動修復結果,不建立外部 ticket,也不 merge PR。
|
||||
"""
|
||||
svc = get_drift_fingerprint_state_service()
|
||||
try:
|
||||
return await svc.record_handoff(
|
||||
report_id=request.report_id,
|
||||
namespace=request.namespace,
|
||||
handoff_kind=request.handoff_kind,
|
||||
pr_url=request.pr_url,
|
||||
note=request.note,
|
||||
)
|
||||
except DriftFingerprintStateNotFoundError as exc:
|
||||
raise HTTPException(status_code=404, detail="drift_report_not_found") from exc
|
||||
|
||||
|
||||
@router.post("/fingerprints/remediation", summary="記錄 Config Drift fingerprint 修復")
|
||||
async def record_drift_fingerprint_remediation(
|
||||
request: DriftFingerprintRemediationRequest,
|
||||
) -> dict:
|
||||
"""
|
||||
記錄 stable fingerprint 已完成的修復 / 驗證證據。
|
||||
|
||||
安全邊界:只寫 alert_operation_log / timeline_events,不修改 drift 狀態、
|
||||
incident 狀態、自動修復結果,不建立外部 ticket,也不執行 kubectl。
|
||||
"""
|
||||
svc = get_drift_fingerprint_state_service()
|
||||
try:
|
||||
return await svc.record_remediation(
|
||||
report_id=request.report_id,
|
||||
namespace=request.namespace,
|
||||
remediation_kind=request.remediation_kind,
|
||||
remediation_status=request.remediation_status,
|
||||
verification_report_id=request.verification_report_id,
|
||||
note=request.note,
|
||||
commands_summary=request.commands_summary,
|
||||
)
|
||||
except DriftFingerprintStateNotFoundError as exc:
|
||||
raise HTTPException(status_code=404, detail="drift_report_not_found") from exc
|
||||
|
||||
|
||||
@router.post("/reports/{report_id}/rollback", summary="覆蓋回 Git 狀態")
|
||||
async def rollback_drift(report_id: str, _csrf_token: CSRFToken) -> dict: # Phase 20: CSRF Protection (驗證用,不需要使用值)
|
||||
"""
|
||||
@@ -266,17 +155,7 @@ async def internal_scan(background_tasks: BackgroundTasks) -> dict:
|
||||
# =============================================================================
|
||||
|
||||
async def _analyze_and_notify(report: DriftReport) -> None:
|
||||
"""
|
||||
背景:Nemotron 意圖分析 + 低風險自動採納嘗試 + Telegram 推送
|
||||
|
||||
2026-04-24 ogt + Claude Sonnet 4.6: 新增低風險自動採納
|
||||
流程:
|
||||
1. Nemotron 意圖分析(同原先)
|
||||
2. 嘗試 auto_adopt_if_safe():
|
||||
- 通過 → 發 TYPE-1 無按鈕通知(PR 已建立,請 SRE 複核),不再推送帶按鈕卡片
|
||||
- 未通過(skipped=True)→ 走原有 narrator TYPE-4D 卡片流程
|
||||
- 採納失敗(skipped=False, success=False)→ 同樣走 narrator 讓人工介入
|
||||
"""
|
||||
"""背景:Nemotron 意圖分析 + Telegram 推送 + Phase 30 AI 人話摘要"""
|
||||
import structlog as _structlog
|
||||
_logger = _structlog.get_logger(__name__)
|
||||
try:
|
||||
@@ -285,56 +164,6 @@ async def _analyze_and_notify(report: DriftReport) -> None:
|
||||
interpretation = await interpreter.analyze(report)
|
||||
repo = get_drift_repository()
|
||||
await repo.update_interpretation(report.report_id, interpretation)
|
||||
# 2026-05-04 ogt + Claude Sonnet 4.6: 修根因 — report 是 in-memory 物件,
|
||||
# update_interpretation 只更新 DB,不會回寫 report.interpretation,
|
||||
# 導致 auto_adopt_if_safe 永遠看到 None → 觸發「尚無 Nemotron 意圖分析」條件
|
||||
report.interpretation = interpretation
|
||||
|
||||
# 2026-04-24: 嘗試低風險自動採納
|
||||
auto_adopted = False
|
||||
auto_block_reason = ""
|
||||
from src.core.config import get_settings as _gs
|
||||
_drift_auto_enabled = _gs().DRIFT_AUTO_ADOPT_ENABLED
|
||||
# flag=False 視為「停用」,不設 auto_block_reason 避免誤觸 escalation
|
||||
try:
|
||||
if _drift_auto_enabled:
|
||||
adopt_svc = get_drift_adopt_service()
|
||||
auto_result = await adopt_svc.auto_adopt_if_safe(report)
|
||||
if auto_result.get("success"):
|
||||
# 自動採納成功:更新狀態,跳過人工卡片
|
||||
await repo.update_status(
|
||||
report.report_id,
|
||||
DriftStatus.ADOPTED,
|
||||
resolved_at=now_taipei(),
|
||||
)
|
||||
auto_adopted = True
|
||||
_logger.info(
|
||||
"drift_auto_adopted",
|
||||
report_id=report.report_id,
|
||||
pr_url=auto_result.get("pr_url"),
|
||||
)
|
||||
else:
|
||||
auto_block_reason = auto_result.get("reason", "") or "auto adopt skipped"
|
||||
_logger.info(
|
||||
"drift_auto_adopt_skipped",
|
||||
report_id=report.report_id,
|
||||
reason=auto_block_reason,
|
||||
skipped=auto_result.get("skipped", True),
|
||||
)
|
||||
except Exception as e:
|
||||
auto_block_reason = f"auto adopt error: {str(e)[:120]}"
|
||||
_logger.warning("drift_auto_adopt_error", report_id=report.report_id, error=str(e))
|
||||
|
||||
if auto_adopted:
|
||||
# 自動採納成功,Telegram 通知已在 auto_adopt_if_safe 內發出,不再推送按鈕卡片
|
||||
return
|
||||
|
||||
if auto_block_reason:
|
||||
await _escalate_drift_auto_adopt_blocked(
|
||||
report=report,
|
||||
reason=auto_block_reason,
|
||||
interpretation=interpretation,
|
||||
)
|
||||
|
||||
# ADR-075: drift_narrator_service 負責發送 TYPE-4D 卡片(含按鈕)
|
||||
# 舊的 send_text() 已移除,改由 narrate_and_notify() 統一處理
|
||||
@@ -350,25 +179,6 @@ async def _analyze_and_notify(report: DriftReport) -> None:
|
||||
structlog.get_logger(__name__).error("drift_analyze_notify_failed", error=str(e))
|
||||
|
||||
|
||||
async def _escalate_drift_auto_adopt_blocked(
|
||||
*,
|
||||
report: DriftReport,
|
||||
reason: str,
|
||||
interpretation,
|
||||
) -> None:
|
||||
"""Delegate drift emergency escalation to the service layer."""
|
||||
|
||||
from src.services.emergency_escalation_service import (
|
||||
escalate_drift_auto_adopt_blocked,
|
||||
)
|
||||
|
||||
await escalate_drift_auto_adopt_blocked(
|
||||
report=report,
|
||||
reason=reason,
|
||||
interpretation=interpretation,
|
||||
)
|
||||
|
||||
|
||||
async def _run_full_scan(namespaces: list[str]) -> None:
|
||||
"""背景:完整漂移掃描"""
|
||||
detector = get_drift_detector()
|
||||
|
||||
@@ -52,11 +52,6 @@ router = APIRouter(prefix="/webhooks/gitea", tags=["Gitea Webhook"])
|
||||
# OpenClaw 配置 (使用 settings 中的 OPENCLAW_URL)
|
||||
OPENCLAW_URL = settings.OPENCLAW_URL
|
||||
|
||||
# Telegram 通知去重 TTL — 10 分鐘,與 Sentry/SLO Watchdog 對齊
|
||||
# 2026-04-25 ogt + Claude Sonnet 4.6 (Task C: Gitea CI/CD 告警轉發 Telegram)
|
||||
GITEA_TG_DEDUP_TTL = 600 # 秒
|
||||
GITEA_TG_DEDUP_KEY_PREFIX = "gitea:tg:dedup:"
|
||||
|
||||
# =============================================================================
|
||||
# Pydantic Models
|
||||
# =============================================================================
|
||||
@@ -92,9 +87,6 @@ class GiteaPullRequest(BaseModel):
|
||||
additions: int = 0
|
||||
deletions: int = 0
|
||||
changed_files: int = 0
|
||||
# Gitea: HasMerged bool json:"merged" — True 代表 PR 已合併 (action=closed + merged=true)
|
||||
# 2026-04-25 ogt + Claude Sonnet 4.6 (Task C: Gitea CI/CD 告警轉發 Telegram)
|
||||
merged: bool = False
|
||||
|
||||
|
||||
class GiteaCommit(BaseModel):
|
||||
@@ -372,65 +364,6 @@ async def handle_gitea_webhook(
|
||||
) from e
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Telegram 通知 Helper (帶 Redis 去重)
|
||||
# 2026-04-25 ogt + Claude Sonnet 4.6 (Task C: Gitea CI/CD 告警轉發 Telegram)
|
||||
# 設計原則:
|
||||
# - 純通知,不加按鈕(遵循 feedback_no_ghost_buttons.md)
|
||||
# - Redis SET NX EX 600s 去重(同一 repo+event+id 10 分鐘內不重複)
|
||||
# - 不改動 incident 通知鏈路,獨立背景任務
|
||||
# - Telegram token/chat_id 從 settings (K8s Secret 注入) 讀取,不寫死
|
||||
# =============================================================================
|
||||
|
||||
async def _send_gitea_notification(
|
||||
dedup_key: str,
|
||||
message: str,
|
||||
) -> None:
|
||||
"""
|
||||
發送 Gitea 事件 Telegram 通知(帶去重)
|
||||
|
||||
Args:
|
||||
dedup_key: Redis 去重 key(格式: {event}:{repo}:{id},不含 prefix)
|
||||
message: HTML 格式 Telegram 訊息
|
||||
"""
|
||||
try:
|
||||
# 去重檢查:同一 key 在 TTL 內不重複發送
|
||||
# 2026-04-26 critic-B1 hotfix by Claude Opus 4.7 — get_redis() 是同步函數,不可 await
|
||||
# 原 await get_redis() 會 raise TypeError 被外層 except 吞 → Telegram 通知永遠發不出去
|
||||
from src.core.redis_client import get_redis # type: ignore[import]
|
||||
redis = get_redis()
|
||||
full_key = GITEA_TG_DEDUP_KEY_PREFIX + dedup_key
|
||||
acquired = await redis.set(
|
||||
full_key,
|
||||
"1",
|
||||
ex=GITEA_TG_DEDUP_TTL,
|
||||
nx=True, # NX: 只在 key 不存在時設定(原子操作)
|
||||
)
|
||||
if not acquired:
|
||||
logger.debug(
|
||||
"gitea_tg_dedup_skip",
|
||||
dedup_key=dedup_key,
|
||||
ttl=GITEA_TG_DEDUP_TTL,
|
||||
)
|
||||
return
|
||||
|
||||
if not settings.OPENCLAW_TG_BOT_TOKEN:
|
||||
logger.debug("gitea_tg_skipped", reason="Bot token not configured")
|
||||
return
|
||||
|
||||
from src.services.telegram_gateway import (
|
||||
get_telegram_gateway, # type: ignore[import]
|
||||
)
|
||||
gateway = get_telegram_gateway()
|
||||
await gateway.initialize()
|
||||
await gateway.send_alert_notification(message)
|
||||
|
||||
logger.info("gitea_tg_notification_sent", dedup_key=dedup_key)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning("gitea_tg_notification_failed", dedup_key=dedup_key, error=str(e))
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Event Handlers (HTTP 層: 解析、驗證、回應 — 業務邏輯在 Service 層)
|
||||
# =============================================================================
|
||||
@@ -447,7 +380,6 @@ async def handle_pull_request(
|
||||
- opened: 新建 PR
|
||||
- synchronize: 推送新 commit 到 PR
|
||||
- reopened: 重新開啟 PR
|
||||
- closed + merged=True: PR 合併完成 → Telegram 通知 (Task C 2026-04-25)
|
||||
"""
|
||||
pr = payload.pull_request
|
||||
if not pr:
|
||||
@@ -457,40 +389,6 @@ async def handle_pull_request(
|
||||
event_type="pull_request",
|
||||
)
|
||||
|
||||
# PR 合併完成通知 (action=closed + merged=True)
|
||||
# 2026-04-25 ogt + Claude Sonnet 4.6 (Task C: Gitea CI/CD 告警轉發 Telegram)
|
||||
if payload.action == "closed" and pr.merged:
|
||||
repo = payload.repository.full_name
|
||||
author = payload.sender.login
|
||||
pr_url = pr.html_url
|
||||
base_branch = pr.base.get("ref", "main") if isinstance(pr.base, dict) else "main"
|
||||
|
||||
# 格式遵循 feedback_telegram_alert_format.md
|
||||
message = (
|
||||
f"<b>PR Merged</b> | {repo}\n"
|
||||
"──────────────────────\n"
|
||||
f"├─ PR: <a href=\"{pr_url}\">#{pr.number} {pr.title[:60]}</a>\n"
|
||||
f"├─ 作者: @{author}\n"
|
||||
f"├─ 目標分支: {base_branch}\n"
|
||||
f"└─ 變更: +{pr.additions} -{pr.deletions} ({pr.changed_files} 檔)"
|
||||
)
|
||||
|
||||
dedup_key = f"pr_merged:{repo}:{pr.number}"
|
||||
background_tasks.add_task(_send_gitea_notification, dedup_key, message)
|
||||
|
||||
logger.info(
|
||||
"gitea_pr_merged_notification_scheduled",
|
||||
repo=repo,
|
||||
pr_number=pr.number,
|
||||
author=author,
|
||||
)
|
||||
|
||||
return GiteaWebhookResponse(
|
||||
status="accepted",
|
||||
message=f"PR #{pr.number} merge notification scheduled",
|
||||
event_type="pull_request",
|
||||
)
|
||||
|
||||
# 只處理需要審查的 action
|
||||
supported_actions = {"opened", "synchronize", "reopened"}
|
||||
if payload.action not in supported_actions:
|
||||
@@ -504,22 +402,15 @@ async def handle_pull_request(
|
||||
review_id = f"gitea-pr-{payload.repository.id}-{pr.number}-{uuid.uuid4().hex[:8]}"
|
||||
|
||||
# 背景執行審查 (委派給 Service)
|
||||
if settings.MOCK_MODE:
|
||||
logger.info(
|
||||
"gitea_pr_review_background_skipped_mock_mode",
|
||||
review_id=review_id,
|
||||
repo=payload.repository.full_name,
|
||||
)
|
||||
else:
|
||||
service = get_gitea_webhook_service()
|
||||
background_tasks.add_task(
|
||||
service.review_pull_request,
|
||||
repo=payload.repository,
|
||||
pr=pr,
|
||||
sender=payload.sender,
|
||||
review_id=review_id,
|
||||
action=payload.action,
|
||||
)
|
||||
service = get_gitea_webhook_service()
|
||||
background_tasks.add_task(
|
||||
service.review_pull_request,
|
||||
repo=payload.repository,
|
||||
pr=pr,
|
||||
sender=payload.sender,
|
||||
review_id=review_id,
|
||||
action=payload.action,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"gitea_pr_review_scheduled",
|
||||
@@ -570,24 +461,17 @@ async def handle_push(
|
||||
review_id = f"gitea-push-{payload.repository.id}-{payload.after[:8]}-{uuid.uuid4().hex[:8]}"
|
||||
|
||||
# 背景執行審查 (委派給 Service)
|
||||
if settings.MOCK_MODE:
|
||||
logger.info(
|
||||
"gitea_push_review_background_skipped_mock_mode",
|
||||
review_id=review_id,
|
||||
repo=payload.repository.full_name,
|
||||
)
|
||||
else:
|
||||
service = get_gitea_webhook_service()
|
||||
background_tasks.add_task(
|
||||
service.review_push,
|
||||
repo=payload.repository,
|
||||
commits=commits,
|
||||
sender=payload.sender,
|
||||
review_id=review_id,
|
||||
ref=ref,
|
||||
before_sha=payload.before,
|
||||
after_sha=payload.after,
|
||||
)
|
||||
service = get_gitea_webhook_service()
|
||||
background_tasks.add_task(
|
||||
service.review_push,
|
||||
repo=payload.repository,
|
||||
commits=commits,
|
||||
sender=payload.sender,
|
||||
review_id=review_id,
|
||||
ref=ref,
|
||||
before_sha=payload.before,
|
||||
after_sha=payload.after,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"gitea_push_review_scheduled",
|
||||
@@ -614,11 +498,7 @@ async def handle_workflow_run(
|
||||
處理 Gitea Actions workflow_run 事件 — ADR-074 M3
|
||||
|
||||
只處理 status=failure(或 conclusion=failure)的管線失敗。
|
||||
雙路並行:
|
||||
1. 建立 TYPE-1 Incident(既有路徑,保持不變)
|
||||
2. 直接發 Telegram 通知(Task C 2026-04-25 新增)
|
||||
- workflow name 含 deploy → "部署失敗"
|
||||
- 否則 → "構建失敗"
|
||||
建立 TYPE-1 Incident(純通知,不自動修復)。
|
||||
"""
|
||||
wf = payload.workflow_run
|
||||
if not wf:
|
||||
@@ -651,7 +531,6 @@ async def handle_workflow_run(
|
||||
run_url=run_url,
|
||||
)
|
||||
|
||||
# 既有路徑:建立 TYPE-1 Incident (保持不變)
|
||||
async def _create_ci_incident() -> None:
|
||||
try:
|
||||
svc = get_incident_service()
|
||||
@@ -683,71 +562,6 @@ async def handle_workflow_run(
|
||||
|
||||
background_tasks.add_task(_create_ci_incident)
|
||||
|
||||
# 2026-04-27 P3.1-T3 by Claude — CI auto-repair 評估(孤立服務整合)
|
||||
# 與 incident 路徑並行,exception 全隔離不影響主流程
|
||||
async def _evaluate_ci_repair() -> None:
|
||||
try:
|
||||
from src.services.ci_auto_repair import get_ci_auto_repair_service
|
||||
ci_svc = get_ci_auto_repair_service()
|
||||
# 推斷 error_type:workflow name 含 deploy → deploy,否則從 name 推斷
|
||||
wf_lower = wf.name.lower()
|
||||
if "deploy" in wf_lower:
|
||||
error_type = "deploy"
|
||||
elif "test" in wf_lower:
|
||||
error_type = "test"
|
||||
elif "lint" in wf_lower:
|
||||
error_type = "lint"
|
||||
elif "build" in wf_lower:
|
||||
error_type = "build"
|
||||
else:
|
||||
error_type = "unknown"
|
||||
|
||||
decision = await ci_svc.evaluate_repair(
|
||||
error_type=error_type,
|
||||
workflow_name=wf.name,
|
||||
repo=repo,
|
||||
failure_context={
|
||||
"branch": branch,
|
||||
"sha": sha_short,
|
||||
"run_url": run_url,
|
||||
"status": wf.status,
|
||||
"conclusion": wf.conclusion,
|
||||
},
|
||||
)
|
||||
logger.info(
|
||||
"ci_auto_repair_evaluated",
|
||||
repo=repo,
|
||||
workflow=wf.name,
|
||||
error_type=error_type,
|
||||
should_repair=decision.should_repair,
|
||||
execution_decision=decision.execution_decision.value,
|
||||
risk_level=decision.risk_level.value,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("ci_auto_repair_evaluation_failed", repo=repo, workflow=wf.name)
|
||||
|
||||
background_tasks.add_task(_evaluate_ci_repair)
|
||||
|
||||
# 新增路徑:直接 Telegram 通知 (Task C 2026-04-25 ogt + Claude Sonnet 4.6)
|
||||
# workflow name 含 deploy 關鍵字 → 部署失敗;否則 → 構建失敗
|
||||
# 格式遵循 feedback_telegram_alert_format.md:狀態 + 資源 + 連結
|
||||
is_deploy = "deploy" in wf.name.lower()
|
||||
event_label = "Deployment Failed" if is_deploy else "Build Failed"
|
||||
run_link = f" | <a href=\"{run_url}\">查看日誌</a>" if run_url else ""
|
||||
|
||||
tg_message = (
|
||||
f"<b>{event_label}</b> | {repo}\n"
|
||||
"──────────────────────\n"
|
||||
f"├─ Workflow: <code>{wf.name}</code>\n"
|
||||
f"├─ 分支: {branch}\n"
|
||||
f"├─ Commit: <code>{sha_short}</code>\n"
|
||||
f"└─ 狀態: failure{run_link}"
|
||||
)
|
||||
|
||||
# 去重 key:同一 repo + workflow + branch + sha 的失敗,10 分鐘內不重複
|
||||
dedup_key = f"workflow_failure:{repo}:{wf.name}:{branch}:{sha_short}"
|
||||
background_tasks.add_task(_send_gitea_notification, dedup_key, tg_message)
|
||||
|
||||
return GiteaWebhookResponse(
|
||||
status="accepted",
|
||||
message=f"CI pipeline failure for '{wf.name}' on '{branch}' queued as TYPE-1 incident",
|
||||
|
||||
@@ -11,7 +11,7 @@ Endpoints:
|
||||
Components Checked:
|
||||
- PostgreSQL (192.168.0.188:5432)
|
||||
- Redis (192.168.0.188:6380)
|
||||
- Ollama ADR-110 provider pool (GCP-A -> GCP-B -> 111)
|
||||
- Ollama (192.168.0.188:11434)
|
||||
- OpenClaw (192.168.0.188:8089)
|
||||
- SigNoz (192.168.0.188:3301)
|
||||
"""
|
||||
@@ -26,16 +26,9 @@ from pydantic import BaseModel
|
||||
from src.core.config import settings
|
||||
from src.core.logging import get_logger
|
||||
from src.services.health_check_service import get_health_check_service
|
||||
from src.services.ollama_endpoint_circuit_breaker import (
|
||||
get_ollama_endpoint_cooldown_remaining_seconds,
|
||||
record_ollama_endpoint_failure,
|
||||
record_ollama_endpoint_success,
|
||||
)
|
||||
from src.services.ollama_endpoint_resolver import resolve_ollama_order
|
||||
|
||||
router = APIRouter()
|
||||
logger = get_logger("awoooi.health")
|
||||
CORE_COMPONENTS = ("api", "postgresql", "redis", "ollama", "openclaw", "signoz")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -47,11 +40,6 @@ class ComponentHealth(BaseModel):
|
||||
status: Literal["up", "down", "degraded"]
|
||||
latency_ms: float | None = None
|
||||
error: str | None = None
|
||||
provider_name: str | None = None
|
||||
diagnosis_code: str | None = None
|
||||
retry_after_seconds: float | None = None
|
||||
cooldown_remaining_seconds: float | None = None
|
||||
is_cooldown: bool = False
|
||||
|
||||
|
||||
class HealthResponse(BaseModel):
|
||||
@@ -62,7 +50,6 @@ class HealthResponse(BaseModel):
|
||||
mock_mode: bool
|
||||
timestamp: datetime
|
||||
components: dict[str, ComponentHealth]
|
||||
ollama_route_order: list[str] = []
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -119,125 +106,8 @@ async def check_redis() -> ComponentHealth:
|
||||
|
||||
|
||||
async def check_ollama() -> ComponentHealth:
|
||||
"""Async aggregate Ollama health check via ADR-110 provider chain."""
|
||||
aggregate, _details = await check_ollama_provider_chain()
|
||||
return aggregate
|
||||
|
||||
|
||||
async def check_ollama_provider_chain() -> tuple[ComponentHealth, dict[str, ComponentHealth]]:
|
||||
"""
|
||||
Check the full Ollama provider chain.
|
||||
|
||||
The aggregate ``ollama`` component represents route availability:
|
||||
- up: GCP-A is reachable
|
||||
- degraded: GCP-A is unavailable but GCP-B or 111 is reachable
|
||||
- down: no configured Ollama endpoint is reachable
|
||||
"""
|
||||
selections = tuple(
|
||||
selection
|
||||
for selection in resolve_ollama_order("healthcheck")
|
||||
if selection.url and selection.provider_name != "ollama_unconfigured"
|
||||
)
|
||||
if not selections:
|
||||
aggregate = ComponentHealth(
|
||||
status="down",
|
||||
error="no Ollama endpoints configured",
|
||||
)
|
||||
return aggregate, {}
|
||||
|
||||
checked = await asyncio.gather(
|
||||
*(
|
||||
_ollama_endpoint_health_check(selection.provider_name, selection.url)
|
||||
for selection in selections
|
||||
)
|
||||
)
|
||||
details = {
|
||||
selection.provider_name: result
|
||||
for selection, result in zip(selections, checked, strict=False)
|
||||
}
|
||||
|
||||
primary = selections[0]
|
||||
primary_status = details[primary.provider_name].status
|
||||
if primary.provider_name == "ollama_gcp_a" and primary_status == "up":
|
||||
return details[primary.provider_name], details
|
||||
|
||||
first_available = next(
|
||||
(
|
||||
selection
|
||||
for selection in selections
|
||||
if details[selection.provider_name].status == "up"
|
||||
),
|
||||
None,
|
||||
)
|
||||
if first_available:
|
||||
fallback = details[first_available.provider_name]
|
||||
return (
|
||||
ComponentHealth(
|
||||
status="degraded",
|
||||
latency_ms=fallback.latency_ms,
|
||||
error=f"primary unavailable; fallback active: {first_available.provider_name}",
|
||||
),
|
||||
details,
|
||||
)
|
||||
|
||||
errors = ", ".join(
|
||||
f"{provider}={health.error or health.status}"
|
||||
for provider, health in details.items()
|
||||
)
|
||||
return (
|
||||
ComponentHealth(
|
||||
status="down",
|
||||
error=f"all Ollama endpoints unavailable: {errors}",
|
||||
),
|
||||
details,
|
||||
)
|
||||
|
||||
|
||||
async def _ollama_endpoint_health_check(name: str, url: str) -> ComponentHealth:
|
||||
cooldown_remaining = get_ollama_endpoint_cooldown_remaining_seconds(url)
|
||||
if cooldown_remaining > 0:
|
||||
return ComponentHealth(
|
||||
status="down",
|
||||
error=f"recent endpoint failure cooldown: {cooldown_remaining:.0f}s",
|
||||
provider_name=name,
|
||||
diagnosis_code="endpoint_cooldown",
|
||||
retry_after_seconds=round(cooldown_remaining, 1),
|
||||
cooldown_remaining_seconds=round(cooldown_remaining, 1),
|
||||
is_cooldown=True,
|
||||
)
|
||||
|
||||
result = await _http_health_check(name, url, "/api/tags")
|
||||
result.provider_name = name
|
||||
if result.status == "up":
|
||||
result.diagnosis_code = "endpoint_reachable"
|
||||
record_ollama_endpoint_success(url)
|
||||
else:
|
||||
result.diagnosis_code = _classify_ollama_endpoint_failure(name, result.error)
|
||||
record_ollama_endpoint_failure(url)
|
||||
return result
|
||||
|
||||
|
||||
def _classify_ollama_endpoint_failure(
|
||||
provider_name: str,
|
||||
error: str | None,
|
||||
) -> str:
|
||||
"""Return a stable diagnosis code for UI/alert rendering."""
|
||||
normalized_error = (error or "").lower()
|
||||
if "cooldown" in normalized_error:
|
||||
return "endpoint_cooldown"
|
||||
if "502" in normalized_error or "bad gateway" in normalized_error:
|
||||
return (
|
||||
"local_proxy_upstream_unreachable"
|
||||
if provider_name == "ollama_local"
|
||||
else "proxy_upstream_unreachable"
|
||||
)
|
||||
if "timeout" in normalized_error:
|
||||
return "endpoint_timeout"
|
||||
if "connection refused" in normalized_error:
|
||||
return "endpoint_connection_refused"
|
||||
if "no route to host" in normalized_error or "network is unreachable" in normalized_error:
|
||||
return "endpoint_network_unreachable"
|
||||
return "endpoint_unreachable"
|
||||
"""Async Ollama health check via /api/tags"""
|
||||
return await _http_health_check("ollama", settings.OLLAMA_URL, "/api/tags")
|
||||
|
||||
|
||||
async def check_openclaw() -> ComponentHealth:
|
||||
@@ -250,30 +120,6 @@ async def check_signoz() -> ComponentHealth:
|
||||
return await _http_health_check("signoz", settings.SIGNOZ_URL, "/api/v1/health")
|
||||
|
||||
|
||||
def _determine_overall_status(
|
||||
components: dict[str, ComponentHealth],
|
||||
) -> Literal["healthy", "degraded", "unhealthy"]:
|
||||
"""Determine overall health from core aggregate components only."""
|
||||
statuses = [
|
||||
components[name].status
|
||||
for name in CORE_COMPONENTS
|
||||
if name in components
|
||||
]
|
||||
down_count = statuses.count("down")
|
||||
degraded_count = statuses.count("degraded")
|
||||
|
||||
critical_down = (
|
||||
components.get("postgresql", ComponentHealth(status="down")).status == "down"
|
||||
or components.get("redis", ComponentHealth(status="down")).status == "down"
|
||||
)
|
||||
|
||||
if critical_down or down_count >= 3:
|
||||
return "unhealthy"
|
||||
if down_count >= 1 or degraded_count > 0:
|
||||
return "degraded"
|
||||
return "healthy"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Endpoints
|
||||
# =============================================================================
|
||||
@@ -296,28 +142,34 @@ async def get_health() -> HealthResponse:
|
||||
results = await asyncio.gather(
|
||||
check_postgresql(),
|
||||
check_redis(),
|
||||
check_ollama_provider_chain(),
|
||||
check_ollama(),
|
||||
check_openclaw(),
|
||||
check_signoz(),
|
||||
)
|
||||
|
||||
ollama_aggregate, ollama_details = results[2]
|
||||
components = {
|
||||
"api": ComponentHealth(status="up", latency_ms=0.0),
|
||||
"postgresql": results[0],
|
||||
"redis": results[1],
|
||||
"ollama": ollama_aggregate,
|
||||
"ollama": results[2],
|
||||
"openclaw": results[3],
|
||||
"signoz": results[4],
|
||||
}
|
||||
components.update(ollama_details)
|
||||
|
||||
overall_status = _determine_overall_status(components)
|
||||
ollama_route_order = [
|
||||
selection.provider_name
|
||||
for selection in resolve_ollama_order("healthcheck")
|
||||
if selection.url and selection.provider_name != "ollama_unconfigured"
|
||||
]
|
||||
# Determine overall status
|
||||
statuses = [c.status for c in components.values()]
|
||||
down_count = statuses.count("down")
|
||||
degraded_count = statuses.count("degraded")
|
||||
|
||||
# Critical services: postgresql, redis
|
||||
critical_down = components["postgresql"].status == "down" or components["redis"].status == "down"
|
||||
|
||||
if critical_down or down_count >= 3:
|
||||
overall_status: Literal["healthy", "degraded", "unhealthy"] = "unhealthy"
|
||||
elif down_count >= 1 or degraded_count > 0:
|
||||
overall_status = "degraded"
|
||||
else:
|
||||
overall_status = "healthy"
|
||||
|
||||
logger.info(
|
||||
"health_check_complete",
|
||||
@@ -333,7 +185,6 @@ async def get_health() -> HealthResponse:
|
||||
mock_mode=settings.MOCK_MODE,
|
||||
timestamp=datetime.now(UTC),
|
||||
components=components,
|
||||
ollama_route_order=ollama_route_order,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -17,10 +17,9 @@ Phase 6.4 核心功能:
|
||||
- Proposal 必須關聯到 Incident
|
||||
"""
|
||||
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query, status
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from src.core.logging import get_logger
|
||||
@@ -31,7 +30,6 @@ from src.models.incident import Incident, IncidentStatus, Severity
|
||||
# Phase 16 R3.3b (2026-03-25 台北時區): Repository 層整合 - 已移至 Service 層
|
||||
from src.services.decision_manager import get_decision_manager
|
||||
from src.services.incident_service import get_incident_service
|
||||
from src.services.incident_timeline_service import fetch_incident_timeline
|
||||
from src.services.proposal_service import get_proposal_service
|
||||
from src.utils.timezone import now_taipei
|
||||
|
||||
@@ -94,49 +92,6 @@ class ProposalGenerateResponse(BaseModel):
|
||||
incident_status: str | None = None
|
||||
|
||||
|
||||
class IncidentTimelineEvent(BaseModel):
|
||||
"""事件處理歷程中的一筆原始或合成事件"""
|
||||
stage: str
|
||||
status: str
|
||||
title: str
|
||||
description: str | None = None
|
||||
actor: str | None = None
|
||||
timestamp: str | None = None
|
||||
source_table: str | None = None
|
||||
data: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class IncidentTimelineStage(BaseModel):
|
||||
"""事件處理歷程的標準階段"""
|
||||
stage: str
|
||||
label: str
|
||||
status: str
|
||||
timestamp: str | None = None
|
||||
title: str
|
||||
description: str | None = None
|
||||
actor: str | None = None
|
||||
source_table: str | None = None
|
||||
data: dict[str, Any] = Field(default_factory=dict)
|
||||
events: list[IncidentTimelineEvent] = Field(default_factory=list)
|
||||
|
||||
|
||||
class IncidentTimelineResponse(BaseModel):
|
||||
"""事件完整處理歷程回應"""
|
||||
incident_id: str
|
||||
title: str
|
||||
status: str
|
||||
severity: str
|
||||
started_at: str | None = None
|
||||
updated_at: str | None = None
|
||||
resolved_at: str | None = None
|
||||
affected_services: list[str] = Field(default_factory=list)
|
||||
approval_ids: list[str] = Field(default_factory=list)
|
||||
timeline: list[IncidentTimelineStage] = Field(default_factory=list)
|
||||
events: list[IncidentTimelineEvent] = Field(default_factory=list)
|
||||
ascii_timeline: str
|
||||
reconciliation: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# GET /api/v1/incidents
|
||||
# =============================================================================
|
||||
@@ -150,26 +105,18 @@ class IncidentTimelineResponse(BaseModel):
|
||||
|
||||
Phase 6.5 升級:
|
||||
- 每個事件自動附帶 decision_token
|
||||
- 預設只讀取已存在的 decision_token
|
||||
- 需要新決策時改由明確的 proposal / operator run 入口觸發
|
||||
- 確保 UI 永遠有決策可操作
|
||||
- 雙軌引擎: LLM (主) + Expert System (備)
|
||||
""",
|
||||
)
|
||||
async def list_incidents(
|
||||
generate_missing_decisions: bool = Query(
|
||||
False,
|
||||
description=(
|
||||
"預設 false,列表查詢只讀既有 decision token;"
|
||||
"true 僅供明確維運操作使用,會背景產生缺少的決策。"
|
||||
),
|
||||
),
|
||||
) -> IncidentListResponse:
|
||||
async def list_incidents() -> IncidentListResponse:
|
||||
"""
|
||||
取得活躍事件清單
|
||||
|
||||
Phase 6.5: 附帶既有決策令牌
|
||||
- 列表查詢必須是低成本純讀路徑
|
||||
- 不可因為前端輪詢就背景觸發 LLM / Ollama / OpenClaw
|
||||
- 需要新決策時,呼叫 POST /api/v1/incidents/{incident_id}/proposal
|
||||
Phase 6.5: 自動為每個事件生成決策令牌
|
||||
- P0/P1 事件優先處理
|
||||
- 30 秒內保證有決策
|
||||
- LLM 失敗時 Expert System 保底
|
||||
|
||||
Returns:
|
||||
IncidentListResponse: 事件清單與計數 (含決策令牌)
|
||||
@@ -184,6 +131,8 @@ async def list_incidents(
|
||||
|
||||
# 按時間排序 (最新優先)
|
||||
# 2026-03-26 修復: 處理 timezone-aware 與 naive datetime 混合問題
|
||||
from datetime import UTC
|
||||
|
||||
def safe_created_at(i: Incident) -> float:
|
||||
"""安全取得 timestamp,處理 timezone 混合問題"""
|
||||
dt = i.created_at
|
||||
@@ -197,24 +146,15 @@ async def list_incidents(
|
||||
# 2026-04-09 Claude Sonnet 4.6: 效能修復 — list endpoint 不同步等待 AI
|
||||
# 原設計: 每個 incident await AI 決策 (120-180s timeout),多 incident 時乘積爆炸
|
||||
# 修復: 只取已存在的決策 token,若無則背景觸發生成,前端 poll 單筆 GET 取得結果
|
||||
#
|
||||
# 2026-05-06 Codex: 成本與推理槽修復 — 預設不再背景觸發 AI。
|
||||
# 根因: 多個前端頁面會輪詢 GET /incidents;若列表查詢偷偷 create_task,
|
||||
# 每次頁面載入都可能消耗 GCP Ollama / OpenClaw 推理槽,甚至 fallback 到 Gemini。
|
||||
# 新規則: GET list 是純讀;生成新修復建議必須走明確 proposal/operator-run 入口。
|
||||
if generate_missing_decisions:
|
||||
import asyncio
|
||||
import asyncio
|
||||
|
||||
responses = []
|
||||
background_tasks = []
|
||||
existing_tokens = await decision_manager._find_existing_tokens_for_incidents(
|
||||
[incident.incident_id for incident in incidents]
|
||||
)
|
||||
|
||||
for incident in incidents:
|
||||
try:
|
||||
# 只查已快取的決策 (不等待 AI,立即返回)
|
||||
existing = existing_tokens.get(incident.incident_id)
|
||||
existing = await decision_manager._find_existing_token(incident.incident_id)
|
||||
if existing:
|
||||
decision_info = DecisionInfo(
|
||||
token=existing.token,
|
||||
@@ -224,20 +164,17 @@ async def list_incidents(
|
||||
)
|
||||
responses.append(IncidentResponse.from_incident(incident, decision_info))
|
||||
else:
|
||||
# 無快取 → 本次返回 None。列表查詢預設不觸發 AI;
|
||||
# 前端若需要修復建議,必須呼叫明確的 proposal 入口。
|
||||
# 無快取 → 背景觸發,本次返回 None(前端看到 decision=null 會 poll)
|
||||
responses.append(IncidentResponse.from_incident(incident, None))
|
||||
if not generate_missing_decisions:
|
||||
continue
|
||||
|
||||
# 2026-04-16 Claude Sonnet 4.6: 只對 48h 內的 incident 觸發 AI 分析
|
||||
# 舊 incident token 每小時過期,若不限制會反覆重新分析歷史事件 → Telegram 洪水
|
||||
from datetime import datetime, timezone, timedelta
|
||||
_created = getattr(incident, "created_at", None)
|
||||
_too_old = False
|
||||
if _created:
|
||||
if _created.tzinfo is None:
|
||||
_created = _created.replace(tzinfo=UTC)
|
||||
_too_old = (_created < datetime.now(UTC) - timedelta(hours=48))
|
||||
_created = _created.replace(tzinfo=timezone.utc)
|
||||
_too_old = (_created < datetime.now(timezone.utc) - timedelta(hours=48))
|
||||
if not _too_old:
|
||||
timeout = 120.0 if incident.severity in (Severity.P0, Severity.P1) else 180.0
|
||||
background_tasks.append(
|
||||
@@ -260,7 +197,6 @@ async def list_incidents(
|
||||
"incidents_listed",
|
||||
count=len(incidents),
|
||||
with_decisions=sum(1 for r in responses if r.decision is not None),
|
||||
generate_missing_decisions=generate_missing_decisions,
|
||||
)
|
||||
|
||||
return IncidentListResponse(
|
||||
@@ -335,50 +271,6 @@ async def get_incident(incident_id: str) -> IncidentResponse:
|
||||
) from e
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# GET /api/v1/incidents/{incident_id}/timeline
|
||||
# =============================================================================
|
||||
|
||||
@router.get(
|
||||
"/{incident_id}/timeline",
|
||||
response_model=IncidentTimelineResponse,
|
||||
summary="取得事件完整處理歷程",
|
||||
description="彙整 webhook、AI、目標、風險、安全閘、執行、驗證、KM 與結案事件。",
|
||||
)
|
||||
async def get_incident_timeline(incident_id: str) -> IncidentTimelineResponse:
|
||||
"""
|
||||
取得單一 Incident 的端到端處理歷程。
|
||||
"""
|
||||
try:
|
||||
timeline = await fetch_incident_timeline(incident_id)
|
||||
if timeline is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Incident not found: {incident_id}",
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"incident_timeline_fetched",
|
||||
incident_id=incident_id,
|
||||
stage_count=len(timeline.get("timeline", [])),
|
||||
event_count=len(timeline.get("events", [])),
|
||||
)
|
||||
return IncidentTimelineResponse.model_validate(timeline)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
"get_incident_timeline_error",
|
||||
incident_id=incident_id,
|
||||
error=str(e),
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to get incident timeline: {str(e)}",
|
||||
) from e
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# POST /api/v1/incidents/{incident_id}/proposal
|
||||
# =============================================================================
|
||||
|
||||
@@ -1,362 +0,0 @@
|
||||
"""
|
||||
IwoooS 安全治理 API。
|
||||
|
||||
Wazuh 接線採用只讀 metadata 模式:預設關閉、不保存 raw payload、
|
||||
不公開 agent 原名 / 內網 IP、不啟用 active response。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from src.services.iwooos_runtime_security_readback import (
|
||||
load_latest_iwooos_runtime_security_readback,
|
||||
)
|
||||
from src.services.iwooos_high_value_config_control_coverage import (
|
||||
load_latest_iwooos_high_value_config_control_coverage,
|
||||
)
|
||||
from src.services.iwooos_owner_evidence_intake_preflight import (
|
||||
load_latest_iwooos_owner_evidence_intake_preflight,
|
||||
)
|
||||
from src.services.iwooos_security_control_coverage import (
|
||||
load_latest_iwooos_security_control_coverage,
|
||||
)
|
||||
from src.services.iwooos_wazuh_readonly_status import (
|
||||
load_iwooos_wazuh_readonly_status,
|
||||
)
|
||||
from src.services.iwooos_wazuh_live_metadata_gate import (
|
||||
load_latest_iwooos_wazuh_live_metadata_gate,
|
||||
)
|
||||
from src.services.iwooos_wazuh_managed_host_coverage import (
|
||||
load_latest_iwooos_wazuh_managed_host_coverage,
|
||||
)
|
||||
from src.services.iwooos_wazuh_manager_registry_reviewer_validation import (
|
||||
load_latest_iwooos_wazuh_manager_registry_reviewer_validation,
|
||||
validate_iwooos_wazuh_manager_registry_acceptance_evidence as validate_wazuh_manager_registry_acceptance_evidence_payload,
|
||||
validate_iwooos_wazuh_manager_registry_owner_export as validate_wazuh_manager_registry_owner_export_payload,
|
||||
)
|
||||
from src.services.iwooos_wazuh_owner_evidence_preflight import (
|
||||
load_latest_iwooos_wazuh_owner_evidence_preflight,
|
||||
)
|
||||
from src.services.public_redaction import redact_public_lan_topology
|
||||
|
||||
|
||||
router = APIRouter(tags=["IwoooS Security"])
|
||||
|
||||
|
||||
async def _wazuh_readonly_status() -> JSONResponse:
|
||||
result = await load_iwooos_wazuh_readonly_status()
|
||||
return JSONResponse(status_code=result.http_status, content=result.payload)
|
||||
|
||||
|
||||
@router.get("/api/iwooos/wazuh")
|
||||
async def get_iwooos_wazuh_readonly_status_compat() -> JSONResponse:
|
||||
return await _wazuh_readonly_status()
|
||||
|
||||
|
||||
@router.get("/api/v1/iwooos/wazuh")
|
||||
async def get_iwooos_wazuh_readonly_status_v1() -> JSONResponse:
|
||||
return await _wazuh_readonly_status()
|
||||
|
||||
|
||||
@router.get(
|
||||
"/api/v1/iwooos/wazuh-live-metadata-gate",
|
||||
response_model=dict[str, Any],
|
||||
summary="取得 Wazuh 即時中繼資料負責人閘門讀回",
|
||||
description=(
|
||||
"讀取已提交的 Wazuh 即時中繼資料負責人閘門,並附上 Wazuh 正式只讀路由的"
|
||||
"公開安全彙總。此端點不讀機密明文、不查主機、不保存原始 Wazuh 載荷、"
|
||||
"不啟用主動回應、不改 K8s / ArgoCD / Docker / Nginx / firewall。"
|
||||
),
|
||||
)
|
||||
async def get_iwooos_wazuh_live_metadata_gate() -> dict[str, Any]:
|
||||
"""回傳 Wazuh 即時中繼資料啟用前負責人閘門只讀狀態。"""
|
||||
try:
|
||||
wazuh_result = await load_iwooos_wazuh_readonly_status()
|
||||
payload = await asyncio.to_thread(
|
||||
load_latest_iwooos_wazuh_live_metadata_gate,
|
||||
wazuh_live_status=wazuh_result.payload,
|
||||
wazuh_live_http_status=wazuh_result.http_status,
|
||||
)
|
||||
return redact_public_lan_topology(payload)
|
||||
except FileNotFoundError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
except (json.JSONDecodeError, ValueError) as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"IwoooS Wazuh 即時中繼資料閘門無效:{exc}",
|
||||
) from exc
|
||||
|
||||
|
||||
@router.get(
|
||||
"/api/v1/iwooos/wazuh-owner-evidence-preflight",
|
||||
response_model=dict[str, Any],
|
||||
summary="取得 Wazuh 負責人證據收件預檢讀回",
|
||||
description=(
|
||||
"讀取已提交的 Wazuh 代理清單負責人證據收件預檢,回傳公開安全的欄位數、"
|
||||
"審查檢查、分流、拒收內容計數與 0 / false 邊界。此端點不查 Wazuh、"
|
||||
"不讀主機、不保存原始載荷、不收機密明文、不啟用主動回應、不改 Nginx / "
|
||||
"Docker / K8s / firewall。"
|
||||
),
|
||||
)
|
||||
async def get_iwooos_wazuh_owner_evidence_preflight() -> dict[str, Any]:
|
||||
"""回傳 Wazuh manager registry 負責人證據收件預檢只讀狀態。"""
|
||||
try:
|
||||
payload = await asyncio.to_thread(load_latest_iwooos_wazuh_owner_evidence_preflight)
|
||||
return redact_public_lan_topology(payload)
|
||||
except FileNotFoundError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
except (json.JSONDecodeError, ValueError) as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"IwoooS Wazuh 負責人證據預檢無效:{exc}",
|
||||
) from exc
|
||||
|
||||
|
||||
@router.get(
|
||||
"/api/v1/iwooos/wazuh-managed-host-coverage",
|
||||
response_model=dict[str, Any],
|
||||
summary="取得 Wazuh 受管主機覆蓋只讀讀回",
|
||||
description=(
|
||||
"讀取已提交的 Wazuh 受管主機覆蓋快照,回傳公開別名主機矩陣、manager registry "
|
||||
"接受數、缺口數、必要驗收證據與 0 / false 邊界。此端點不查 Wazuh API、"
|
||||
"不讀主機、不重新註冊 agent、不重啟 Wazuh、不保存原始載荷、不收機密明文、"
|
||||
"不啟用主動回應、不改 Nginx / Docker / K8s / firewall。"
|
||||
),
|
||||
)
|
||||
async def get_iwooos_wazuh_managed_host_coverage() -> dict[str, Any]:
|
||||
"""回傳 Wazuh 受管主機覆蓋公開安全只讀狀態。"""
|
||||
try:
|
||||
payload = await asyncio.to_thread(load_latest_iwooos_wazuh_managed_host_coverage)
|
||||
return redact_public_lan_topology(payload)
|
||||
except FileNotFoundError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
except (json.JSONDecodeError, ValueError) as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"IwoooS Wazuh 受管主機覆蓋無效:{exc}",
|
||||
) from exc
|
||||
|
||||
|
||||
@router.get(
|
||||
"/api/v1/iwooos/wazuh-manager-registry-reviewer-validation",
|
||||
response_model=dict[str, Any],
|
||||
summary="取得 Wazuh manager registry reviewer validation 只讀讀回",
|
||||
description=(
|
||||
"讀取已提交的 Wazuh manager registry reviewer validation contract,回傳 owner export "
|
||||
"必要欄位、reviewer 檢查、evidence slots、結果分流、拒收內容與 0 / false 邊界。"
|
||||
"此端點不收 raw payload、不查 Wazuh API、不讀主機、不重新註冊 agent、不重啟服務、"
|
||||
"不保存機密、不啟用主動回應、不改 Nginx / Docker / K8s / firewall。"
|
||||
),
|
||||
)
|
||||
async def get_iwooos_wazuh_manager_registry_reviewer_validation() -> dict[str, Any]:
|
||||
"""回傳 Wazuh manager registry reviewer validation 公開安全只讀狀態。"""
|
||||
try:
|
||||
payload = await asyncio.to_thread(load_latest_iwooos_wazuh_manager_registry_reviewer_validation)
|
||||
return redact_public_lan_topology(payload)
|
||||
except FileNotFoundError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
except (json.JSONDecodeError, ValueError) as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"IwoooS Wazuh manager registry reviewer validation 無效:{exc}",
|
||||
) from exc
|
||||
|
||||
|
||||
@router.post(
|
||||
"/api/v1/iwooos/wazuh-manager-registry-reviewer-validation/validate-owner-export",
|
||||
response_model=dict[str, Any],
|
||||
summary="驗證 Wazuh manager registry 脫敏 owner export",
|
||||
description=(
|
||||
"針對單次 owner-provided redacted Wazuh manager registry export 進行 no-persist reviewer "
|
||||
"validation,回傳 accepted / needs supplement / quarantined / rejected runtime action 分流。"
|
||||
"此端點不保存 payload、不查 Wazuh API、不讀主機、不重新註冊 agent、不重啟服務、不讀或回傳"
|
||||
"機密明文、不啟用主動回應、不改 Nginx / Docker / K8s / firewall,也不更新 manager registry "
|
||||
"accepted 總帳。"
|
||||
),
|
||||
)
|
||||
async def validate_iwooos_wazuh_manager_registry_owner_export(owner_export: dict[str, Any]) -> dict[str, Any]:
|
||||
"""回傳單次 Wazuh manager registry 脫敏匯出的公開安全驗證結果。"""
|
||||
try:
|
||||
payload = await asyncio.to_thread(
|
||||
validate_wazuh_manager_registry_owner_export_payload,
|
||||
owner_export,
|
||||
)
|
||||
return redact_public_lan_topology(payload)
|
||||
except FileNotFoundError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
except (json.JSONDecodeError, ValueError) as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"IwoooS Wazuh manager registry owner export 驗證器無效:{exc}",
|
||||
) from exc
|
||||
|
||||
|
||||
@router.post(
|
||||
"/api/v1/iwooos/wazuh-manager-registry-reviewer-validation/validate-manager-registry-acceptance",
|
||||
response_model=dict[str, Any],
|
||||
summary="驗證 Wazuh manager registry accepted 脫敏 evidence packet",
|
||||
description=(
|
||||
"針對單次 owner / reviewer 提供的 redacted Wazuh manager registry acceptance evidence "
|
||||
"packet 進行 no-persist review readiness validation,回傳 accepted-for-review / needs supplement / "
|
||||
"quarantined / rejected runtime action 分流。此端點不保存 payload、不查 Wazuh API、不讀主機、"
|
||||
"不重新註冊 agent、不重啟服務、不讀或回傳機密明文、不啟用主動回應、不改 Nginx / Docker / "
|
||||
"K8s / firewall,也不更新 manager registry accepted 總帳。"
|
||||
),
|
||||
)
|
||||
async def validate_iwooos_wazuh_manager_registry_acceptance_evidence(
|
||||
acceptance_evidence: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
"""回傳單次 Wazuh manager registry accepted evidence 的公開安全驗證結果。"""
|
||||
try:
|
||||
payload = await asyncio.to_thread(
|
||||
validate_wazuh_manager_registry_acceptance_evidence_payload,
|
||||
acceptance_evidence,
|
||||
)
|
||||
return redact_public_lan_topology(payload)
|
||||
except FileNotFoundError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
except (json.JSONDecodeError, ValueError) as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"IwoooS Wazuh manager registry acceptance evidence 驗證器無效:{exc}",
|
||||
) from exc
|
||||
|
||||
|
||||
@router.get(
|
||||
"/api/v1/iwooos/runtime-security-readback",
|
||||
response_model=dict[str, Any],
|
||||
summary="取得 IwoooS runtime security readback",
|
||||
description=(
|
||||
"讀取最新已提交的 IwoooS 資安只讀快照,彙總 Wazuh、Kali、SOC/SIEM、"
|
||||
"告警可讀性、owner dispatch 與外部入侵防護 Gate,並附上 Wazuh 只讀路由的"
|
||||
"公開安全 aggregate 讀回。此端點不呼叫 Kali / 主機 / Docker / Nginx / firewall / "
|
||||
"Telegram,不保存 raw Wazuh payload,不收集 secret,不授權 runtime 寫入。"
|
||||
),
|
||||
)
|
||||
async def get_iwooos_runtime_security_readback() -> dict[str, Any]:
|
||||
"""回傳 IwoooS 資安 runtime readback 只讀總板。"""
|
||||
try:
|
||||
wazuh_result = await load_iwooos_wazuh_readonly_status()
|
||||
payload = await asyncio.to_thread(
|
||||
load_latest_iwooos_runtime_security_readback,
|
||||
wazuh_live_status=wazuh_result.payload,
|
||||
wazuh_live_http_status=wazuh_result.http_status,
|
||||
)
|
||||
return redact_public_lan_topology(payload)
|
||||
except FileNotFoundError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
except (json.JSONDecodeError, ValueError) as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"IwoooS runtime security readback 無效:{exc}",
|
||||
) from exc
|
||||
|
||||
|
||||
@router.get(
|
||||
"/api/v1/iwooos/security-control-coverage",
|
||||
response_model=dict[str, Any],
|
||||
summary="取得 IwoooS 資安納管覆蓋總表",
|
||||
description=(
|
||||
"彙整已提交的主機、產品、服務、配置、監控、Wazuh、AI Agent 與 agent-bounty "
|
||||
"資安納管 snapshot,形成只讀覆蓋總表。此端點不查 live host、不讀 secret、不啟動掃描、"
|
||||
"不送告警、不開 runtime gate。"
|
||||
),
|
||||
)
|
||||
async def get_iwooos_security_control_coverage() -> dict[str, Any]:
|
||||
"""回傳 IwoooS 資安納管覆蓋只讀總表。"""
|
||||
try:
|
||||
payload = await asyncio.to_thread(load_latest_iwooos_security_control_coverage)
|
||||
return redact_public_lan_topology(payload)
|
||||
except FileNotFoundError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
except (json.JSONDecodeError, ValueError) as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"IwoooS security control coverage 無效:{exc}",
|
||||
) from exc
|
||||
|
||||
|
||||
@router.get(
|
||||
"/api/v1/iwooos/high-value-config-control-coverage",
|
||||
response_model=dict[str, Any],
|
||||
summary="取得 IwoooS 高價值配置控管覆蓋矩陣",
|
||||
description=(
|
||||
"讀取已提交的高價值配置控管 snapshot,回傳 Nginx、DNS / TLS、K8s、"
|
||||
"Secrets、runner、Firewall、Backup、AI provider 與 agent-bounty runtime 的"
|
||||
"公開安全只讀投影。此端點不查 live host、不讀 secret、不執行 nginx -t、"
|
||||
"不 reload、不 sync、不啟動掃描、不開 runtime gate。"
|
||||
),
|
||||
)
|
||||
async def get_iwooos_high_value_config_control_coverage() -> dict[str, Any]:
|
||||
"""回傳高價值配置控管矩陣公開安全只讀狀態。"""
|
||||
try:
|
||||
payload = await asyncio.to_thread(load_latest_iwooos_high_value_config_control_coverage)
|
||||
return redact_public_lan_topology(payload)
|
||||
except FileNotFoundError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
except (json.JSONDecodeError, ValueError) as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"IwoooS high-value config control coverage 無效:{exc}",
|
||||
) from exc
|
||||
|
||||
|
||||
@router.get(
|
||||
"/api/v1/iwooos/owner-evidence-intake-preflight",
|
||||
response_model=dict[str, Any],
|
||||
summary="取得 IwoooS 負責人脫敏證據收件預檢",
|
||||
description=(
|
||||
"整合 high-value config owner packet、配置覆蓋矩陣與 Wazuh 負責人證據預檢,"
|
||||
"回傳 Nginx、DNS / TLS、K8s、secret / runner、public runtime config 與 Wazuh registry "
|
||||
"的公開安全收件欄位、拒收規則與 0 / false 邊界。此端點不送 owner request、不收回覆、"
|
||||
"不寫 reviewer queue、不讀 secret、不查 live host、不查 Wazuh API、不啟動 runtime action。"
|
||||
),
|
||||
)
|
||||
async def get_iwooos_owner_evidence_intake_preflight() -> dict[str, Any]:
|
||||
"""回傳 IwoooS 負責人脫敏證據收件預檢公開安全只讀狀態。"""
|
||||
try:
|
||||
payload = await asyncio.to_thread(load_latest_iwooos_owner_evidence_intake_preflight)
|
||||
return redact_public_lan_topology(payload)
|
||||
except FileNotFoundError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
except (json.JSONDecodeError, ValueError) as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"IwoooS owner evidence intake preflight 無效:{exc}",
|
||||
) from exc
|
||||
@@ -18,7 +18,6 @@ from datetime import UTC, datetime
|
||||
import httpx
|
||||
from fastapi import APIRouter
|
||||
|
||||
from src.core.config import settings
|
||||
from src.core.logging import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
@@ -27,23 +26,6 @@ router = APIRouter(prefix="/monitoring", tags=["Monitoring"])
|
||||
|
||||
TIMEOUT = 3.0
|
||||
|
||||
PUBLIC_TOOL_URLS = {
|
||||
"Sentry": "https://sentry.wooo.work",
|
||||
"Langfuse": "https://langfuse.wooo.work",
|
||||
"SigNoz": "https://signoz.wooo.work",
|
||||
"Gitea": "https://gitea.wooo.work",
|
||||
}
|
||||
|
||||
|
||||
def public_monitoring_tool_payload(tool: dict) -> dict:
|
||||
"""Drop internal probe URLs before returning tool status to browsers."""
|
||||
payload = dict(tool)
|
||||
payload.pop("url", None)
|
||||
public_url = PUBLIC_TOOL_URLS.get(str(payload.get("name") or ""))
|
||||
if public_url:
|
||||
payload["url"] = public_url
|
||||
return payload
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Probes
|
||||
@@ -56,16 +38,15 @@ async def _probe_grafana(client: httpx.AsyncClient) -> dict:
|
||||
if r.status_code == 200:
|
||||
data = r.json()
|
||||
version = data.get("version")
|
||||
dash_count = None
|
||||
grafana_api_key = settings.GRAFANA_API_KEY.strip()
|
||||
if grafana_api_key and grafana_api_key != "CHANGE_ME":
|
||||
dash_r = await client.get(
|
||||
f"{base}/api/search?type=dash-db",
|
||||
headers={"Authorization": f"Bearer {grafana_api_key}"},
|
||||
timeout=TIMEOUT,
|
||||
)
|
||||
if dash_r.status_code == 200 and isinstance(dash_r.json(), list):
|
||||
dash_count = len(dash_r.json())
|
||||
# Dashboard count requires basic auth (internal probe only)
|
||||
import base64 as _b64
|
||||
_token = _b64.b64encode(b"admin:WoooTech2026").decode()
|
||||
dash_r = await client.get(
|
||||
f"{base}/api/search?type=dash-db",
|
||||
headers={"Authorization": f"Basic {_token}"},
|
||||
timeout=TIMEOUT,
|
||||
)
|
||||
dash_count = len(dash_r.json()) if dash_r.status_code == 200 and isinstance(dash_r.json(), list) else None
|
||||
return {
|
||||
"name": "Grafana",
|
||||
"status": "up",
|
||||
@@ -83,9 +64,7 @@ async def _probe_grafana(client: httpx.AsyncClient) -> dict:
|
||||
|
||||
|
||||
async def _probe_prometheus(client: httpx.AsyncClient) -> dict:
|
||||
# 2026-04-29 ogt + Claude Opus 4.7: 改用 settings 對齊單一事實源
|
||||
# 原本寫死 110:9090 雖巧合正確,但繞過 ConfigMap 注入機制
|
||||
base = settings.PROMETHEUS_URL
|
||||
base = "http://192.168.0.110:9090"
|
||||
try:
|
||||
health_r = await client.get(f"{base}/-/healthy", timeout=TIMEOUT)
|
||||
if health_r.status_code == 200:
|
||||
@@ -260,7 +239,7 @@ async def get_monitoring_status() -> dict:
|
||||
if isinstance(r, Exception):
|
||||
logger.error("monitoring_probe_exception", error=str(r))
|
||||
continue
|
||||
tools.append({**public_monitoring_tool_payload(r), "checked_at": now})
|
||||
tools.append({**r, "checked_at": now})
|
||||
|
||||
return {
|
||||
"tools": tools,
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
"""
|
||||
AwoooP Platform API — Operator Console Router 彙整
|
||||
===================================================
|
||||
Phase 4 Shadow Mode + Phase 8 Operator Console
|
||||
ADR-106/ADR-107/ADR-114/ADR-115/ADR-116
|
||||
2026-05-05 ogt + Claude Sonnet 4.6(新增 Operator Console 四 router)
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from src.api.v1.platform.contracts import router as contracts_router
|
||||
from src.api.v1.platform.events import router as events_router
|
||||
from src.api.v1.platform.operator_runs import router as operator_runs_router
|
||||
from src.api.v1.platform.runs import router as runs_router
|
||||
from src.api.v1.platform.tenants import router as tenants_router
|
||||
from src.api.v1.platform.truth_chain import router as truth_chain_router
|
||||
|
||||
router = APIRouter()
|
||||
router.include_router(events_router)
|
||||
router.include_router(truth_chain_router)
|
||||
# 2026-05-06 Codex: FastAPI 依註冊順序比對路由。Operator Console 的
|
||||
# `/runs/list` 必須排在 `/runs/{run_id}` 前面,否則 `list` 會被當成
|
||||
# run_id,造成前端 Run 監控頁 HTTP 422。
|
||||
router.include_router(operator_runs_router)
|
||||
router.include_router(runs_router)
|
||||
router.include_router(tenants_router)
|
||||
router.include_router(contracts_router)
|
||||
|
||||
__all__ = ["router"]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user