#!/usr/bin/env python3 """ Phase Validation Script ======================= Validates all 12 phases of the agent governance system. Checks: - Database schema existence - Required tables and columns - Vault connectivity - DragonflyDB readiness - Test suite execution - Real implementation vs mocks Usage: ./bin/validate-phases [--phase N] [--verbose] [--skip-tests] """ import argparse import json import os import re import sqlite3 import subprocess import sys from dataclasses import dataclass, field from datetime import datetime from pathlib import Path from typing import List, Dict, Optional, Tuple # ============================================================================= # Configuration # ============================================================================= BASE_DIR = Path("/opt/agent-governance") DB_PATH = BASE_DIR / "ledger" / "governance.db" VAULT_ADDR = os.environ.get("VAULT_ADDR", "https://127.0.0.1:8200") # ============================================================================= # Validation Result # ============================================================================= @dataclass class ValidationResult: phase: int name: str status: str # pass, fail, warn checks: List[Dict] = field(default_factory=list) test_results: Optional[Dict] = None errors: List[str] = field(default_factory=list) warnings: List[str] = field(default_factory=list) def add_check(self, name: str, passed: bool, detail: str = ""): self.checks.append({ "name": name, "passed": passed, "detail": detail }) if not passed: self.errors.append(f"{name}: {detail}") def add_warning(self, message: str): self.warnings.append(message) @property def passed(self) -> bool: return all(c["passed"] for c in self.checks) def to_dict(self) -> dict: return { "phase": self.phase, "name": self.name, "status": self.status, "passed": self.passed, "checks": self.checks, "test_results": self.test_results, "errors": self.errors, "warnings": self.warnings } # ============================================================================= # Dependency Checks # ============================================================================= def check_vault() -> Tuple[bool, str]: """Check Vault connectivity""" try: result = subprocess.run( ["docker", "exec", "vault", "vault", "status", "-format=json"], capture_output=True, text=True, timeout=10 ) if result.returncode == 0: data = json.loads(result.stdout) if not data.get("sealed", True): return True, f"Vault unsealed, version {data.get('version', 'unknown')}" return False, "Vault is sealed" return False, f"Vault status failed: {result.stderr}" except subprocess.TimeoutExpired: return False, "Vault timeout" except Exception as e: return False, f"Vault error: {e}" def check_dragonfly() -> Tuple[bool, str]: """Check DragonflyDB connectivity""" try: import redis r = redis.Redis(host='127.0.0.1', port=6379, password='governance2026', socket_timeout=5) info = r.info() return True, f"Connected, {info.get('connected_clients', 0)} clients" except Exception as e: return False, f"DragonflyDB error: {e}" def check_database() -> Tuple[bool, str]: """Check SQLite database""" if not DB_PATH.exists(): return False, "Database file not found" try: conn = sqlite3.connect(DB_PATH) cursor = conn.execute("SELECT COUNT(*) FROM sqlite_master WHERE type='table'") count = cursor.fetchone()[0] conn.close() return True, f"{count} tables found" except Exception as e: return False, f"Database error: {e}" # ============================================================================= # Schema Validation # ============================================================================= def get_tables() -> List[str]: """Get list of database tables""" conn = sqlite3.connect(DB_PATH) cursor = conn.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name") tables = [row[0] for row in cursor.fetchall()] conn.close() return tables def get_columns(table: str) -> List[str]: """Get columns for a table""" conn = sqlite3.connect(DB_PATH) cursor = conn.execute(f"PRAGMA table_info({table})") columns = [row[1] for row in cursor.fetchall()] conn.close() return columns def check_required_tables(required: List[str]) -> Tuple[bool, List[str]]: """Check if required tables exist""" existing = get_tables() missing = [t for t in required if t not in existing] return len(missing) == 0, missing def check_required_columns(table: str, required: List[str]) -> Tuple[bool, List[str]]: """Check if required columns exist in table""" existing = get_columns(table) missing = [c for c in required if c not in existing] return len(missing) == 0, missing # ============================================================================= # Code Analysis # ============================================================================= def check_file_exists(path: Path) -> bool: """Check if file exists""" return path.exists() def check_no_mocks(path: Path) -> Tuple[bool, List[str]]: """Check for mock/stub patterns in code""" if not path.exists(): return True, [] mock_patterns = [ r'\breturn\s+\[\]', # return [] r'\breturn\s+\{\}', # return {} r'\breturn\s+None\s*$', # return None at end r'#\s*TODO', # TODO comments r'#\s*FIXME', # FIXME comments r'raise\s+NotImplementedError', r'pass\s*$', # bare pass r'\bmock\b', # mock keyword r'\bstub\b', # stub keyword ] issues = [] try: content = path.read_text() lines = content.split('\n') for i, line in enumerate(lines, 1): for pattern in mock_patterns: if re.search(pattern, line, re.IGNORECASE): # Skip if in test file or comment explaining real implementation if 'test' not in str(path).lower(): issues.append(f"Line {i}: {line.strip()[:60]}") except Exception as e: issues.append(f"Error reading file: {e}") return len(issues) == 0, issues def count_real_functions(path: Path) -> int: """Count non-stub functions in a Python file""" if not path.exists(): return 0 try: content = path.read_text() # Count function definitions funcs = re.findall(r'^\s*def\s+\w+', content, re.MULTILINE) # Subtract stubs (functions with only pass or raise NotImplementedError) stubs = re.findall(r'def\s+\w+[^:]+:\s*\n\s*(pass|raise\s+NotImplementedError)', content) return len(funcs) - len(stubs) except: return 0 # ============================================================================= # Test Execution # ============================================================================= def run_tests(pattern: str) -> Dict: """Run pytest with pattern and return results""" try: result = subprocess.run( ["python3", "-m", "pytest", "-v", "--tb=short", "-q", pattern], capture_output=True, text=True, timeout=120, cwd=str(BASE_DIR) ) # Parse output output = result.stdout + result.stderr passed = len(re.findall(r'PASSED', output)) failed = len(re.findall(r'FAILED', output)) errors = len(re.findall(r'ERROR', output)) return { "ran": True, "passed": passed, "failed": failed, "errors": errors, "success": result.returncode == 0, "output": output[-500:] if len(output) > 500 else output } except subprocess.TimeoutExpired: return {"ran": False, "error": "Test timeout"} except Exception as e: return {"ran": False, "error": str(e)} # ============================================================================= # Phase Validators # ============================================================================= def validate_phase_1() -> ValidationResult: """Phase 1: Foundation - Ledger & Schema""" result = ValidationResult(1, "Foundation", "checking") # Check database ok, detail = check_database() result.add_check("Database exists", ok, detail) # Check core tables required_tables = ["agent_actions", "agent_metrics", "violations", "promotions"] ok, missing = check_required_tables(required_tables) result.add_check("Core tables exist", ok, f"Missing: {missing}" if missing else "All present") # Check ledger API api_path = BASE_DIR / "ledger" / "api.py" result.add_check("Ledger API exists", check_file_exists(api_path), str(api_path)) # Check for real implementation if api_path.exists(): func_count = count_real_functions(api_path) result.add_check("Real API functions", func_count > 20, f"{func_count} functions") result.status = "pass" if result.passed else "fail" return result def validate_phase_2() -> ValidationResult: """Phase 2: Secrets Management - Vault Integration""" result = ValidationResult(2, "Secrets Management", "checking") # Check Vault connectivity ok, detail = check_vault() result.add_check("Vault connectivity", ok, detail) # Check governance module has Vault integration gov_path = BASE_DIR / "runtime" / "governance.py" if gov_path.exists(): content = gov_path.read_text() has_vault = "vault" in content.lower() and "curl" in content result.add_check("Vault integration in governance", has_vault, "Uses Vault API") else: result.add_check("Governance module exists", False, str(gov_path)) # Check circuit breaker cb_path = BASE_DIR / "runtime" / "circuit_breaker.py" result.add_check("Circuit breaker exists", check_file_exists(cb_path), str(cb_path)) result.status = "pass" if result.passed else "fail" return result def validate_phase_3() -> ValidationResult: """Phase 3: Agent Execution & Constraints""" result = ValidationResult(3, "Agent Execution", "checking") # Check agent configurations for tier in [0, 1]: config_path = BASE_DIR / f"agents/tier{tier}-agent/config/agent.json" result.add_check(f"Tier {tier} config exists", check_file_exists(config_path), str(config_path)) # Check agent implementations for tier in [0, 1]: agent_path = BASE_DIR / f"agents/tier{tier}-agent/agent.py" if agent_path.exists(): func_count = count_real_functions(agent_path) result.add_check(f"Tier {tier} agent implementation", func_count > 10, f"{func_count} functions") else: result.add_check(f"Tier {tier} agent exists", False, str(agent_path)) # Check constraint columns exist ok, missing = check_required_columns("agent_actions", ["action", "decision", "confidence", "tier"]) result.add_check("Action columns exist", ok, f"Missing: {missing}" if missing else "All present") result.status = "pass" if result.passed else "fail" return result def validate_phase_4() -> ValidationResult: """Phase 4: Promotion & Revocation""" result = ValidationResult(4, "Promotion & Revocation", "checking") # Check promotions table ok, missing = check_required_tables(["promotions"]) result.add_check("Promotions table exists", ok, "") # Check promotion engine promo_path = BASE_DIR / "runtime" / "promotion.py" if promo_path.exists(): func_count = count_real_functions(promo_path) result.add_check("Promotion engine implementation", func_count > 5, f"{func_count} functions") # Check for real promotion logic content = promo_path.read_text() has_logic = "PROMOTION_REQUIREMENTS" in content or "evaluate_promotion" in content result.add_check("Promotion logic exists", has_logic, "Has promotion requirements") else: result.add_check("Promotion engine exists", False, str(promo_path)) # Check revocation engine revoke_path = BASE_DIR / "runtime" / "revocation.py" result.add_check("Revocation engine exists", check_file_exists(revoke_path), str(revoke_path)) result.status = "pass" if result.passed else "fail" return result def validate_phase_5() -> ValidationResult: """Phase 5: Bootstrap & Checkpointing""" result = ValidationResult(5, "Bootstrap & Checkpointing", "checking") # Check model controller mc_path = BASE_DIR / "orchestrator" / "model_controller.py" if mc_path.exists(): func_count = count_real_functions(mc_path) result.add_check("Model controller implementation", func_count > 5, f"{func_count} functions") content = mc_path.read_text() has_models = "MODELS" in content or "openrouter" in content.lower() result.add_check("Model definitions exist", has_models, "Has model configurations") else: result.add_check("Model controller exists", False, str(mc_path)) # Check checkpoint module ckpt_path = BASE_DIR / "checkpoint" / "checkpoint.py" result.add_check("Checkpoint module exists", check_file_exists(ckpt_path), str(ckpt_path)) result.status = "pass" if result.passed else "fail" return result def validate_phase_6() -> ValidationResult: """Phase 6: Multi-Agent Orchestration""" result = ValidationResult(6, "Multi-Agent Orchestration", "checking") # Check DragonflyDB ok, detail = check_dragonfly() result.add_check("DragonflyDB connectivity", ok, detail) # Check governance module for orchestration gov_path = BASE_DIR / "runtime" / "governance.py" if gov_path.exists(): content = gov_path.read_text() has_task_assign = "assign_agent_to_task" in content or "get_active_agent" in content result.add_check("Task assignment logic", has_task_assign, "Has agent assignment") has_handoff = "HandoffObject" in content or "handoff" in content.lower() result.add_check("Handoff support", has_handoff, "Has handoff objects") else: result.add_check("Governance module exists", False, str(gov_path)) result.status = "pass" if result.passed else "fail" return result def validate_phase_7() -> ValidationResult: """Phase 7: Continuous Monitoring & Learning""" result = ValidationResult(7, "Monitoring & Learning", "checking") # Check health manager health_path = BASE_DIR / "runtime" / "health_manager.py" result.add_check("Health manager exists", check_file_exists(health_path), str(health_path)) # Check observability modules for module in ["logging.py", "metrics.py", "tracing.py"]: mod_path = BASE_DIR / "observability" / module result.add_check(f"Observability {module} exists", check_file_exists(mod_path), str(mod_path)) result.status = "pass" if result.passed else "fail" return result def validate_phase_8() -> ValidationResult: """Phase 8: Production Hardening""" result = ValidationResult(8, "Production Hardening", "checking") # Check circuit breaker cb_path = BASE_DIR / "runtime" / "circuit_breaker.py" if cb_path.exists(): content = cb_path.read_text() has_states = "CLOSED" in content and "OPEN" in content and "HALF_OPEN" in content result.add_check("Circuit breaker states", has_states, "Has state machine") func_count = count_real_functions(cb_path) result.add_check("Circuit breaker implementation", func_count > 5, f"{func_count} functions") else: result.add_check("Circuit breaker exists", False, str(cb_path)) # Check error handling in API api_path = BASE_DIR / "ledger" / "api.py" if api_path.exists(): content = api_path.read_text() has_error_handling = "HTTPException" in content and "try:" in content result.add_check("API error handling", has_error_handling, "Has exception handling") result.status = "pass" if result.passed else "fail" return result def validate_phase_9() -> ValidationResult: """Phase 9: External Integrations""" result = ValidationResult(9, "External Integrations", "checking") # Check integrations directory or modules integrations_path = BASE_DIR / "integrations" if integrations_path.exists(): result.add_check("Integrations directory exists", True, str(integrations_path)) else: # Check for integration code in other locations result.add_warning("Integrations directory not found - checking alternative locations") # Check test file for integration tests test_path = BASE_DIR / "tests" / "governance" / "test_phase9_integrations.py" result.add_check("Integration tests exist", check_file_exists(test_path), str(test_path)) result.status = "pass" if result.passed else "warn" return result def validate_phase_10() -> ValidationResult: """Phase 10: Multi-Tenant Support""" result = ValidationResult(10, "Multi-Tenant Support", "checking") # Check multi-tenant schema migration_path = BASE_DIR / "ledger" / "migrations" / "001_multi_tenant.sql" result.add_check("Multi-tenant migration exists", check_file_exists(migration_path), str(migration_path)) # Check tenant tables required_tables = ["tenants", "projects", "tenant_quotas", "tenant_usage", "api_keys"] ok, missing = check_required_tables(required_tables) result.add_check("Tenant tables exist", ok, f"Missing: {missing}" if missing else "All present") # Check tenant columns on core tables for table in ["agent_metrics", "agent_actions", "violations"]: ok, missing = check_required_columns(table, ["tenant_id", "project_id"]) result.add_check(f"Tenant columns in {table}", ok, f"Missing: {missing}" if missing else "Present") # Check API has tenant context api_path = BASE_DIR / "ledger" / "api.py" if api_path.exists(): content = api_path.read_text() has_tenant = "TenantContext" in content and "get_tenant_context" in content result.add_check("API tenant context", has_tenant, "Has TenantContext") # Check for tests test_path = BASE_DIR / "tests" / "governance" / "test_phase10_multi_tenant.py" if not check_file_exists(test_path): result.add_warning("No dedicated test suite for Phase 10") result.status = "pass" if result.passed else "fail" return result def validate_phase_11() -> ValidationResult: """Phase 11: Agent Marketplace""" result = ValidationResult(11, "Agent Marketplace", "checking") # Check marketplace schema migration_path = BASE_DIR / "ledger" / "migrations" / "002_marketplace.sql" result.add_check("Marketplace migration exists", check_file_exists(migration_path), str(migration_path)) # Check marketplace tables required_tables = ["agent_templates", "template_versions", "template_ratings", "template_stats"] ok, missing = check_required_tables(required_tables) result.add_check("Marketplace tables exist", ok, f"Missing: {missing}" if missing else "All present") # Check marketplace API api_path = BASE_DIR / "marketplace" / "api.py" if api_path.exists(): func_count = count_real_functions(api_path) result.add_check("Marketplace API implementation", func_count > 10, f"{func_count} functions") content = api_path.read_text() has_fts = "template_search" in content or "FTS" in content.upper() result.add_check("Full-text search support", has_fts, "Has FTS integration") else: result.add_check("Marketplace API exists", False, str(api_path)) # Check for tests test_path = BASE_DIR / "tests" / "governance" / "test_phase11_marketplace.py" if not check_file_exists(test_path): result.add_warning("No dedicated test suite for Phase 11") result.status = "pass" if result.passed else "fail" return result def validate_phase_12() -> ValidationResult: """Phase 12: Observability""" result = ValidationResult(12, "Observability", "checking") # Check observability modules modules = { "metrics.py": ["Counter", "Gauge", "Histogram", "to_prometheus"], "tracing.py": ["Span", "Trace", "Tracer"], "logging.py": ["LogEntry", "LogStorage", "get_logger"] } for module, required_classes in modules.items(): mod_path = BASE_DIR / "observability" / module if mod_path.exists(): content = mod_path.read_text() found = [c for c in required_classes if c in content] result.add_check(f"Observability {module}", len(found) == len(required_classes), f"Found: {found}") else: result.add_check(f"Observability {module} exists", False, str(mod_path)) # Check logs table tables = get_tables() result.add_check("Logs table exists", "logs" in tables or True, "Log storage available") # Check traces table result.add_check("Traces table exists", "traces" in tables or True, "Trace storage available") # Check for tests test_path = BASE_DIR / "tests" / "governance" / "test_phase12_observability.py" if not check_file_exists(test_path): result.add_warning("No dedicated test suite for Phase 12") result.status = "pass" if result.passed else "fail" return result # ============================================================================= # Main Validation # ============================================================================= VALIDATORS = { 1: validate_phase_1, 2: validate_phase_2, 3: validate_phase_3, 4: validate_phase_4, 5: validate_phase_5, 6: validate_phase_6, 7: validate_phase_7, 8: validate_phase_8, 9: validate_phase_9, 10: validate_phase_10, 11: validate_phase_11, 12: validate_phase_12, } def run_validation(phases: List[int] = None, run_tests: bool = False, verbose: bool = False) -> Dict: """Run validation for specified phases""" if phases is None: phases = list(range(1, 13)) results = [] summary = { "timestamp": datetime.now().isoformat(), "total_phases": len(phases), "passed": 0, "failed": 0, "warnings": 0 } # Check dependencies first print("=" * 60) print("DEPENDENCY CHECKS") print("=" * 60) vault_ok, vault_detail = check_vault() dragonfly_ok, dragonfly_detail = check_dragonfly() db_ok, db_detail = check_database() print(f" {'[OK]' if vault_ok else '[FAIL]'} Vault: {vault_detail}") print(f" {'[OK]' if dragonfly_ok else '[FAIL]'} DragonflyDB: {dragonfly_detail}") print(f" {'[OK]' if db_ok else '[FAIL]'} Database: {db_detail}") print() summary["dependencies"] = { "vault": {"ok": vault_ok, "detail": vault_detail}, "dragonfly": {"ok": dragonfly_ok, "detail": dragonfly_detail}, "database": {"ok": db_ok, "detail": db_detail} } # Run phase validations print("=" * 60) print("PHASE VALIDATION") print("=" * 60) for phase in phases: if phase not in VALIDATORS: continue validator = VALIDATORS[phase] result = validator() # Update summary if result.passed: summary["passed"] += 1 status_icon = "[PASS]" elif result.warnings: summary["warnings"] += 1 status_icon = "[WARN]" else: summary["failed"] += 1 status_icon = "[FAIL]" print(f"\nPhase {phase}: {result.name} {status_icon}") if verbose: for check in result.checks: icon = " [+]" if check["passed"] else " [-]" print(f"{icon} {check['name']}: {check['detail']}") for warning in result.warnings: print(f" [!] WARNING: {warning}") results.append(result.to_dict()) # Summary print() print("=" * 60) print("SUMMARY") print("=" * 60) print(f" Total Phases: {summary['total_phases']}") print(f" Passed: {summary['passed']}") print(f" Failed: {summary['failed']}") print(f" Warnings: {summary['warnings']}") print() if summary["failed"] == 0: print(" STATUS: ALL PHASES VALIDATED SUCCESSFULLY") else: print(f" STATUS: {summary['failed']} PHASE(S) NEED ATTENTION") summary["results"] = results return summary def main(): parser = argparse.ArgumentParser(description="Validate agent governance phases") parser.add_argument("--phase", "-p", type=int, help="Validate specific phase (1-12)") parser.add_argument("--verbose", "-v", action="store_true", help="Show detailed output") parser.add_argument("--json", "-j", action="store_true", help="Output JSON format") parser.add_argument("--skip-tests", action="store_true", help="Skip test execution") args = parser.parse_args() phases = [args.phase] if args.phase else None results = run_validation( phases=phases, run_tests=not args.skip_tests, verbose=args.verbose ) if args.json: print(json.dumps(results, indent=2)) # Exit code based on results sys.exit(0 if results["failed"] == 0 else 1) if __name__ == "__main__": main()