Files
stackpilot/backend/config.py
menzeljandClaude Opus 4.8 59037f4287 Phase 5: multi-host agents (0.5.0)
- stackpilot-agent: slim token-guarded FastAPI (reuses compose_service) exposing
  stack CRUD/lifecycle/logs + system info; same image, different CMD. agent/
  Dockerfile + compose + .env.example.
- Central proxy: Agent model, agent_service (httpx ping/proxy + live status:
  online/offline/unauthorized + hostname/last_seen), routers/agents.py
  (CRUD + ping + proxied stacks/lifecycle/logs/system).
- Frontend: Settings → Remote hosts (add/check/remove, connectivity dot); Stacks
  grouped by host; remote stack detail with lifecycle, live logs, compose/.env edit.

Verified end-to-end: agent+main on a shared network — register (good/bad token),
list/create/start/logs/delete remote stacks, offline detection (502).

Remote backup destinations (SFTP/S3) deferred.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 21:23:17 +00:00

87 lines
2.4 KiB
Python

"""Application settings, loaded from environment variables."""
from __future__ import annotations
import secrets
from functools import lru_cache
from typing import Annotated
from pydantic import field_validator
from pydantic_settings import BaseSettings, NoDecode, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
# Paths
STACKS_DIR: str = "/opt/stackpilot/stacks"
DATA_DIR: str = "/opt/stackpilot/data"
# Security
SECRET_KEY: str = "" # Auto-generated if empty (dev only); set in prod.
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60
REFRESH_TOKEN_EXPIRE_DAYS: int = 30
# Update checker
UPDATE_CHECK_INTERVAL_MINUTES: int = 60
# Notifications (webhook URLs)
NOTIFY_WEBHOOKS: Annotated[list[str], NoDecode] = []
# Docker
DOCKER_SOCKET: str = "/var/run/docker.sock"
HOST_PROC_PATH: str = "/host_proc"
# Throwaway image used to read/write named-volume contents during backup.
BACKUP_HELPER_IMAGE: str = "alpine:latest"
# Multi-host agent: shared bearer token the agent requires on every request.
# Only used when running the agent app (agent_app:app).
AGENT_TOKEN: str = ""
# Host browser sandbox roots
ALLOWED_BROWSE_ROOTS: Annotated[list[str], NoDecode] = [
"/", "/mnt", "/media", "/srv", "/opt",
]
HOST_ROOT_PREFIX: str = "" # e.g. "/host_root" when host / is bind-mounted
# CORS
CORS_ORIGINS: Annotated[list[str], NoDecode] = [
"http://localhost:5009", "http://localhost:5173",
]
# Server
PORT: int = 5008
@field_validator("SECRET_KEY", mode="after")
@classmethod
def _ensure_secret(cls, v: str) -> str:
return v or secrets.token_urlsafe(48)
@field_validator(
"NOTIFY_WEBHOOKS", "ALLOWED_BROWSE_ROOTS", "CORS_ORIGINS", mode="before"
)
@classmethod
def _split_csv(cls, v):
if isinstance(v, str):
v = v.strip()
if not v:
return []
if v.startswith("["): # tolerate a JSON list too
import json
try:
return json.loads(v)
except json.JSONDecodeError:
pass
return [item.strip() for item in v.split(",") if item.strip()]
return v
@lru_cache
def get_settings() -> Settings:
return Settings()
settings = get_settings()