Coverage for agentos/config_validator.py: 0%
91 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-08 12:22 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-08 12:22 +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 logging
23import os
24import socket
25from dataclasses import dataclass, field
26from enum import StrEnum
28logger = logging.getLogger(__name__)
31class Severity(StrEnum):
32 CRITICAL = "critical" # Server MUST NOT start
33 ERROR = "error" # Feature degraded
34 WARNING = "warning" # Non-blocking concern
35 OK = "ok"
38@dataclass
39class Issue:
40 component: str
41 message: str
42 severity: Severity
43 suggestion: str = ""
46@dataclass
47class ValidationReport:
48 issues: list[Issue] = field(default_factory=list)
50 @property
51 def has_critical(self) -> bool:
52 return any(i.severity == Severity.CRITICAL for i in self.issues)
54 def add(self, component: str, message: str, severity: Severity, suggestion: str = ""):
55 self.issues.append(Issue(component, message, severity, suggestion))
57 def report(self) -> str:
58 lines = [f"\n{'='*60}", " AgentOS Startup Validation", f"{'='*60}"]
59 for issue in self.issues:
60 tag = f"[{issue.severity.upper()}]"
61 lines.append(f" {tag:12s} {issue.component}: {issue.message}")
62 if issue.suggestion:
63 lines.append(f" → {issue.suggestion}")
64 lines.append(f"{'='*60}")
66 statuses = [i.severity for i in self.issues]
67 if Severity.CRITICAL in statuses:
68 lines.append(" RESULT: CRITICAL — server will NOT start")
69 elif Severity.ERROR in statuses:
70 lines.append(" RESULT: DEGRADED — some features unavailable")
71 else:
72 lines.append(" RESULT: OK")
73 return "\n".join(lines)
76# ── 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(
89 "env",
90 f"{var} not set",
91 Severity.WARNING,
92 f"Set {var} for production; using default for dev",
93 )
95 for var, hint in optional.items():
96 if not os.environ.get(var):
97 report.add(
98 "env",
99 f"{var} not set — {hint}",
100 Severity.WARNING,
101 f"Set {var} for full production readiness",
102 )
105def _check_disk(report: ValidationReport, paths: list[str]):
106 for path in paths:
107 try:
108 os.makedirs(path, exist_ok=True)
109 test_file = os.path.join(path, ".agentos_write_test")
110 with open(test_file, "w") as f:
111 f.write("ok")
112 os.remove(test_file)
113 report.add("disk", f"{path} writable", Severity.OK)
114 except PermissionError:
115 report.add(
116 "disk",
117 f"Cannot write to {path}",
118 Severity.CRITICAL,
119 "Fix permissions or change AGENTOS_LOG_DIR / AGENTOS_DATA_DIR",
120 )
121 except OSError as e:
122 report.add("disk", f"{path}: {e}", Severity.ERROR)
125def _check_connectivity(report: ValidationReport, name: str, url: str, timeout: float = 3.0):
126 """Quick TCP connectivity check."""
127 from urllib.parse import urlparse
129 parsed = urlparse(url)
130 host = parsed.hostname or "localhost"
131 port = parsed.port or (443 if parsed.scheme == "https" else 80)
133 try:
134 sock = socket.create_connection((host, port), timeout=timeout)
135 sock.close()
136 report.add("connectivity", f"{name} reachable ({host}:{port})", Severity.OK)
137 except (TimeoutError, ConnectionRefusedError, OSError) as e:
138 report.add(
139 "connectivity",
140 f"{name} unreachable ({host}:{port}): {e}",
141 Severity.WARNING,
142 f"Verify {name} is running or disable related features",
143 )
146# ── Public API ──────────────────────────────────────────────────────────────
149def validate_startup(
150 data_dir: str | None = None,
151 log_dir: str | None = None,
152) -> ValidationReport:
153 """Run all startup checks and return a report.
155 Returns a ValidationReport — call `.has_critical` to decide whether to abort.
156 """
157 report = ValidationReport()
159 _check_env_vars(report)
161 disk_paths = [
162 data_dir or os.environ.get("AGENTOS_DATA_DIR", "./data"),
163 log_dir or os.environ.get("AGENTOS_LOG_DIR", "./logs"),
164 ]
165 _check_disk(report, disk_paths)
167 db_url = os.environ.get("AGENTOS_DATABASE_URL")
168 if db_url:
169 _check_connectivity(report, "DB", db_url)
171 redis_url = os.environ.get("AGENTOS_REDIS_URL")
172 if redis_url:
173 _check_connectivity(report, "Redis", redis_url)
175 otlp = os.environ.get("AGENTOS_OTLP_ENDPOINT")
176 if otlp:
177 _check_connectivity(report, "OTLP", otlp)
179 logger.info(report.report())
180 return report
183__all__ = ["validate_startup", "ValidationReport", "Severity"]