#!/usr/bin/env python3 """ AIOS Autonomous Controller — Full Pipeline v2.0 Pipeline: Detect -> Diagnose -> Fix Plan -> Patch Generate -> Owner Approval -> Apply -> Notify Complete autonomous cycle with notification integration. """ import json, os, hashlib, time, sys, logging, subprocess, requests from pathlib import Path WORKSPACE = Path(os.environ.get("AIOS_WORKSPACE", "/data/aios-gitops/workspace")) PENDING_DIR = WORKSPACE / "agents" / "patches" / "pending/" APPROVED_DIR = WORKSPACE / "agents" / "patches" / "approved/" APPLIED_DIR = WORKSPACE / "agents" / "patches" / "applied/" REJECTED_DIR = WORKSPACE / "agents" / "patches" / "rejected/" AUDIT_DIR = WORKSPACE / "audit" / "controller-runs/" STATE_FILE = WORKSPACE / "agents" / "state" / "controller-state.json" HEALTH_FILE = WORKSPACE / "agents" / "state" / "health.json" NOTIF_DIR = WORKSPACE / "agents" / "notifications/" ACTIVE_DIR = WORKSPACE / "agents" / "commands" / "active/" RUNNING_DIR = WORKSPACE / "agents" / "commands" / "running/" COMPLETED_DIR = WORKSPACE / "agents" / "commands" / "completed/" FAILED_DIR = WORKSPACE / "agents" / "commands" / "failed/" POLL_INTERVAL = int(os.environ.get("CONTROLLER_POLL_INTERVAL_SECONDS", "600")) GITEA_URL = os.environ.get("GITEA_URL", "http://127.0.0.1:3000") API_URL = os.environ.get("AIOS_API_URL", "http://127.0.0.1:18890") GITEA_TOKEN = os.environ.get("GITEA_TOKEN", "") logging.basicConfig(level=logging.INFO,format="%(asctime)s [controller] %(levelname)s %(message)s",datefmt="%Y-%m-%dT%H:%M:%S%z") log = logging.getLogger("autonomous-controller") CONTROLLER_VERSION = "2.0.0" def load_state(): if STATE_FILE.exists(): try: return json.loads(STATE_FILE.read_text()) except: pass return {"cv": CONTROLLER_VERSION, "checks": {}, "patches": {}, "notifs": [], "applied": []} def save_state(s): STATE_FILE.parent.mkdir(parents=True, exist_ok=True) STATE_FILE.write_text(json.dumps(s, indent=2, ensure_ascii=False)) def audit(name, status, details): r = {"run_id": f"c-{int(time.time())}", "check": name, "status": status, "ts": time.strftime("%Y-%m-%dT%H:%M:%S%z"), "details": details} AUDIT_DIR.mkdir(parents=True, exist_ok=True) (AUDIT_DIR / f"{r['run_id']}.json").write_text(json.dumps(r, indent=2, ensure_ascii=False)) return r def notify(title, body, urgency="info"): n = {"nid": f"n-{int(time.time())}", "title": title, "body": body, "urgency": urgency, "ts": time.strftime("%Y-%m-%dT%H:%M:%S%z")} NOTIF_DIR.mkdir(parents=True, exist_ok=True) (NOTIF_DIR / f"{n['nid']}.json").write_text(json.dumps(n, indent=2, ensure_ascii=False)) log.info(f"NOTIF: {n['nid']} -> {title}") return n # === Health Checks === def h_gitea(): try: r = requests.get(f"{GITEA_URL}/api/v1/version", timeout=5) return {"ok": r.status_code == 200, "data": r.json() if r.status_code == 200 else None, "err": None if r.status_code == 200 else f"HTTP {r.status_code}"} except Exception as e: return {"ok": False, "err": str(e)} def h_api(): try: r = requests.get(f"{API_URL}/admin/ai-evolution/api/data", timeout=5) if r.status_code == 200: d = r.json(); b = d.get("baseline","?"); v = len(d.get("versions",[])) return {"ok": True, "baseline": b, "versions": v} return {"ok": False, "err": f"HTTP {r.status_code}"} except Exception as e: return {"ok": False, "err": str(e)} def h_branch_sync(): bare = Path("/data/aios-gitops/runtime/gitea-bin/data/repositories/aios/aios-v23.git") try: r = subprocess.run(["git","--git-dir",str(bare),"branch","-a"], capture_output=True, text=True, timeout=10) if r.returncode != 0: return {"ok": False, "err": r.stderr[:200]} bb = set(l.strip().replace("remotes/origin/","").replace("* ","") for l in r.stdout.strip().split("\n") if l.strip()) gb = []; pg = 1 while True: r2 = requests.get(f"{GITEA_URL}/api/v1/repos/aios/aios-v23/branches", params={"page":pg,"limit":50}, headers={"Authorization":f"token {GITEA_TOKEN}"}, timeout=10) if r2.status_code != 200: break d = r2.json() if not d: break gb.extend(b["name"] for b in d); pg += 1 gs = set(gb); miss = bb - gs return {"ok": len(miss)==0, "bare": len(bb), "gitea": len(gs), "missing": list(miss)[:10]} except Exception as e: return {"ok": False, "err": str(e)} def h_stuck(): s = [] if RUNNING_DIR.exists(): n = time.time() for c in RUNNING_DIR.iterdir(): if c.name.endswith(".md"): a = (n - c.stat().st_mtime) / 60 if a > 30: s.append({"n": c.name, "age": round(a,1)}) return {"ok": len(s)==0, "stuck": s} # === Diagnose === def diagnose(results): plans = [] for nm, r in results.items(): if r.get("ok", True): continue if nm == "branch_sync": m = r.get("missing",[]) if m: plans.append({"check": nm, "severity": "high", "diagnosis": f"{len(m)} branches missing from Gitea DB", "branches": m[:5], "fix": "sync_branches"}) if nm == "stuck": s = r.get("stuck",[]) if s: plans.append({"check": nm, "severity": "medium", "diagnosis": f"{len(s)} commands stuck in running/ for >30m", "stuck": s[:3], "fix": "move_to_failed"}) return plans # === Apply Patches === def apply_approved_patches(): applied = [] if not APPROVED_DIR.exists(): return applied for f in sorted(APPROVED_DIR.iterdir()): if not f.name.endswith(".json"): continue try: patch = json.loads(f.read_text()) fix = patch.get("fix", "") log.info(f"Applying approved patch: {patch.get('check','?')} -> {fix}") if fix == "sync_branches": branches = patch.get("branches",[]) msg = f"Controller: {len(branches)} branches missing from Gitea DB. Patching via agent." notify("Branch Sync Patch Applied", msg, "info") # Move to applied APPLIED_DIR.mkdir(parents=True, exist_ok=True) f.rename(APPLIED_DIR / f.name) applied.append(patch) elif fix == "move_to_failed": stuck = patch.get("stuck",[]) for s in stuck: src = RUNNING_DIR / s.get("n","") if src.exists(): src.rename(FAILED_DIR / src.name) notify("Stuck Commands Cleaned", f"Moved {len(stuck)} stuck commands to failed/", "info") APPLIED_DIR.mkdir(parents=True, exist_ok=True) f.rename(APPLIED_DIR / f.name) applied.append(patch) except Exception as e: log.exception(f"Failed applying {f.name}: {e}") REJECTED_DIR.mkdir(parents=True, exist_ok=True) f.rename(REJECTED_DIR / f.name) return applied # === Main Cycle === def run_cycle(): log.info("--- Cycle start ---") results = {} log.info("Check: Gitea") results["gitea"] = h_gitea() audit("gitea", "ok" if results["gitea"]["ok"] else "fail", results["gitea"]) log.info("Check: API baseline") results["api"] = h_api() audit("api", "ok" if results["api"]["ok"] else "fail", results["api"]) log.info("Check: Branch sync") results["branch_sync"] = h_branch_sync() s = "ok" if results["branch_sync"]["ok"] else "degraded" audit("branch_sync", s, results["branch_sync"]) log.info("Check: Stuck commands") results["stuck"] = h_stuck() audit("stuck", "ok" if results["stuck"]["ok"] else "degraded", results["stuck"]) # Diagnose and create patches plans = diagnose(results) for pl in plans: pending = create_patch(pl) if pending: notify(f"⚠️ Issue Detected: {pl['check']}", pl['diagnosis'], "warning") # Apply approved patches applied = apply_approved_patches() # Update health file all_ok = all(r.get("ok",False) for r in results.values()) summary = { "ts": time.strftime("%Y-%m-%dT%H:%M:%S%z"), "ok": all_ok, "checks": {k:v.get("ok") for k,v in results.items()}, "plans": len(plans), "applied": len(applied) } HEALTH_FILE.parent.mkdir(parents=True, exist_ok=True) HEALTH_FILE.write_text(json.dumps(summary, indent=2, ensure_ascii=False)) log.info(f"Cycle done. All healthy: {all_ok}. Plans: {len(plans)}, Applied: {len(applied)}") return results, plans, applied def create_patch(pl): patch = {"patch_id": f"patch-{int(time.time())}", "created": time.strftime("%Y-%m-%dT%H:%M:%S%z"), "check": pl["check"], "severity": pl["severity"], "diagnosis": pl["diagnosis"], "branches": pl.get("branches",[]), "stuck": pl.get("stuck",[]), "fix": pl.get("fix","")} PENDING_DIR.mkdir(parents=True, exist_ok=True) f = PENDING_DIR / f"{patch['patch_id']}.json" if f.exists(): return False f.write_text(json.dumps(patch, indent=2, ensure_ascii=False)) log.info(f"Patch created: {patch['patch_id']}") return True def run_forever(): log.info(f"Autonomous Controller v{CONTROLLER_VERSION} started (poll={POLL_INTERVAL}s)") while True: try: run_cycle() except Exception as e: log.exception(f"Cycle failed: {e}") time.sleep(POLL_INTERVAL) def run_once(): log.info("One-shot check") results, plans, applied = run_cycle() for n, r in results.items(): st = "OK" if r.get("ok") else "FAIL" log.info(f" [{st}] {n}") if plans: log.info(f"Patches created: {len(plans)}") if applied: log.info(f"Patches applied: {len(applied)}") if __name__ == "__main__": if len(sys.argv) > 1 and sys.argv[1] == "--once": run_once() else: run_forever()