# Nemotron 主動防禦三方向設計文件

**日期**: 2026-04-04 (台北時區)  
**作者**: Claude Code + 首席架構師 ogt  
**狀態**: ✅ 首席架構師 100% 技術背書，正式批准  
**關聯 ADR**: 待起草 ADR-055, ADR-056, ADR-057  

---

## 背景與動機

AWOOOI 目前已完成：
- Phase 22：OpenClaw + Nemotron 雙 AI 協作（仲裁-執行分工）
- Phase 24：AIRouter Provider Registry（USE_AI_ROUTER 絞殺者開關）
- KB Phase 1+2：pgvector 語意搜尋上線
- Auto Repair 閉環：Playbook 自動執行 + KM 沉澱串接

本設計將 AI 從「被動分析」推向「主動防禦與知識沉澱」，涵蓋三個方向：

1. **Knowledge Auto-Harvesting** — 修復後自動生成 Runbook，並建立 Anti-Pattern 反饋閉環
2. **DIAGNOSE Privacy-First Routing** — 本地 Nemotron 優先，FORCE_LOCAL 絕不觸碰雲端
3. **Config Drift Detection** — GitOps 守門員，Nemotron 做意圖分析而非猜修復指令

---

## 方向一：Knowledge Auto-Harvesting（自動化 Runbook 生成）

### 設計目標

- 修復成功 → Nemotron 生成完整 9 段 Runbook → DRAFT 待人工審核
- 修復失敗 → Nemotron 生成失敗案例 → ANTI_PATTERN 直接發布
- 執行前加入 Anti-Pattern 閉環攔截 → 阻止 AI 在同一個坑重複摔倒

### 現有程式碼整合點

| 檔案 | 現況 | 需要的變更 |
|------|------|-----------|
| `models/knowledge.py` | `EntryType` 有 `RUNBOOK`，沒有 `ANTI_PATTERN`/`AUTO_RUNBOOK` | 新增兩個 EntryType |
| `models/knowledge.py` | `KnowledgeEntry` 有 `related_incident_id`，沒有 `symptoms_hash` | 新增 `symptoms_hash: str \| None` |
| `models/playbook.py` | `SymptomPattern` 有 `alert_names` / `affected_services` / `label_patterns` | 新增 `compute_hash()` 方法 |
| `auto_repair_service.py` | `decide()` 無 Anti-Pattern 閘門；`execute()` 無 Runbook 生成 | 兩處各加一個 hook |
| `knowledge_service.py` | `create_entry()` 已存在 | 新增 `check_anti_pattern()` 方法 |
| `migrations/` | knowledge 表無 `symptoms_hash` 欄位 | 新增 migration |

### `SymptomPattern.compute_hash()` 設計

```python
def compute_hash(self) -> str:
    """
    確定性 hash：alert_names + affected_services + label_patterns
    目的：O(1) 精確比對，避免純語意搜尋的模糊性
    """
    import hashlib, json
    key = (
        "|".join(sorted(self.alert_names)) + "||" +
        "|".join(sorted(self.affected_services)) + "||" +
        json.dumps(self.label_patterns, sort_keys=True)
    )
    return hashlib.sha256(key.encode()).hexdigest()[:16]
```

### Anti-Pattern 閉環攔截（`auto_repair_service.py` `decide()` 修改）

```
現有流程：
  get_recommendations() → 評估風險 → 回傳 AutoRepairDecision

修正後流程：
  get_recommendations()
      ↓
  symptoms_hash = symptom_pattern.compute_hash()
  anti_patterns = knowledge_service.check_anti_pattern(symptoms_hash, days=7)
  if anti_patterns:
      return AutoRepairDecision(
          can_auto_repair=False,
          blocked_by="ANTI_PATTERN",
          reason=f"過去 7 天有失敗案例: {anti_patterns[0].title}"
      )
      ↓
  繼續原有風險評估流程
```

### Runbook 完整 9 段結構（Nemotron 生成）

