Some checks failed
Smoke Test / smoke (pull_request) Failing after 23s
398 lines
12 KiB
Python
398 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
sovereignty_audit.py — Audit cloud dependencies across the fleet.
|
|
|
|
Checks every component of the sovereign stack and reports what's local
|
|
vs what still depends on cloud services. Produces a sovereignty score.
|
|
|
|
Usage:
|
|
python3 scripts/sovereignty_audit.py # Full audit
|
|
python3 scripts/sovereignty_audit.py --json # JSON output
|
|
python3 scripts/sovereignty_audit.py --check # Exit 1 if score < threshold
|
|
|
|
Exit codes:
|
|
0 = sovereignty score >= threshold (default 50%)
|
|
1 = sovereignty score < threshold
|
|
2 = audit error
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import urllib.request
|
|
import urllib.error
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
CLOUD_PROVIDERS = [
|
|
"anthropic", "claude", "openai", "gpt-4", "gpt-5",
|
|
"openrouter", "nousresearch", "nous", "groq",
|
|
"together", "replicate", "cohere", "mistral",
|
|
]
|
|
|
|
LOCAL_INDICATORS = [
|
|
"ollama", "localhost:11434", "gemma", "llama", "qwen",
|
|
"mimo", "local", "127.0.0.1",
|
|
]
|
|
|
|
BANNED_PROVIDERS = ["anthropic", "claude"]
|
|
|
|
|
|
class Finding:
|
|
def __init__(self, component, status, detail, cloud=False):
|
|
self.component = component
|
|
self.status = status # "local", "cloud", "unknown", "down"
|
|
self.detail = detail
|
|
self.cloud = cloud
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"component": self.component,
|
|
"status": self.status,
|
|
"detail": self.detail,
|
|
"cloud": self.cloud,
|
|
}
|
|
|
|
|
|
def check_ollama():
|
|
"""Check if Ollama is running locally."""
|
|
try:
|
|
req = urllib.request.Request("http://localhost:11434/api/tags", timeout=5)
|
|
with urllib.request.urlopen(req) as resp:
|
|
data = json.loads(resp.read())
|
|
models = [m["name"] for m in data.get("models", [])]
|
|
if models:
|
|
return Finding("ollama", "local", f"Running with {len(models)} models: {', '.join(models[:5])}")
|
|
return Finding("ollama", "local", "Running but no models loaded")
|
|
except urllib.error.URLError:
|
|
return Finding("ollama", "down", "Not reachable at localhost:11434")
|
|
except Exception as e:
|
|
return Finding("ollama", "unknown", f"Error: {e}")
|
|
|
|
|
|
def check_config_files():
|
|
"""Scan ~/.hermes and ~/.timmy config files for cloud dependencies."""
|
|
findings = []
|
|
config_dirs = [
|
|
Path.home() / ".hermes",
|
|
Path.home() / ".timmy",
|
|
]
|
|
|
|
for config_dir in config_dirs:
|
|
if not config_dir.exists():
|
|
continue
|
|
for yaml_file in config_dir.glob("**/*.yaml"):
|
|
try:
|
|
content = yaml_file.read_text().lower()
|
|
rel = yaml_file.relative_to(config_dir)
|
|
|
|
cloud_refs = []
|
|
local_refs = []
|
|
banned_refs = []
|
|
|
|
for provider in CLOUD_PROVIDERS:
|
|
if provider in content:
|
|
cloud_refs.append(provider)
|
|
for indicator in LOCAL_INDICATORS:
|
|
if indicator in content:
|
|
local_refs.append(indicator)
|
|
for banned in BANNED_PROVIDERS:
|
|
if banned in content:
|
|
banned_refs.append(banned)
|
|
|
|
if banned_refs:
|
|
findings.append(Finding(
|
|
f"config:{rel}", "cloud",
|
|
f"BANNED provider(s): {', '.join(banned_refs)}",
|
|
cloud=True,
|
|
))
|
|
elif cloud_refs and not local_refs:
|
|
findings.append(Finding(
|
|
f"config:{rel}", "cloud",
|
|
f"Cloud-only: {', '.join(cloud_refs)}",
|
|
cloud=True,
|
|
))
|
|
elif cloud_refs and local_refs:
|
|
findings.append(Finding(
|
|
f"config:{rel}", "mixed",
|
|
f"Cloud: {', '.join(cloud_refs)} | Local: {', '.join(local_refs)}",
|
|
cloud=True,
|
|
))
|
|
elif local_refs:
|
|
findings.append(Finding(
|
|
f"config:{rel}", "local",
|
|
f"Local: {', '.join(local_refs)}",
|
|
))
|
|
except Exception:
|
|
pass
|
|
|
|
return findings
|
|
|
|
|
|
def check_cron_jobs():
|
|
"""Check crontab for cloud model references."""
|
|
findings = []
|
|
try:
|
|
result = subprocess.run(
|
|
"crontab -l 2>/dev/null", shell=True,
|
|
capture_output=True, text=True, timeout=10,
|
|
)
|
|
if result.returncode != 0:
|
|
return [Finding("crontab", "unknown", "No crontab or access denied")]
|
|
|
|
crontab = result.stdout.lower()
|
|
cloud_lines = []
|
|
local_lines = []
|
|
|
|
for line in crontab.split("
|
|
"):
|
|
if line.startswith("#") or not line.strip():
|
|
continue
|
|
for provider in CLOUD_PROVIDERS:
|
|
if provider in line:
|
|
cloud_lines.append(line.strip()[:80])
|
|
for indicator in LOCAL_INDICATORS:
|
|
if indicator in line:
|
|
local_lines.append(line.strip()[:80])
|
|
|
|
if cloud_lines:
|
|
findings.append(Finding(
|
|
"crontab", "cloud",
|
|
f"{len(cloud_lines)} job(s) reference cloud providers",
|
|
cloud=True,
|
|
))
|
|
if local_lines:
|
|
findings.append(Finding(
|
|
"crontab", "local",
|
|
f"{len(local_lines)} job(s) use local models",
|
|
))
|
|
if not cloud_lines and not local_lines:
|
|
findings.append(Finding("crontab", "unknown", "No model references found"))
|
|
|
|
except Exception as e:
|
|
findings.append(Finding("crontab", "unknown", f"Error: {e}"))
|
|
|
|
return findings
|
|
|
|
|
|
def check_tmux_sessions():
|
|
"""Check tmux sessions for cloud model usage."""
|
|
findings = []
|
|
try:
|
|
result = subprocess.run(
|
|
"tmux list-sessions -F '#{session_name}' 2>/dev/null",
|
|
shell=True, capture_output=True, text=True, timeout=10,
|
|
)
|
|
if result.returncode != 0:
|
|
return [Finding("tmux", "unknown", "No tmux sessions or tmux not running")]
|
|
|
|
sessions = result.stdout.strip().split("
|
|
")
|
|
findings.append(Finding("tmux", "local", f"{len(sessions)} session(s) active: {', '.join(sessions[:5])}"))
|
|
|
|
except Exception as e:
|
|
findings.append(Finding("tmux", "unknown", f"Error: {e}"))
|
|
|
|
return findings
|
|
|
|
|
|
def check_network_deps():
|
|
"""Check for outbound connections to cloud APIs."""
|
|
findings = []
|
|
cloud_hosts = [
|
|
"api.openai.com", "api.anthropic.com", "openrouter.ai",
|
|
"inference-api.nousresearch.com", "api.groq.com",
|
|
]
|
|
local_hosts = ["localhost", "127.0.0.1"]
|
|
|
|
try:
|
|
result = subprocess.run(
|
|
"netstat -an 2>/dev/null | grep ESTABLISHED || ss -tn 2>/dev/null | grep ESTAB",
|
|
shell=True, capture_output=True, text=True, timeout=10,
|
|
)
|
|
connections = result.stdout.lower()
|
|
|
|
active_cloud = []
|
|
for host in cloud_hosts:
|
|
if host in connections:
|
|
active_cloud.append(host)
|
|
|
|
if active_cloud:
|
|
findings.append(Finding(
|
|
"network", "cloud",
|
|
f"Active connections to: {', '.join(active_cloud)}",
|
|
cloud=True,
|
|
))
|
|
else:
|
|
findings.append(Finding("network", "local", "No active cloud API connections"))
|
|
|
|
except Exception as e:
|
|
findings.append(Finding("network", "unknown", f"Error: {e}"))
|
|
|
|
return findings
|
|
|
|
|
|
def check_api_keys():
|
|
"""Check for cloud API keys in environment and config."""
|
|
findings = []
|
|
key_vars = [
|
|
"OPENAI_API_KEY", "ANTHROPIC_API_KEY", "OPENROUTER_API_KEY",
|
|
"GROQ_API_KEY", "NOUS_API_KEY",
|
|
]
|
|
|
|
active_keys = []
|
|
for var in key_vars:
|
|
if os.environ.get(var):
|
|
active_keys.append(var)
|
|
|
|
if active_keys:
|
|
findings.append(Finding(
|
|
"env_keys", "cloud",
|
|
f"Active env vars: {', '.join(active_keys)}",
|
|
cloud=True,
|
|
))
|
|
else:
|
|
findings.append(Finding("env_keys", "local", "No cloud API keys in environment"))
|
|
|
|
# Check auth.json
|
|
auth_path = Path.home() / ".hermes" / "auth.json"
|
|
if auth_path.exists():
|
|
try:
|
|
auth = json.loads(auth_path.read_text())
|
|
providers = auth.get("providers", {})
|
|
cloud_providers = {k: v for k, v in providers.items()
|
|
if any(p in k.lower() for p in CLOUD_PROVIDERS)}
|
|
if cloud_providers:
|
|
findings.append(Finding(
|
|
"auth.json", "cloud",
|
|
f"Cloud providers configured: {', '.join(cloud_providers.keys())}",
|
|
cloud=True,
|
|
))
|
|
except Exception:
|
|
pass
|
|
|
|
return findings
|
|
|
|
|
|
def compute_score(findings):
|
|
"""Compute sovereignty score (0-100)."""
|
|
if not findings:
|
|
return 0
|
|
|
|
local_count = sum(1 for f in findings if f.status == "local")
|
|
cloud_count = sum(1 for f in findings if f.cloud)
|
|
total = len(findings)
|
|
|
|
if total == 0:
|
|
return 100
|
|
|
|
# Score: local findings boost, cloud findings penalize
|
|
score = (local_count / total) * 100
|
|
|
|
# Hard penalty for banned providers
|
|
banned_count = sum(1 for f in findings if "BANNED" in f.detail)
|
|
score -= banned_count * 20
|
|
|
|
return max(0, min(100, score))
|
|
|
|
|
|
def run_audit():
|
|
"""Run full sovereignty audit."""
|
|
now = datetime.now(timezone.utc).isoformat()
|
|
all_findings = []
|
|
|
|
all_findings.append(Finding("audit", "local", f"Started at {now}"))
|
|
|
|
all_findings.extend([check_ollama()])
|
|
all_findings.extend(check_config_files())
|
|
all_findings.extend(check_cron_jobs())
|
|
all_findings.extend(check_tmux_sessions())
|
|
all_findings.extend(check_network_deps())
|
|
all_findings.extend(check_api_keys())
|
|
|
|
score = compute_score(all_findings)
|
|
|
|
return {
|
|
"timestamp": now,
|
|
"sovereignty_score": score,
|
|
"findings": [f.to_dict() for f in all_findings],
|
|
"summary": {
|
|
"total_checks": len(all_findings),
|
|
"local": sum(1 for f in all_findings if f.status == "local"),
|
|
"cloud": sum(1 for f in all_findings if f.cloud),
|
|
"down": sum(1 for f in all_findings if f.status == "down"),
|
|
"unknown": sum(1 for f in all_findings if f.status == "unknown"),
|
|
},
|
|
}
|
|
|
|
|
|
def print_report(result):
|
|
"""Print human-readable sovereignty report."""
|
|
score = result["sovereignty_score"]
|
|
status = "OPTIMAL" if score >= 90 else "WARNING" if score >= 50 else "COMPROMISED"
|
|
icon = "OPTIMAL" if score >= 90 else "WARNING" if score >= 50 else "COMPROMISED"
|
|
|
|
print()
|
|
print("=" * 60)
|
|
print(f" SOVEREIGNTY AUDIT — {result['timestamp'][:10]}")
|
|
print("=" * 60)
|
|
print(f" Score: {score:.0f}% [{status}]")
|
|
print()
|
|
|
|
s = result["summary"]
|
|
print(f" Local: {s['local']}")
|
|
print(f" Cloud: {s['cloud']}")
|
|
print(f" Down: {s['down']}")
|
|
print(f" Unknown: {s['unknown']}")
|
|
print()
|
|
|
|
for f in result["findings"]:
|
|
status_icon = {
|
|
"local": "[LOCAL]",
|
|
"cloud": "[CLOUD]",
|
|
"mixed": "[MIXED]",
|
|
"down": "[DOWN]",
|
|
"unknown": "[?????]",
|
|
}.get(f["status"], "[?????]")
|
|
|
|
if f["component"] == "audit":
|
|
continue
|
|
print(f" {status_icon} {f['component']:<25} {f['detail'][:55]}")
|
|
|
|
print()
|
|
print("=" * 60)
|
|
if score >= 90:
|
|
print(" The fleet is sovereign. No one can turn it off.")
|
|
elif score >= 50:
|
|
print(" Partial sovereignty. Cloud dependencies remain.")
|
|
else:
|
|
print(" Cloud-dependent. Sovereignty compromised.")
|
|
print("=" * 60)
|
|
print()
|
|
|
|
|
|
def main():
|
|
import argparse
|
|
parser = argparse.ArgumentParser(description="Sovereignty Audit")
|
|
parser.add_argument("--json", action="store_true", help="JSON output")
|
|
parser.add_argument("--check", action="store_true", help="Exit 1 if score < threshold")
|
|
parser.add_argument("--threshold", type=int, default=50, help="Minimum score for --check")
|
|
args = parser.parse_args()
|
|
|
|
result = run_audit()
|
|
|
|
if args.json:
|
|
print(json.dumps(result, indent=2))
|
|
else:
|
|
print_report(result)
|
|
|
|
if args.check and result["sovereignty_score"] < args.threshold:
|
|
sys.exit(1)
|
|
|
|
sys.exit(0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|