Some checks failed
CD Pipeline / build-and-deploy (push) Failing after 35s
## Critical 修復 (C1-C5) - C1: git rm --cached 03-secrets.yaml(CHANGE_ME 模板不再追蹤) - C2: git rm --cached awoooi.db + .gitignore 加 *.db(SQLite HARD_RULES 違規) - C3: sentry-tunnel SENTRY_HOST 改為 process.env fallback - C4: config.py DATABASE_URL 移除 changeme default,改為必填 - C5: run_migration.py 改為 os.environ["DATABASE_URL"] ## Major 修復 (M1-M4) - M1: auto_repair /execute 加 CSRF 保護 + AutoRepairPanel.tsx 同步 - M2: drift /rollback /adopt 加 CSRF 保護(/internal/scan 保持無 CSRF) - M3: terminal /intent 加 CSRF 保護 + terminal.store.ts 同步 - M4: live-dashboard HOST_IPS + host-grid VIP 改為 env var ## 其他 - 新增 apps/web/.env.example(6 個 env var 說明) - K8s deployment-web 補入 3 個新 env var - 整合測試:新增 aider_event_repository + ai_router_feedback 真實 DB 測試 - test_terminal.py CSRF dependency override 修復 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
636 lines
26 KiB
Python
636 lines
26 KiB
Python
"""
|
||
AWOOOI API Configuration
|
||
========================
|
||
Pydantic Settings + Environment Variables
|
||
|
||
ADR-005: BFF Architecture
|
||
ADR-006: AI Fallback Strategy (Ollama -> Gemini -> Claude)
|
||
|
||
Four Iron Laws:
|
||
1. Async-First
|
||
2. CORS Whitelist (NO wildcard)
|
||
3. Pydantic Config (this file)
|
||
4. structlog
|
||
"""
|
||
|
||
from functools import lru_cache
|
||
from typing import Literal
|
||
|
||
from pydantic import Field, HttpUrl, field_validator
|
||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||
|
||
|
||
class Settings(BaseSettings):
|
||
"""
|
||
Application settings from environment variables
|
||
|
||
All settings can be overridden via .env file or environment variables.
|
||
"""
|
||
|
||
model_config = SettingsConfigDict(
|
||
env_file=".env",
|
||
env_file_encoding="utf-8",
|
||
case_sensitive=True,
|
||
extra="ignore",
|
||
)
|
||
|
||
# ==========================================================================
|
||
# Application
|
||
# ==========================================================================
|
||
VERSION: str = "1.0.0"
|
||
ENVIRONMENT: Literal["dev", "prod"] = "dev"
|
||
DEBUG: bool = False
|
||
LOG_LEVEL: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = "INFO"
|
||
SYSTEM_NAME: str = "awoooi"
|
||
|
||
# ==========================================================================
|
||
# Mock Mode - 開發時模擬外部服務
|
||
# ==========================================================================
|
||
MOCK_MODE: bool = Field(
|
||
default=False,
|
||
description="Enable mock mode for external services (Redis, Ollama, OpenClaw, PostgreSQL, SigNoz)",
|
||
)
|
||
|
||
# ==========================================================================
|
||
# ==========================================================================
|
||
# Phase 24: AI Provider Registry (ADR-052)
|
||
# 2026-04-02 ogt: 絞殺者開關 — true=新 AIRouter, false=舊 openclaw.py if/else
|
||
# 回滾指令: kubectl set env deployment/awoooi-api USE_AI_ROUTER=false
|
||
# ==========================================================================
|
||
USE_AI_ROUTER: bool = Field(
|
||
default=False,
|
||
description="Phase 24: True=新 AIRouter 路由, False=舊 openclaw.py fallback chain",
|
||
)
|
||
|
||
# ==========================================================================
|
||
# aider-watch v2 integration (2026-04-20 ADR-091)
|
||
# 整合 Mac aider CLI 監控進 awoooi 飛輪(events → incident → ai_router feedback)
|
||
# 回滾:kubectl set env deployment/awoooi-api USE_AIDER_FEEDBACK=false
|
||
# ==========================================================================
|
||
AIDER_WEBHOOK_SECRET: str = Field(
|
||
default="",
|
||
description="HMAC secret for /api/v1/aider/events webhook verification",
|
||
)
|
||
AIDER_EVENTS_STREAM_KEY: str = Field(
|
||
default="signals:aider:events",
|
||
description="Redis stream key for aider event ingestion",
|
||
)
|
||
AIDER_PATTERN_EXTRACT_INTERVAL_HOURS: float = Field(
|
||
default=24.0,
|
||
description="Aider event pattern extraction interval (future use)",
|
||
)
|
||
USE_AIDER_FEEDBACK: bool = Field(
|
||
default=False,
|
||
description="Phase 24 A8: True=ai_router.route() 讀 aider 成功率調權重, False=不讀(預設)",
|
||
)
|
||
|
||
# Phase 22: OpenClaw + Nemotron 協作 (ADR-044)
|
||
# 2026-03-31 Claude Code: 統帥批准實作
|
||
#
|
||
# 功能:
|
||
# - ENABLE_NEMOTRON_COLLABORATION: 啟用 OpenClaw + Nemotron 雙軌協作
|
||
# - NEMOTRON_TIMEOUT_SECONDS: Nemotron API 呼叫超時
|
||
# - NEMOTRON_ASYNC_UPDATE: 異步更新模式 (先推 OpenClaw,後更新 Nemotron)
|
||
#
|
||
# 回滾指令: kubectl set env deployment/awoooi-api ENABLE_NEMOTRON_COLLABORATION=false
|
||
# ==========================================================================
|
||
ENABLE_NEMOTRON_COLLABORATION: bool = Field(
|
||
default=True,
|
||
description="Phase 22: True=啟用 OpenClaw+Nemotron 協作, False=僅 OpenClaw",
|
||
)
|
||
NEMOTRON_TIMEOUT_SECONDS: int = Field(
|
||
default=45,
|
||
description="Phase 22: Nemotron API 呼叫超時 (秒)",
|
||
)
|
||
NEMOTRON_ASYNC_UPDATE: bool = Field(
|
||
default=True,
|
||
description="Phase 22: True=異步更新 (先推 OpenClaw), False=同步等待",
|
||
)
|
||
# 2026-04-05 Claude Code: Phase 25 P0 v4.3 — DIAGNOSE timeout 依實測修正
|
||
# 實測依據 (2026-04-05):
|
||
# NIM (nvidia/nemotron-mini-4b-instruct): 2.2s~27.3s,平均 10.6s → 60s timeout (27s * 2 + buffer)
|
||
# Ollama llama3.2:3b CPU-only: 238s 回 {"ok":true} → 不可用於生產,timeout 保留但實際走 NIM
|
||
NEMOTRON_DIAGNOSE_TIMEOUT_SECONDS: int = Field(
|
||
default=60,
|
||
description="Phase 25 P0: DIAGNOSE NIM timeout (秒),實測 2.2-27.3s avg 10.6s,60s 含 buffer",
|
||
)
|
||
OLLAMA_DIAGNOSE_TIMEOUT_SECONDS: int = Field(
|
||
default=200,
|
||
description="Phase 25 P0: Ollama timeout (秒),實測 CPU-only 238s,保留欄位但 DIAGNOSE 不再走 Ollama",
|
||
)
|
||
|
||
# ==========================================================================
|
||
# Gitea — ADR-057 adopt() Gitea PR API (2026-04-05)
|
||
# ==========================================================================
|
||
GITEA_API_URL: str = Field(
|
||
default="http://192.168.0.110:3001",
|
||
description="Gitea 內網 API base URL",
|
||
)
|
||
GITEA_API_TOKEN: str = Field(
|
||
default="",
|
||
description="Gitea API Token(需 write:repository scope),ADR-057 adopt() 使用",
|
||
)
|
||
GITEA_REPO_OWNER: str = Field(default="wooo", description="Gitea repo owner")
|
||
GITEA_REPO_NAME: str = Field(default="awoooi", description="Gitea repo name")
|
||
|
||
# ==========================================================================
|
||
# CORS - 嚴格白名單 (無 UAT, 無 wildcard)
|
||
# ==========================================================================
|
||
CORS_ORIGINS: list[str] = Field(
|
||
default=[
|
||
"http://localhost:3000",
|
||
"http://localhost:3001",
|
||
"http://localhost:3002",
|
||
"http://localhost:3003",
|
||
"http://localhost:3333",
|
||
"http://192.168.0.168:3000", # 168 MacBook 本機開發
|
||
"http://192.168.0.188:3000", # 188 本機開發
|
||
"http://192.168.0.125:32335", # K3s VIP NodePort (staging/QA)
|
||
"http://192.168.0.120:32335", # K3s node-1 NodePort
|
||
"http://192.168.0.121:32335", # K3s node-2 NodePort
|
||
"https://awoooi.wooo.work",
|
||
],
|
||
description="Allowed CORS origins - NO wildcards allowed",
|
||
)
|
||
|
||
@field_validator("CORS_ORIGINS", mode="before")
|
||
@classmethod
|
||
def parse_cors_origins(cls, v: str | list[str]) -> list[str]:
|
||
if isinstance(v, str):
|
||
origins = [origin.strip() for origin in v.split(",")]
|
||
else:
|
||
origins = v
|
||
# Security check: reject wildcards
|
||
if "*" in origins:
|
||
raise ValueError("Wildcard (*) is NOT allowed in CORS_ORIGINS")
|
||
return origins
|
||
|
||
# ==========================================================================
|
||
# Database (PostgreSQL on 192.168.0.188)
|
||
# ==========================================================================
|
||
# 2026-04-22 ogt: 移除含 changeme 的 default,改為必填。
|
||
# 來源: K8s Secret awoooi-secrets → DATABASE_URL
|
||
DATABASE_URL: str = Field(
|
||
description="PostgreSQL connection URL (必填,從 K8s Secret awoooi-secrets → DATABASE_URL 取得)",
|
||
)
|
||
|
||
# ==========================================================================
|
||
# Redis (192.168.0.188:6380, DB 0 - 與 OpenClaw 共用)
|
||
# ==========================================================================
|
||
REDIS_URL: str = Field(
|
||
default="redis://192.168.0.188:6380/0",
|
||
description="Redis connection URL (DB 0 shared with OpenClaw)",
|
||
)
|
||
|
||
# ==========================================================================
|
||
# External Services - Four Host Architecture
|
||
# ==========================================================================
|
||
OLLAMA_URL: str = Field(
|
||
default="http://192.168.0.111:11434", # 2026-04-08 ogt: 切換至 M1 Pro (40+ tok/s vs 0.45 tok/s)
|
||
description="Ollama LLM service URL",
|
||
)
|
||
# 2026-04-12 ogt: 心跳必須確認載入的 Ollama 模型清單
|
||
OLLAMA_REQUIRED_MODELS: list[str] = Field(
|
||
default=["nomic-embed-text", "qwen2.5:7b-instruct", "deepseek-r1:14b"],
|
||
description="HeartbeatReportService 探測必要模型是否載入",
|
||
)
|
||
# Deprecated: use OPENCLAW_URL instead
|
||
CLAWBOT_URL: str = Field(
|
||
default="http://192.168.0.188:8088", # 🔧 修正: OpenClaw 實際 port 是 8088
|
||
description="[Deprecated] Legacy OpenClaw URL - use OPENCLAW_URL",
|
||
)
|
||
KALI_SCANNER_URL: str = Field(
|
||
default="http://192.168.0.112:8080",
|
||
description="Kali security scanner URL",
|
||
)
|
||
SIGNOZ_URL: str = Field(
|
||
default="http://192.168.0.188:3301",
|
||
description="SigNoz observability URL",
|
||
)
|
||
CLICKHOUSE_URL: str = Field(
|
||
default="http://192.168.0.188:8123",
|
||
description="ClickHouse HTTP API URL (SignOz backend, direct query)",
|
||
)
|
||
|
||
# ==========================================================================
|
||
# Sentry Self-Hosted (Phase 10: Error Tracking + AI Analysis)
|
||
# 端點: http://192.168.0.110:9000 (DevOps 金庫)
|
||
# ==========================================================================
|
||
SENTRY_SELF_HOSTED_URL: str = Field(
|
||
default="http://192.168.0.110:9000",
|
||
description="Sentry Self-Hosted API URL",
|
||
)
|
||
SENTRY_ORG: str = Field(
|
||
default="sentry",
|
||
description="Sentry organization slug",
|
||
)
|
||
SENTRY_PROJECT: str = Field(
|
||
default="awoooi-api",
|
||
description="Sentry project slug",
|
||
)
|
||
SENTRY_AUTH_TOKEN: str = Field(
|
||
default="",
|
||
description="Sentry Auth Token for API access (from K8s Secret)",
|
||
)
|
||
|
||
# ==========================================================================
|
||
# OpenTelemetry (可觀測性鐵律)
|
||
# 四主機架構強制校驗: OTEL 必須指向 192.168.0.188
|
||
# ==========================================================================
|
||
OTEL_ENABLED: bool = Field(
|
||
default=True,
|
||
description="Enable OpenTelemetry tracing (disable in MOCK_MODE)",
|
||
)
|
||
OTEL_EXPORTER_OTLP_ENDPOINT: str = Field(
|
||
default="192.168.0.188:24317",
|
||
description="SigNoz OTLP gRPC endpoint (Host port 24317 -> Container 4317) - NO http:// prefix for gRPC",
|
||
)
|
||
OTEL_SERVICE_NAME: str = Field(
|
||
default="awoooi-api",
|
||
description="Service name for tracing",
|
||
)
|
||
OTEL_TRACES_SAMPLER_ARG: float = Field(
|
||
default=1.0,
|
||
description="Trace sampling rate (1.0 = 100%)",
|
||
)
|
||
|
||
# ==========================================================================
|
||
# Langfuse LLMOps (Phase 15.1)
|
||
# LLM 呼叫追蹤、成本監控、Prompt 版本管理
|
||
# 端點: http://192.168.0.110:3100 (DevOps 金庫)
|
||
# ==========================================================================
|
||
LANGFUSE_ENABLED: bool = Field(
|
||
default=True,
|
||
description="Enable Langfuse LLM observability",
|
||
)
|
||
LANGFUSE_URL: str = Field(
|
||
default="http://192.168.0.110:3100",
|
||
description="Langfuse self-hosted URL",
|
||
)
|
||
LANGFUSE_PUBLIC_KEY: str = Field(
|
||
default="",
|
||
description="Langfuse public key (from K8s Secret)",
|
||
)
|
||
LANGFUSE_SECRET_KEY: str = Field(
|
||
default="",
|
||
description="Langfuse secret key (from K8s Secret)",
|
||
)
|
||
|
||
# ==========================================================================
|
||
# AI Fallback Strategy (ADR-006 v1.3 + ADR-036)
|
||
# Order: Ollama (local) -> Gemini (cloud) -> Claude (cloud)
|
||
# Tool Calling: Nemotron (專用) -> Gemini -> Claude
|
||
# ==========================================================================
|
||
AI_FALLBACK_ORDER: list[str] = Field(
|
||
default=["ollama", "gemini", "claude"],
|
||
description="AI provider fallback order",
|
||
)
|
||
GEMINI_API_KEY: str = Field(default="", description="Google Gemini API key")
|
||
CLAUDE_API_KEY: str = Field(default="", description="Anthropic Claude API key")
|
||
# 2026-03-29 ogt: ADR-036 Nemotron Tool Calling 整合
|
||
NVIDIA_API_KEY: str = Field(
|
||
default="",
|
||
description="NVIDIA NIM API key for Nemotron Tool Calling (ADR-036)",
|
||
)
|
||
# 2026-04-09 Claude Sonnet 4.6: Ollama Tool Calling — 替代 NVIDIA 雲端,本機推理
|
||
USE_OLLAMA_TOOL_CALLING: bool = Field(
|
||
default=True,
|
||
description="使用 Ollama 本機做 Tool Calling,取代 NVIDIA NIM 雲端 (44s→5s)",
|
||
)
|
||
OLLAMA_TOOL_MODEL: str = Field(
|
||
default="llama3.1:8b",
|
||
description="Ollama Tool Calling 模型 (支援 function calling 格式)",
|
||
)
|
||
|
||
@field_validator("AI_FALLBACK_ORDER", mode="before")
|
||
@classmethod
|
||
def parse_ai_fallback(cls, v: str | list[str]) -> list[str]:
|
||
"""
|
||
解析 AI_FALLBACK_ORDER,支援三種格式:
|
||
1. JSON: '["gemini","ollama","claude"]'
|
||
2. CSV: 'gemini,ollama,claude'
|
||
3. List: ["gemini", "ollama", "claude"]
|
||
|
||
2026-03-27 修復: ConfigMap 用 JSON 格式,原本只支援 CSV
|
||
"""
|
||
import json
|
||
|
||
if isinstance(v, str):
|
||
v = v.strip()
|
||
# 嘗試 JSON 解析 (ConfigMap 格式)
|
||
if v.startswith("["):
|
||
try:
|
||
parsed = json.loads(v)
|
||
return [p.strip().lower() for p in parsed]
|
||
except json.JSONDecodeError:
|
||
pass # 降級到 CSV 解析
|
||
# CSV 格式
|
||
return [provider.strip().lower() for provider in v.split(",")]
|
||
return [p.lower() for p in v]
|
||
|
||
# ==========================================================================
|
||
# Kubernetes / K3s (CTO-201)
|
||
# ==========================================================================
|
||
KUBECONFIG_PATH: str = Field(
|
||
default="k3s-prod.yaml",
|
||
description="Path to kubeconfig file for K3s cluster (192.168.0.120)",
|
||
)
|
||
K8S_NAMESPACE_DEFAULT: str = Field(
|
||
default="default",
|
||
description="Default Kubernetes namespace for operations",
|
||
)
|
||
K8S_OPERATION_TIMEOUT: int = Field(
|
||
default=30,
|
||
description="Timeout for K8s operations in seconds",
|
||
)
|
||
K8S_API_KEY: str = Field(
|
||
default="",
|
||
description="API Key for K8s admin endpoints (X-K8s-Api-Key header)",
|
||
)
|
||
|
||
# ==========================================================================
|
||
# 統帥鐵律:禁止 SQLite (AWOOOI 憲法)
|
||
# ==========================================================================
|
||
# ❌ 已移除 SQLITE_DATABASE_URL - 違反 AWOOOI 憲法
|
||
# 所有持久化必須使用 PostgreSQL (DATABASE_URL)
|
||
# 審計日誌請使用 PostgreSQL audit_logs 表
|
||
# ==========================================================================
|
||
|
||
# ==========================================================================
|
||
# Cache TTL (seconds)
|
||
# ==========================================================================
|
||
CACHE_TTL_DASHBOARD: int = Field(default=300, description="Dashboard cache TTL (5 min)")
|
||
CACHE_TTL_HOST_STATUS: int = Field(default=30, description="Host status cache TTL (30 sec)")
|
||
CACHE_TTL_AI_RESPONSE: int = Field(default=3600, description="AI response cache TTL (1 hour)")
|
||
|
||
# ==========================================================================
|
||
# Health Check Timeouts (seconds)
|
||
# ==========================================================================
|
||
HEALTH_CHECK_TIMEOUT: float = Field(default=5.0, description="Health check timeout")
|
||
|
||
# ==========================================================================
|
||
# Phase 5: OpenClaw AI Engine (正名自 OpenClaw)
|
||
# Synced from models.json - Ollama First Strategy
|
||
# ==========================================================================
|
||
OPENCLAW_URL: str = Field(
|
||
default="http://192.168.0.188:8088", # 🔧 修正: OpenClaw 實際 port 是 8088
|
||
description="OpenClaw AI Agent service URL",
|
||
)
|
||
OPENCLAW_DEFAULT_MODEL: str = Field(
|
||
default="deepseek-r1:14b", # 2026-04-08 ogt: SRE最強推理,M1 Pro實測 13 tok/s
|
||
description="Default Ollama model for RCA analysis",
|
||
)
|
||
OPENCLAW_TIMEOUT: int = Field(
|
||
default=30, # 2026-04-14 Claude Sonnet 4.6: 從 120s 改 30s,配合 ADR-052 GAP-B4
|
||
# 25s LLM hard timeout + 5s buffer。原 120s 違反 defense-in-depth 設計,
|
||
# 導致 Ollama 過載時 thread 飢餓 120s 才降級 fallback。
|
||
description="Timeout for OpenClaw AI calls (seconds, aligned with GAP-B4 25s)",
|
||
)
|
||
|
||
# ==========================================================================
|
||
# Phase 5: Telegram Gateway (繼承自 AIOPS)
|
||
# CISO 要求: Token 必須存放於 K8s Secret,此處為開發預設
|
||
# ==========================================================================
|
||
OPENCLAW_TG_BOT_TOKEN: str = Field(
|
||
default="",
|
||
description="Telegram Bot Token (from K8s Secret in prod)",
|
||
)
|
||
OPENCLAW_TG_CHAT_ID: str = Field(
|
||
default="",
|
||
description="Telegram Chat ID for notifications",
|
||
)
|
||
# 使用 str 避免 pydantic-settings 自動 JSON 解析
|
||
# Pydantic v2 禁止底線開頭的 Field 名稱
|
||
OPENCLAW_TG_USER_WHITELIST: str = Field(
|
||
default="",
|
||
description="Telegram user IDs allowed to sign approvals (comma-separated or JSON array)",
|
||
)
|
||
# 2026-03-23 架構修正 (遵循 C-Suite 決議)
|
||
# 鐵律: .188 為唯一大腦,禁止腦分裂
|
||
# OpenClaw (192.168.0.188) = 唯一 Telegram Gateway
|
||
# AWOOOI API (K8s) = Web API + Sensor,不做 Polling
|
||
TELEGRAM_ENABLE_POLLING: bool = Field(
|
||
default=False,
|
||
description="Telegram Polling (False: OpenClaw handles it; True: only if OpenClaw unavailable)",
|
||
)
|
||
# 2026-04-03 ogt: SRE 戰情室群組三頭政治 (Triumvirate) — ADR-053
|
||
OPENCLAW_BOT_TOKEN: str = Field(
|
||
default="",
|
||
description="@OpenClawAwoooI_Bot Token — 群組內代表 OpenClaw AI 發言",
|
||
)
|
||
NEMOTRON_BOT_TOKEN: str = Field(
|
||
default="",
|
||
description="@NemoTronAwoooI_Bot Token — 群組內代表 NemoClaw AI 發言",
|
||
)
|
||
SRE_GROUP_CHAT_ID: str = Field(
|
||
default="",
|
||
description="AwoooI SRE 戰情室群組 Chat ID",
|
||
)
|
||
|
||
def get_tg_user_whitelist(self) -> list[int]:
|
||
"""Parse comma-separated or JSON array user IDs to list[int]"""
|
||
raw = self.OPENCLAW_TG_USER_WHITELIST
|
||
# 已是 list(測試 monkeypatch 或程式碼直接傳入)
|
||
if isinstance(raw, list):
|
||
return [int(uid) for uid in raw]
|
||
if not raw or not raw.strip():
|
||
return []
|
||
# Handle JSON array format or comma-separated
|
||
if raw.startswith("["):
|
||
import json
|
||
return json.loads(raw)
|
||
return [int(uid.strip()) for uid in raw.split(",")]
|
||
|
||
# ==========================================================================
|
||
# Phase 5: Webhook Security (CISO 要求)
|
||
# HMAC-SHA256 簽章驗證 + Nonce 防重放
|
||
# ==========================================================================
|
||
WEBHOOK_HMAC_SECRET: str = Field(
|
||
default="",
|
||
description="HMAC secret for webhook signature verification",
|
||
)
|
||
WEBHOOK_NONCE_TTL: int = Field(
|
||
default=300,
|
||
description="Nonce TTL in seconds for replay attack prevention",
|
||
)
|
||
|
||
# ==========================================================================
|
||
# Phase 5: Shadow Mode (物理繳械)
|
||
# 統帥戰略 C: 接入真實告警,但物理閹割 AI 破壞力
|
||
# ==========================================================================
|
||
SHADOW_MODE_ENABLED: bool = Field(
|
||
default=True,
|
||
description="Shadow Mode: Force dry-run for all K8s operations (safe by default)",
|
||
)
|
||
SHADOW_MODE_LOG_ONLY: bool = Field(
|
||
default=True,
|
||
description="Shadow Mode: Only log operations without any K8s API calls",
|
||
)
|
||
|
||
# ==========================================================================
|
||
# Phase 5: Context Gatherer (首席架構師要求)
|
||
# 日誌清洗: 僅保留 ERROR/FATAL/CRITICAL
|
||
# ==========================================================================
|
||
CONTEXT_LOG_LEVELS: list[str] = Field(
|
||
default=["ERROR", "FATAL", "CRITICAL", "WARN", "WARNING"],
|
||
description="Log levels to include in AI context (ERROR Only principle)",
|
||
)
|
||
CONTEXT_MAX_LINES: int = Field(
|
||
default=100,
|
||
description="Maximum log lines to include in context",
|
||
)
|
||
|
||
@field_validator("CONTEXT_LOG_LEVELS", mode="before")
|
||
@classmethod
|
||
def parse_log_levels(cls, v: str | list[str]) -> list[str]:
|
||
if isinstance(v, str):
|
||
return [level.strip().upper() for level in v.split(",")]
|
||
return [level.upper() for level in v]
|
||
|
||
# ==========================================================================
|
||
# Notification Plugins (leWOOOgo Output)
|
||
# Fail-Fast: HttpUrl 驗證確保啟動時攔截設定錯誤
|
||
# ==========================================================================
|
||
DISCORD_WEBHOOK_URL: str = Field(
|
||
default="",
|
||
description="Discord webhook URL for sending execution reports",
|
||
)
|
||
SLACK_WEBHOOK_URL: str = Field(
|
||
default="",
|
||
description="Slack webhook URL for sending execution reports",
|
||
)
|
||
NOTIFICATION_ENABLED: bool = Field(
|
||
default=True,
|
||
description="Enable post-execution notifications",
|
||
)
|
||
|
||
@field_validator("DISCORD_WEBHOOK_URL", "SLACK_WEBHOOK_URL", mode="before")
|
||
@classmethod
|
||
def validate_webhook_url(cls, v: str | None) -> str:
|
||
"""
|
||
Fail-Fast Webhook URL 驗證
|
||
|
||
- 空字串 = 停用 (合法)
|
||
- 非空字串必須是合法 HttpUrl (否則啟動失敗)
|
||
"""
|
||
if not v or v.strip() == "":
|
||
return ""
|
||
# Validate as HttpUrl (raises ValueError if invalid)
|
||
HttpUrl(v)
|
||
return v
|
||
|
||
# ==========================================================================
|
||
# Phase 23 (ADR-048): Sentry Webhook → OpenClaw AI Triage
|
||
# Sentry Issue Alert Webhook 簽章驗證 (sentry-hook-signature header)
|
||
# ==========================================================================
|
||
SENTRY_WEBHOOK_SECRET: str = Field(
|
||
default="",
|
||
description="Sentry Webhook secret for HMAC-SHA256 signature verification",
|
||
)
|
||
|
||
# ==========================================================================
|
||
# Phase 13.1: GitHub Webhook → OpenClaw 整合
|
||
# Gitea PR/Push 事件自動觸發 AI 代碼審查 (ADR-059: GitHub → Gitea 遷移)
|
||
# ==========================================================================
|
||
GITEA_WEBHOOK_SECRET: str = Field(
|
||
default="",
|
||
description="Gitea Webhook secret for HMAC-SHA256 signature verification (X-Gitea-Signature)",
|
||
)
|
||
GITEA_ALLOWED_REPOS: str = Field(
|
||
default="wooo/awoooi",
|
||
description="Comma-separated list of allowed Gitea repositories (e.g., 'wooo/awoooi')",
|
||
)
|
||
|
||
def get_gitea_allowed_repos(self) -> list[str]:
|
||
"""Parse comma-separated allowed repos to list"""
|
||
# 2026-04-05 Claude Code (ADR-059): GitHub → Gitea webhook 遷移
|
||
raw = self.GITEA_ALLOWED_REPOS
|
||
if not raw or not raw.strip():
|
||
return []
|
||
return [repo.strip() for repo in raw.split(",") if repo.strip()]
|
||
|
||
# ==========================================================================
|
||
# MCP Phase 2b: Prometheus MCP Server (ADR-071, 2026-04-11 Claude Sonnet 4.6)
|
||
# ==========================================================================
|
||
PROMETHEUS_URL: str = Field(
|
||
default="http://192.168.0.188:9090",
|
||
description="Prometheus server URL",
|
||
)
|
||
PROMETHEUS_MCP_ENABLED: bool = Field(
|
||
default=True,
|
||
description="啟用 Prometheus MCP Provider",
|
||
)
|
||
|
||
# MCP Phase 2a: SSH MCP Server (ADR-071, 2026-04-11 Claude Sonnet 4.6)
|
||
# ==========================================================================
|
||
SSH_MCP_ENABLED: bool = Field(
|
||
default=False,
|
||
description="啟用 SSH MCP Provider(需 K8s Secret ssh-mcp-key 掛載)",
|
||
)
|
||
SSH_MCP_ALLOWED_HOSTS: str = Field(
|
||
default="192.168.0.188,192.168.0.110,192.168.0.111",
|
||
description="允許 SSH 的主機 IP 清單(逗號分隔)",
|
||
)
|
||
|
||
# MCP Phase 3: ArgoCD MCP Server (2026-04-11 Claude Sonnet 4.6)
|
||
# ==========================================================================
|
||
ARGOCD_URL: str = Field(
|
||
default="https://192.168.0.125:30443",
|
||
description="ArgoCD API Server URL(K3s NodePort HTTPS)",
|
||
)
|
||
ARGOCD_API_TOKEN: str = Field(
|
||
default="",
|
||
description="ArgoCD API Token(從 K8s Secret 取得)",
|
||
)
|
||
ARGOCD_MCP_ENABLED: bool = Field(
|
||
default=True,
|
||
description="啟用 ArgoCD MCP Provider(需 ARGOCD_API_TOKEN)",
|
||
)
|
||
|
||
# MCP Phase 3: Sentry MCP Server (2026-04-11 Claude Sonnet 4.6)
|
||
# ==========================================================================
|
||
SENTRY_MCP_ENABLED: bool = Field(
|
||
default=True,
|
||
description="啟用 Sentry MCP Provider(需 SENTRY_AUTH_TOKEN)",
|
||
)
|
||
|
||
# ==========================================================================
|
||
# Phase 13.2: Grafana MCP Tool (#83)
|
||
# ==========================================================================
|
||
GRAFANA_URL: str = Field(
|
||
default="http://192.168.0.188:3000",
|
||
description="Grafana server URL",
|
||
)
|
||
GRAFANA_API_KEY: str = Field(
|
||
default="",
|
||
description="Grafana API key for authentication (Bearer token)",
|
||
)
|
||
|
||
# ==========================================================================
|
||
# Computed Properties
|
||
# ==========================================================================
|
||
@property
|
||
def is_production(self) -> bool:
|
||
"""Check if running in production"""
|
||
return self.ENVIRONMENT == "prod"
|
||
|
||
@property
|
||
def four_hosts(self) -> dict[str, str]:
|
||
"""Four host architecture reference"""
|
||
return {
|
||
"devops": "192.168.0.110", # Harbor, GH Runner
|
||
"security": "192.168.0.112", # Kali Scanner
|
||
"k3s_master": "192.168.0.120", # K3s Master
|
||
"ai_web": "192.168.0.188", # Nginx, Postgres, Redis, Ollama
|
||
}
|
||
|
||
|
||
@lru_cache
|
||
def get_settings() -> Settings:
|
||
"""Get cached settings instance"""
|
||
return Settings()
|
||
|
||
|
||
# Singleton for direct import
|
||
settings = get_settings()
|