fix(awooop): Phase 2 第二批 P0 安全強化 + Redis key 命名空間修正

## P0-05 Callback Nonce 防偽造(ADR-116)
- security_interceptor.py:generate_callback_nonce() 新增 HMAC-SHA256[:16] 附加
  - 新 5-part 格式:{action}:{short_id}:{ts}:{rand}:{hmac16}
  - CALLBACK_HMAC_SECRET 未設定時降級 warning(向後相容)
- security_interceptor.py:parse_callback_data() 新增 5-part 分支 + HMAC 驗證
- config.py:新增 CALLBACK_HMAC_SECRET: str = Field(default="")

## P0-06 Webhook HMAC Replay 防護(ADR-116)
- security_interceptor.py:新增 check_webhook_nonce()(Service 層,get_redis 在此層合法)
- webhooks.py:verify_webhook_signature() 新增兩個可選 Header
  - X-Webhook-Timestamp:±300s 窗口驗證(若提供)
  - X-Webhook-Nonce:呼叫 check_webhook_nonce()(Redis NX dedup,fail open)
  - 移除直接 get_redis import(leWOOOgo 積木化修正)

## P0-11 ollama:current_primary Redis key 遷移 Phase A(ADR-110)
- ollama_auto_recovery.py:_REDIS_PRIMARY_KEY = "platform:ollama:current_primary"
  - 雙寫舊 key "ollama:current_primary"(Phase A 30 天)
  - 讀取以新 key 為主,fallback 舊 key

## P0-12 consensus Redis key 加 project namespace Phase A
- consensus_engine.py:新增 _consensus_key() / _consensus_legacy_key() helper
  - 新 key:{project_id}:consensus:{consensus_id}
  - project_id=None 時 fallback __platform__:consensus:{consensus_id}
  - Phase A 雙寫 + fallback 讀取,現有呼叫方零修改

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-05-04 13:54:38 +08:00
parent 2b2359e367
commit f2f5148ca6
6 changed files with 265 additions and 22 deletions

View File

