feat(wave6-8): P2.1 fusion + P2.2 governance + P2.4 consensus + Wave 7/8 BLOCKER 修復

承接 Wave 6/7/8 多 engineer 在 agent 限額前完成的代碼,補 commit 解 production
HEAD 隱性 import error(decision_fusion 已被 decision_manager 引用但檔案 untracked)。

新增(後端核心):
- decision_fusion.py (562 行) — P2.1 方法 III(OpenClaw + Hermes + Elephant 三 LLM 融合)
- aiops_timeline.py + aiops_timeline_service.py — critic B4 修復
  /api/v1/aiops/timeline endpoint,DB 存取抽到 service 層遵守 leWOOOgo 積木化
- migrations/p2_decision_fusion_columns.sql + rollback — approval_records fusion 欄位

修改(後端整合):
- decision_manager.py — fusion 三斷鏈修補(critic B1+B2+B3):
  · B1: 寫 _evidence_snapshot_ref 到 token.proposal_data
  · B2: fusion 前計算 complexity_score 並寫 token
  · B3: fusion composite 寫 token.proposal_data["decision_fusion"]
- auto_approve.py — fusion + consensus 認識(critic B3+B5):
  · composite > 0.7 → auto_execute_eligible bypass min_confidence
  · source=consensus_engine + score>=0.6 → 規則可信路徑
- consensus_engine.py — db-fix _save_consensus 重用 agent_sessions
- governance_agent.py — db-fix _alert PG 寫入 ai_governance_events
- approval_db.py — fusion 3 欄位 + 2 partial index + CheckConstraint
- db/models.py — schema 對齊 migration
- core/config.py — vuln #1 修復:OLLAMA_URL/_FALLBACK_URL field_validator
  拒絕公網 IP + 外部域名,僅允許私網/loopback/K8s SVC 白名單
- core/feature_flags.py — P2 fusion + consensus flags
- main.py — governance_agent lifespan 啟動
- failover_alerter.py — Wave8-X2: in-memory dedup fallback(Redis 拒絕後不 fail-open)
- ollama_*.py — metrics 整合 + recovery 改善
- auto_repair_service.py — verifier 接線

