# ADR-087: AutoApprove 安全強化 — kubectl 強制執行閘門

**狀態**: 已接受  
**日期**: 2026-04-17  
**作者**: ogt + Claude Sonnet 4.6  
**關聯**: ADR-070（全自動 AIOps 閉環）、ADR-082（多 Agent 協作）

---

## 背景

Phase 2 多 Agent 協作（ADR-082）Solver Agent 透過 OpenClaw 的 openclaw_nemo provider 產出修復方案時，有兩條路徑：

1. **標準格式**：`{"candidates": [{"action": "kubectl rollout restart ...", ...}]}`
2. **Nemo 格式**：`{"action_title": "重啟 Crash Looping Pod", "confidence": 0.85}`

Nemo 格式的 `action_title` 是自然語言描述，不是可執行的 kubectl 指令。

### 問題一：auto_approve 誤放行自然語言 action

原有條件：
- 條件 1c：`action` 為空 → 拒絕
- 條件 1d（無）：未驗證 action 是否可執行

結果：Solver Nemo 路徑輸出「重啟 Crash Looping Pod」→ 條件 1c 通過（非空）→ 自動批准 → `kubectl_command` 欄位為空 → 實際無法執行 → incident 卡在 INVESTIGATING 狀態。

### 問題二：Solver 降級動作為自然語言

`_default_action_for_category()` 回傳：
- `"restart_pod"`、`"check_disk_usage"` 等非 kubectl 字串

這些字串通過條件 1c（非空），但無法被執行層使用。

---

## 決策

### 1. 條件 1d：kubectl 格式強制閘

位置：`auto_approve.py:evaluate()`，在條件 1c 之後、條件 2 之前。

```python
# 條件 1d: 自然語言描述不可自動執行
_raw_action = proposal_data.get("action", "") or ""      # 原始值，不 fallback
_kubectl_cmd = proposal_data.get("kubectl_command", "") or ""
_has_kubectl = "kubectl" in _raw_action.lower() or "kubectl" in _kubectl_cmd.lower()
if not _has_kubectl:
    return self._reject(
        reason=AutoApproveReason.NO_EXECUTABLE_ACTION,
        detail=f"Action '{_raw_action[:60]}' is natural language — no kubectl command",
        ...
    )
```

**設計要點**：
- 使用 `proposal_data.get("action", "")` 原始值（非 line 232 已 fallback 的 `action` 變數），避免 fallback 語意混淆（Code Review P0-1）
- 新增 `AutoApproveReason.NO_EXECUTABLE_ACTION` enum 值，與 `NO_PLAYBOOK`（無匹配 Playbook）語意分離，防止污染 KM 飛輪學習資料分類（Code Review P1-2）

### 2. Solver Nemo 格式：kubectl 驗證

位置：`solver_agent.py:_extract_candidates()`

```python
if "action_title" in parsed and "candidates" not in parsed:
    action_title = str(parsed.get("action_title", ""))
    if "kubectl" not in action_title.lower():
        return []   # → 觸發 _degraded_plan，輸出真實 kubectl 調查指令
```

### 3. 降級指令改為真實 kubectl

`_default_action_for_category()` 改為按類別回傳唯讀調查指令：

| Category | 降級指令 |
|----------|---------|
| pod/kube/crash | `kubectl get pods -n awoooi-prod -o wide` |
| disk/storage/pvc | `kubectl exec -n awoooi-prod deployment/postgresql -- df -h` |
| cpu/load | `kubectl top pods -n awoooi-prod --sort-by=cpu` |
| memory/oom | `kubectl top pods -n awoooi-prod --sort-by=memory` |
| network/connect | `kubectl get services -n awoooi-prod` |
| default | `kubectl get pods -n awoooi-prod` |

**設計原則**：降級指令均為唯讀調查操作，無副作用，blast_radius=20（低）。

---

## min_trust_score 設計決定

`min_trust_score: int = 0` 保持不變。

**理由**：TrustEngine 資料儲存於記憶體（`_trust_records` dict），Pod 重啟後歸零。若將門檻提高至 0.8，每次 Pod 重啟後所有動作都無法自動執行，直到累積足夠歷史記錄。這在生產環境中不可接受。

**正確方向**：將 TrustEngine 持久化至 PostgreSQL（已列為 Phase 3.5 技術債）。

---

## 後果

- 自然語言 action 不再通過 auto_approve，強制走 TYPE-3 人工審核
- Solver 降級路徑輸出真實 kubectl 調查指令，可被 auto_approve 正確評估
- `NO_EXECUTABLE_ACTION` reason 與 `NO_PLAYBOOK` 區分，KM 飛輪學習資料分類正確

---

## 已知風險 (P0-2 — 下次 Security Review)

`kubectl exec` 指令（降級路徑中的 disk 類別）被允許通過條件 1d，但 `kubectl exec` 可以在容器內執行任意命令。目前降級指令固定為 `df -h`（唯讀），但若未來 LLM 輸出包含 `kubectl exec` 的高風險變體，現有 `_DESTRUCTIVE_PATTERNS` 不包含 `exec` 關鍵字，可能漏過。

**建議**：下次 Security Review 時評估是否將 `kubectl exec` 加入 `_DESTRUCTIVE_PATTERNS` 或獨立 allow-list。

---

## 相關 Commits

| Commit | 說明 |
|--------|------|
| `93205ce` | P1 kubectl gate + P2 Nemo path 強制 |
| `fb225c5` | Code Review P0-1 action fallback + P1-2 NO_EXECUTABLE_ACTION enum |
