#!/usr/bin/env python3 """ Pre-Flight Provider Check Script Issue #508: [Robustness] Credential drain detection — provider health checks Pre-flight check before session launch: verifies provider credentials and balance. Usage: python3 preflight-provider-check.py # Check all providers python3 preflight-provider-check.py --launch # Check and return exit code python3 preflight-provider-check.py --balance # Check OpenRouter balance """ import os, sys, json, yaml, urllib.request from datetime import datetime, timezone from pathlib import Path # Configuration HERMES_HOME = Path(os.environ.get("HERMES_HOME", Path.home() / ".hermes")) LOG_DIR = Path.home() / ".local" / "timmy" / "fleet-health" LOG_FILE = LOG_DIR / "preflight-check.log" def log(msg): """Log message to file and optionally console.""" timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S") log_entry = "[" + timestamp + "] " + msg LOG_DIR.mkdir(parents=True, exist_ok=True) with open(LOG_FILE, "a") as f: f.write(log_entry + "\n") if "--quiet" not in sys.argv: print(log_entry) def get_provider_api_key(provider): """Get API key for a provider from .env or environment.""" env_file = HERMES_HOME / ".env" if env_file.exists(): with open(env_file) as f: for line in f: line = line.strip() if line.startswith(provider.upper() + "_API_KEY="): return line.split("=", 1)[1].strip().strip("'\"") return os.environ.get(provider.upper() + "_API_KEY") def check_openrouter_balance(api_key): """Check OpenRouter balance via /api/v1/auth/key.""" if not api_key: return False, "No API key", 0 try: req = urllib.request.Request( "https://openrouter.ai/api/v1/auth/key", headers={"Authorization": "Bearer " + api_key} ) resp = urllib.request.urlopen(req, timeout=10) data = json.loads(resp.read()) # Check for credits credits = data.get("data", {}).get("limit", 0) usage = data.get("data", {}).get("usage", 0) remaining = credits - usage if credits else None if remaining is not None and remaining <= 0: return False, "No credits remaining", 0 elif remaining is not None: return True, "Credits available", remaining else: return True, "Unlimited or unknown balance", None except urllib.error.HTTPError as e: if e.code == 401: return False, "Invalid API key", 0 else: return False, "HTTP " + str(e.code), 0 except Exception as e: return False, str(e)[:100], 0 def check_nous_key(api_key): """Check Nous API key with minimal test call.""" if not api_key: return False, "No API key" try: req = urllib.request.Request( "https://inference.nousresearch.com/v1/models", headers={"Authorization": "Bearer " + api_key} ) resp = urllib.request.urlopen(req, timeout=10) if resp.status == 200: return True, "Valid key" else: return False, "HTTP " + str(resp.status) except urllib.error.HTTPError as e: if e.code == 401: return False, "Invalid API key" elif e.code == 403: return False, "Forbidden" else: return False, "HTTP " + str(e.code) except Exception as e: return False, str(e)[:100] def check_anthropic_key(api_key): """Check Anthropic API key with minimal test call.""" if not api_key: return False, "No API key" try: req = urllib.request.Request( "https://api.anthropic.com/v1/models", headers={ "x-api-key": api_key, "anthropic-version": "2023-06-01" } ) resp = urllib.request.urlopen(req, timeout=10) if resp.status == 200: return True, "Valid key" else: return False, "HTTP " + str(resp.status) except urllib.error.HTTPError as e: if e.code == 401: return False, "Invalid API key" elif e.code == 403: return False, "Forbidden" else: return False, "HTTP " + str(e.code) except Exception as e: return False, str(e)[:100] def check_ollama(): """Check if Ollama is running.""" try: req = urllib.request.Request("http://localhost:11434/api/tags") resp = urllib.request.urlopen(req, timeout=5) if resp.status == 200: data = json.loads(resp.read()) models = data.get("models", []) return True, str(len(models)) + " models loaded" else: return False, "HTTP " + str(resp.status) except Exception as e: return False, str(e)[:100] def get_configured_provider(): """Get the configured provider from global config.""" config_file = HERMES_HOME / "config.yaml" if not config_file.exists(): return None try: with open(config_file) as f: config = yaml.safe_load(f) model_config = config.get("model", {}) if isinstance(model_config, dict): return model_config.get("provider") except: pass return None def run_preflight_check(): """Run pre-flight check on all providers.""" log("=== Pre-Flight Provider Check ===") results = {} # Check OpenRouter or_key = get_provider_api_key("openrouter") or_ok, or_msg, or_balance = check_openrouter_balance(or_key) results["openrouter"] = {"healthy": or_ok, "message": or_msg, "balance": or_balance} # Check Nous nous_key = get_provider_api_key("nous") nous_ok, nous_msg = check_nous_key(nous_key) results["nous"] = {"healthy": nous_ok, "message": nous_msg} # Check Anthropic anthropic_key = get_provider_api_key("anthropic") anthropic_ok, anthropic_msg = check_anthropic_key(anthropic_key) results["anthropic"] = {"healthy": anthropic_ok, "message": anthropic_msg} # Check Ollama ollama_ok, ollama_msg = check_ollama() results["ollama"] = {"healthy": ollama_ok, "message": ollama_msg} # Get configured provider configured = get_configured_provider() # Summary healthy_count = sum(1 for r in results.values() if r["healthy"]) total_count = len(results) log("Results: " + str(healthy_count) + "/" + str(total_count) + " providers healthy") for provider, result in results.items(): status = "HEALTHY" if result["healthy"] else "UNHEALTHY" extra = "" if provider == "openrouter" and result.get("balance") is not None: extra = " (balance: " + str(result["balance"]) + ")" log(" " + provider + ": " + status + " - " + result["message"] + extra) if configured: log("Configured provider: " + configured) if configured in results and not results[configured]["healthy"]: log("WARNING: Configured provider " + configured + " is UNHEALTHY!") return results, configured def check_launch_readiness(): """Check if we're ready to launch sessions.""" results, configured = run_preflight_check() # Check if configured provider is healthy if configured and configured in results: if not results[configured]["healthy"]: log("LAUNCH BLOCKED: Configured provider " + configured + " is unhealthy") return False, configured + " is unhealthy" # Check if at least one provider is healthy healthy_providers = [p for p, r in results.items() if r["healthy"]] if not healthy_providers: log("LAUNCH BLOCKED: No healthy providers available") return False, "No healthy providers" log("LAUNCH READY: " + str(len(healthy_providers)) + " healthy providers available") return True, "Ready" def show_balance(): """Show OpenRouter balance.""" api_key = get_provider_api_key("openrouter") if not api_key: print("No OpenRouter API key found") return ok, msg, balance = check_openrouter_balance(api_key) if ok: if balance is not None: print("OpenRouter balance: " + str(balance) + " credits") else: print("OpenRouter: " + msg) else: print("OpenRouter: " + msg) def main(): if "--balance" in sys.argv: show_balance() elif "--launch" in sys.argv: ready, message = check_launch_readiness() if ready: print("READY") sys.exit(0) else: print("BLOCKED: " + message) sys.exit(1) else: run_preflight_check() if __name__ == "__main__": main()