@@ -337,10 +337,36 @@ class PerformanceAgent(ExpertAgent):
# Consensus Engine
# =============================================================================
CONSENSUS_PREFIX = "consensus:"
# P0-12 修正 2026-05-04 ogt + Claude Sonnet 4.6:
# 舊格式(無 project 前綴consensus:{consensus_id}
# 新格式(含 project 前綴):{project_id}:consensus:{consensus_id}
# 遷移策略Phase A 雙寫 + fallback 讀舊 key待全部遷移後移除 fallback
CONSENSUS_PREFIX = "consensus:" # 舊格式前綴(讀 fallback 用)
PLATFORM_INTERNAL = "__platform__" # project_id 不可得時的 sentinel namespace
CONSENSUS_TTL = 3600 # 1 小時
def _consensus_key(consensus_id: str, project_id: str | None) -> str:
"""
建構 consensus Redis key含 project_id namespace
Args:
consensus_id: 共識 ID如 CON-20260504-ABCD1234
project_id: 租戶 project ID若為 None 則使用 __platform__ sentinel
Returns:
新格式 key{project_id}:consensus:{consensus_id}
或 fallback__platform__:consensus:{consensus_id}
"""
ns = project_id if project_id else PLATFORM_INTERNAL
return f"{ns}:consensus:{consensus_id}"
def _consensus_legacy_key(consensus_id: str) -> str:
"""舊格式 keyPhase A fallback 讀取用)"""
return f"{CONSENSUS_PREFIX}{consensus_id}"
class ConsensusEngine:
"""
共識引擎 - Phase 9.4 核心
@@ -526,6 +552,7 @@ class ConsensusEngine:
consensus_score: float,
recommended_action_type: str,
dissenting: list[str],
project_id: str | None = None,
) -> ConsensusResult:
"""
產生最終決策
@@ -578,8 +605,8 @@ class ConsensusEngine:
dissenting_opinions=dissenting,
)
# 儲存到 Redis
await self._save_consensus(result)
# 儲存到 Redis(含 project_id namespace
await self._save_consensus(result, project_id=project_id)
logger.info(
"consensus_generated",
@@ -595,6 +622,7 @@ class ConsensusEngine:
self,
incident: Incident,
timeout_sec: float = 30.0,
project_id: str | None = None,
) -> ConsensusResult:
"""
執行完整的共識流程
@@ -603,6 +631,11 @@ class ConsensusEngine:
1. 收集意見
2. 計算共識
3. 產生決策
Args:
incident: 要分析的事件
timeout_sec: 超時秒數
project_id: 租戶 project ID用於 Redis key namespace 隔離P0-12
"""
# Step 1: 收集意見
opinions = await self.gather_opinions(incident, timeout_sec)
@@ -610,32 +643,43 @@ class ConsensusEngine:
# Step 2: 計算共識
consensus_score, recommended_action, dissenting = self.calculate_consensus(opinions)
# Step 3: 產生決策
# Step 3: 產生決策(傳入 project_id 供 Redis key namespace 隔離)
result = await self.generate_final_decision(
incident=incident,
opinions=opinions,
consensus_score=consensus_score,
recommended_action_type=recommended_action,
dissenting=dissenting,
project_id=project_id,
)
return result
async def _save_consensus(self, result: ConsensusResult) -> None:
async def _save_consensus(
self,
result: ConsensusResult,
project_id: str | None = None,
) -> None:
"""儲存共識結果到 Redis熱快取+ PG永久記錄
2026-04-26 P2-DB-Fix by Claude — db-expert P0 三修P0.2:
補 PG 寫入 agent_sessions符合 ADR-085 鐵律
Redis TTL 到期不再造成共識記憶消失
P0-12 修正 2026-05-04 ogt + Claude Sonnet 4.6:
Phase A 雙寫:新 key含 project_id 前綴)+ 舊 key向後相容
待全部遷移完成後移除舊 key 寫入
"""
# 1. 既有 Redis 寫(熱快取,保留)
redis_client = get_redis()
key = f"{CONSENSUS_PREFIX}{result.consensus_id}"
await redis_client.set(
key,
json.dumps(result.to_dict()),
ex=CONSENSUS_TTL,
)
payload = json.dumps(result.to_dict())
# 1a. 新 key含 project namespace— Phase A 主要 key
new_key = _consensus_key(result.consensus_id, project_id)
await redis_client.set(new_key, payload, ex=CONSENSUS_TTL)
# 1b. 舊 key無 project 前綴)— Phase A fallback向後相容
legacy_key = _consensus_legacy_key(result.consensus_id)
await redis_client.set(legacy_key, payload, ex=CONSENSUS_TTL)
# 2. 補 PG 永久寫入ADR-085 鐵律 — 失敗不阻斷主流程)
try:
@@ -691,12 +735,34 @@ class ConsensusEngine:
consensus_id=result.consensus_id,
)
async def get_consensus(self, consensus_id: str) -> ConsensusResult | None:
"""取得共識結果"""
redis_client = get_redis()
key = f"{CONSENSUS_PREFIX}{consensus_id}"
async def get_consensus(
self,
consensus_id: str,
project_id: str | None = None,
) -> ConsensusResult | None:
"""取得共識結果
P0-12 修正 2026-05-04 ogt + Claude Sonnet 4.6:
Phase A 雙讀:先讀新 key含 project 前綴),若 miss 再 fallback 舊 key
"""
redis_client = get_redis()
# 先嘗試新格式 key含 project namespace
new_key = _consensus_key(consensus_id, project_id)
data = await redis_client.get(new_key)
if not data:
# Phase A fallback讀舊格式 key無 project 前綴)
legacy_key = _consensus_legacy_key(consensus_id)
data = await redis_client.get(legacy_key)
if data:
logger.info(
"consensus_legacy_key_hit",
consensus_id=consensus_id,
project_id=project_id,
note="Phase A fallback 命中,建議觸發資料遷移",
)
data = await redis_client.get(key)
if data:
return ConsensusResult.from_dict(json.loads(data))
return None