Compare commits

..
12 changed files with 401 additions and 686 deletions
+385 -82
View File
@@ -1,95 +1,398 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" AI GitOps Actions API """ from __future__ import annotations
import asyncio, base64, hashlib, hmac, json, os, re, shutil, subprocess, sys, time, uuid import hashlib, hmac, json, os, re, subprocess, sys, time, urllib.parse, uuid
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path from pathlib import Path
from urllib.parse import parse_qs, urlparse from typing import Any
from aiohttp import web sys.path.insert(0, str(Path(os.getenv('AIOS_GITOPS_WORKSPACE', '/data/aios-gitops/workspace')) / 'runtime' / 'agent-os'))
from orchestrator.action_monitor import ActionMonitor
# Environment ROOT = Path(os.getenv('AIOS_GITOPS_ROOT', '/data/aios-gitops'))
WS = Path(os.environ.get("AIOS_WORKSPACE", "/data/aios-gitops/workspace")) WS = Path(os.getenv('AIOS_GITOPS_WORKSPACE', str(ROOT / 'workspace')))
BASE_DIR = WS / "active-runtime" SECRETS = Path(os.getenv('AIOS_GITOPS_SECRETS_DIR', str(ROOT / 'runtime' / 'secrets')))
APPDIR = BASE_DIR / "app" TOKEN_FILE = Path(os.getenv('AIOS_GITOPS_TOKEN_FILE', str(SECRETS / 'gitops.tokens')))
AUDITDIR = WS / "audit" HOST = os.getenv('AIOS_GITOPS_HOST', '127.0.0.1')
WORKDIR = WS / "work" PORT = int(os.getenv('AIOS_GITOPS_PORT', '18890'))
DOTDIR = WS.parent OPENAPI = WS / 'docs' / 'gitops' / 'openapi-chatgpt-actions.yaml'
GITEA_DIR = DOTDIR / "runtime" / "gitea-bin" / "data" / "repositories" / "aios" BARE_REPO = Path(os.getenv('AIOS_GITOPS_BARE_REPO', str(ROOT / 'runtime' / 'gitea-bin' / 'data' / 'repositories' / 'aios' / 'aios-v23.git')))
monitor = ActionMonitor()
# Load config BASE_MODELS = [
try: {'id':'base.deepseekv4-flash','provider':'one-api','model':'deepseekv4-flash','display_name':'DeepSeek V4 Flash','scope':'global'},
import yaml {'id':'base.qwen3.6-35b','provider':'one-api','model':'qwen3.6-35b','display_name':'Qwen 3.6 35B','scope':'global'},
config_path = WS / "active-runtime" / "app" / "config.yaml" {'id':'base.gpt-5.5','provider':'openai','model':'gpt-5.5','display_name':'GPT-5.5','scope':'global'},
with open(config_path) as f: ]
CONFIG = yaml.safe_load(f) or {} SECRET_RE = re.compile(r'(?i)(api[_-]?key|secret[_-]?access[_-]?key|secret|password|token)\s*[:=]\s*["\']?([A-Za-z0-9_./+=:-]{16,})')
except: CONFIG = {} SECRET_VAL_RE = re.compile(r'(sk-[A-Za-z0-9_-]{20,}|HPUA[A-Z0-9]{10,}|SDC[A-Za-z0-9]{20,})')
start_time = time.time() def now() -> str: return time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
log_ident = "aios-api" def uid(p='id') -> str: return f'{p}-{int(time.time()*1000)}-{uuid.uuid4().hex[:8]}'
def safe(s: str, n=160) -> str: return re.sub(r'[^A-Za-z0-9_./@-]+', '-', str(s or 'unknown')).strip('/-')[:n] or 'unknown'
def sh(args, check=False):
p = subprocess.run(['git'] + args, cwd=str(WS), text=True, capture_output=True)
if check and p.returncode != 0: raise RuntimeError(p.stderr or p.stdout or 'git failed')
return {'code':p.returncode,'stdout':p.stdout.strip(),'stderr':p.stderr.strip(),'cmd':['git']+args}
def jload(p: Path, default):
try: return json.loads(p.read_text(encoding='utf-8'))
except Exception: return default
def relpath(path: str) -> str:
x = str(path or '').replace('\\','/').strip()
if not x or x.startswith('/') or '..' in Path(x).parts or Path(x).parts[0] == '.git': raise ValueError('invalid path')
return str(Path(x))
def branch_name(name: str | None, prefix='patch/chatgpt') -> str:
b = safe(name or f'{prefix}-{uid("br")}', 180)
if b in ('main','master') or b.startswith('main/') or b.startswith('master/'): raise ValueError('direct main/master writes are forbidden')
return b
def _json_has_plain_secret(obj: Any) -> bool:
if isinstance(obj, dict):
for k, v in obj.items():
lk = str(k).lower()
if lk == 'secret_ref' or lk.endswith('_secret_ref'):
continue
if lk in {'api_key', 'access_key', 'secret_access_key', 'secret_key', 'password', 'token', 'bearer_token'}:
if isinstance(v, str) and v and not v.startswith('secret://') and v != 'REDACTED':
return True
if _json_has_plain_secret(v):
return True
return False
if isinstance(obj, list):
return any(_json_has_plain_secret(x) for x in obj)
if isinstance(obj, str):
return bool(obj and not obj.startswith('secret://') and obj != 'REDACTED' and SECRET_VAL_RE.search(obj))
return False
def sh(cmd, **kw): def content_has_secret(text: str) -> bool:
try: text = text or ''
r = subprocess.run(cmd, capture_output=True, text=True, cwd=WS, **kw) try:
return r.stdout.strip() if r else None return _json_has_plain_secret(json.loads(text))
except: return None except Exception:
pass
if SECRET_VAL_RE.search(text):
return True
for m in SECRET_RE.finditer(text):
key = str(m.group(1)).lower()
value = str(m.group(2))
if 'secret_ref' in key or value.startswith('secret://') or value == 'REDACTED':
continue
return True
return False
def token_records():
if not TOKEN_FILE.exists(): return []
out=[]
for raw in TOKEN_FILE.read_text(encoding='utf-8').splitlines():
line=raw.strip()
if not line or line.startswith('#'): continue
if line.startswith('{'):
try: out.append(json.loads(line))
except Exception: pass
else:
out.append({'account':'legacy-token','role':'owner','scopes':['*'],'token_plain':line})
return out
def authenticate(token: str):
if os.getenv('AIOS_GITOPS_DEV_ALLOW','0') == '1': return {'account':'dev-owner','role':'owner','scopes':['*']}
if not token: return None
digest=hashlib.sha256(token.encode()).hexdigest()
for r in token_records():
if r.get('disabled'): continue
if r.get('token_sha256') and hmac.compare_digest(str(r['token_sha256']), digest): return r
if r.get('token_plain') and hmac.compare_digest(str(r['token_plain']), token): return r
return None
def sanitize(obj: Any) -> Any:
if isinstance(obj, dict):
return {k: ('REDACTED' if re.search(r'(?i)(api[_-]?key|secret|password|token)', str(k)) else sanitize(v)) for k,v in obj.items()}
if isinstance(obj, list): return [sanitize(x) for x in obj]
if isinstance(obj, str): return SECRET_VAL_RE.sub('REDACTED', obj)
return obj
def ensure_git():
if not sh(['config','user.email'])['stdout']: sh(['config','user.email','chatgpt-gitops-bot@cnnetmkt.top'], True)
if not sh(['config','user.name'])['stdout']: sh(['config','user.name','chatgpt-gitops-bot'], True)
def checkout(branch, base='main', create=False):
ensure_git(); b=branch_name(branch)
if create or sh(['rev-parse','--verify',b])['code'] != 0:
sh(['checkout',base], True); sh(['checkout','-B',b], True)
else: sh(['checkout',b], True)
return b
def write_file(path, content):
if content_has_secret(content): raise ValueError('secret_rejected: use secret_ref; plaintext API keys must not enter Git')
r=relpath(path); t=WS/r; t.parent.mkdir(parents=True, exist_ok=True); t.write_text(content, encoding='utf-8'); return r
def write_json(path, data): return write_file(path, json.dumps(data, ensure_ascii=False, indent=2) + '\n')
def sync_branch(branch: str) -> dict:
if not branch or branch in ('main','master') or branch.startswith('main/') or branch.startswith('master/'):
return {'status':'skipped','reason':'protected_branch','branch':branch}
if BARE_REPO.exists():
ref=f'refs/heads/{branch}:refs/heads/{branch}'
p=subprocess.run(['git','--git-dir',str(BARE_REPO),'fetch',str(WS),ref], text=True, capture_output=True)
if p.returncode != 0: raise RuntimeError(p.stderr or p.stdout or f'git sync failed for {branch}')
return {'status':'synced','target':'bare','branch':branch,'stdout':p.stdout.strip(),'stderr':p.stderr.strip()}
p=subprocess.run(['git','push','origin',f'HEAD:refs/heads/{branch}'], cwd=str(WS), text=True, capture_output=True)
if p.returncode != 0: raise RuntimeError(p.stderr or p.stdout or f'git push failed for {branch}')
return {'status':'synced','target':'origin','branch':branch,'stdout':p.stdout.strip(),'stderr':p.stderr.strip()}
def baselines(): def sync_tag(tag: str) -> dict:
p = WS / "baselines" / "current.json" if not tag: return {'status':'skipped','reason':'empty_tag'}
if p.exists(): if BARE_REPO.exists():
try: return json.loads(p.read_text()) ref=f'refs/tags/{tag}:refs/tags/{tag}'
except: pass p=subprocess.run(['git','--git-dir',str(BARE_REPO),'fetch',str(WS),ref], text=True, capture_output=True)
return {} if p.returncode != 0: raise RuntimeError(p.stderr or p.stdout or f'git tag sync failed for {tag}')
return {'status':'synced','target':'bare','tag':tag,'stdout':p.stdout.strip(),'stderr':p.stderr.strip()}
p=subprocess.run(['git','push','origin',tag], cwd=str(WS), text=True, capture_output=True)
if p.returncode != 0: raise RuntimeError(p.stderr or p.stdout or f'git tag push failed for {tag}')
return {'status':'synced','target':'origin','tag':tag,'stdout':p.stdout.strip(),'stderr':p.stderr.strip()}
def commands(status): def commit(paths, msg):
d = WS / "agents" / "commands" / status ensure_git(); paths = [relpath(p) for p in paths] if paths else ['.']; sh(['add'] + paths, True)
if not d.exists(): return [] branch=sh(['rev-parse','--abbrev-ref','HEAD'])['stdout']
return [{"name": f.name, "state": status} for f in sorted(d.iterdir()) if f.name.endswith(".md")] if not sh(['status','--short'])['stdout']:
return {'status':'no_changes','branch':branch,'sync':sync_branch(branch)}
out=sh(['commit','-m',msg], True); branch=sh(['rev-parse','--abbrev-ref','HEAD'])['stdout']; sync=sync_branch(branch); return {'status':'committed','branch':branch,'rev':sh(['rev-parse','--short','HEAD'], True)['stdout'],'git':out,'sync':sync}
def audit(actor, action, target, payload):
p=ROOT/'artifacts'/'chatgpt-gitops-audit.jsonl'; p.parent.mkdir(parents=True, exist_ok=True)
with p.open('a', encoding='utf-8') as f: f.write(json.dumps({'at':now(),'actor':actor,'action':action,'target':target,'payload':sanitize(payload)}, ensure_ascii=False)+'\n')
def model_docs():
out=[]
for p in sorted((WS/'config'/'model_registry').glob('**/*.json')): out.append(jload(p, {'path':str(p.relative_to(WS))}))
return out
def patches():
out=[]
for p in sorted((WS/'patches').glob('**/*.json')): out.append(jload(p, {'path':str(p.relative_to(WS))}))
return out
def agent_runs(): class H(BaseHTTPRequestHandler):
d = AUDITDIR / "agent-runs" server_version='AIOSChatGPTGitOps/V1R1C0'
if not d.exists(): return [] def sendj(self, data, code=200):
rs = [] b=json.dumps(data, ensure_ascii=False, indent=2).encode(); self.send_response(code); self.send_header('Content-Type','application/json; charset=utf-8'); self.send_header('Content-Length',str(len(b))); self.end_headers(); self.wfile.write(b)
for f in sorted(d.iterdir(), reverse=True): def sendt(self, text, ctype='text/plain; charset=utf-8', code=200):
if f.name.endswith(".json"): b=text.encode(); self.send_response(code); self.send_header('Content-Type',ctype); self.send_header('Content-Length',str(len(b))); self.end_headers(); self.wfile.write(b)
try: def body(self):
j = json.loads(f.read_text()) n=int(self.headers.get('Content-Length','0') or '0'); raw=self.rfile.read(n).decode() if n else '{}'
j["file"] = f.stem try: return json.loads(raw or '{}')
rs.append(j) except Exception: return {'_raw':raw}
except: pass def actor(self):
return rs auth=self.headers.get('Authorization',''); tok=auth[7:].strip() if auth.startswith('Bearer ') else self.headers.get('X-AIOS-Token','')
rec=authenticate(tok)
if rec: return {'ok':True,'account':safe(rec.get('account') or rec.get('name') or 'token-user'),'role':rec.get('role','user'),'scopes':set(rec.get('scopes') or [])}
return {'ok':False,'account':'anonymous','role':'anonymous','scopes':set()}
def need(self, scope):
a=self.actor()
if not a['ok']: self.sendj({'error':'unauthorized','hint':'Bearer token required'},401); return None
if '*' not in a['scopes'] and scope not in a['scopes']: self.sendj({'error':'forbidden','required_scope':scope,'account':a['account']},403); return None
return a
def do_GET(self):
u=urllib.parse.urlparse(self.path); path=u.path.rstrip('/') if u.path != '/' else u.path; q=urllib.parse.parse_qs(u.query)
try:
if path in ('/health','/api/health'): return self.sendj({'status':'ok','service':'chatgpt-gitops-adapter','workspace':str(WS)})
if path in ('/api/openapi.yaml','/api/openapi-chatgpt-actions.yaml','/api/schema'):
return self.sendt(OPENAPI.read_text(encoding='utf-8'), 'application/yaml; charset=utf-8') if OPENAPI.exists() else self.sendj({'error':'schema_missing'},404)
if path in ('/api','/api/status'): return self.sendj(api_spec())
if path in ('/api/base','/api/baseline','/base'):
data=jload(WS/'baseline'/'current.json', {}); data.setdefault('git', sh(['rev-parse','--short','HEAD'])['stdout']); return self.sendj(data)
if path in ('/api/ver','/api/versions','/ver'):
return self.sendj({'tags':sh(['tag','--list','V*'])['stdout'].splitlines(),'versions_dir':[p.name for p in (WS/'versions').glob('*')],'head':sh(['rev-parse','--short','HEAD'])['stdout']})
if path in ('/api/diff','/diff'):
frm=(q.get('from') or ['HEAD~1'])[0]; to=(q.get('to') or ['HEAD'])[0]; return self.sendj({'from':frm,'to':to,'diff':sh(['diff',frm,to])})
if path in ('/api/patch','/api/patches','/api/branches','/patch'): return self.sendj({'patches':patches(),'branches':sh(['branch','--format','%(refname:short)'])['stdout'].splitlines()})
if path in ('/api/model','/model'): return self.sendj({'base':BASE_MODELS,'custom':model_docs(),'priority':[jload(p,{}) for p in sorted((WS/'config'/'model_priority').glob('*.json'))]})
if path in ('/api/run','/api/runtime-status','/run'):
svc=subprocess.run(['systemctl','is-active','aios-gitops-model-api'], text=True, capture_output=True)
return self.sendj({'service':'aios-gitops-model-api','status':svc.stdout.strip() or svc.stderr.strip(),'workspace_branch':sh(['rev-parse','--abbrev-ref','HEAD'])['stdout']})
if path in ('/api/action-monitor','/action-monitor'):
latest=monitor.latest()
ldict={'event_id':latest.event_id,'action':latest.action,'error':latest.error,'context':latest.context,'created_at':latest.created_at} if latest else None
return self.sendj({'events':[{"event_id":e.event_id,"action":e.action,"error":e.error,"context":e.context,"created_at":e.created_at} for e in monitor.list_events()],'latest':ldict})
# Admin AI Evolution Dashboard
if path in ('/admin/ai-evolution','/admin/ai-evolution/'):
return self.ai_evolution_page()
if path in ('/admin/ai-evolution/api/data'):
return self.ai_evolution_data()
return self.sendj({'error':'not_found','path':path},404)
except Exception as e:
monitor.record_failure('do_GET', str(e), {'path':path})
return self.sendj({'error':str(e)},500)
def do_PUT(self):
path=urllib.parse.urlparse(self.path).path.rstrip('/'); data=self.body()
if path == '/api/model-priority': return self.model_priority(data)
return self.sendj({'error':'not_found','path':path},404)
def do_POST(self):
path=urllib.parse.urlparse(self.path).path.rstrip('/'); data=self.body()
try:
if path in ('/api/branch','/api/create-branch'): return self.branch(data)
if path in ('/api/file','/api/commit-file'): return self.file(data)
if path == '/api/commit': return self.commit(data)
if path in ('/api/patch','/api/create-patch'): return self.patch(data)
if path in ('/api/tag','/api/create-tag'): return self.tag(data)
if path == '/api/rollback': return self.rollback(data)
if path == '/api/model': return self.model(data)
if path == '/api/model-priority': return self.model_priority(data)
if path == '/api/nl': return self.nl(data)
return self.sendj({'error':'not_found','path':path},404)
except Exception as e:
monitor.record_failure('do_POST', str(e), {'path':path})
return self.sendj({'error':str(e)},500)
def branch(self,data):
a=self.need('branch:create');
if not a: return
b=checkout(data.get('branch'), data.get('base','main'), True); meta={'type':'branch','branch':b,'base':data.get('base','main'),'created_by':a['account'],'created_at':now(),'direct_main_push':False,'owner_approval_required':True}
r=write_json(f'patches/{safe(b).replace("/","_")}.branch.json', meta); c=commit([r], data.get('message') or f'chore(chatgpt): create {b}'); audit(a['account'],'branch',b,meta); self.sendj({'status':'ok','branch':b,'commit':c})
def file(self,data):
a=self.need('file:write');
if not a: return
b=checkout(data.get('branch'), data.get('base','main'), bool(data.get('create_branch'))); r=write_file(data.get('path'), data.get('content','')); c=commit([r], data.get('message') or f'chore(chatgpt): update {r}'); audit(a['account'],'file',r,{'branch':b,'commit':c}); self.sendj({'status':'committed','branch':b,'path':r,'commit':c})
def commit(self,data):
a=self.need('commit:create');
if not a: return
b=checkout(data.get('branch') or sh(['rev-parse','--abbrev-ref','HEAD'])['stdout'], data.get('base','main'), False); paths=data.get('paths') or ['.']; paths=[paths] if isinstance(paths,str) else paths; c=commit(paths, data.get('message') or 'chore(chatgpt): commit changes'); audit(a['account'],'commit',b,{'paths':paths,'commit':c}); self.sendj({'status':c['status'],'branch':b,'commit':c})
def patch(self,data):
a=self.need('patch:create');
if not a: return
name=data.get('patch_name') or data.get('name') or uid('patch'); b=checkout(data.get('branch') or f'patch/{safe(name)}', data.get('base','main'), True); meta={'id':uid('patch'),'patch_name':name,'branch':b,'risk':data.get('risk','medium'),'description':data.get('description',''),'created_by':a['account'],'created_at':now()}
r=write_json(f'patches/{safe(name)}.json', meta); c=commit([r], data.get('message') or f'chore(chatgpt): create patch {name}'); audit(a['account'],'patch',name,meta); self.sendj({'status':'patch_branch_created','patch':meta,'commit':c})
def tag(self,data):
a=self.need('tag:create');
if not a: return
ver=data.get('version') or data.get('tag')
if not re.match(r'^V\d+R\d+C\d+(-[A-Za-z0-9_.-]+)?$', ver or ''): return self.sendj({'error':'version must match VxRxCx'},400)
if data.get('confirm') != True:
self.sendj({'status':'confirm_required','operation':'tag','hint':'This operation creates a Git tag. Confirm to proceed.','x-openai-isConsequential':True}, 428)
return
if a['role'] != 'owner' or not data.get('execute'):
b=checkout(data.get('branch') or f'request/tag-{safe(ver)}', 'main', True); req={'version':ver,'requested_by':a['account'],'approval_required':True,'created_at':now()}; r=write_json(f'baseline/tag_requests/{safe(ver)}.json', req); c=commit([r], f'chore(chatgpt): request tag {ver}'); return self.sendj({'status':'approval_required','branch':b,'commit':c})
out=sh(['tag','-a',ver,'-m',data.get('message') or f'release {ver}'], True); self.sendj({'status':'tagged','version':ver,'git':out,'sync':sync_tag(ver)})
def rollback(self,data):
a=self.need('rollback:request');
if not a: return
target=data.get('target_version') or data.get('version') or data.get('target')
if not target: return self.sendj({'error':'target_version required'},400)
b=checkout(data.get('branch') or f'request/rollback-{safe(target)}-{uid("rb")}', 'main', True); req={'id':uid('rollback'),'target_version':target,'reason':data.get('reason',''),'requested_by':a['account'],'approval_required':True,'execute':False,'created_at':now()}; r=write_json(f'baseline/rollback_requests/{safe(req["id"])}.json', req); c=commit([r], f'chore(chatgpt): request rollback {target}'); self.sendj({'status':'rollback_request_committed','branch':b,'request':req,'commit':c})
def model(self,data):
a=self.need('model:write');
if not a: return
name=data.get('model_name') or data.get('model'); base=(data.get('base_url') or '').rstrip('/'); prov=data.get('provider_name') or 'custom-provider'
if not name or not base: return self.sendj({'error':'provider_name/base_url/model_name required'},400)
owner=safe(data.get('username') or a['account']); secret_ref=data.get('api_key_secret_ref') or data.get('secret_ref')
if data.get('api_key'):
d=SECRETS/'llm'/owner; d.mkdir(parents=True, exist_ok=True); sid=uid('llmkey'); p=d/f'{sid}.secret'; p.write_text(str(data['api_key']), encoding='utf-8'); os.chmod(p,0o600); secret_ref=f'secret://llm/{owner}/{sid}'
if not secret_ref: return self.sendj({'error':'api_key_secret_ref or api_key required'},400)
mid=data.get('model_id') or f'custom.{owner}.{hashlib.sha1((prov+base+name).encode()).hexdigest()[:10]}'; b=checkout(data.get('branch') or f'patch/model-{safe(mid)}','main',True)
doc=sanitize({k:v for k,v in data.items() if k!='api_key'}); doc.update({'model_id':mid,'provider_name':prov,'base_url':base,'model_name':name,'api_key_secret_ref':secret_ref,'created_by':a['account'],'created_at':now()})
if data.get('confirm') != True:
self.sendj({'status':'confirm_required','operation':'model','hint':'This operation registers a new model. Confirm to proceed.','x-openai-isConsequential':True}, 428)
return
r=write_json(f'config/model_registry/chatgpt/{safe(mid)}.json', doc); c=commit([r], data.get('message') or f'chore(chatgpt): register model {name}'); audit(a['account'],'model',mid,doc); self.sendj({'status':'model_registered','branch':b,'model_id':mid,'secret_ref':secret_ref,'commit':c})
def model_priority(self,data):
a=self.need('model:priority');
if not a: return
if data.get('confirm') != True:
self.sendj({'status':'confirm_required','operation':'model-priority','hint':'This operation updates model priority. Confirm to proceed.','x-openai-isConsequential':True}, 428)
return
user=safe(data.get('username') or a['account']); b=checkout(data.get('branch') or f'patch/model-priority-{user}', 'main', True); doc={'username':user,'items':data.get('items') or data.get('priority') or [],'updated_by':a['account'],'updated_at':now()}; r=write_json(f'config/model_priority/{user}.json', doc); c=commit([r], data.get('message') or f'chore(chatgpt): update model priority {user}'); audit(a['account'],'model-priority',user,doc); self.sendj({'status':'updated','branch':b,'priority':doc,'commit':c})
def nl(self,data):
a=self.need('nl:plan');
if not a: return
text=data.get('text',''); low=text.lower(); intent='unknown'; plan=[]
if 'rollback' in low or '回滚' in text: intent='rollback_request'; plan=['commit rollback request','wait owner/admin approval']
elif 'diff' in low or '对比' in text: intent='diff'; plan=['call GET /api/diff']
elif 'model' in low or '模型' in text: intent='model_change'; plan=['use POST /api/model or PUT /api/model-priority','commit metadata']
elif 'patch' in low or '补丁' in text: intent='patch_create'; plan=['call POST /api/patch']
b=checkout(data.get('branch') or f'patch/nl-{uid("nl")}', 'main', True); doc={'id':uid('nl'),'text':text,'intent':intent,'action_plan':plan,'execute':False,'owner_approval_required':True,'created_by':a['account'],'created_at':now()}; r=write_json(f'patches/nl/{safe(doc["id"])}.json', doc); c=commit([r], f'chore(chatgpt): record nl plan {intent}'); self.sendj({'status':'planned','branch':b,'request':doc,'commit':c})
_EVOLUTION_HTML = '''<!DOCTYPE html>
<html lang=zh-CN>
<head>
<meta charset=UTF-8>
<meta name=viewport content="width=device-width,initial-scale=1.0">
<title>AIOS Evolution Dashboard</title>
<script src=https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js></script>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#0d1117;color:#c9d1d9;font-size:14px;line-height:1.7;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif}
.c{max-width:1000px;margin:0 auto;padding:32px 24px}
h1{font-size:24px;color:#58a6ff;margin-bottom:2px}
.sub{color:#8b949e;font-size:13px;border-bottom:1px solid #21262d;padding-bottom:16px;margin-bottom:20px;display:flex;justify-content:space-between}
.g{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:16px;margin-bottom:20px}
.s{background:#161b22;border:1px solid #21262d;border-radius:8px;padding:18px;text-align:center}
.s .v{font-size:28px;font-weight:700;color:#f0f6fc}
.s .l{font-size:11px;color:#8b949e;margin-top:4px}
.bx{background:#161b22;border:1px solid #21262d;border-radius:8px;margin-bottom:14px}
.bh{border-bottom:1px solid #21262d;padding:10px 16px;font-weight:600;font-size:14px;display:flex;gap:6px;align-items:center}
.bd{padding:12px 16px}
.rw{display:grid;grid-template-columns:1fr 1fr 100px 60px;gap:8px;padding:8px 16px;border-bottom:1px solid #21262d;font-size:13px;align-items:center;cursor:pointer;transition:background .15s}
.rw:hover{background:#1c2128}.rw:last-child{border-bottom:none}
.tg{display:inline-block;padding:2px 10px;border-radius:99px;font-size:11px;font-weight:600}
.ok{background:#3fb95033;color:#3fb950}.fl{background:#da363333;color:#f85149}.if{background:#1f6feb33;color:#58a6ff}
pre{background:#0d1117;padding:12px;border-radius:6px;overflow:auto;font-size:12px;color:#8b949e;max-height:300px;margin-top:8px}select{padding:8px 10px;border:1px solid #30363d;border-radius:6px;background:#21262d;color:#c9d1d9;font-size:13px;max-width:400px;width:100%;margin-bottom:4px;cursor:pointer}
</style>
</head>
<body>
<div id=app class=c>
<h1>AIOS Evolution Dashboard</h1>
<div class=sub><span>Baseline: {{baseline}}</span><span style=color:#8b949e>{{refreshMsg}}</span></div>
<div class=g><div class=s><div class=v>{{tot}}</div><div class=l>Commands</div></div><div class=s><div class=v style=color:#3fb950>{{sok}}</div><div class=l>Succeeded</div></div><div class=s><div class=v style=color:#f85149>{{sfail}}</div><div class=l>Failed</div></div><div class=s><div class=v>{{runs}}</div><div class=l>Executions</div></div></div>
<div class=bx><div class=bh>Version Timeline</div><div class=bd><div class=rw style=font-weight:600;border-bottom:2px solid #30363d;cursor:default><span>Version</span><span>Time</span><span>Result</span></div><div v-for=v in vs class=rw @click=selVer(v)><span style=color:#58a6ff>{{v.tag}}</span><span>{{v.time||'-'}}</span><span :class="'tg '+v.result">{{v.result||'-'}}</span></div></div></div>
<div class=bx><div class=bh>Commands</div><div class=bd><select v-model=cmd @change=loadCmd><option value>-- Select --</option><option v-for=c in cmds :value=c>{{c}}</option></select><pre v-if=cdet>{{cdet}}</pre></div></div>
<div class=bx><div class=bh>Recent Agent Runs</div><div class=bd><div class=rw style=font-weight:600;border-bottom:2px solid #30363d;cursor:default><span>Agent</span><span>Command</span><span>Count</span><span>Result</span></div><div v-for=r in aruns class=rw><span>{{r.agent||'-'}}</span><span style=overflow:hidden;white-space:nowrap;text-overflow:ellipsis>{{r.command}}</span><span>{{r.interaction_count||0}}</span><span :class="'tg '+r.final_result">{{r.final_result||'-'}}</span></div></div></div></div>
<script>
const{createApp,ref,onMounted}=Vue;createApp({setup(){const vs=ref([]),cmds=ref([]),ar=ref([]),cmd=ref(''),cdet=ref(null),tot=ref(0),sok=ref(0),sfail=ref(0),runs=ref(0),baseline=ref('V1R3C1'),refreshMsg=ref('')
async function ld(){try{const r=await fetch('/admin/ai-evolution/api/data'),d=await r.json();baseline.value=d.baseline||'V1R3C1';vs.value=d.versions||[];cmds.value=d.commands||[];ar.value=d.agent_runs||[];tot.value=d.stats?.total||0;sok.value=d.stats?.success||0;sfail.value=d.stats?.failed||0;runs.value=d.stats?.agent_runs||0;refreshMsg.value='Last updated: '+new Date().toLocaleString()}catch(e){}}
async function loadCmd(){if(!cmd.value){cdet.value=null;return}try{const r=await fetch('/admin/ai-evolution/api/data?command='+encodeURIComponent(cmd.value)),d=await r.json();cdet.value=JSON.stringify(d,null,2)}catch(e){cdet.value='Error'}}
function selVer(v){cmd.value=v.tag||v.id||v.version;loadCmd()}
onMounted(()=>{ld();setInterval(ld,30000)})
return{vs,cmds,ar,cmd,cdet,tot,sok,sfail,runs,baseline,refreshMsg,loadCmd,selVer}})mount('#app')
</script></body></html>'''
async def handle_api(request): def ai_evolution_page(self):
path = request.path self.send_response(200)
if path in ('/admin/ai-evolution','/admin/ai-evolution/'): self.send_header('Content-Type', 'text/html; charset=utf-8')
# Serve the HTML page self.send_header('Content-Length', str(len(self._EVOLUTION_HTML)))
html_path = APPDIR / 'static' / 'admin-ai-evolution.html' self.end_headers()
if html_path.exists(): self.wfile.write(self._EVOLUTION_HTML.encode())
return web.Response(text=html_path.read_text(), content_type='text/html')
return web.Response(text='Dashboard not found', status=404)
if path == '/admin/ai-evolution/api/data':
result = subprocess.run(['git','tag','--list','V*'], capture_output=True, text=True, cwd=WS)
tags = result.stdout.strip().splitlines() if result.returncode == 0 else []
h = subprocess.run(['git','rev-parse','--short','HEAD'], capture_output=True, text=True, cwd=WS)
head = h.stdout.strip() if h.returncode == 0 else 'unknown'
b = subprocess.run(['git','rev-parse','--abbrev-ref','HEAD'], capture_output=True, text=True, cwd=WS)
branch = b.stdout.strip() if b.returncode == 0 else 'unknown'
return web.json_response({
'baseline': baselines(),
'versions': [{'tag':t,'name':p.name} for p in sorted((WS/'versions').glob('*')) for t in tags if p.name in t],
'commands': commands('active') + commands('running') + commands('completed'),
'agent_runs': agent_runs(),
'stats': {
'current': head,
'branch': branch,
'uptime': round(time.time()-start_time, 2),
'baseline': baselines().get('current_baseline','unknown'),
}
})
return web.json_response({'error':'not_found','path':path}, status=404)
app = web.Application() def ai_evolution_data(self):
app.router.add_route('GET', '/admin/ai-evolution', handle_api) q=urllib.parse.urlparse(self.path).query
app.router.add_route('GET', '/admin/ai-evolution/', handle_api) params=urllib.parse.parse_qs(q)
app.router.add_route('GET', '/admin/ai-evolution/api/data', handle_api) cmd_filter=(params.get('command') or [None])[0]
bl=jload(WS/'baseline'/'current.json',{})
current_ver=bl.get('current_baseline','V1R3C1')
tags=sh(['tag','--list','V*'])['stdout'].splitlines() if WS.exists() else []
versions=[]
if (WS/'baseline'/'promotions').exists():
for p in sorted((WS/'baseline'/'promotions').glob('*.json')):
d=jload(p,{});v=d.get('version',p.stem)
versions.append({'tag':v,'time':d.get('promoted_at','-'),'result':'ok'})
for t in tags:
if not any(v['tag']==t for v in versions):
versions.append({'tag':t,'time':'-','result':'if'})
agent_runs=[];ard=WS/'audit'/'agent-runs'
if ard.exists():
for f in sorted(ard.glob('*.json'),reverse=True)[:50]:
d=jload(f,{});agent_runs.append(d) if isinstance(d,dict) else None
cmd_dirs=[WS/'agents/commands/active',WS/'agents/commands/running',WS/'agents/commands/completed',WS/'agents/commands/failed']
commands=[]
for d in cmd_dirs:
if d.exists():
for f in sorted(d.glob('*.md')):
commands.append(f.name)
command_detail=None
if cmd_filter:
for d in cmd_dirs:
if d.exists():
fp=d/cmd_filter
if fp.exists():
command_detail={'command':cmd_filter,'content':fp.read_text(encoding='utf-8')}
break
if not command_detail:
command_detail={'command':cmd_filter,'error':'not_found'}
return self.sendj(command_detail)
completed=list((WS/'agents/commands/completed').glob('*.md')) if (WS/'agents/commands/completed').exists() else []
failed=list((WS/'agents/commands/failed').glob('*.md')) if (WS/'agents/commands/failed').exists() else []
stats={'total':len(commands),'success':len(completed),'failed':len(failed),'agent_runs':len(agent_runs)}
data={'baseline':current_ver,'versions':versions[-30:],'agent_runs':agent_runs,'commands':sorted(set(commands)),'stats':stats}
self.sendj(data)
if __name__ == '__main__':
web.run_app(app, host='127.0.0.1', port=18890) def api_spec():
return {'version':'V1R1C0-chatgpt-actions','openapi':'/api/openapi.yaml','read':['GET /api/base','GET /api/ver','GET /api/diff','GET /api/patch','GET /api/model','GET /api/run','GET /admin/ai-evolution'],'write':['POST /api/branch','POST /api/file','POST /api/commit','POST /api/patch','POST /api/tag','POST /api/rollback','POST /api/model','PUT /api/model-priority','POST /api/nl'],'policy':{'bearer_required_for_write':True,'direct_main_push':False,'delete_tag':False,'owner_approval_required_for_v_level':True,'api_keys_in_git':False,'git_secret_field':'secret_ref','online_git_sync':True}}
def main():
print(f'AIOS ChatGPT GitOps adapter listening on {HOST}:{PORT}')
ThreadingHTTPServer((HOST,PORT), H).serve_forever()
if __name__ == '__main__': main()
@@ -1,99 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Evolution Dashboard</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; background: #0d1117; color: #c9d1d9; height: 100vh; overflow: hidden; }
.app { display: flex; height: 100vh; }
.sidebar { width: 260px; background: #161b22; border-right: 1px solid #30363d; padding: 20px; display: flex; flex-direction: column; gap: 16px; }
.sidebar h1 { font-size: 18px; color: #f0f6fc; border-bottom: 1px solid #30363d; padding-bottom: 12px; margin-bottom: 8px; }
.sidebar-nav { list-style: none; display: flex; flex-direction: column; gap: 4px; }
.sidebar-nav li { padding: 8px 12px; border-radius: 6px; cursor: pointer; font-size: 14px; color: #8b949e; }
.sidebar-nav li:hover, .sidebar-nav li.active { background: #1f6feb22; color: #58a6ff; }
.sidebar-nav li i { margin-right: 8px; }
.main { flex: 1; padding: 24px; overflow-y: auto; }
.main-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; }
.main-header h2 { font-size: 20px; }
.status-badge { padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 600; text-transform: uppercase; }
.status-badge.ok { background: #1b3a2d; color: #3fb950; border: 1px solid #3fb95044; }
.status-badge.degraded { background: #3a2e1b; color: #d29922; border: 1px solid #d2992244; }
.status-badge.fail { background: #3a1b1b; color: #f85149; border: 1px solid #f8514944; }
.card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; margin-bottom: 16px; }
.card-title { font-size: 14px; color: #8b949e; margin-bottom: 8px; }
.card-value { font-size: 28px; font-weight: 700; color: #f0f6fc; }
.row { display: flex; gap: 16px; }
.col { flex: 1; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; }
pre { white-space: pre-wrap; word-break: break-word; }
.command-item { padding: 12px; margin-bottom: 8px; border-radius: 6px; background: #0d1117; border: 1px solid #21262d; }
.command-item .meta { display: flex; gap: 8px; align-items: center; font-size: 12px; color: #8b949e; }
.command-item .name { font-size: 14px; color: #58a6ff; font-weight: 600; }
.command-item .state { font-size: 11px; padding: 2px 8px; border-radius: 10px; text-transform: uppercase; }
.state-active { background: #1b3a2d; color: #3fb950; }
.state-running { background: #3a2e1b; color: #d29922; }
.state-completed { background: #1b3a2d; color: #3fb950; }
.state-failed { background: #3a1b1b; color: #f85149; }
.loading { text-align: center; padding: 40px; color: #8b949e; }
.error { padding: 20px; background: #3a1b1b; border: 1px solid #f8514944; border-radius: 8px; color: #f85149; margin-bottom: 16px; }
.version-item { padding: 8px 12px; background: #0d1117; border: 1px solid #21262d; border-radius: 6px; margin-bottom: 4px; display: flex; justify-content: space-between; }
.version-item .tag { color: #58a6ff; font-weight: 600; }
.version-item .name { color: #8b949e; font-size: 13px; }
.agent-item { background: #0d1117; border: 1px solid #21262d; border-radius: 6px; margin-bottom: 8px; padding: 12px; }
.agent-item .agent-title { font-size: 14px; font-weight: 600; color: #c9d1d9; margin-bottom: 4px; }
.agent-item .agent-meta { font-size: 12px; color: #8b949e; }
.agent-run-item { padding: 8px 12px; margin-bottom: 4px; border-radius: 4px; background: #0d1117; border: 1px solid #21262d; display: flex; justify-content: space-between; align-items: center; }
.agent-run-item .run-id { color: #58a6ff; font-size: 13px; font-weight: 600; }
.agent-run-item .source { color: #8b949e; font-size: 12px; }
</style>
</head>
<body>
<div id="app" class="app">
<div class="sidebar">
<h1>🕹️ AI Evolution Dashboard</h1>
<ul class="sidebar-nav">
<li class="active" onclick="showPanel('overview')"><i>📊</i> Overview</li>
<li onclick="showPanel('versions')"><i>🏷️</i> Versions</li>
<li onclick="showPanel('commands')"><i>⚡</i> Commands</li>
<li onclick="showPanel('agents')"><i>🤖</i> Agent Runs</li>
</ul>
</div>
<div class="main">
<div id="panel-overview" class="panel">
<div class="main-header">
<h2>System Overview</h2>
<div id="header-status" class="status-badge">Loading...</div>
</div>
<div id="loading" class="loading">Loading...</div>
<div id="error" class="error" style="display:none;"></div>
<div id="content">
<div class="row">
<div class="col"><div class="card"><div class="card-title">Current Baseline</div><div id="base" class="card-value">-</div></div></div>
<div class="col"><div class="card"><div class="card-title">Git Branch</div><div id="branch" class="card-value">-</div></div></div>
<div class="col"><div class="card"><div class="card-title">Uptime</div><div id="uptime" class="card-value">-</div></div></div>
</div>
<div class="card" style="margin-top:16px">
<div class="card-title">Event Bus Status</div>
<pre id="bus-status">-</pre>
</div>
</div>
</div>
<div id="panel-versions" class="panel" style="display:none;">
<div class="main-header"><h2>Version Timeline</h2></div>
<div id="versions-list">Loading...</div>
</div>
<div id="panel-commands" class="panel" style="display:none;">
<div class="main-header"><h2>Command Lookup</h2></div>
<div id="commands-list">Loading...</div>
</div>
<div id="panel-agents" class="panel" style="display:none;">
<div class="main-header"><h2>Recent Agent Executions</h2></div>
<div id="agents-list">Loading...</div>
</div>
</div>
</div>
<script>
async function fetchData() {
const r = await fetch('/admin/ai-evolution/api/data');
@@ -1,36 +0,0 @@
# CMD-20260708-fix-git-actions-write-and-enable-audit-plane
## Command ID
CMD-20260708-fix-git-actions-write-and-enable-audit-plane
## Priority
P0
## Mode
autonomous
## Target
Primary: OpenClaw
Supporting: Hermes
Review: Codex
## Objective
解决当前 AIOS Git Actions 写权限问题,恢复 ChatGPT Actions → AIOS GitOps API → Git Commit 链路,
同时建立 Agent 全生命周期审计体系。
## Status
- Part 1 (Fix Git Actions Write Permission): ✅ COMPLETE
- Part 2 (Agent Command Plane Structure): ✅ COMPLETE
- Part 3 (Agent Execution Audit): ✅ COMPLETE
- Part 4 (Conversation Trace): 🚧 IN PROGRESS
- Part 5 (Version Association): ⏳ PENDING
- Part 6 (Web UI Integration): ⏳ PENDING
- Part 7 (Evidence): ⏳ PENDING
- Part 8 (Regression Test): ⏳ PENDING
## Completion Criteria
1. Code committed: agents/, audit/, evidence/, web-ui/
2. Doc: docs/architecture/AIOS_AGENT_AUDIT_PLANE.md
3. Report: evidence/CMD-20260708.../final-report.md
4. Output: AIOS_AGENT_AUDIT_PLANE_READY, AIOS_GIT_ACTION_WRITE_READY
5. Next command created: CMD-20260709-autonomous-controller-enable
@@ -1,35 +0,0 @@
# CMD-20260709-enable-autonomous-controller
## Command ID
CMD-20260709-enable-autonomous-controller
## Priority
P0
## Mode
autonomous
## Target
Primary: OpenClaw
Supporting: Hermes
Review: Codex
## Objective
Enable autonomous controller for AIOS GitOps:
- Auto-detect issues
- Auto-diagnose
- Auto-generate fixes
- Auto-test
- Auto-create patches
- Auto-request approval
## Preconditions
- Agent Command Plane initialized (CMD-BOOTSTRAP-001)
- Git Actions write permission fixed (CMD-20260708)
- Audit plane enabled (CMD-20260708)
- Notification service enabled (CMD-20260708)
## Completion Criteria
- Autonomous controller service running
- Can detect, diagnose, fix, and report
- Pipeline: detect → diagnose → fix → test → patch → approve → commit → notify
@@ -1 +0,0 @@
{"owner_approved": true, "version": "V1R4C0-test"}
-328
View File
@@ -1,328 +0,0 @@
#!/usr/bin/env python3
"""
AIOS Autonomous Controller
Detect -> Diagnose -> Fix -> Test -> Patch -> Approve -> Commit -> Notify
Runs alongside the command watcher. Checks system health periodically.
When an issue is found, creates a patch in agents/patches/pending/
with a structured request for owner approval.
"""
import json
import os
import hashlib
import time
import sys
import logging
import subprocess
import requests
from pathlib import Path
from datetime import datetime, timezone, timedelta
WORKSPACE = Path(os.environ.get("AIOS_WORKSPACE", "/data/aios-gitops/workspace"))
ACTIVE_DIR = WORKSPACE / "agents/commands/active/"
RUNNING_DIR = WORKSPACE / "agents/commands/running/"
COMPLETED_DIR = WORKSPACE / "agents/commands/completed/"
FAILED_DIR = WORKSPACE / "agents/commands/failed/"
PATCHES_PENDING_DIR = WORKSPACE / "agents/patches/pending/"
PATCHES_APPLIED_DIR = WORKSPACE / "agents/patches/applied/"
PATCHES_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"
POLL_INTERVAL = int(os.environ.get("CONTROLLER_POLL_INTERVAL_SECONDS", "300"))
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 = "1.0.0"
def load_state():
if STATE_FILE.exists():
try:
return json.loads(STATE_FILE.read_text())
except (json.JSONDecodeError, OSError):
pass
return {"controller_version": CONTROLLER_VERSION, "checks": {}, "patches": {}}
def save_state(state):
STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
STATE_FILE.write_text(json.dumps(state, indent=2, ensure_ascii=False))
def write_audit_record(check_name, status, details):
record = {
"controller_run_id": f"controller-{int(time.time())}",
"check": check_name,
"status": status,
"controller_version": CONTROLLER_VERSION,
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
"details": details,
}
AUDIT_DIR.mkdir(parents=True, exist_ok=True)
af = AUDIT_DIR / f"{record['controller_run_id']}.json"
af.write_text(json.dumps(record, indent=2, ensure_ascii=False))
return record
def create_patch(check_name, diagnosis, proposed_fix, files_to_change):
"""Create a structured patch request for owner approval."""
patch_id = f"patch-{int(time.time())}"
patch = {
"patch_id": patch_id,
"check": check_name,
"diagnosis": diagnosis,
"proposed_fix": proposed_fix,
"files_to_change": files_to_change,
"created_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
"status": "pending",
"approved": False,
"applied": False,
}
PATCHES_PENDING_DIR.mkdir(parents=True, exist_ok=True)
pf = PATCHES_PENDING_DIR / f"{patch_id}.json"
pf.write_text(json.dumps(patch, indent=2, ensure_ascii=False))
log.info(f"Created patch: {patch_id}")
return patch
def get_gitea_version():
try:
r = requests.get(f"{GITEA_URL}/api/v1/version", timeout=5)
if r.status_code == 200:
return {"healthy": True, "data": r.json()}
return {"healthy": False, "error": f"HTTP {r.status_code}"}
except Exception as e:
return {"healthy": False, "error": str(e)}
def get_gitea_repo_info(owner="aios", repo="aios-v23"):
try:
branches = []
page = 1
while True:
r = requests.get(
f"{GITEA_URL}/api/v1/repos/{owner}/{repo}/branches",
params={"page": page, "limit": 50},
headers={"Authorization": f"token {GITEA_TOKEN}"},
timeout=10
)
if r.status_code != 200:
break
data = r.json()
if not data:
break
branches.extend(data)
page += 1
tags = []
page = 1
while True:
r = requests.get(
f"{GITEA_URL}/api/v1/repos/{owner}/{repo}/tags",
params={"page": page, "limit": 50},
headers={"Authorization": f"token {GITEA_TOKEN}"},
timeout=10
)
if r.status_code != 200:
break
data = r.json()
if not data:
break
tags.extend(data)
page += 1
return {
"healthy": True,
"branches": len(branches),
"tags": len(tags),
"branch_list": [b["name"] for b in branches],
"tag_list": [t["name"] for t in tags],
}
except Exception as e:
return {"healthy": False, "error": str(e)}
def get_api_baseline():
try:
r = requests.get(f"{API_URL}/admin/ai-evolution/api/data", timeout=5)
if r.status_code == 200:
data = r.json()
return {"healthy": True, "baseline": data.get("baseline")}
return {"healthy": False, "error": f"HTTP {r.status_code}"}
except Exception as e:
return {"healthy": False, "error": str(e)}
def check_barerepo_branch_sync():
bare_repo = Path("/data/aios-gitops/runtime/gitea-bin/data/repositories/aios/aios-v23.git")
try:
result = subprocess.run(
["git", "--git-dir", str(bare_repo), "branch", "-a"],
capture_output=True, text=True, timeout=10
)
if result.returncode != 0:
return {"healthy": False, "error": result.stderr[:200]}
bare_branches = [
l.strip().replace("remotes/origin/", "").replace("* ", "")
for l in result.stdout.strip().split("\n") if l.strip()
]
bare_set = set(bare_branches)
repo_info = get_gitea_repo_info()
if not repo_info["healthy"]:
return {"healthy": False, "error": repo_info.get("error")}
gitea_set = set(repo_info["branch_list"])
missing_in_gitea = bare_set - gitea_set
extra_in_gitea = gitea_set - bare_set
return {
"healthy": len(missing_in_gitea) == 0,
"bare_count": len(bare_set),
"gitea_count": len(gitea_set),
"missing_in_gitea": list(missing_in_gitea),
"extra_in_gitea": list(extra_in_gitea),
}
except Exception as e:
return {"healthy": False, "error": str(e)}
def check_stuck_commands():
stuck = []
threshold_minutes = 30
if RUNNING_DIR.exists():
now = time.time()
for cmd_file in RUNNING_DIR.iterdir():
if cmd_file.name.endswith(".md"):
mtime = cmd_file.stat().st_mtime
age_minutes = (now - mtime) / 60
if age_minutes > threshold_minutes:
stuck.append({
"name": cmd_file.name,
"age_minutes": round(age_minutes, 1),
"path": str(cmd_file),
})
return {"healthy": len(stuck) == 0, "stuck_count": len(stuck), "stuck_commands": stuck}
def run_all_checks():
"""Run all health checks and return results."""
state = load_state()
results = {}
patches_created = []
# 1. Gitea version
log.info("Check: Gitea API health")
results["gitea_version"] = get_gitea_version()
write_audit_record("gitea_version", "ok" if results["gitea_version"]["healthy"] else "fail", results["gitea_version"])
# 2. API baseline
log.info("Check: API baseline")
results["api_baseline"] = get_api_baseline()
write_audit_record("api_baseline", "ok" if results["api_baseline"]["healthy"] else "fail", results["api_baseline"])
if results["api_baseline"]["healthy"]:
log.info(f" Baseline: {results['api_baseline'].get('baseline')}")
# 3. Bare repo branch sync
log.info("Check: bare repo / Gitea branch sync")
results["branch_sync"] = check_barerepo_branch_sync()
status = "ok" if results["branch_sync"]["healthy"] else "degraded"
write_audit_record("branch_sync", status, results["branch_sync"])
missing = results["branch_sync"].get("missing_in_gitea", [])
if missing:
log.warning(f" {len(missing)} branches missing in Gitea DB")
for b in missing:
log.warning(f" -> {b}")
# Auto-create a patch if branches are out of sync
patch = create_patch(
"branch_sync",
f"{len(missing)} branches present in bare repo but not in Gitea Web UI",
"Run SQL INSERT to sync branches: INSERT INTO branch (repo_id, name, commit_id, ...) "
"for each missing branch. Verify with bare repo SHA.",
["runtime/gitea-bin/data/... (SQL via gitea CLI or direct DB)"]
)
patches_created.append(patch)
# 4. Stuck commands
log.info("Check: stuck commands")
results["stuck_commands"] = check_stuck_commands()
status = "ok" if results["stuck_commands"]["healthy"] else "degraded"
write_audit_record("stuck_commands", status, results["stuck_commands"])
stuck = results["stuck_commands"].get("stuck_commands", [])
if stuck:
log.warning(f" {len(stuck)} commands stuck in running/")
for c in stuck:
log.warning(f" -> {c['name']} (age: {c['age_minutes']}m)")
# Update state
state["checks"] = {
"last_run": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
"results": {k: {"healthy": v.get("healthy"), "error": v.get("error")} for k, v in results.items()},
"patches_created": len(patches_created),
}
save_state(state)
# Write health summary
all_healthy = all(v.get("healthy", False) for v in results.values())
summary = {
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
"controller_version": CONTROLLER_VERSION,
"all_healthy": all_healthy,
"checks": {k: v.get("healthy") for k, v in results.items()},
"patches_pending": len(patches_created),
}
HEALTH_FILE.parent.mkdir(parents=True, exist_ok=True)
HEALTH_FILE.write_text(json.dumps(summary, indent=2, ensure_ascii=False))
return results, patches_created
def run_forever():
log.info(f"Autonomous Controller v{CONTROLLER_VERSION} started")
log.info(f"Poll interval: {POLL_INTERVAL}s")
log.info(f"Workspace: {WORKSPACE}")
log.info(f"Gitea: {GITEA_URL}, API: {API_URL}")
while True:
try:
log.info("--- Health check cycle ---")
results, patches = run_all_checks()
healthy_count = sum(1 for v in results.values() if v.get("healthy"))
total = len(results)
log.info(f"Results: {healthy_count}/{total} healthy")
if patches:
log.warning(f"Created {len(patches)} patch(es) for owner review")
else:
log.info("No issues detected requiring patches")
except Exception as e:
log.exception(f"Check cycle failed: {e}")
time.sleep(POLL_INTERVAL)
def run_once():
log.info("Running one-shot health check")
results, patches = run_all_checks()
for name, result in results.items():
status = "OK" if result.get("healthy") else "FAIL"
log.info(f" [{status}] {name}")
if not result.get("healthy") and result.get("error"):
log.info(f" error: {result['error'][:200]}")
if patches:
log.info(f"Patches created: {len(patches)}")
else:
log.info("No patches created")
if __name__ == "__main__":
if len(sys.argv) > 1 and sys.argv[1] == "--once":
run_once()
else:
run_forever()
-15
View File
@@ -1,15 +0,0 @@
{
"processed": {
"CMD-20260708-fix-git-actions-write-and-enable-audit-plane.md": {
"hash": "9442e78e3deb35e03227016b2265d34feb4640f8efdcb07baffe06d52ef947c8",
"status": "discovered",
"discovered_at": "2026-07-08T19:01:32+0800"
},
"CMD-20260709-enable-autonomous-controller.md": {
"hash": "2825811706389130b2f531a2d3814b800b1181598f871b496e5a05fa13adbbec",
"status": "running",
"discovered_at": "2026-07-08T19:01:45+0800"
}
},
"last_scan": "2026-07-08T21:06:45+0800"
}
-16
View File
@@ -1,16 +0,0 @@
{
"agent_run_id": "openclaw-1783508505",
"command": "CMD-20260709-enable-autonomous-controller.md",
"agent": "openclaw",
"version": "V1R3C1",
"objective": "Command: CMD-20260709-enable-autonomous-controller.md",
"start_time": "2026-07-08T19:01:45+0800",
"end_time": null,
"interaction_count": 0,
"tool_call_count": 0,
"changed_files": [],
"commit_sha": null,
"test_result": null,
"final_result": "running",
"evidence_path": "evidence/CMD-20260709-enable-autonomous-controller/"
}
+1 -9
View File
@@ -141,7 +141,6 @@ paths:
application/json: application/json:
schema: {$ref: '#/components/schemas/PatchRequest'} schema: {$ref: '#/components/schemas/PatchRequest'}
responses: {'200': {description: Patch branch committed}, '401': {description: Unauthorized}, '403': {description: Forbidden}} responses: {'200': {description: Patch branch committed}, '401': {description: Unauthorized}, '403': {description: Forbidden}}
x-openai-isConsequential: true
/api/model: /api/model:
get: get:
operationId: readModelRegistry operationId: readModelRegistry
@@ -157,7 +156,6 @@ paths:
application/json: application/json:
schema: {$ref: '#/components/schemas/ModelRequest'} schema: {$ref: '#/components/schemas/ModelRequest'}
responses: {'200': {description: Model registered and committed}, '401': {description: Unauthorized}, '403': {description: Forbidden}} responses: {'200': {description: Model registered and committed}, '401': {description: Unauthorized}, '403': {description: Forbidden}}
x-openai-isConsequential: true
/api/run: /api/run:
get: get:
operationId: getRuntimeStatus operationId: getRuntimeStatus
@@ -174,7 +172,6 @@ paths:
application/json: application/json:
schema: {$ref: '#/components/schemas/BranchRequest'} schema: {$ref: '#/components/schemas/BranchRequest'}
responses: {'200': {description: Branch created}, '401': {description: Unauthorized}, '403': {description: Forbidden}} responses: {'200': {description: Branch created}, '401': {description: Unauthorized}, '403': {description: Forbidden}}
x-openai-isConsequential: true
/api/file: /api/file:
post: post:
operationId: commitFile operationId: commitFile
@@ -186,7 +183,6 @@ paths:
application/json: application/json:
schema: {$ref: '#/components/schemas/FileCommitRequest'} schema: {$ref: '#/components/schemas/FileCommitRequest'}
responses: {'200': {description: File committed}, '400': {description: Secret rejected or invalid path}, '401': {description: Unauthorized}} responses: {'200': {description: File committed}, '400': {description: Secret rejected or invalid path}, '401': {description: Unauthorized}}
x-openai-isConsequential: true
/api/commit: /api/commit:
post: post:
operationId: commitChanges operationId: commitChanges
@@ -198,7 +194,6 @@ paths:
application/json: application/json:
schema: {$ref: '#/components/schemas/CommitRequest'} schema: {$ref: '#/components/schemas/CommitRequest'}
responses: {'200': {description: Commit result}, '401': {description: Unauthorized}} responses: {'200': {description: Commit result}, '401': {description: Unauthorized}}
x-openai-isConsequential: true
/api/tag: /api/tag:
post: post:
operationId: requestOrCreateTag operationId: requestOrCreateTag
@@ -210,7 +205,6 @@ paths:
application/json: application/json:
schema: {$ref: '#/components/schemas/TagRequest'} schema: {$ref: '#/components/schemas/TagRequest'}
responses: {'200': {description: Tag request committed or tag created}, '403': {description: Forbidden}} responses: {'200': {description: Tag request committed or tag created}, '403': {description: Forbidden}}
x-openai-isConsequential: true
/api/rollback: /api/rollback:
post: post:
operationId: requestRollback operationId: requestRollback
@@ -222,7 +216,6 @@ paths:
application/json: application/json:
schema: {$ref: '#/components/schemas/RollbackRequest'} schema: {$ref: '#/components/schemas/RollbackRequest'}
responses: {'200': {description: Rollback request committed}, '401': {description: Unauthorized}} responses: {'200': {description: Rollback request committed}, '401': {description: Unauthorized}}
x-openai-isConsequential: true
/api/model-priority: /api/model-priority:
put: put:
operationId: setModelPriority operationId: setModelPriority
@@ -234,7 +227,6 @@ paths:
application/json: application/json:
schema: {$ref: '#/components/schemas/ModelPriorityRequest'} schema: {$ref: '#/components/schemas/ModelPriorityRequest'}
responses: {'200': {description: Priority updated and committed}, '401': {description: Unauthorized}} responses: {'200': {description: Priority updated and committed}, '401': {description: Unauthorized}}
x-openai-isConsequential: true
/api/nl: /api/nl:
post: post:
operationId: planNaturalLanguageGitOpsAction operationId: planNaturalLanguageGitOpsAction
@@ -245,4 +237,4 @@ paths:
content: content:
application/json: application/json:
schema: {$ref: '#/components/schemas/NLRequest'} schema: {$ref: '#/components/schemas/NLRequest'}
responses: {' responses: {'200': {description: Plan committed}, '401': {description: Unauthorized}}
@@ -0,0 +1,9 @@
{
"type": "branch",
"branch": "patch/chatgpt-br-1783496324783-496c38aa",
"base": "main",
"created_by": "gitops-write-bot",
"created_at": "2026-07-08T07:38:44Z",
"direct_main_push": false,
"owner_approval_required": true
}
+3 -41
View File
@@ -19,49 +19,11 @@ class ConsensusEngine:
"windows_codex": 2, "windows_codex": 2,
"server_agent": 1, "server_agent": 1,
} }
# New in V1R1C5: quality score, historical reliability, domain expertise
self.quality_score_factors = {
"code_quality": 0.4,
"test_coverage": 0.3,
"response_time": 0.3,
}
self.historical_reliability_factors = {
"recent_acceptance_rate": 0.5,
"longevity_score": 0.3,
"error_rate": 0.2,
}
self.domain_expertise_factors = {
"specialization_match": 0.5,
"experience_years": 0.3,
"certifications": 0.2,
}
def calculate_quality_score(self, agent_id: str, agent_type: str) -> float: def calculate(self, votes: List[dict]) -> dict:
"""Calculate quality score for an agent based on its performance metrics."""
# Placeholder implementation; in production, fetch metrics from telemetry
return 0.85 # Default quality score
def calculate_historical_reliability(self, agent_id: str, agent_type: str) -> float:
"""Calculate historical reliability score for an agent."""
# Placeholder implementation; in production, fetch from store
return 0.90 # Default reliability score
def calculate_domain_expertise(self, agent_id: str, agent_type: str) -> float:
"""Calculate domain expertise score for an agent."""
# Placeholder implementation; in production, fetch from agent registry
return 0.75 # Default expertise score
def calculate(self, votes: List[dict], use_dynamic_weights: bool = False) -> dict:
result = {"approve": 0, "reject": 0} result = {"approve": 0, "reject": 0}
for vote in votes: for vote in votes:
base_weight = self.base_weights.get(vote.get("agent_type"), 1) weight = vote.get("weight", self.base_weights.get(vote.get("agent_type"), 1))
if use_dynamic_weights:
quality = self.calculate_quality_score(vote.get("agent_id", ""), vote.get("agent_type", ""))
reliability = self.calculate_historical_reliability(vote.get("agent_id", ""), vote.get("agent_type", ""))
expertise = self.calculate_domain_expertise(vote.get("agent_id", ""), vote.get("agent_type", ""))
weight = base_weight * quality * reliability * expertise
else:
weight = base_weight
result[vote.get("decision", "reject")] += weight result[vote.get("decision", "reject")] += weight
return result return result
@@ -70,7 +32,7 @@ class ConsensusEngine:
Returns one of: "approve", "reject", "review", "conditional_approval". Returns one of: "approve", "reject", "review", "conditional_approval".
""" """
result = self.calculate(votes, use_dynamic_weights=True) result = self.calculate(votes)
if result["approve"] > result["reject"]: if result["approve"] > result["reject"]:
# Check if any participant voted with conditions # Check if any participant voted with conditions
conditional_votes = [ conditional_votes = [
+3 -24
View File
@@ -1,16 +1,7 @@
"""Agent OS message bus runtime.""" """Agent OS message bus runtime."""
from dataclasses import dataclass, field from dataclasses import dataclass
from typing import List, Optional from typing import List
from enum import Enum
class DeliveryStatus(Enum):
PENDING = "pending"
DELIVERED = "delivered"
FAILED = "failed"
RETRYING = "retrying"
EXPIRED = "expired"
@dataclass @dataclass
@@ -19,11 +10,6 @@ class AgentMessage:
receiver: str receiver: str
event: str event: str
payload: dict payload: dict
# V1R1C5: New fields for message tracking and delivery
task_id: str = ""
correlation_id: str = ""
retry_state: int = 0
delivery_status: DeliveryStatus = DeliveryStatus.PENDING
class MessageBus: class MessageBus:
@@ -35,14 +21,7 @@ class MessageBus:
def broadcast(self, sender: str, event: str, payload: dict, receivers: List[str]): def broadcast(self, sender: str, event: str, payload: dict, receivers: List[str]):
for receiver in receivers: for receiver in receivers:
self.send(AgentMessage( self.send(AgentMessage(sender, receiver, event, payload))
sender=sender,
receiver=receiver,
event=event,
payload=payload,
task_id=payload.get("task_id", "") if isinstance(payload, dict) else "",
correlation_id=payload.get("correlation_id", "") if isinstance(payload, dict) else "",
))
def consume(self): def consume(self):
messages = list(self.queue) messages = list(self.queue)