Coverage for agentos/config_validator.py: 0%
93 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-06 08:01 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-06 08:01 +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 sys
24import socket
25import logging
26from dataclasses import dataclass, field
27from enum import Enum
28from typing import Optional
30logger = logging.getLogger(__name__)
33class Severity(str, Enum):
34 CRITICAL = "critical" # Server MUST NOT start
35 ERROR = "error" # Feature degraded
36 WARNING = "warning" # Non-blocking concern
37 OK = "ok"
40@dataclass
41class Issue:
42 component: str
43 message: str
44 severity: Severity
45 suggestion: str = ""
48@dataclass
49class ValidationReport:
50 issues: list[Issue] = field(default_factory=list)
52 @property
53 def has_critical(self) -> bool:
54 return any(i.severity == Severity.CRITICAL for i in self.issues)
56 def add(self, component: str, message: str, severity: Severity, suggestion: str = ""):
57 self.issues.append(Issue(component, message, severity, suggestion))
59 def report(self) -> str:
60 lines = [f"\n{'='*60}", " AgentOS Startup Validation", f"{'='*60}"]
61 for issue in self.issues:
62 tag = f"[{issue.severity.upper()}]"
63 lines.append(f" {tag:12s} {issue.component}: {issue.message}")
64 if issue.suggestion:
65 lines.append(f" → {issue.suggestion}")
66 lines.append(f"{'='*60}")
68 statuses = [i.severity for i in self.issues]
69 if Severity.CRITICAL in statuses:
70 lines.append(" RESULT: CRITICAL — server will NOT start")
71 elif Severity.ERROR in statuses:
72 lines.append(" RESULT: DEGRADED — some features unavailable")
73 else:
74 lines.append(" RESULT: OK")
75 return "\n".join(lines)
78# ── Checks ──────────────────────────────────────────────────────────────────
80def _check_env_vars(report: ValidationReport):
81 required = ["AGENTOS_SECRET_KEY"]
82 optional = {
83 "AGENTOS_DATABASE_URL": "Database-backed features disabled",
84 "AGENTOS_REDIS_URL": "Distributed cache/locks disabled",
85 "AGENTOS_OTLP_ENDPOINT": "Distributed tracing disabled",
86 }
87 for var in required:
88 if not os.environ.get(var):
89 report.add("env", f"{var} not set", Severity.WARNING,
90 f"Set {var} for production; using default for dev")
92 for var, hint in optional.items():
93 if not os.environ.get(var):
94 report.add("env", f"{var} not set — {hint}", Severity.WARNING,
95 f"Set {var} for full production readiness")
98def _check_disk(report: ValidationReport, paths: list[str]):
99 for path in paths:
100 try:
101 os.makedirs(path, exist_ok=True)
102 test_file = os.path.join(path, ".agentos_write_test")
103 with open(test_file, "w") as f:
104 f.write("ok")
105 os.remove(test_file)
106 report.add("disk", f"{path} writable", Severity.OK)
107 except PermissionError:
108 report.add("disk", f"Cannot write to {path}", Severity.CRITICAL,
109 "Fix permissions or change AGENTOS_LOG_DIR / AGENTOS_DATA_DIR")
110 except OSError as e:
111 report.add("disk", f"{path}: {e}", Severity.ERROR)
114def _check_connectivity(report: ValidationReport, name: str, url: str, timeout: float = 3.0):
115 """Quick TCP connectivity check."""
116 from urllib.parse import urlparse
117 parsed = urlparse(url)
118 host = parsed.hostname or "localhost"
119 port = parsed.port or (443 if parsed.scheme == "https" else 80)
121 try:
122 sock = socket.create_connection((host, port), timeout=timeout)
123 sock.close()
124 report.add("connectivity", f"{name} reachable ({host}:{port})", Severity.OK)
125 except (socket.timeout, ConnectionRefusedError, OSError) as e:
126 report.add("connectivity", f"{name} unreachable ({host}:{port}): {e}",
127 Severity.WARNING, f"Verify {name} is running or disable related features")
130# ── Public API ──────────────────────────────────────────────────────────────
132def validate_startup(
133 data_dir: Optional[str] = None,
134 log_dir: Optional[str] = None,
135) -> ValidationReport:
136 """Run all startup checks and return a report.
138 Returns a ValidationReport — call `.has_critical` to decide whether to abort.
139 """
140 report = ValidationReport()
142 _check_env_vars(report)
144 disk_paths = [
145 data_dir or os.environ.get("AGENTOS_DATA_DIR", "./data"),
146 log_dir or os.environ.get("AGENTOS_LOG_DIR", "./logs"),
147 ]
148 _check_disk(report, disk_paths)
150 db_url = os.environ.get("AGENTOS_DATABASE_URL")
151 if db_url:
152 _check_connectivity(report, "DB", db_url)
154 redis_url = os.environ.get("AGENTOS_REDIS_URL")
155 if redis_url:
156 _check_connectivity(report, "Redis", redis_url)
158 otlp = os.environ.get("AGENTOS_OTLP_ENDPOINT")
159 if otlp:
160 _check_connectivity(report, "OTLP", otlp)
162 logger.info(report.report())
163 return report
166__all__ = ["validate_startup", "ValidationReport", "Severity"]