""" 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-25 Claude Engineer-C (P1.1): Ollama 188 CPU-only 備援 (方案 C) # 若空字串則 OllamaFailoverManager 僅使用 OLLAMA_URL(單節點模式) OLLAMA_FALLBACK_URL: str = Field( default="", description="Ollama CPU-only fallback URL (188 備援,P1.1),空字串=停用", ) # 2026-04-27 Wave8-X2 by Claude — vuln #1 URL endpoint poisoning 修復 # 攻擊情境:攻擊者改 ConfigMap OLLAMA_FALLBACK_URL=http://attacker.com:11434 # → ai_router 盲信 → C&C 通道。修法:啟動時拒絕非私網/loopback 的外部 URL。 @field_validator("OLLAMA_URL", "OLLAMA_FALLBACK_URL") @classmethod def _validate_ollama_url(cls, v: str) -> str: """ Ollama URL 安全校驗:拒絕非 private/loopback IP 或非已知服務名稱的 URL。 允許: - 空字串(未設定,OLLAMA_FALLBACK_URL 預設空字串) - 已知 Kubernetes Service hostname 白名單 - 私網 IP(RFC 1918)或 loopback(127.x.x.x) 拒絕: - 公網 IP(8.8.8.8) - 外部域名(attacker.com) """ if not v: return v import ipaddress from urllib.parse import urlparse try: host = urlparse(v).hostname or "" except Exception as exc: raise ValueError(f"OLLAMA URL 格式無效:{v!r},錯誤:{exc}") from exc if not host: raise ValueError(f"OLLAMA URL 缺少 hostname:{v!r}") # Kubernetes Service hostname 白名單(K8s DNS + 開發別名) _ALLOWED_HOSTNAMES = { "localhost", "ollama", "ollama-svc", "ollama-fallback-svc", "ollama-111", "ollama-188", } if host in _ALLOWED_HOSTNAMES: return v # 否則必須是 private/loopback IP try: ip = ipaddress.ip_address(host) except ValueError: # hostname 不是 IP 也不在白名單 → 拒絕 raise ValueError( f"OLLAMA URL host 不允許的外部域名:{host!r}(完整 URL:{v!r})" ",必須使用私網 IP 或已知 K8s Service hostname" ) if not (ip.is_private or ip.is_loopback): raise ValueError( f"OLLAMA URL 必須是私網/loopback IP 或已知 K8s SVC," f"收到公網 IP {host!r}({v!r}),可能是端點中毒攻擊" ) return v # 2026-04-25 Claude Engineer-C (P1.1): Ollama 健康檢測推理測試模型 OLLAMA_HEALTH_CHECK_MODEL: str = Field( default="qwen2.5:7b-instruct", description="OllamaHealthMonitor 推理測試使用模型(P1.1)", ) # 2026-04-12 ogt: 心跳必須確認載入的 Ollama 模型清單 OLLAMA_REQUIRED_MODELS: list[str] = Field( default=["nomic-embed-text", "qwen2.5:7b-instruct", "deepseek-r1:14b"], description="HeartbeatReportService 探測必要模型是否載入", ) # 2026-04-25 critic-fix Part2 H7 by Claude Engineer-C2 # Gemini 帳單熔斷:每日呼叫上限,超過改走 188+Nemotron # 超過上限後寫 Redis key ollama:gemini_daily_count:{date},TTL 86400s GEMINI_DAILY_QUOTA: int = Field( default=1000, description="每日 Gemini 呼叫上限,超過切到 188+Nemotron(P1.1 帳單熔斷)", ) # 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", ) # ADR-093 灰階切流:True 時由 notification_matrix 控制所有路由 # False(預設)時維持舊行為(TYPE-3/4/4D/8M 僅 DM) TG_GROUP_CUTOVER: bool = Field( default=False, description="ADR-093: True 時啟用 notification_matrix 路由矩陣,取代 telegram_gateway 硬碼", ) # ADR-095 2026-04-25 ogt + Claude Sonnet 4.6: 12-Agent ConsensusEngine ENABLE_12AGENT_CONSENSUS: bool = Field( default=False, description="ADR-095: 啟用 12-Agent ConsensusEngine weights(預設關閉)", ) # 2026-04-27 P3.1-T2 by Claude — Tier-2 感知強化:DiagnosisAggregator 整合開關 # 預設關閉:DiagnosisAggregator 與 PreDecisionInvestigator 存在 K8s+SignOz 資料重疊, # 待重疊分析完成(獨立審查任務)確認互補性後再啟用。 # 啟用:kubectl set env deployment/awoooi-api ENABLE_DIAGNOSIS_AGGREGATOR=true ENABLE_DIAGNOSIS_AGGREGATOR: bool = Field( default=False, description="P3.1-T2: 啟用 DiagnosisAggregator 在 PreDecisionInvestigator 中補充 Pod 診斷(預設關閉,待重疊分析完成後評估)", ) 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", ) # 2026-04-24 Claude Sonnet 4.6 (ADR-094): Telegram Webhook Secret Token # 與 setWebhook API 呼叫時的 secret_token 相同;空字串 → dev 環境跳過驗證 TELEGRAM_WEBHOOK_SECRET: str = Field( default="", description="Telegram Webhook Secret Token(setWebhook 設定的同一值)", ) # 2026-04-24 Claude Sonnet 4.6 (ADR-095 WS4): Hermes NL 自然語言閘道 # false=不啟用(預設),true=啟用 @mention 問答(需 ANTHROPIC_API_KEY) HERMES_NL_ENABLED: bool = Field( default=False, description="Hermes NL 對話功能開關(ADR-095)", ) TELEGRAM_BOT_USERNAME: str = Field( default="tsenyangbot", description="Telegram Bot username(不含 @),用於 @mention 識別", ) 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()