新增(測試 2438 行):
- test_decision_fusion.py / test_governance_agent.py / test_consensus_integration.py
- test_p2_db_fixes.py / test_wave8_fusion_fixes.py
- test_config_url_validation.py(vuln #1 12 tests)
- test_failover_alerter.py +Wave8-X2 in-memory dedup 補測

驗收: 116 tests pass (decision_fusion + wave8_fusion + config_url + consensus +
                      governance + p2_db_fixes + failover_alerter)

Conflict resolution:
- 3 檔(config.py + auto_approve.py + decision_manager.py)git stash pop 衝突
  保留 stashed (engineer 最終版),補回 ValueError 「公網 IP」字樣對齊 test

Note: 此 commit 解 production HEAD 隱性 import error
仍未修: vuln #4 prompt injection / debugger B14 quota fail-closed
       / B25-B26 drain_pending_tasks / B8 governance fail alert

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Multiple Engineers (Wave 6/7/8) <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-04-27 08:10:28 +08:00
parent b0bf3783e4
commit cc547736ab
34 changed files with 4205 additions and 25 deletions

View File

@@ -1423,6 +1423,9 @@ class DecisionManager:
token.state = DecisionState.READY
token.proposal_data = proposal_data
token.updated_at = datetime.now(UTC)
# 2026-04-27 Wave8-B1 by Claude — EvidenceSnapshot 不可 JSON 序列化,
# 從 proposal_data pop 後存入 local var供下方 fusion block 讀取
_pre_fusion_evidence = token.proposal_data.pop("_evidence_snapshot_ref", None)
logger.info(
"decision_ready",
@@ -1519,6 +1522,79 @@ class DecisionManager:
logger.debug("yaml_gate_error", error=str(_gate_err))
# 閘門查詢失敗 → 降級繼續正常流程(不阻塞)
# 4d. P2.1 決策融合(方法 III— feature flag 守衛,失敗不阻塞主流程
# 2026-04-26 P2.1 by Claude — decision fusion 方法 III
# 融合分數寫入 token.proposal_data["decision_fusion"],供 TG 卡片 + 學習服務使用。
# 不修改 auto_approve 邏輯(遵循最小變更原則),僅補充 metadata。
if token.state == DecisionState.READY and token.proposal_data:
try:
from src.core.feature_flags import aiops_flags as _ff
if _ff.is_sub_flag_enabled("AIOPS_P2_FUSION_ENABLED"):
from src.services.decision_fusion import (
get_decision_fusion_engine as _get_fusion,
complexity_from_score as _complexity_from_score,
)
_fusion_engine = _get_fusion()
# 2026-04-27 Wave8-B2 by Claude — fusion 三斷鏈修復:
# complexity_score 從未被寫入 token導致 fusion 永遠使用預設值 3。
# 修法:在 fusion 前呼叫 ComplexityScorer.score(),結果寫入 token。
if not token.proposal_data.get("complexity_score"):
try:
from src.services.complexity_scorer import (
get_complexity_scorer as _get_complexity_scorer,
)
_cs_context = {
"affected_services": incident.affected_services or [],
"resource_count": len(incident.affected_services or []),
"severity": (
incident.severity.value
if hasattr(incident.severity, "value")
else "medium"
),
}
_cs_result = _get_complexity_scorer().score(_cs_context)
token.proposal_data["complexity_score"] = _cs_result.score
except Exception as _cs_err:
logger.debug("complexity_score_calc_error", error=str(_cs_err))
# 計算失敗 → 保持預設值 3下面 .get() 仍會正確 fallback
_complexity_score = token.proposal_data.get("complexity_score", 3)
_complexity_tier = _complexity_from_score(
int(_complexity_score) if isinstance(_complexity_score, (int, float, str)) else 3
)
_openclaw_proposal = (
token.proposal_data.get("kubectl_command", "")
or token.proposal_data.get("description", "")
or ""
)
# 2026-04-27 Wave8-B1 by Claude — fusion 三斷鏈修復:
# 從 local var 讀取(已在 line 1428 pop 出,避免 EvidenceSnapshot
# object 污染 _save_token 的 json.dumps 序列化)
_fusion_evidence = _pre_fusion_evidence
_fusion_score = await _fusion_engine.fuse_decision(
incident=incident,
openclaw_proposal=_openclaw_proposal,
evidence=_fusion_evidence,
complexity=_complexity_tier,
)
token.proposal_data["decision_fusion"] = _fusion_score.to_dict()
await self._save_token(token)
# ADR-085 鐵律fusion 分數落地 PG失敗不阻塞主流程
# 2026-04-26 P2-DB-Fix by Claude — db-expert P0 三修P0.3
try:
from src.services.approval_db import get_approval_service as _get_appr_svc
await _get_appr_svc().update_decision_fusion(
incident_id=incident.incident_id,
composite_score=_fusion_score.composite,
complexity_tier=_complexity_tier.value,
fusion_details=_fusion_score.to_dict(),
)
except Exception as _appr_err:
logger.debug("decision_fusion_pg_write_error", error=str(_appr_err))
except Exception as _fusion_err:
logger.debug("decision_fusion_error", error=str(_fusion_err))
# fusion 失敗不阻塞主流程
# 5. ADR-030 Phase 4: 自動執行判斷
if token.state == DecisionState.READY and token.proposal_data:
# 評估是否可以自動執行
@@ -2353,7 +2429,11 @@ class DecisionManager:
snapshot=p2_snapshot,
incident_id=incident.incident_id,
)
return _package_to_proposal_data(package)
# 2026-04-27 Wave8-B1 by Claude — fusion 三斷鏈修復:
# evidence_snapshot 攜帶進 proposal_data避免 singleton 並發污染
_p2_result = _package_to_proposal_data(package)
_p2_result["_evidence_snapshot_ref"] = p2_snapshot
return _p2_result
# snapshot 仍為 None → 降級繼續走原路徑(不阻塞)
logger.warning("p2_no_snapshot_fallback", incident_id=incident.incident_id)
@@ -2459,6 +2539,9 @@ class DecisionManager:
logger.warning("nemoclaw_second_opinion_failed",
incident_id=incident.incident_id, error=str(_soe))
# 2026-04-27 Wave8-B1 by Claude — evidence_snapshot 攜帶進 resultP1 LLM 路徑)
if evidence_snapshot is not None:
result["_evidence_snapshot_ref"] = evidence_snapshot
return result
except asyncio.TimeoutError:
@@ -2753,6 +2836,12 @@ class DecisionManager:
Returns:
DecisionToken
"""
# 2026-04-26 P2.4 by Claude — 12-Agent Consensus 整合
# ENABLE_12AGENT_CONSENSUS=False預設→ 跳過 consensus走原有雙軌決策
# ENABLE_12AGENT_CONSENSUS=True + P0/P1 + use_consensus=True → 走 consensus 路徑
if not settings.ENABLE_12AGENT_CONSENSUS:
return await self.get_or_create_decision(incident, timeout_sec)
# 判斷是否需要共識 (P0/P1 或明確要求)
should_use_consensus = use_consensus and incident.severity.value in ["P0", "P1"]
@@ -2806,7 +2895,10 @@ class DecisionManager:
"risk_level": consensus_result.risk_level,
"kubectl_command": consensus_result.recommended_kubectl,
"reasoning": consensus_result.final_reasoning,
"confidence": 0.0, # 🔴 Consensus Engine 共識分數不是 AI 信心度,設 0
# 2026-04-27 Wave8-B5 by Claude — Consensus auto_approve confidence 修復:
# 原設 0.0 → auto_approve 永遠因 confidence<0.5 拒絕 → 所有 consensus 送人工
# 改為真實共識分數,配合 _is_rule_based 的 consensus_engine+score>=0.6 分支
"confidence": consensus_result.consensus_score,
"agent_count": len(consensus_result.opinions),
"dissenting_opinions": consensus_result.dissenting_opinions,
"from_cache": False,