```
1. symptoms_description    症狀描述（告警內容摘要）
2. root_cause_analysis     根因分析（AI 推斷）
3. execution_steps         執行指令（逐步，含驗證指令）
4. verification_steps      驗證步驟（修復後如何確認成功）
5. precautions             注意事項（高風險操作警示）
6. impact_scope            影響範圍（哪些服務/用戶受影響）
7. related_incident_ids    相關 Incident ID 列表
8. prevention_suggestions  下次預防建議
9. applicable_conditions   適用條件（何時套用此 Runbook）
```

### 完整執行後流程

```
AutoRepairService.execute() 完成
    │
    ├── [成功]
    │     NemotronRunbookGenerator.generate(incident, playbook, result)
    │         └── KnowledgeService.create_entry(
    │                 entry_type=AUTO_RUNBOOK,
    │                 status=DRAFT,
    │                 symptoms_hash=symptom_pattern.compute_hash()
    │             )
    │         └── TelegramGateway.push_runbook_review_card(
    │                 摘要 + [✅ 發布] [✏️ 編輯] [🗑️ 丟棄] 按鈕
    │             )
    │
    └── [失敗]
          NemotronRunbookGenerator.generate_anti_pattern(incident, playbook, error)
              └── KnowledgeService.create_entry(
                      entry_type=ANTI_PATTERN,
                      status=PUBLISHED,   ← 直接發布，無需審核
                      symptoms_hash=symptom_pattern.compute_hash()
                  )
              └── TelegramGateway.push_notification(
                      "⚠️ 已記錄失敗案例: {title}"
                  )
```

### 新增檔案清單

| 動作 | 檔案 | 說明 |
|------|------|------|
| 新增 | `services/runbook_generator.py` | `NemotronRunbookGenerator`，呼叫 NemotronProvider |
| 修改 | `models/knowledge.py` | 新增 `ANTI_PATTERN` / `AUTO_RUNBOOK` EntryType；`KnowledgeEntry` 新增 `symptoms_hash` |
| 修改 | `models/playbook.py` | `SymptomPattern` 新增 `compute_hash()` |
| 修改 | `services/auto_repair_service.py` | `decide()` 加 anti_pattern_gate；`execute()` 結束後呼叫 runbook_generator |
| 修改 | `services/knowledge_service.py` | 新增 `check_anti_pattern(symptoms_hash, days)` |
| 新增 | `migrations/phase8_symptoms_hash.sql` | knowledge 表新增 `symptoms_hash VARCHAR(16)` + index |

---

## 方向二：DIAGNOSE Privacy-First Routing

### ⚠️ 實作修正記錄（2026-04-05 v4.3 最終版）

**設計討論時的兩個錯誤假設已全部驗證並修正：**

| 假設 | 實測結果 | 修正 |
|------|---------|------|
| Nemotron NIM 在 188 本地 | `integrate.api.nvidia.com` 是雲端 GPU | NIM 一直是主力，Phase 22 起就接收 Incident 資料，無隱私問題 |
| Ollama 可作備援（~173s） | 實測 238s 僅回 `{"ok":true}`（CPU-only） | Ollama 生產不可用，廢棄 `_local_fallback_chain` |

---

### 設計目標（修正後）

- DIAGNOSE 任務路由 Nemotron NIM（主力，實測 avg 10.6s）
- NIM 失敗 → `_full_fallback_chain` 正常降級
- Timeout 依實測：NIM 60s（實測 2.2~27.3s，avg 10.6s）

### 實作（v4.3）

```python
# ai_router.py — DIAGNOSE override（不變）
IntentType.DIAGNOSE: AIProviderEnum.NEMOTRON,

# ai_router.py — _local_fallback_chain 廢棄（空 list）
self._local_fallback_chain = []  # Ollama CPU-only 238s，不可用

# ai_router.py — DIAGNOSE 走 _full_fallback_chain（NIM 主力）
fallback_chain = self._build_fallback_chain(provider)
```

### Timeout 配置（依實測）

```bash
NEMOTRON_DIAGNOSE_TIMEOUT_SECONDS=60   # 實測 2.2-27.3s avg 10.6s，60s 含 buffer
NEMOTRON_TIMEOUT_SECONDS=45            # Tool Calling 等其他用途
OLLAMA_DIAGNOSE_TIMEOUT_SECONDS=200    # 保留欄位，實際 DIAGNOSE 不走 Ollama
```

