Files
aios/agents/scripts/autonomous_controller.py
T
openclaw e4bcf47f79 feat(cmd): enable Agent Command Plane, audit plane, and Web UI dashboard
CMD-20260708-fix-git-actions-write-and-enable-audit-plane
CMD-BOOTSTRAP-001-enable-command-plane

- Add admin AI evolution dashboard (Web UI)
- Add Agent Command Plane structure (agents/, cmd/)
- Add audit plane with agent-run records
- Add architecture documentation
- Fix gitops-write-bot token authentication

Baseline: V1R3C1
2026-07-08 21:07:54 +08:00

241 lines
9.7 KiB
Python

#!/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()