docs(adr): ADR-086 Telegram UI 清洗規範 + ADR-087 AutoApprove kubectl 閘門

ADR-086: Telegram 通知卡片 UI 清洗規範
- _parse_debate_summary() 設計決定與各 TYPE 欄位清洗規則
- TYPE-3 鍵盤重構:批准/拒絕永遠第一行
- 技術債:_parse_debate_summary 提升模組層級(P1-1)

ADR-087: AutoApprove 安全強化 — kubectl 強制執行閘門
- 條件 1d 設計:_raw_action 語意 + NO_EXECUTABLE_ACTION reason
- Solver Nemo 格式 kubectl 驗證
- 降級指令改為真實 kubectl 唯讀調查
- min_trust_score=0 保留理由記錄(TrustEngine 記憶體持久化技術債)
- P0-2 風險記錄:kubectl exec 未加入 _DESTRUCTIVE_PATTERNS

2026-04-17 ogt + Claude Sonnet 4.6(亞太): Session 技術債清理

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-04-17 15:25:34 +08:00
parent 1ae9e9f389
commit ba8cf6105d
2 changed files with 210 additions and 0 deletions

View File

@@ -0,0 +1,92 @@
# ADR-086: Telegram 通知卡片 UI 清洗規範
**狀態**: 已接受
**日期**: 2026-04-17
**作者**: ogt + Claude Sonnet 4.6
**關聯**: ADR-071通知類型定義、ADR-075Telegram 通知標準)
---
## 背景
Phase 2 多 Agent 協作ADR-082上線後5-Agent debate 的輸出格式為:
```
診斷:{hypothesis};方案:{action};安全審查:{verdict};質疑:{critic}
```
`decision_manager.py` 路由層在準備 Telegram 卡片資料時,多個分支直接截斷 `reasoning` 字串,導致完整 debate_summary 傾倒到 UI 欄位,使用者在 Telegram 看到:
- TYPE-1純資訊`message=reasoning[:200]` → 顯示「診斷:...;方案:...;安全審查:...;質疑:...」
- TYPE-4DConfig Drift`diff_summary=description[:500]` → 顯示 LLM 輸出的 JSON 原文
- TYPE-3人工審核`root_cause=_smt(reasoning, 500)` → 顯示完整 debate_summary 前 500 字
- TYPE-5S資安`threat_behavior=reasoning[:150]` → 顯示 debate_summary 開頭
---
## 決策
### 1. `_parse_debate_summary()` 標準化清洗函數
`decision_manager.py` 函數體內定義(後續應提升為模組層級 — P1-1 技術債):
```python
def _parse_debate_summary(reasoning: str) -> dict[str, str]:
result = {"diagnosis": "", "plan": "", "review": "", "critic": ""}
for part in reasoning.split(""):
part = part.strip()
if part.startswith("診斷:"): result["diagnosis"] = part[3:]
elif part.startswith("方案:"): result["plan"] = part[3:]
elif part.startswith("安全審查:"): result["review"] = part[5:]
elif part.startswith("質疑:"): result["critic"] = part[3:]
return result
```
### 2. 各 TYPE 分支修正規則
| TYPE | 欄位 | 修正前 | 修正後 |
|------|------|--------|--------|
| TYPE-1 | `message` | `reasoning[:200]` | `_parse_debate_summary(reasoning)["diagnosis"] + _smt(..., 200)` |
| TYPE-4D | `diff_summary` | `description[:500]` | JSON Catcher`json.loads` → 格式化;失敗 → `description[:500]` |
| TYPE-3 | `root_cause` | `_smt(reasoning, 500)` | `_parse_debate_summary(reasoning)["diagnosis"] + _smt(..., 300)` |
| TYPE-5S | `threat_behavior` | `reasoning[:150]` | `_parse_debate_summary(reasoning)["diagnosis"] + _smt(..., 150)` |
### 3. TYPE-3 鍵盤按鈕重構
**原問題**動態按鈕K8s 操作類)可能蓋台 [批准][拒絕],使審核按鈕不可見。
**新規則**`telegram_gateway.py:_build_inline_keyboard()`
1. 第一行永遠是 `[✅ 批准][❌ 拒絕]`(無條件)
2. 第二行以後K8s/DB/Host 類操作按鈕(每行最多 3 個)
3. 最後一行:`[📋 詳情][🔕 忽略]`
移除 `requires_human_approval` 參數——由有無 `_dynamic_buttons` 決定佈局,不再需要額外條件。
---
## 後果
- **TYPE-1**:純資訊通知只顯示 AI 診斷摘要,不顯示完整辯論過程
- **TYPE-3**:批准/拒絕永遠可見,不會被操作按鈕覆蓋
- **TYPE-4D**Config Drift 卡片顯示結構化的「建議操作/說明/回滾方案」三段式
- **TYPE-5S**:資安告警的威脅行為欄位只顯示診斷結論
---
## 技術債
| 項目 | 優先 | 說明 |
|------|------|------|
| `_parse_debate_summary` 提升為模組層級 | P1 | 目前定義在函數體內,無法在同模組其他地方重用 |
| `import re as _re` 移出函數體 | P2 | 目前在函數體內 import每次呼叫重建閉包 |
---
## 相關 Commits
| Commit | 說明 |
|--------|------|
| `6baa2e9` | TYPE-8M 三連修(第一波) |
| `418d735` | BUG-A TYPE-1 + BUG-B TYPE-4D第二波 |
| `f421e65` | BUG-C TYPE-3 + 按鈕置頂(第三波) |
| `fb225c5` | P2-2 TYPE-5S secops 分支清洗Code Review 補完) |

View File

@@ -0,0 +1,118 @@
# ADR-087: AutoApprove 安全強化 — kubectl 強制執行閘門
**狀態**: 已接受
**日期**: 2026-04-17
**作者**: ogt + Claude Sonnet 4.6
**關聯**: ADR-070全自動 AIOps 閉環、ADR-082多 Agent 協作)
---
## 背景
Phase 2 多 Agent 協作ADR-082Solver 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. 條件 1dkubectl 格式強制閘
位置:`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` dictPod 重啟後歸零。若將門檻提高至 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 |