#!/usr/bin/env python3 """ Agent: meta.config.router Configure Claude Code Router for multi-model LLM support """ import json import sys import os import hashlib import uuid from datetime import datetime from pathlib import Path from typing import Dict, Any, List import subprocess import yaml class MetaConfigRouter: """Configure Claude Code Router for Betty Framework""" def __init__(self): self.betty_root = Path(__file__).parent.parent.parent self.skills_root = self.betty_root / "skills" self.audit_log_path = self.betty_root / "registry" / "audit_log.json" def run( self, routing_config_path: str, apply_config: bool = False, output_mode: str = "preview" ) -> Dict[str, Any]: """ Main execution method Args: routing_config_path: Path to router config input file (YAML or JSON) apply_config: Whether to write config to disk output_mode: "preview" | "file" | "both" Returns: Result with routing_config, write_status, and audit_id """ print(f"🔧 meta.config.router v0.1.0") print(f"📋 Config input: {routing_config_path}") print(f"📝 Output mode: {output_mode}") print(f"💾 Apply config: {apply_config}") print() # Load input config config_input = self._load_config_input(routing_config_path) # Extract inputs llm_backends = config_input.get("llm_backends", []) routing_rules = config_input.get("routing_rules", {}) config_options = config_input.get("config_options", {}) metadata = config_input.get("metadata", {}) # For audit logging only # Step 1: Validate inputs print("🔍 Validating router configuration...") validation_result = self._validate_config(llm_backends, routing_rules) if not validation_result["valid"]: print("❌ Validation failed:") for error in validation_result["errors"]: print(f" - {error}") return { "success": False, "errors": validation_result["errors"], "warnings": validation_result["warnings"] } if validation_result["warnings"]: print("⚠️ Warnings:") for warning in validation_result["warnings"]: print(f" - {warning}") print("✅ Validation passed") print() # Step 2: Generate router config print("🏗️ Generating router configuration...") router_config = self._generate_config( llm_backends, routing_rules, config_options ) print("✅ Configuration generated") print() # Step 3: Write config if requested write_status = "skipped" config_path = None if apply_config and output_mode != "preview": print("💾 Writing configuration to disk...") config_path, write_status = self._write_config(router_config) if write_status == "success": print(f"✅ Configuration written to: {config_path}") else: print(f"❌ Failed to write configuration") print() # Step 4: Log audit record print("📝 Logging audit record...") audit_id = self._log_audit( config_input=config_input, write_status=write_status, metadata=metadata ) print(f"✅ Audit ID: {audit_id}") print() # Step 5: Output results result = { "success": True, "routing_config": router_config, "write_status": write_status, "audit_id": audit_id } if config_path: result["config_path"] = str(config_path) # Display preview if requested if output_mode in ["preview", "both"]: print("📄 Router Configuration Preview:") print("─" * 80) print(json.dumps(router_config, indent=2)) print("─" * 80) print() return result def _load_config_input(self, config_path: str) -> Dict[str, Any]: """Load router config input from YAML or JSON file""" path = Path(config_path) if not path.exists(): raise FileNotFoundError(f"Config file not found: {config_path}") with open(path, 'r') as f: if path.suffix in ['.yaml', '.yml']: return yaml.safe_load(f) else: return json.load(f) def _validate_config( self, llm_backends: List[Dict[str, Any]], routing_rules: Dict[str, Any] ) -> Dict[str, Any]: """Validate router configuration using config.validate.router skill""" validator_script = self.skills_root / "config.validate.router" / "validate_router.py" config_json = json.dumps({ "llm_backends": llm_backends, "routing_rules": routing_rules }) try: result = subprocess.run( [sys.executable, str(validator_script), config_json], capture_output=True, text=True, check=False ) return json.loads(result.stdout) except Exception as e: return { "valid": False, "errors": [f"Validation error: {e}"], "warnings": [] } def _generate_config( self, llm_backends: List[Dict[str, Any]], routing_rules: Dict[str, Any], config_options: Dict[str, Any] ) -> Dict[str, Any]: """Generate router configuration using config.generate.router skill""" generator_script = self.skills_root / "config.generate.router" / "generate_router.py" input_json = json.dumps({ "llm_backends": llm_backends, "routing_rules": routing_rules, "config_options": config_options }) try: result = subprocess.run( [sys.executable, str(generator_script), input_json], capture_output=True, text=True, check=True ) return json.loads(result.stdout) except Exception as e: raise RuntimeError(f"Config generation failed: {e}") def _write_config(self, router_config: Dict[str, Any]) -> tuple[Path, str]: """Write router config to ~/.claude-code-router/config.json""" try: config_dir = Path.home() / ".claude-code-router" config_dir.mkdir(parents=True, exist_ok=True) config_path = config_dir / "config.json" with open(config_path, 'w') as f: json.dump(router_config, f, indent=2) return config_path, "success" except Exception as e: print(f"Error writing config: {e}") return None, "error" def _log_audit( self, config_input: Dict[str, Any], write_status: str, metadata: Dict[str, Any] ) -> str: """Log audit record for configuration event""" audit_id = str(uuid.uuid4()) # Calculate hash of input input_hash = hashlib.sha256( json.dumps(config_input, sort_keys=True).encode() ).hexdigest()[:16] audit_entry = { "audit_id": audit_id, "timestamp": datetime.utcnow().isoformat() + "Z", "agent": "meta.config.router", "version": "0.1.0", "action": "router_config_generated", "write_status": write_status, "input_hash": input_hash, "environment": self._detect_environment(), "initiator": metadata.get("initiator", "unknown"), "metadata": metadata } # Append to audit log try: if self.audit_log_path.exists(): with open(self.audit_log_path, 'r') as f: audit_log = json.load(f) else: audit_log = [] audit_log.append(audit_entry) with open(self.audit_log_path, 'w') as f: json.dump(audit_log, f, indent=2) except Exception as e: print(f"Warning: Failed to write audit log: {e}") return audit_id def _detect_environment(self) -> str: """Detect execution environment (local, cloud, ci)""" if os.getenv("CI"): return "ci" elif os.getenv("CLOUD_ENV"): return "cloud" else: return "local" def main(): """CLI entrypoint""" if len(sys.argv) < 2: print("Usage: meta_config_router.py [--apply_config] [--output_mode=]") print() print("Arguments:") print(" routing_config_path Path to router config input file (YAML or JSON)") print(" --apply_config Write config to ~/.claude-code-router/config.json") print(" --output_mode=MODE Output mode: preview, file, or both (default: preview)") sys.exit(1) # Parse arguments routing_config_path = sys.argv[1] apply_config = "--apply_config" in sys.argv or "--apply-config" in sys.argv output_mode = "preview" for arg in sys.argv[2:]: if arg.startswith("--output_mode=") or arg.startswith("--output-mode="): output_mode = arg.split("=")[1] # Run agent agent = MetaConfigRouter() try: result = agent.run( routing_config_path=routing_config_path, apply_config=apply_config, output_mode=output_mode ) if result["success"]: print("✅ meta.config.router completed successfully") print(f"📋 Audit ID: {result['audit_id']}") print(f"💾 Write status: {result['write_status']}") sys.exit(0) else: print("❌ meta.config.router failed") for error in result.get("errors", []): print(f" - {error}") sys.exit(1) except Exception as e: print(f"❌ Error: {e}") import traceback traceback.print_exc() sys.exit(1) if __name__ == "__main__": main()