"""Config loader for per-sandbox Beaver runtime settings.""" from __future__ import annotations import json import os from pathlib import Path from typing import Any from .schema import AgentDefaultsConfig, BeaverConfig, EmbeddingConfig, ProviderConfig def default_config_path(*, workspace: str | Path | None = None) -> Path: """Resolve the default config path for a single-user sandbox instance. Priority: 1. `BEAVER_CONFIG_PATH` 2. `NANOBOT_CONFIG_PATH` for compatibility during migration 3. `BEAVER_HOME/config.json` 4. `NANOBOT_HOME/config.json` for migration compatibility 5. `/.beaver/config.json` 6. `./.beaver/config.json` """ explicit = os.getenv("BEAVER_CONFIG_PATH") or os.getenv("NANOBOT_CONFIG_PATH") if explicit: return Path(explicit).expanduser() beaver_home = os.getenv("BEAVER_HOME") if beaver_home: return Path(beaver_home).expanduser() / "config.json" nanobot_home = os.getenv("NANOBOT_HOME") if nanobot_home: return Path(nanobot_home).expanduser() / "config.json" root = Path(workspace).expanduser() if workspace is not None else Path.cwd() return root / ".beaver" / "config.json" def load_config( *, workspace: str | Path | None = None, config_path: str | Path | None = None, ) -> BeaverConfig: """Load backend config; missing config is treated as an empty config.""" path = Path(config_path).expanduser() if config_path is not None else default_config_path(workspace=workspace) if not path.exists(): return BeaverConfig(config_path=path) data = json.loads(path.read_text(encoding="utf-8")) if not isinstance(data, dict): raise ValueError(f"Beaver config must be a JSON object: {path}") return BeaverConfig( agents_defaults=_parse_agent_defaults(data), providers=_parse_providers(data.get("providers")), embedding=_parse_embedding(data), config_path=path, ) def _parse_agent_defaults(data: dict[str, Any]) -> AgentDefaultsConfig: agents = _as_dict(data.get("agents")) defaults = _as_dict(agents.get("defaults")) return AgentDefaultsConfig( workspace=_string(defaults.get("workspace") or data.get("workspace")), model=_string(defaults.get("model") or data.get("model")), provider=_string(defaults.get("provider") or data.get("provider")), embedding_model=_string(defaults.get("embeddingModel") or defaults.get("embedding_model") or data.get("embeddingModel")), ) def _parse_providers(raw: Any) -> dict[str, ProviderConfig]: providers: dict[str, ProviderConfig] = {} for name, payload in _as_dict(raw).items(): if not isinstance(payload, dict): continue providers[str(name)] = ProviderConfig( api_key=_string(payload.get("apiKey") or payload.get("api_key")), api_base=_string(payload.get("apiBase") or payload.get("api_base") or payload.get("baseUrl") or payload.get("base_url")), extra_headers=_string_dict(payload.get("extraHeaders") or payload.get("extra_headers") or payload.get("headers")), request_timeout_seconds=_float( payload.get("requestTimeoutSeconds") or payload.get("request_timeout_seconds") or payload.get("timeout") ), ) return providers def _parse_embedding(data: dict[str, Any]) -> EmbeddingConfig: raw = _as_dict(data.get("embedding") or data.get("embeddings")) return EmbeddingConfig( provider=_string(raw.get("provider") or raw.get("provider_name")), model=_string(raw.get("model") or data.get("embeddingModel") or data.get("embedding_model")), api_key=_string(raw.get("apiKey") or raw.get("api_key")), api_base=_string(raw.get("apiBase") or raw.get("api_base") or raw.get("baseUrl") or raw.get("base_url")), extra_headers=_string_dict(raw.get("extraHeaders") or raw.get("extra_headers") or raw.get("headers")), request_timeout_seconds=_float( raw.get("requestTimeoutSeconds") or raw.get("request_timeout_seconds") or raw.get("timeout") ), ) def _as_dict(value: Any) -> dict[str, Any]: return value if isinstance(value, dict) else {} def _string(value: Any) -> str | None: if value is None: return None value = str(value).strip() return value or None def _string_dict(value: Any) -> dict[str, str]: if not isinstance(value, dict): return {} return {str(key): str(item) for key, item in value.items() if item is not None} def _float(value: Any) -> float | None: if value in (None, ""): return None return float(value)