Files
CTXD/app/src/ctxd/config.py
T

210 lines
7.4 KiB
Python
Raw Normal View History

2026-06-23 23:54:37 +00:00
"""
Configuration for ctxd — context daemon.
Environment variables take precedence over ctxd.yaml, which takes precedence
over built-in defaults. This allows fully env-driven deployments (Docker,
Compose, Kubernetes) while preserving file-based config for local dev.
2026-06-23 23:54:37 +00:00
"""
import os
from pathlib import Path
def _env_bool(key: str, fallback: bool = False) -> bool:
val = os.environ.get(key, "")
if not val:
return fallback
return val.lower() in ("1", "true", "yes", "on")
def _env_int(key: str, fallback: int) -> int:
val = os.environ.get(key, "")
if not val:
return fallback
try:
return int(val)
except ValueError:
return fallback
def _env_str(key: str, fallback: str = "") -> str:
return os.environ.get(key, fallback)
2026-06-23 23:54:37 +00:00
# Default home directory (~/.ctx) — overridable via CTXD_HOME env var
DEFAULT_HOME = Path(os.environ.get("CTXD_HOME", Path.home() / ".ctx"))
# Built-in defaults (lowest precedence)
2026-06-23 23:54:37 +00:00
DEFAULT_CONFIG = {
"server": {
"host": "0.0.0.0",
"port": 9091,
},
"snapshots": {
"min_keep": 5,
"max_keep": 25,
},
"auth": {
"enabled": False,
"api_key": "",
"external_readonly_key": "",
},
"oauth": {
"enabled": False,
"issuer": "",
"approval_key": "",
"approval_user_id": "admin",
"access_token_ttl_seconds": 3600,
"refresh_token_ttl_seconds": 2592000,
},
"web_sessions": {
"ttl_seconds": 604800,
2026-06-23 23:54:37 +00:00
},
"seed": {
"admin_user": "admin",
"admin_display": "Administrator",
"service_user": "hermes-gateway",
"service_display": "Hermes Agent",
},
}
class CtxConfig:
"""Holds resolved paths and config for a ctxd runtime.
Precedence: env var > ctxd.yaml value > built-in default.
"""
2026-06-23 23:54:37 +00:00
def __init__(self, home: Path | str | None = None, config: dict | None = None):
resolved = Path(home) if home else DEFAULT_HOME
self.home = resolved.resolve()
self._cfg = config or dict(DEFAULT_CONFIG)
# ── Directory layout ──────────────────────────────────────────
@property
def db_path(self) -> Path:
return self.home / "ctxd.db"
@property
def snapshots_dir(self) -> Path:
return self.home / "snapshots"
@property
def projects_dir(self) -> Path:
return self.home / "projects"
@property
def user_dir(self) -> Path:
return self.home / "users"
@property
def config_path(self) -> Path:
return self.home / "ctxd.yaml"
@property
def oauth_state_path(self) -> Path:
return self.home / "oauth_state.json"
@property
def web_sessions_path(self) -> Path:
return self.home / "web_sessions.json"
# ── Database ──────────────────────────────────────────────────
@property
def database_url(self) -> str:
return _env_str("DATABASE_URL")
@property
def use_postgres(self) -> bool:
return bool(self.database_url)
# ── Server ────────────────────────────────────────────────────
2026-06-23 23:54:37 +00:00
@property
def host(self) -> str:
return _env_str("CTXD_HOST", self._cfg.get("server", {}).get("host", "0.0.0.0"))
2026-06-23 23:54:37 +00:00
@property
def port(self) -> int:
return _env_int("CTXD_PORT", self._cfg.get("server", {}).get("port", 9091))
2026-06-23 23:54:37 +00:00
@property
def log_level(self) -> str:
return _env_str("LOG_LEVEL", "info")
# ── Snapshots ─────────────────────────────────────────────────
2026-06-23 23:54:37 +00:00
@property
def min_snapshots(self) -> int:
return _env_int("SNAPSHOT_MIN_KEEP", self._cfg.get("snapshots", {}).get("min_keep", 5))
2026-06-23 23:54:37 +00:00
@property
def max_snapshots(self) -> int:
return _env_int("SNAPSHOT_MAX_KEEP", self._cfg.get("snapshots", {}).get("max_keep", 25))
2026-06-23 23:54:37 +00:00
# ── Auth ──────────────────────────────────────────────────────
@property
def auth_enabled(self) -> bool:
return _env_bool("CTXD_AUTH_ENABLED", self._cfg.get("auth", {}).get("enabled", False))
2026-06-23 23:54:37 +00:00
@property
def api_key(self) -> str:
return _env_str("CTXD_API_KEY", self._cfg.get("auth", {}).get("api_key", ""))
@property
def external_readonly_key(self) -> str:
return _env_str("CTXD_EXTERNAL_READONLY_KEY", self._cfg.get("auth", {}).get("external_readonly_key", ""))
# ── OAuth ─────────────────────────────────────────────────────
@property
def oauth_enabled(self) -> bool:
return _env_bool("OAUTH_ENABLED", self._cfg.get("oauth", {}).get("enabled", False))
@property
def oauth_issuer(self) -> str:
return _env_str("OAUTH_ISSUER", self._cfg.get("oauth", {}).get("issuer", ""))
@property
def oauth_approval_key(self) -> str:
return _env_str("OAUTH_APPROVAL_KEY", self._cfg.get("oauth", {}).get("approval_key", ""))
@property
def oauth_approval_user_id(self) -> str:
return _env_str("OAUTH_APPROVAL_USER_ID", self._cfg.get("oauth", {}).get("approval_user_id", "admin"))
@property
def oauth_access_token_ttl_seconds(self) -> int:
return _env_int("OAUTH_ACCESS_TOKEN_TTL", self._cfg.get("oauth", {}).get("access_token_ttl_seconds", 3600))
@property
def oauth_refresh_token_ttl_seconds(self) -> int:
return _env_int("OAUTH_REFRESH_TOKEN_TTL", self._cfg.get("oauth", {}).get("refresh_token_ttl_seconds", 2592000))
# ── Web Sessions ───────────────────────────────────────────────
@property
def web_session_ttl_seconds(self) -> int:
return _env_int("WEB_SESSION_TTL", self._cfg.get("web_sessions", {}).get("ttl_seconds", 604800))
2026-06-23 23:54:37 +00:00
# ── Bootstrap ─────────────────────────────────────────────────
def ensure_dirs(self):
"""Create all required directories if they don't exist."""
for d in [self.home, self.snapshots_dir, self.projects_dir, self.user_dir]:
d.mkdir(parents=True, exist_ok=True)
@classmethod
def from_home(cls, home: Path | str | None = None) -> "CtxConfig":
"""Load from ctxd.yaml if it exists, otherwise use defaults.
Env vars always take precedence over yaml values at read time.
"""
2026-06-23 23:54:37 +00:00
home = Path(home).resolve() if home else DEFAULT_HOME
cfg_path = home / "ctxd.yaml"
if cfg_path.exists():
import yaml
with open(cfg_path) as f:
data = yaml.safe_load(f) or {}
return cls(home=str(home), config=data)
return cls(home=str(home))
def save(self):
"""Write config to ctxd.yaml. Rarely needed in env-driven deployments."""
2026-06-23 23:54:37 +00:00
import yaml
self.ensure_dirs()
with open(self.config_path, "w") as f:
yaml.dump(self._cfg, f, default_flow_style=False, sort_keys=False)