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:
OG T
2026-03-22 18:57:44 +08:00
parent a840bf975b
commit 196d269b92
245 changed files with 42207 additions and 6 deletions

348
apps/api/src/core/config.py Normal file
View 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()