""" 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, ClawBot, PostgreSQL, SigNoz)", ) # ========================================================================== # 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 本機開發 "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) # ========================================================================== DATABASE_URL: str = Field( default="postgresql+asyncpg://awoooi:changeme@192.168.0.188:5432/awoooi_prod", description="PostgreSQL connection URL", ) # ========================================================================== # Redis (192.168.0.188:6380, DB 10-15 for AWOOOI) # ========================================================================== REDIS_URL: str = Field( default="redis://192.168.0.188:6380/10", description="Redis connection URL (DB 10-15 reserved for AWOOOI)", ) # ========================================================================== # External Services - Four Host Architecture # ========================================================================== OLLAMA_URL: str = Field( default="http://192.168.0.188:11434", description="Ollama LLM service URL", ) # Deprecated: use OPENCLAW_URL instead CLAWBOT_URL: str = Field( default="http://192.168.0.188:8088", # 🔧 修正: ClawBot 實際 port 是 8088 description="[Deprecated] ClawBot 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)", ) # ========================================================================== # 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="http://192.168.0.188:4317", description="SigNoz OTLP gRPC endpoint (MUST be 192.168.0.188)", ) 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%)", ) # ========================================================================== # AI Fallback Strategy (ADR-006) # Order: Ollama (local) -> Gemini (cloud) -> Claude (cloud) # ========================================================================== 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") @field_validator("AI_FALLBACK_ORDER", mode="before") @classmethod def parse_ai_fallback(cls, v: str | list[str]) -> list[str]: if isinstance(v, str): 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", ) # ========================================================================== # SQLite Database (CTO-201 Audit Log) # ========================================================================== SQLITE_DATABASE_URL: str = Field( default="sqlite+aiosqlite:///./awoooi.db", description="SQLite database URL for local audit logs (PostgreSQL-ready schema)", ) # ========================================================================== # 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 (正名自 ClawBot) # 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="llama3.2:3b", description="Default Ollama model for RCA analysis", ) OPENCLAW_TIMEOUT: int = Field( default=90, description="Timeout for OpenClaw AI calls (seconds)", ) # ========================================================================== # 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", ) OPENCLAW_TG_USER_WHITELIST: list[int] = Field( default=[], description="Telegram user IDs allowed to sign approvals", ) @field_validator("OPENCLAW_TG_USER_WHITELIST", mode="before") @classmethod def parse_tg_whitelist(cls, v: str | list[int] | int) -> list[int]: if isinstance(v, int): return [v] if isinstance(v, str): if not v.strip(): return [] return [int(uid.strip()) for uid in v.split(",")] return v # ========================================================================== # 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 # ========================================================================== # 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()