Files
timmy-home/scripts/sovereignty_audit.py
Alexander Whitestone 8b02ae03ca
Some checks failed
Smoke Test / smoke (pull_request) Failing after 23s
Add scripts/sovereignty_audit.py
2026-04-21 11:27:38 +00:00

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()