Coverage for agentos/config_validator.py: 0%
92 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-06 10:59 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-06 10:59 +0800
1"""
2Startup configuration validator for AgentOS.
4Runs at server boot before accepting connections. Validates:
5- Required env vars present
6- Database connectivity (optional)
7- Redis connectivity (optional)
8- OTLP endpoint reachable (optional, timeout 3s)
9- Disk write permissions on log/output dirs
10- SSL/TLS cert validity if HTTPS enabled
12Usage:
13 from agentos.config_validator import validate_startup
15 issues = validate_startup()
16 if issues.has_critical:
17 raise SystemExit(issues.report())
18"""
20from __future__ import annotations
22import os
23import socket
24import logging
25from dataclasses import dataclass, field
26from enum import Enum
27from typing import Optional
29logger = logging.getLogger(__name__)
32class Severity(str, Enum):
33 CRITICAL = "critical" # Server MUST NOT start
34 ERROR = "error" # Feature degraded
35 WARNING = "warning" # Non-blocking concern
36 OK = "ok"
39@dataclass
40class Issue:
41 component: str
42 message: str
43 severity: Severity
44 suggestion: str = ""
47@dataclass
48class ValidationReport:
49 issues: list[Issue] = field(default_factory=list)
51 @property
52 def has_critical(self) -> bool:
53 return any(i.severity == Severity.CRITICAL for i in self.issues)
55 def add(self, component: str, message: str, severity: Severity, suggestion: str = ""):
56 self.issues.append(Issue(component, message, severity, suggestion))
58 def report(self) -> str:
59 lines = [f"\n{'='*60}", " AgentOS Startup Validation", f"{'='*60}"]
60 for issue in self.issues:
61 tag = f"[{issue.severity.upper()}]"
62 lines.append(f" {tag:12s} {issue.component}: {issue.message}")
63 if issue.suggestion:
64 lines.append(f" → {issue.suggestion}")
65 lines.append(f"{'='*60}")
67 statuses = [i.severity for i in self.issues]
68 if Severity.CRITICAL in statuses:
69 lines.append(" RESULT: CRITICAL — server will NOT start")
70 elif Severity.ERROR in statuses:
71 lines.append(" RESULT: DEGRADED — some features unavailable")
72 else:
73 lines.append(" RESULT: OK")
74 return "\n".join(lines)
77# ── Checks ──────────────────────────────────────────────────────────────────
79def _check_env_vars(report: ValidationReport):
80 required = ["AGENTOS_SECRET_KEY"]
81 optional = {
82 "AGENTOS_DATABASE_URL": "Database-backed features disabled",
83 "AGENTOS_REDIS_URL": "Distributed cache/locks disabled",
84 "AGENTOS_OTLP_ENDPOINT": "Distributed tracing disabled",
85 }
86 for var in required:
87 if not os.environ.get(var):
88 report.add("env", f"{var} not set", Severity.WARNING,
89 f"Set {var} for production; using default for dev")
91 for var, hint in optional.items():
92 if not os.environ.get(var):
93 report.add("env", f"{var} not set — {hint}", Severity.WARNING,
94 f"Set {var} for full production readiness")
97def _check_disk(report: ValidationReport, paths: list[str]):
98 for path in paths:
99 try:
100 os.makedirs(path, exist_ok=True)
101 test_file = os.path.join(path, ".agentos_write_test")
102 with open(test_file, "w") as f:
103 f.write("ok")
104 os.remove(test_file)
105 report.add("disk", f"{path} writable", Severity.OK)
106 except PermissionError:
107 report.add("disk", f"Cannot write to {path}", Severity.CRITICAL,
108 "Fix permissions or change AGENTOS_LOG_DIR / AGENTOS_DATA_DIR")
109 except OSError as e:
110 report.add("disk", f"{path}: {e}", Severity.ERROR)
113def _check_connectivity(report: ValidationReport, name: str, url: str, timeout: float = 3.0):
114 """Quick TCP connectivity check."""
115 from urllib.parse import urlparse
116 parsed = urlparse(url)
117 host = parsed.hostname or "localhost"
118 port = parsed.port or (443 if parsed.scheme == "https" else 80)
120 try:
121 sock = socket.create_connection((host, port), timeout=timeout)
122 sock.close()
123 report.add("connectivity", f"{name} reachable ({host}:{port})", Severity.OK)
124 except (socket.timeout, ConnectionRefusedError, OSError) as e:
125 report.add("connectivity", f"{name} unreachable ({host}:{port}): {e}",
126 Severity.WARNING, f"Verify {name} is running or disable related features")
129# ── Public API ──────────────────────────────────────────────────────────────
131def validate_startup(
132 data_dir: Optional[str] = None,
133 log_dir: Optional[str] = None,
134) -> ValidationReport:
135 """Run all startup checks and return a report.
137 Returns a ValidationReport — call `.has_critical` to decide whether to abort.
138 """
139 report = ValidationReport()
141 _check_env_vars(report)
143 disk_paths = [
144 data_dir or os.environ.get("AGENTOS_DATA_DIR", "./data"),
145 log_dir or os.environ.get("AGENTOS_LOG_DIR", "./logs"),
146 ]
147 _check_disk(report, disk_paths)
149 db_url = os.environ.get("AGENTOS_DATABASE_URL")
150 if db_url:
151 _check_connectivity(report, "DB", db_url)
153 redis_url = os.environ.get("AGENTOS_REDIS_URL")
154 if redis_url:
155 _check_connectivity(report, "Redis", redis_url)
157 otlp = os.environ.get("AGENTOS_OTLP_ENDPOINT")
158 if otlp:
159 _check_connectivity(report, "OTLP", otlp)
161 logger.info(report.report())
162 return report
165__all__ = ["validate_startup", "ValidationReport", "Severity"]