278 lines
21 KiB
Python
278 lines
21 KiB
Python
#!/usr/bin/env python3
|
|
from __future__ import annotations
|
|
import hashlib, hmac, json, os, re, subprocess, time, urllib.parse, uuid
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
ROOT = Path(os.getenv('AIOS_GITOPS_ROOT', '/data/aios-gitops'))
|
|
WS = Path(os.getenv('AIOS_GITOPS_WORKSPACE', str(ROOT / 'workspace')))
|
|
SECRETS = Path(os.getenv('AIOS_GITOPS_SECRETS_DIR', str(ROOT / 'runtime' / 'secrets')))
|
|
TOKEN_FILE = Path(os.getenv('AIOS_GITOPS_TOKEN_FILE', str(SECRETS / 'gitops.tokens')))
|
|
HOST = os.getenv('AIOS_GITOPS_HOST', '127.0.0.1')
|
|
PORT = int(os.getenv('AIOS_GITOPS_PORT', '18890'))
|
|
OPENAPI = WS / 'docs' / 'gitops' / 'openapi-chatgpt-actions.yaml'
|
|
BARE_REPO = Path(os.getenv('AIOS_GITOPS_BARE_REPO', str(ROOT / 'runtime' / 'gitea-bin' / 'data' / 'repositories' / 'aios' / 'aios-v23.git')))
|
|
BASE_MODELS = [
|
|
{'id':'base.deepseekv4-flash','provider':'one-api','model':'deepseekv4-flash','display_name':'DeepSeek V4 Flash','scope':'global'},
|
|
{'id':'base.qwen3.6-35b','provider':'one-api','model':'qwen3.6-35b','display_name':'Qwen 3.6 35B','scope':'global'},
|
|
{'id':'base.gpt-5.5','provider':'openai','model':'gpt-5.5','display_name':'GPT-5.5','scope':'global'},
|
|
]
|
|
SECRET_RE = re.compile(r'(?i)(api[_-]?key|secret[_-]?access[_-]?key|secret|password|token)\s*[:=]\s*["\']?([A-Za-z0-9_./+=:-]{16,})')
|
|
SECRET_VAL_RE = re.compile(r'(sk-[A-Za-z0-9_-]{20,}|HPUA[A-Z0-9]{10,}|SDC[A-Za-z0-9]{20,})')
|
|
|
|
def now() -> str: return time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
|
|
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 content_has_secret(text: str) -> bool:
|
|
text = text or ''
|
|
try:
|
|
return _json_has_plain_secret(json.loads(text))
|
|
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 sync_tag(tag: str) -> dict:
|
|
if not tag: return {'status':'skipped','reason':'empty_tag'}
|
|
if BARE_REPO.exists():
|
|
ref=f'refs/tags/{tag}:refs/tags/{tag}'
|
|
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 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 commit(paths, msg):
|
|
ensure_git(); paths = [relpath(p) for p in paths] if paths else ['.']; sh(['add'] + paths, True)
|
|
branch=sh(['rev-parse','--abbrev-ref','HEAD'])['stdout']
|
|
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
|
|
|
|
class H(BaseHTTPRequestHandler):
|
|
server_version='AIOSChatGPTGitOps/V1R1C0'
|
|
def sendj(self, data, code=200):
|
|
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)
|
|
def sendt(self, text, ctype='text/plain; charset=utf-8', code=200):
|
|
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)
|
|
def body(self):
|
|
n=int(self.headers.get('Content-Length','0') or '0'); raw=self.rfile.read(n).decode() if n else '{}'
|
|
try: return json.loads(raw or '{}')
|
|
except Exception: return {'_raw':raw}
|
|
def actor(self):
|
|
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','/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','/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','/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','/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']})
|
|
return self.sendj({'error':'not_found','path':path},404)
|
|
except Exception as e: 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 == '/api/branch': return self.branch(data)
|
|
if path == '/api/file': return self.file(data)
|
|
if path == '/api/commit': return self.commit(data)
|
|
if path == '/api/patch': return self.patch(data)
|
|
if path == '/api/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: 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 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()})
|
|
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
|
|
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})
|
|
|
|
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'],'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() |