### 完整路由流程（v4.3）

```
DIAGNOSE 請求
    │
    AIRouter.route(intent=DIAGNOSE)
    │
    NemotronProvider（integrate.api.nvidia.com，免費 GPU）
        timeout = 60s（實測 avg 10.6s）
        ├── 成功 → 回傳，記錄 Langfuse trace
        └── 失敗 → _full_fallback_chain 正常降級（Gemini → Claude）
```

### 修改清單

| 動作 | 檔案 | 變更 |
|------|------|------|
| 修改 | `services/ai_router.py` | v4.3: `_local_fallback_chain` 廢棄；DIAGNOSE 改回 `_full_fallback_chain` |
| 修改 | `services/ai_providers/nemotron.py` | `analyze()` 依 `context["task_type"]` 讀取 timeout |
| 修改 | `core/config.py` | timeout 說明更新反映實測結果 |

---

## 方向三：Config Drift Detection（GitOps Guardianship）

### 設計目標

- 偵測 K8s 實際狀態 vs Git YAML 的漂移
- 白名單欄位（replicas 等）靜默記錄，不告警
- 結構性漂移 → Nemotron 做**意圖分析**（不猜修復指令）
- 確定性修復：`kubectl apply -f <git-yaml>` 或 `git push`（人工確認後）
- 觸發：部署後 Gitea Webhook + 每小時 CronJob

### 職責分工（核心設計原則）

| 組件 | 職責 | 不做什麼 |
|------|------|---------|
| `DriftDetector` | 比對 Git vs K8s，輸出結構化 Diff | 不判斷嚴重性 |
| `DriftAnalyzer` | 白名單過濾、DriftLevel 分級 | 不解釋意圖 |
| `NemotronDriftInterpreter` | **意圖分析**：人為操作？Hotfix？異常？ | **不生成修復指令** |
| `DriftRemediator` | 確定性修復：kubectl apply / git push | 不使用 AI 判斷如何修復 |

### DriftLevel 分級

```python
class DriftLevel(str, Enum):
    INFO = "info"       # 白名單欄位（replicas, resources, annotations）→ 靜默記錄
    MEDIUM = "medium"   # 非關鍵欄位變更 → Telegram 通知，無需緊急處理
    HIGH = "high"       # 關鍵欄位（image, env, ports）→ 立即通知，需確認

class DriftIntent(str, Enum):
    EMERGENCY_HOTFIX = "emergency_hotfix"   # 繞過 CI 的緊急修補
    HUMAN_ERROR = "human_error"             # 誤操作
    AUTOMATED_CHANGE = "automated_change"   # 系統自動變更（HPA 等）
    UNKNOWN = "unknown"                     # 無法判斷
```

### 白名單配置（環境變數）

```bash
DRIFT_ALLOW_FIELDS=replicas,resources.requests,resources.limits,annotations,labels.pod-template-hash
DRIFT_CRITICAL_FIELDS=image,env,ports,volumes,serviceAccountName
DRIFT_SCAN_NAMESPACES=awoooi-prod,awoooi-staging
DRIFT_CRON_INTERVAL_HOURS=1
```

### 完整流程

```
觸發來源：
  ├── [事件] Gitea CD Webhook → POST /internal/drift/scan
  └── [定期] K8s CronJob（每小時）

DriftDetector.scan()
    ├── GitStateReader.read()     ← git show HEAD:k8s/
    ├── K8sStateReader.read()     ← kubectl get all -o json
    └── diff() → List[DriftItem]

DriftAnalyzer.classify(drift_items)
    ├── INFO 漂移 → 靜默記錄到 DB，不推送
    └── MEDIUM/HIGH 漂移 → 繼續處理

NemotronDriftInterpreter.analyze(diff)
    輸出：
    {
      "intent": "emergency_hotfix" | "human_error" | "unknown",
      "explanation": "Image tag 從 v1.2 改為 v1.2-hotfix，繞過 Gitea CI，疑似緊急修補",
      "risk": "HIGH",
      "confidence": 0.85
    }

TelegramGateway.push_drift_card(
    diff_summary,
    nemotron_analysis,
    buttons=[
        "🔄 覆蓋回 Git（kubectl apply）",
        "✅ 承認變更（更新 Git）",
        "🔍 查看詳情"
    ]
)

使用者按下按鈕：
    ├── [覆蓋回 Git]
    │     DriftRemediator.rollback(resource)
    │         kubectl apply -f k8s/<resource>.yaml
    │         → Telegram: "✅ 已覆蓋回 Git 狀態"
    │
    └── [承認變更]
          GitAdopter.adopt(resource, actual_value)
              git commit -m "chore: adopt drift — {resource} {field}: {actual_value}"
              git push gitea main
              → Telegram: "✅ 已更新 Git，漂移已承認"
```

