Files
awoooi/apps/api/src/core/config.py
OG T 196d269b92 feat: add all application source code
- apps/api: FastAPI backend with Dockerfile
- apps/web: Next.js frontend with Dockerfile
- apps/sensor: Signal collection agent
- packages: shared packages

Co-Authored-By: Claude <noreply@anthropic.com>
2026-03-22 18:57:44 +08:00

349 lines
13 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, 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()