"""Env-driven configuration. All settings flow through environment variables so the same image runs in prod (compose.yml env_file) and tests (monkeypatched envs). No config files. """ from __future__ import annotations import os from dataclasses import dataclass, field from pathlib import Path # Default LAN allowlist mirrors the rules baked into the network: anything # inside RFC1918 plus loopback. Override with CRAFTING_LAN_CIDRS if a deploy # wants stricter scoping. DEFAULT_LAN_CIDRS = ( "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "127.0.0.0/8", "::1/128", ) @dataclass(frozen=True) class Config: db_path: Path workspace_root: Path log_dir: Path admin_bearer_path: Path max_concurrent_jobs: int api_port: int api_bind: str default_job_timeout_secs: int lan_cidrs: tuple[str, ...] workspace_gc_interval_secs: int workspace_gc_age_secs: int def load() -> Config: cidrs_raw = os.environ.get("CRAFTING_LAN_CIDRS", "").strip() if cidrs_raw: cidrs = tuple(c.strip() for c in cidrs_raw.split(",") if c.strip()) else: cidrs = DEFAULT_LAN_CIDRS return Config( db_path=Path(os.environ.get("CRAFTING_DB", "/data/crafting.db")), workspace_root=Path(os.environ.get("CRAFTING_WORKSPACE", "/workspace")), log_dir=Path(os.environ.get("CRAFTING_LOG_DIR", "/data/jobs")), admin_bearer_path=Path(os.environ.get("CRAFTING_ADMIN_BEARER", "/data/admin-bearer.txt")), max_concurrent_jobs=int(os.environ.get("CRAFTING_MAX_CONCURRENT", "4")), api_port=int(os.environ.get("CRAFTING_PORT", "8810")), api_bind=os.environ.get("CRAFTING_BIND", "0.0.0.0"), default_job_timeout_secs=int(os.environ.get("CRAFTING_DEFAULT_JOB_TIMEOUT", "1800")), lan_cidrs=cidrs, workspace_gc_interval_secs=int(os.environ.get("CRAFTING_GC_INTERVAL", "3600")), workspace_gc_age_secs=int(os.environ.get("CRAFTING_GC_AGE", "86400")), )