### 新增檔案清單

| 動作 | 檔案 | 說明 |
|------|------|------|
| 新增 | `services/drift_detector.py` | GitStateReader + K8sStateReader + diff 邏輯 |
| 新增 | `services/drift_analyzer.py` | 白名單過濾 + DriftLevel 分級 |
| 新增 | `services/drift_interpreter.py` | NemotronDriftInterpreter（意圖分析） |
| 新增 | `services/drift_remediator.py` | rollback（kubectl apply）+ adopt（git push） |
| 新增 | `api/v1/drift.py` | POST /internal/drift/scan endpoint |
| 新增 | `models/drift.py` | DriftItem, DriftReport, DriftLevel, DriftIntent |
| 新增 | `k8s/drift-cronjob.yaml` | 每小時 K8s CronJob |
| 新增 | `migrations/phase9_drift_reports.sql` | drift_reports 表 |

---

## 三方向整合關係圖

```
Incident 觸發
    │
    AutoRepairService
    │    ├── [執行前] anti_pattern_gate
    │    │         → KnowledgeService.check_anti_pattern(symptoms_hash)
    │    │         → 有記錄 → 強制 HITL，阻斷自動執行
    │    │
    │    ├── [執行中] AIRouter (DIAGNOSE, require_local=True)
    │    │         → _local_fallback_chain: [NEMOTRON → OLLAMA → REJECT]
    │    │         → 絕不觸碰雲端
    │    │
    │    └── [執行後] NemotronRunbookGenerator
    │              ├── 成功 → AUTO_RUNBOOK (DRAFT) → Telegram 審核
    │              └── 失敗 → ANTI_PATTERN (PUBLISHED) → Telegram 通知
    │
Config Drift（獨立觸發）
    │
    DriftDetector → DriftAnalyzer → NemotronDriftInterpreter（意圖分析）
         └── [人工確認後]
               ├── DriftRemediator.rollback() → kubectl apply
               └── GitAdopter.adopt() → git push gitea main
```

---

## 實作優先順序建議

| 優先級 | 方向 | 理由 |
|--------|------|------|
| P0 | 方向二：DIAGNOSE Privacy-First | 最小改動，最高資安價值；修改 3 個現有檔案 |
| P1 | 方向一：Knowledge Auto-Harvesting | 直接延伸現有 AutoRepair 閉環；新增 1 個 Service + 修改 4 個檔案 |
| P2 | 方向三：Config Drift Detection | 全新組件，工作量最大；但戰略價值最高 |

---

## 邊界條件與防禦措施

### 方向一

- Nemotron 生成超時 → 記錄 `generation_failed` log，不阻塞 AutoRepair 主流程（背景異步生成）
- KB 寫入失敗 → 記錄 Sentry error，不影響修復結果
- Telegram 推送失敗 → 記錄 log，Runbook 仍存在 KB 等待後續審核

### 方向二

- 本地全部 Provider 不可用 → REJECT + Telegram 通知 + Sentry alert
- Nemotron CB 開啟 → 直接跳 Ollama，不等 timeout
- Ollama CB 開啟 → 直接 REJECT，不嘗試雲端

### 方向三

- Git 讀取失敗（網路問題）→ 跳過本次掃描，記錄 log，下次 CronJob 補掃
- kubectl 執行失敗 → Telegram 通知失敗，不重試（避免重複操作）
- git push 失敗（衝突）→ Telegram 通知，需人工處理

---

**Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>**
