"""Application configuration, loaded from environment variables.""" from __future__ import annotations from functools import lru_cache from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict(env_file=".env", extra="ignore", case_sensitive=False) # Core security secret_key: str = "dev-insecure-secret-change-me" credential_encryption_key: str = "" access_token_expire_minutes: int = 60 * 12 algorithm: str = "HS256" first_admin_email: str = "admin@agentic.local" first_admin_password: str = "changeme" # Database / cache database_url: str = "postgresql+asyncpg://agentic:agentic@postgres:5432/agentic_os" redis_url: str = "redis://redis:6379/0" # CORS: comma-separated allowed browser origins (no wildcard with credentials). cors_allow_origins: str = "http://localhost:3000" # LLM routing litellm_base_url: str = "http://litellm:4000" litellm_master_key: str = "sk-litellm-master-change-me" ollama_base_url: str = "http://host.docker.internal:11434" local_triage_model: str = "qwen2.5:7b-instruct" cloud_reasoning_model: str = "openrouter/deepseek/deepseek-chat" # Tiered LLM routing (local → gemini → deepseek → openrouter) llm_local_model: str = "qwen2.5:7b-instruct" llm_economy_gemini: str = "gemini-2.5-flash" llm_economy_deepseek: str = "deepseek-chat" llm_economy_openrouter: str = "deepseek/deepseek-chat" llm_premium_gemini: str = "gemini-2.5-pro" llm_premium_deepseek: str = "deepseek-reasoner" llm_premium_openrouter: str = "anthropic/claude-3.5-sonnet" llm_auto_escalate: bool = True llm_max_tier: str = "premium" llm_cloud_provider_order: str = "gemini,deepseek,openrouter" # Cloud API keys gemini_api_key: str = "" openrouter_api_key: str = "" openrouter_provisioning_key: str = "" deepseek_api_key: str = "" # Balance polling balance_poll_interval_seconds: int = 900 min_balance_usd: float = 1.0 # Gitea / MCP repos gitea_base_url: str = "http://10.77.30.250:3000" gitea_username: str = "nearxos" gitea_token: str = "" mcp_repos: str = "ssh-generic-mcp,fortigate-mcp,fortiswitch-mcp,pfsense-mcp,proxmox-mcp,asterisk-mcp" # Obsidian obsidian_vault_repo: str = "homelab-vault" obsidian_tasks_folder: str = "agentic-os-tasks" obsidian_auto_push: bool = True # Project memory MCP memory_mcp_url: str = "http://10.77.30.184:8787/mcp" memory_mcp_token: str = "" memory_task_project_id: str = "agentic-os-task" memory_search_projects: str = "agentic-os-task,agentic-os,global" obsidian_search_paths: str = "agentic-os-tasks,Meta/Agent-Memory" # Troubleshooting decision rules (YAML, editable via Admin → Rules) troubleshooting_rules_path: str = "/app/rules/troubleshooting.yaml" # Agent skills (procedural guides in skills/*/SKILL.md) skills_path: str = "/app/skills" skills_max_matched: int = 2 skills_max_body_chars: int = 8000 # Runtime paths (inside container) runtime_dir: str = "/runtime" # MCP self-development: agent patches MCP source on tool failure / missing capability mcp_development_enabled: bool = True mcp_dev_require_approval: bool = True # when true, apply/reload waits for human approval mcp_dev_auto_push: bool = False # when false, patch applies + reload; git push needs approval mcp_dev_model_tier: str = "premium" # Worker: max tasks executed in parallel (MCP sessions are shared — use 1 for same-vessel safety) worker_max_concurrent_tasks: int = 1 @property def cors_origin_list(self) -> list[str]: return [o.strip() for o in self.cors_allow_origins.split(",") if o.strip()] @property def mcp_repo_list(self) -> list[str]: return [r.strip() for r in self.mcp_repos.split(",") if r.strip()] def gitea_clone_url(self, repo: str) -> str: """Authenticated clone URL for a Gitea repo (preserves http/https from base URL).""" base = self.gitea_base_url.rstrip("/") if self.gitea_token: from urllib.parse import urlparse parsed = urlparse(base if "://" in base else f"http://{base}") scheme = parsed.scheme or "http" host = parsed.netloc or parsed.path return f"{scheme}://{self.gitea_username}:{self.gitea_token}@{host}/{self.gitea_username}/{repo}.git" return f"{base}/{self.gitea_username}/{repo}.git" @lru_cache def get_settings() -> Settings: return Settings() settings = get_settings()