import os from pathlib import Path from typing import Dict, Any, List, Optional from pydantic import BaseModel, Field, field_validator from pydantic_settings import BaseSettings BASE_DIR = Path(__file__).resolve().parent.parent DATA_DIR = Path(os.getenv("HUB_DATA_DIR", BASE_DIR / "data")) PERSONAS_DIR = DATA_DIR / "personas" CLONES_DIR = DATA_DIR / "clones" DB_PATH = DATA_DIR / "hub.db" class UniverseConfig(BaseModel): id: str name: str tagline: str description: str backend_url: str scope: str accent_token: str icon: str persona_file: str enabled: bool = True supports_files: bool = False files_url: Optional[str] = None is_external_app: bool = False class Settings(BaseSettings): app_name: str = "Hermes Hub" host: str = "0.0.0.0" port: int = 8080 debug: bool = False # NAS & VPS Network Configurations nas_tailscale_ip: str = os.getenv("NAS_TAILSCALE_IP", "100.86.197.88") vps_tailscale_ip: str = os.getenv("VPS_TAILSCALE_IP", "100.94.90.119") # Backends hermes_tt_url: str = os.getenv("HERMES_TT_URL", "http://100.86.197.88:3010") hermes_nyora_url: str = os.getenv("HERMES_NYORA_URL", "http://100.86.197.88:3020") hermes_perso_url: str = os.getenv("HERMES_PERSO_URL", "http://100.86.197.88:3031") hermes_nabil_url: str = os.getenv("HERMES_NABIL_URL", "http://hermes-nabil:9119") hermes_dsh_url: str = os.getenv("HERMES_DSH_URL", "http://dsh-vps:3080") dsh_filebrowser_url: str = os.getenv("DSH_FILEBROWSER_URL", "http://dsh-vps-filebrowser:8080") # Backend Passwords hermes_tt_password: str = os.getenv("HERMES_TT_PASSWORD", "XiEdCtyWETbzpQ7dxrRyvAYu") hermes_nyora_password: str = os.getenv("HERMES_NYORA_PASSWORD", "juoKPfPGuo39wKCn9WJ0kY0_") hermes_perso_password: str = os.getenv("HERMES_PERSO_PASSWORD", "-6Q12oViKsgZgIL82Kspa_dV") hermes_nabil_username: str = os.getenv("HERMES_NABIL_USERNAME", "nabil") hermes_nabil_password: str = os.getenv("HERMES_NABIL_PASSWORD", "NabilMasterHermes2026!") # Hub Secret — OBLIGATOIRE, aucun fallback codé en dur (Fail-Fast au démarrage) hub_secret: str = Field(..., min_length=16, description="Clé secrète maîtresse requise pour Hermes Hub") @field_validator("hub_secret") @classmethod def validate_secret(cls, v: str) -> str: if not v or v.strip() == "" or "change-this" in v or "hermes-hub-master-key-2026" in v: raise ValueError("HUB_SECRET doit être défini avec une clé sécurisée valide et ne doit pas utiliser de valeur par défaut.") return v class Config: env_file = ".env" extra = "ignore" try: settings = Settings() except Exception as e: if "HUB_SECRET" in os.environ: raise e settings = None UNIVERSES: Dict[str, UniverseConfig] = { "tt": UniverseConfig( id="tt", name="Tunisie Telecom", tagline="Achats Zone Sud", description="Direction Régionale — Marchés, RLA & Appels d'Offres", backend_url=os.getenv("HERMES_TT_URL", "http://100.86.197.88:3010"), scope="tt", accent_token="--accent-tt", icon="briefcase", persona_file="tt.yaml", supports_files=False, is_external_app=False ), "nyora": UniverseConfig( id="nyora", name="Nyora", tagline="Venture & Dr Nexum", description="Projets entrepreneuriaux, conseil & veille stratégique", backend_url=os.getenv("HERMES_NYORA_URL", "http://100.86.197.88:3020"), scope="nyora", accent_token="--accent-nyora", icon="sparkles", persona_file="nyora.yaml", supports_files=False, is_external_app=False ), "perso": UniverseConfig( id="perso", name="Personnel", tagline="Famille & Santé", description="Espace privé, santé familiale, gestion du quotidien", backend_url=os.getenv("HERMES_PERSO_URL", "http://100.86.197.88:3031"), scope="perso", accent_token="--accent-perso", icon="home", persona_file="perso.yaml", supports_files=False, is_external_app=False ), "nabil": UniverseConfig( id="nabil", name="Nabil Master", tagline="Orchestration & Code", description="Master Agent VPS, exécution de code & supervision DSH", backend_url=os.getenv("HERMES_NABIL_URL", "http://hermes-nabil:9119"), scope="nabil", accent_token="--accent-nabil", icon="terminal", persona_file="nabil.yaml", supports_files=True, files_url=os.getenv("DSH_FILEBROWSER_URL", "http://dsh-vps-filebrowser:8080"), is_external_app=False ), "dsh": UniverseConfig( id="dsh", name="DeepSeek Harness", tagline="DSH Autonomous Agent", description="Plateforme DeepSeek Harness : agents autonomes, sous-agents, trajectoires et tâches", backend_url=os.getenv("HERMES_DSH_URL", "http://dsh-vps:3080"), scope="dsh", accent_token="--accent-dsh", icon="cpu", persona_file="dsh.yaml", supports_files=False, is_external_app=True ) } def get_universe(universe_id: str) -> UniverseConfig: if universe_id not in UNIVERSES: raise KeyError(f"Universe '{universe_id}' does not exist.") return UNIVERSES[universe_id]