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>
This commit is contained in:
348
apps/api/src/core/config.py
Normal file
348
apps/api/src/core/config.py
Normal file
@@ -0,0 +1,348 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user