"""Agent configuration via environment variables.""" import socket from functools import lru_cache from typing import Optional from pydantic import Field from pydantic_settings import BaseSettings, SettingsConfigDict from app import __version__ class Settings(BaseSettings): """Agent settings loaded from environment variables. All settings are frozen after initialization to prevent runtime mutation. """ model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", frozen=True, # Prevent runtime mutation ) # Agent identity agent_version: str = Field(default=__version__, description="Agent version for API headers") hostname: str = Field(default_factory=socket.gethostname, description="Agent hostname") agent_id: Optional[str] = Field(default=None, description="Assigned by orchestrator after registration") # New secure registration (recommended) registration_token: Optional[str] = Field( default=None, description="Registration token from orchestrator. Required for first-time registration." ) # Agent credentials (set after registration, persisted to disk) agent_secret: Optional[str] = Field( default=None, description="Agent secret for authentication. Set after registration." ) # Tenant assignment (derived from registration token, or can be set directly for legacy) tenant_id: Optional[str] = Field( default=None, description="Tenant UUID this agent belongs to. Set after registration." ) # Orchestrator connection # Default URL is for Docker-based dev where orchestrator runs on the host. # When running directly on a Linux tenant server, set ORCHESTRATOR_URL to # the orchestrator's public URL (e.g., "https://orchestrator.letsbe.io"). orchestrator_url: str = Field( default="http://host.docker.internal:8000", description="Orchestrator API base URL" ) # Legacy auth (deprecated - use registration_token + agent_secret instead) agent_token: Optional[str] = Field( default=None, description="[DEPRECATED] Legacy authentication token. Use agent_secret instead." ) # Timing intervals (seconds) heartbeat_interval: int = Field(default=30, ge=5, le=300, description="Heartbeat interval") poll_interval: int = Field(default=5, ge=1, le=60, description="Task polling interval") # Logging log_level: str = Field(default="INFO", description="Log level (DEBUG, INFO, WARNING, ERROR)") log_json: bool = Field(default=True, description="Output logs as JSON") # Resilience max_concurrent_tasks: int = Field(default=3, ge=1, le=10, description="Max concurrent task executions") backoff_base: float = Field(default=1.0, ge=0.1, le=10.0, description="Base backoff time in seconds") backoff_max: float = Field(default=60.0, ge=10.0, le=300.0, description="Max backoff time in seconds") circuit_breaker_threshold: int = Field(default=5, ge=1, le=20, description="Consecutive failures to trip breaker") circuit_breaker_cooldown: int = Field(default=30, ge=10, le=900, description="Cooldown period in seconds") # Security - File operations allowed_file_root: str = Field(default="/opt/letsbe", description="Root directory for file operations") allowed_env_root: str = Field(default="/opt/letsbe/env", description="Root directory for ENV file operations") max_file_size: int = Field(default=10 * 1024 * 1024, description="Max file size in bytes (default 10MB)") # Security - Shell operations shell_timeout: int = Field(default=60, ge=5, le=600, description="Default shell command timeout") # Security - Docker operations allowed_compose_paths: list[str] = Field( default=["/opt/letsbe", "/home/letsbe"], description="Allowed directories for compose files" ) allowed_stacks_root: str = Field( default="/opt/letsbe/stacks", description="Root directory for Docker stack operations" ) # Local persistence pending_results_path: str = Field( default="~/.letsbe-agent/pending_results.json", description="Path for buffering unsent task results" ) credentials_path: str = Field( default="~/.letsbe-agent/credentials.json", description="Path for persisting agent credentials after registration" ) # Playwright browser automation playwright_artifacts_dir: str = Field( default="/opt/letsbe/playwright-artifacts", description="Directory for screenshots, traces, and other browser artifacts" ) playwright_default_timeout_ms: int = Field( default=60000, ge=5000, le=300000, description="Default timeout for Playwright actions in milliseconds" ) playwright_navigation_timeout_ms: int = Field( default=120000, ge=10000, le=300000, description="Timeout for page navigation in milliseconds" ) mcp_service_url: Optional[str] = Field( default=None, description="URL for Playwright MCP sidecar service (for exploratory mode)" ) @lru_cache def get_settings() -> Settings: """Get cached settings instance. Settings are loaded once and cached for the lifetime of the process. """ return Settings()