from __future__ import annotations import os import re from dataclasses import dataclass from pathlib import Path from typing import Optional, Pattern from dotenv import load_dotenv def _bool(name: str, default: bool = False) -> bool: raw = os.getenv(name) if raw is None or raw == "": return default return raw.strip().lower() in {"1", "true", "yes", "on", "y"} def _required(name: str) -> str: val = os.getenv(name) if not val: raise RuntimeError( f"Missing required env var {name}. Copy .env.example to .env and fill it in." ) return val def _optional_pattern(name: str) -> Optional[Pattern[str]]: raw = os.getenv(name) if not raw: return None try: return re.compile(raw) except re.error as exc: raise RuntimeError(f"Invalid regex in {name}: {exc}") from exc @dataclass(frozen=True) class Config: anthropic_api_key: str model: str max_tokens: int gpfs_base: str gpfs_username: str gpfs_password: str verify_tls: bool | str timeout: float allow_writes: bool allow_destructive: bool confirm_mutations: bool path_allow: Optional[Pattern[str]] = None path_deny: Optional[Pattern[str]] = None extra_instructions: str = "" @property def allowed_methods(self) -> set[str]: methods = {"GET"} if self.allow_writes: methods |= {"POST", "PUT"} if self.allow_writes and self.allow_destructive: methods.add("DELETE") return methods def load_config(env_file: Optional[Path] = None) -> Config: if env_file is not None: load_dotenv(env_file, override=False) else: load_dotenv(override=False) verify_raw = os.getenv("GPFS_VERIFY_TLS", "true").strip().lower() ca_bundle = os.getenv("GPFS_CA_BUNDLE") if ca_bundle: verify: bool | str = ca_bundle elif verify_raw in {"0", "false", "no", "off", "n"}: verify = False else: verify = True instructions = os.getenv("GPFS_INSTRUCTIONS", "").strip() instructions_file = os.getenv("GPFS_INSTRUCTIONS_FILE") if instructions_file: path = Path(instructions_file) if path.is_file(): file_text = path.read_text(encoding="utf-8").strip() instructions = f"{instructions}\n\n{file_text}".strip() if instructions else file_text return Config( anthropic_api_key=_required("ANTHROPIC_API_KEY"), model=os.getenv("ANTHROPIC_MODEL", "claude-sonnet-4-6"), max_tokens=int(os.getenv("ANTHROPIC_MAX_TOKENS", "4096")), gpfs_base=_required("GPFS_API_BASE").rstrip("/"), gpfs_username=_required("GPFS_USERNAME"), gpfs_password=_required("GPFS_PASSWORD"), verify_tls=verify, timeout=float(os.getenv("GPFS_TIMEOUT", "30")), allow_writes=_bool("GPFS_ALLOW_WRITES", False), allow_destructive=_bool("GPFS_ALLOW_DESTRUCTIVE", False), confirm_mutations=_bool("GPFS_CONFIRM_MUTATIONS", True), path_allow=_optional_pattern("GPFS_PATH_ALLOW"), path_deny=_optional_pattern("GPFS_PATH_DENY"), extra_instructions=instructions, )