|
| 1 | +"""Centralized environment-based configuration for OpenSpace deployments. |
| 2 | +
|
| 3 | +All deployment-relevant settings are read from environment variables with |
| 4 | +sensible defaults. Configuration is frozen (immutable) after creation. |
| 5 | +
|
| 6 | +Environment variables: |
| 7 | + OPENSPACE_MCP_HOST MCP server bind address (default: 0.0.0.0) |
| 8 | + OPENSPACE_MCP_PORT MCP server port (default: 8000) |
| 9 | + OPENSPACE_MCP_TRANSPORT Transport: stdio|sse|streamable-http (default: stdio) |
| 10 | + OPENSPACE_LOG_LEVEL Log level: DEBUG|INFO|WARNING|ERROR (default: INFO) |
| 11 | + OPENSPACE_SHUTDOWN_TIMEOUT Graceful shutdown timeout in seconds (default: 30) |
| 12 | + OPENSPACE_METRICS_ENABLED Enable Prometheus metrics (default: true) |
| 13 | + OPENSPACE_SKILL_STORE_PATH Path to skill store directory (default: skills/) |
| 14 | + OPENSPACE_DEBUG Enable debug mode (default: false) |
| 15 | +
|
| 16 | +Usage:: |
| 17 | +
|
| 18 | + from openspace.deploy.config import DeployConfig |
| 19 | +
|
| 20 | + cfg = DeployConfig.from_env() |
| 21 | + print(cfg.mcp_port) # 8000 |
| 22 | +""" |
| 23 | + |
| 24 | +from __future__ import annotations |
| 25 | + |
| 26 | +import os |
| 27 | +from dataclasses import dataclass |
| 28 | +from typing import Any, Dict |
| 29 | + |
| 30 | +_VALID_TRANSPORTS = ("stdio", "sse", "streamable-http") |
| 31 | +_VALID_LOG_LEVELS = ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL") |
| 32 | + |
| 33 | + |
| 34 | +@dataclass(frozen=True) |
| 35 | +class DeployConfig: |
| 36 | + """Immutable deployment configuration. |
| 37 | +
|
| 38 | + All values have sane defaults and can be overridden via environment |
| 39 | + variables using :meth:`from_env`. |
| 40 | + """ |
| 41 | + |
| 42 | + mcp_host: str = "0.0.0.0" |
| 43 | + mcp_port: int = 8000 |
| 44 | + mcp_transport: str = "stdio" |
| 45 | + log_level: str = "INFO" |
| 46 | + shutdown_timeout: int = 30 |
| 47 | + metrics_enabled: bool = True |
| 48 | + skill_store_path: str = "skills/" |
| 49 | + debug: bool = False |
| 50 | + |
| 51 | + def __post_init__(self) -> None: |
| 52 | + if not (1 <= self.mcp_port <= 65535): |
| 53 | + raise ValueError( |
| 54 | + f"port must be 1-65535, got {self.mcp_port}" |
| 55 | + ) |
| 56 | + if self.mcp_transport not in _VALID_TRANSPORTS: |
| 57 | + raise ValueError( |
| 58 | + f"transport must be one of {_VALID_TRANSPORTS}, " |
| 59 | + f"got {self.mcp_transport!r}" |
| 60 | + ) |
| 61 | + if self.log_level.upper() not in _VALID_LOG_LEVELS: |
| 62 | + raise ValueError( |
| 63 | + f"log_level must be one of {_VALID_LOG_LEVELS}, " |
| 64 | + f"got {self.log_level!r}" |
| 65 | + ) |
| 66 | + if self.shutdown_timeout < 0: |
| 67 | + raise ValueError( |
| 68 | + f"shutdown_timeout must be non-negative, got {self.shutdown_timeout}" |
| 69 | + ) |
| 70 | + |
| 71 | + @classmethod |
| 72 | + def from_env(cls) -> DeployConfig: |
| 73 | + """Create config from environment variables.""" |
| 74 | + return cls( |
| 75 | + mcp_host=os.environ.get("OPENSPACE_MCP_HOST", "0.0.0.0"), |
| 76 | + mcp_port=int(os.environ.get("OPENSPACE_MCP_PORT", "8000")), |
| 77 | + mcp_transport=os.environ.get("OPENSPACE_MCP_TRANSPORT", "stdio"), |
| 78 | + log_level=os.environ.get("OPENSPACE_LOG_LEVEL", "INFO").upper(), |
| 79 | + shutdown_timeout=int( |
| 80 | + os.environ.get("OPENSPACE_SHUTDOWN_TIMEOUT", "30") |
| 81 | + ), |
| 82 | + metrics_enabled=os.environ.get( |
| 83 | + "OPENSPACE_METRICS_ENABLED", "true" |
| 84 | + ).lower() |
| 85 | + in ("true", "1", "yes"), |
| 86 | + skill_store_path=os.environ.get( |
| 87 | + "OPENSPACE_SKILL_STORE_PATH", "skills/" |
| 88 | + ), |
| 89 | + debug=os.environ.get("OPENSPACE_DEBUG", "false").lower() |
| 90 | + in ("true", "1", "yes"), |
| 91 | + ) |
| 92 | + |
| 93 | + def to_safe_dict(self) -> Dict[str, Any]: |
| 94 | + """Serialize config without secrets. |
| 95 | +
|
| 96 | + Returns only deployment-relevant fields. API keys, tokens, |
| 97 | + and other secrets are never included. |
| 98 | + """ |
| 99 | + return { |
| 100 | + "mcp_host": self.mcp_host, |
| 101 | + "mcp_port": self.mcp_port, |
| 102 | + "mcp_transport": self.mcp_transport, |
| 103 | + "log_level": self.log_level, |
| 104 | + "shutdown_timeout": self.shutdown_timeout, |
| 105 | + "metrics_enabled": self.metrics_enabled, |
| 106 | + "skill_store_path": self.skill_store_path, |
| 107 | + "debug": self.debug, |
| 108 | + } |
0 commit comments