agentic-os/scripts/integrity_check.py

157 lines
5.7 KiB
Python

#!/usr/bin/env python3
"""
Agentic-OS Codebase Integrity Check (run hourly via cron)
Detects STALE state in the codebase — references that contradict the
single source of truth:
SOURCE OF TRUTH
- dashboard port ........ data/settings.json -> dashboard.port (currently 8080)
- canonical repo ........ github.com/zumayaaustin-creator/agentic-os
- canonical location .... /home/austin/agentic-os
- terminal endpoint ..... /ws/terminal on the MAIN port (NOT a separate 8082)
Any contradiction of the above is reported as a finding. This script is
read-only: it never edits files. It prints a report and exits 0 if clean,
1 if any finding, 2 on a check error (so the cron can alert).
Run: python3 scripts/integrity_check.py
"""
import json
import re
import sys
from pathlib import Path
BASE = Path(__file__).resolve().parent.parent
SETTINGS = BASE / "data" / "settings.json"
# ── Source of truth ────────────────────────────────────────────────
TRUTH = {
"expected_port": 8080, # overwritten below from settings.json if present
"canonical_repo": "zumayaaustin-creator/agentic-os",
"canonical_location": "/home/austin/agentic-os",
"terminal_endpoint": "/ws/terminal",
}
STALE_PATTERNS = [
(r"modimihir07", "stale GitHub repo (should be zumayaaustin-creator/agentic-os)"),
(r"Desktop/Agentic\s*OS\s*Project", "stale project location (should be /home/austin/agentic-os)"),
(r"127\.0\.0\.1:8082", "dead port 8082 referenced (terminal consolidated to /ws/terminal on main port)"),
(r"port\s*8082", "dead port 8082 referenced"),
(r":8081", "stale port 8081 referenced as the server/terminal port"),
(r"wsPort\s*=\s*8082", "terminal.js still targets dead 8082"),
(r"action:\s*'(input|resize|output)'", "terminal.js uses wrong 'action:' protocol (backend expects 'type:')"),
]
# Files to skip (build artifacts, venv, node_modules)
SKIP_DIRS = {"venv", "node_modules", "__pycache__", ".git", "graphify-out"}
SKIP_EXT = {".pyc"}
findings = []
def add(file, line_no, detail, excerpt):
findings.append(f"[{file}:{line_no}] {detail}\n > {excerpt.strip()[:120]}")
def load_truth():
if SETTINGS.exists():
try:
s = json.loads(SETTINGS.read_text(encoding="utf-8"))
p = int(s.get("dashboard", {}).get("port", 8080))
TRUTH["expected_port"] = p
except Exception:
pass
def scan_text_files():
for p in BASE.rglob("*"):
if not p.is_file():
continue
if any(part in SKIP_DIRS for part in p.relative_to(BASE).parts):
continue
if p.name == "integrity_check.py":
continue # don't flag our own pattern strings
# only scan text-like files
if p.suffix.lower() not in {
".md", ".py", ".js", ".ts", ".html", ".json", ".sh", ".yml", ".yaml", ".toml", ".ini", ".txt"
}:
continue
try:
text = p.read_text(encoding="utf-8", errors="ignore")
except Exception:
continue
rel = p.relative_to(BASE)
for pat, desc in STALE_PATTERNS:
for m in re.finditer(pat, text, re.IGNORECASE):
# find line number
line_no = text.count("\n", 0, m.start()) + 1
excerpt = text.splitlines()[line_no - 1] if line_no <= len(text.splitlines()) else ""
add(str(rel), line_no, desc, excerpt)
def check_live_server():
"""Confirm the running server is on the expected port and serving."""
import urllib.request
port = TRUTH["expected_port"]
url = f"http://127.0.0.1:{port}/"
try:
with urllib.request.urlopen(url, timeout=4) as r:
if r.status != 200:
findings.append(f"[LIVE] server on :{port} returned HTTP {r.status} (expected 200)")
except Exception as e:
findings.append(f"[LIVE] server NOT reachable on :{port} ({type(e).__name__}: {e})")
def check_terminal_endpoint():
"""The terminal page must reference /ws/terminal and the main host."""
tjs = BASE / "dashboard" / "pages" / "terminal.js"
if not tjs.exists():
findings.append("[terminal.js] file missing — Terminal page broken")
return
txt = tjs.read_text(encoding="utf-8", errors="ignore")
if "/ws/terminal" not in txt:
findings.append("[terminal.js] does NOT connect to /ws/terminal (terminal disconnected from OS)")
if "wsPort" in txt or ":8082" in txt or "port 8082" in txt:
findings.append("[terminal.js] still references dead 8082 port")
def check_agent_enabled():
"""opencode should be enabled (it is the code/DevOps agent)."""
if not SETTINGS.exists():
return
try:
s = json.loads(SETTINGS.read_text(encoding="utf-8"))
oc = s.get("agent_preferences", {}).get("opencode", {})
if oc.get("enabled") is not True:
findings.append(f"[settings.json] opencode.enabled={oc.get('enabled')!r} (expected true)")
except Exception:
pass
def main():
load_truth()
scan_text_files()
check_terminal_endpoint()
check_agent_enabled()
check_live_server()
print("=" * 64)
print(" Agentic-OS Integrity Check")
print(f" port(source of truth) = {TRUTH['expected_port']}")
print("=" * 64)
if not findings:
print(" ✓ CLEAN — no stale references detected.")
return 0
print(f"{len(findings)} FINDING(S):\n")
for f in findings:
print(" - " + f)
print()
return 1
if __name__ == "__main__":
try:
sys.exit(main())
except Exception as e:
print(f"CHECK ERROR: {type(e).__name__}: {e}")
sys.exit(2)