from __future__ import annotations import os from dataclasses import dataclass from functools import lru_cache def _bool(name: str, default: bool) -> bool: value = os.getenv(name) if value is None: return default return value.strip().lower() in {"1", "true", "yes", "on"} def _int(name: str, default: int) -> int: value = os.getenv(name) return int(value) if value else default def _float(name: str, default: float) -> float: value = os.getenv(name) return float(value) if value else default @dataclass(frozen=True, slots=True) class Settings: model_api_base_url: str model_api_key: str deepseek_api_base_url: str deepseek_api_key: str openwebui_forward_jwt_secret: str internal_provider_key: str internal_gateway_key: str database_url: str redis_url: str gateway_url: str execution_provider: str workspace_image: str workspace_network_enabled: bool workspace_memory_limit: str workspace_cpu_limit: float workspace_pids_limit: int workspace_ssh_host: str workspace_download_max_bytes: int model_timeout_seconds: int tool_timeout_seconds: int max_tool_output_chars: int @classmethod def from_env(cls) -> Settings: return cls( model_api_base_url=os.getenv("MODEL_API_BASE_URL", "https://api.k1412.top").rstrip("/"), model_api_key=os.getenv("MODEL_API_KEY", ""), deepseek_api_base_url=os.getenv("DEEPSEEK_API_BASE_URL", "https://api.deepseek.com").rstrip("/"), deepseek_api_key=os.getenv("DEEPSEEK_API_KEY", ""), openwebui_forward_jwt_secret=os.getenv("OPENWEBUI_FORWARD_JWT_SECRET", ""), internal_provider_key=os.getenv("INTERNAL_PROVIDER_KEY", ""), internal_gateway_key=os.getenv("INTERNAL_GATEWAY_KEY", ""), database_url=os.getenv("DATABASE_URL", "sqlite+aiosqlite:///./.runtime/agent.db"), redis_url=os.getenv("REDIS_URL", "redis://localhost:6379/0"), gateway_url=os.getenv("GATEWAY_URL", "http://gateway:8001").rstrip("/"), execution_provider=os.getenv("EXECUTION_PROVIDER", "local-docker"), workspace_image=os.getenv("WORKSPACE_IMAGE", "k1412-agent-workspace:dev"), workspace_network_enabled=_bool("WORKSPACE_NETWORK_ENABLED", True), workspace_memory_limit=os.getenv("WORKSPACE_MEMORY_LIMIT", "2g"), workspace_cpu_limit=_float("WORKSPACE_CPU_LIMIT", 2.0), workspace_pids_limit=_int("WORKSPACE_PIDS_LIMIT", 512), workspace_ssh_host=os.getenv("WORKSPACE_SSH_HOST", ""), workspace_download_max_bytes=_int("WORKSPACE_DOWNLOAD_MAX_BYTES", 128 * 1024 * 1024), model_timeout_seconds=_int("MODEL_TIMEOUT_SECONDS", 600), tool_timeout_seconds=_int("TOOL_TIMEOUT_SECONDS", 900), max_tool_output_chars=_int("MAX_TOOL_OUTPUT_CHARS", 24_000), ) def validate_runtime(self) -> None: missing = [ name for name, value in ( ("MODEL_API_KEY", self.model_api_key), ("DEEPSEEK_API_KEY", self.deepseek_api_key), ("OPENWEBUI_FORWARD_JWT_SECRET", self.openwebui_forward_jwt_secret), ("INTERNAL_PROVIDER_KEY", self.internal_provider_key), ("INTERNAL_GATEWAY_KEY", self.internal_gateway_key), ) if not value ] if missing: raise RuntimeError(f"Missing required runtime secrets: {', '.join(missing)}") self._validate_internal_secret_lengths() def validate_gateway(self) -> None: missing = [ name for name, value in ( ("OPENWEBUI_FORWARD_JWT_SECRET", self.openwebui_forward_jwt_secret), ("INTERNAL_GATEWAY_KEY", self.internal_gateway_key), ) if not value ] if missing: raise RuntimeError(f"Missing required gateway secrets: {', '.join(missing)}") self._validate_internal_secret_lengths() if self.execution_provider not in {"local-docker", "ssh-docker"}: raise RuntimeError("EXECUTION_PROVIDER must be local-docker or ssh-docker") if self.execution_provider == "ssh-docker" and not self.workspace_ssh_host: raise RuntimeError("WORKSPACE_SSH_HOST is required for ssh-docker") def _validate_internal_secret_lengths(self) -> None: weak = [ name for name, value in ( ("OPENWEBUI_FORWARD_JWT_SECRET", self.openwebui_forward_jwt_secret), ("INTERNAL_PROVIDER_KEY", self.internal_provider_key), ("INTERNAL_GATEWAY_KEY", self.internal_gateway_key), ) if value and len(value.encode("utf-8")) < 32 ] if weak: raise RuntimeError(f"Internal secrets must be at least 32 bytes: {', '.join(weak)}") @lru_cache(maxsize=1) def get_settings() -> Settings: return Settings.from_env()