Files
awoooi/apps/api/src/core/config.py
Your Name d0591c54b0
Some checks failed
CD Pipeline / build-and-deploy (push) Failing after 35s
fix(security): 體健修復 — 7項 Critical/Major 安全問題全修
## 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>
2026-04-22 01:27:39 +08:00

636 lines
26 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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.6s60s 含 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 scopeADR-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 URLK3s 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()