2025-12-03 11:05:54 +01:00
|
|
|
"""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")
|
|
|
|
|
|
|
|
|
|
# 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"
|
|
|
|
|
)
|
|
|
|
|
# Token may be None initially; will be set after registration or provided via env
|
|
|
|
|
agent_token: Optional[str] = Field(default=None, description="Authentication token for API calls")
|
|
|
|
|
|
|
|
|
|
# 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=300, ge=30, le=900, description="Cooldown period in seconds")
|
|
|
|
|
|
|
|
|
|
# Security - File operations
|
2025-12-03 15:20:07 +01:00
|
|
|
allowed_file_root: str = Field(default="/opt/letsbe", description="Root directory for file operations")
|
2025-12-03 11:05:54 +01:00
|
|
|
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"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@lru_cache
|
|
|
|
|
def get_settings() -> Settings:
|
|
|
|
|
"""Get cached settings instance.
|
|
|
|
|
|
|
|
|
|
Settings are loaded once and cached for the lifetime of the process.
|
|
|
|
|
"""
|
|
|
|
|
return Settings()
|