Compare commits

...
50 changed files with 5181 additions and 25 deletions
+1 -1
View File
@@ -1 +1 @@
V1R1C0
V1R1C4
@@ -0,0 +1,365 @@
#!/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 normalize_auth_token(raw: str | None) -> tuple[str, str]:
value = str(raw or '').strip()
state = 'missing'
if not value:
return '', state
state = 'present'
for _ in range(4):
lowered = value.lower().strip()
if lowered.startswith('authorization:'):
value = value.split(':', 1)[1].strip()
state = 'authorization_prefix_stripped'
continue
if lowered.startswith('bearer '):
value = value[7:].strip()
state = 'bearer_prefix_stripped'
continue
break
if (value.startswith('"') and value.endswith('"')) or (value.startswith("'") and value.endswith("'")):
value = value[1:-1].strip()
return value, state
def authenticate(token: str):
if os.getenv('AIOS_GITOPS_DEV_ALLOW','0') == '1': return {'account':'dev-owner','role':'owner','scopes':['*']}
token, _ = normalize_auth_token(token)
if not token or token.startswith('secret_ref://'):
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
plain = str(r.get('token_plain') or '')
norm_plain, _ = normalize_auth_token(plain)
if norm_plain and hmac.compare_digest(norm_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_summary(c: Any) -> dict:
if not isinstance(c, dict):
return {'rev': None, 'sync_status': None, 'sync': None}
sync = c.get('sync') if isinstance(c.get('sync'), dict) else None
return {
'rev': c.get('rev'),
'sync_status': sync.get('status') if sync else None,
'sync': sync,
}
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 send_head_only(self, code=200, ctype='application/json; charset=utf-8', length=0):
self.send_response(code); self.send_header('Content-Type',ctype); self.send_header('Content-Length',str(length)); self.end_headers()
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','')
raw = auth if auth else self.headers.get('X-AIOS-Token','')
tok, auth_state = normalize_auth_token(raw)
if not raw:
auth_state = 'missing_bearer'
elif tok.startswith('secret_ref://'):
auth_state = 'secret_ref_literal_not_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 []),'auth_state':'authenticated'}
if raw and auth_state not in ('missing_bearer', 'secret_ref_literal_not_token'):
auth_state = 'invalid_token'
return {'ok':False,'account':'anonymous','role':'anonymous','scopes':set(),'auth_state':auth_state or 'invalid_token'}
def need(self, scope):
a=self.actor()
if not a['ok']:
hint = 'Bearer token required for GitOps write scope'
if a.get('auth_state') == 'invalid_token':
hint = 'Bearer token was received but does not match the GitOps write token registry'
elif a.get('auth_state') == 'secret_ref_literal_not_token':
hint = 'secret_ref is a pointer, not a credential; paste the raw runtime token into GPT Builder'
self.sendj({
'error':'unauthorized',
'hint':hint,
'auth_state':a.get('auth_state') or 'invalid_token',
'required_scope':scope,
'token_secret_ref':'secret_ref://aios-gitops/runtime/gitops-write-bot',
'auth_header':'Authorization: Bearer <raw-token>',
'rebind_required':True,
'gpt_builder_note':'When GPT Builder auth type is Bearer, paste only the raw token value. Do not paste secret_ref://... and do not include the word Bearer in the API key field.'
},401); return None
if '*' not in a['scopes'] and scope not in a['scopes']:
self.sendj({'error':'forbidden','required_scope':scope,'account':a['account'],'auth_state':a.get('auth_state'),'token_secret_ref':'secret_ref://aios-gitops/runtime/gitops-write-bot'},403); return None
return a
def do_HEAD(self):
u=urllib.parse.urlparse(self.path); path=u.path.rstrip('/') if u.path != '/' else u.path
try:
if path in ('/api/openapi.yaml','/api/openapi-chatgpt-actions.yaml','/api/schema'):
if not OPENAPI.exists():
return self.send_head_only(404)
return self.send_head_only(200, 'application/yaml; charset=utf-8', OPENAPI.stat().st_size)
if path in ('/health','/api/health','/api','/api/status','/api/base','/base','/api/ver','/ver','/api/diff','/diff','/api/patch','/patch','/api/model','/model','/api/run','/run'):
return self.send_head_only(200)
return self.send_head_only(404)
except Exception:
return self.send_head_only(500)
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}'); summary=commit_summary(c); audit(a['account'],'branch',b,meta); self.sendj({'status':'ok','branch':b,'commit':c, **summary})
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}'); summary=commit_summary(c); audit(a['account'],'file',r,{'branch':b,'commit':c}); self.sendj({'status':'committed','branch':b,'path':r,'commit':c, **summary})
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'); summary=commit_summary(c); audit(a['account'],'commit',b,{'paths':paths,'commit':c}); self.sendj({'status':c['status'],'branch':b,'commit':c, **summary})
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}'); summary=commit_summary(c); audit(a['account'],'patch',name,meta); self.sendj({'status':'patch_branch_created','branch':b,'patch':meta,'commit':c, **summary})
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'],
'auth':{
'write_bearer_required':True,
'write_token_account':'gitops-write-bot',
'write_token_secret_ref':'secret_ref://aios-gitops/runtime/gitops-write-bot',
'required_header':'Authorization: Bearer <raw-token>',
'write_scopes':['branch:create','file:write','commit:create','patch:create','rollback:request','model:write','model:priority','nl:plan','runtime:test'],
'bridge_gpts':[{'name':'codex-chartgpt-bridge','gpt_id':'g-6a4ba1b4ea588191bab125d53ce83612','url':'https://chatgpt.com/g/g-6a4ba1b4ea588191bab125d53ce83612-codex-chartgpt-bridge','auth_binding_status':'requires_gpt_builder_bearer_secret_binding','api_key_field':'raw token only, without Bearer prefix'}]
},
'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()
+118 -8
View File
@@ -1,20 +1,20 @@
{
"active_version": "V1R1C0",
"active_tag": "V1R1C0-baseline-20260706",
"active_version": "V1R1C4",
"active_tag": "V1R1C4",
"mode": "gitops-online",
"version_policy": "VxRxCx",
"update_mode": "git_patch_only",
"zip_update_deprecated": true,
"entry": "https://cnnetmkt.top",
"links": {
"git": "/git",
"git": "https://g.cnnetmkt.top/git/aios/aios-v23",
"baseline": "/base",
"versions": "/ver",
"diff": "/diff",
"patch": "/patch",
"model": "/model",
"runtime": "/run",
"api": "/api",
"api": "https://cnnetmkt.top/api",
"hook": "/hook"
},
"features": {
@@ -23,16 +23,18 @@
"custom_llm": true,
"user_model_priority": true,
"chatgpt_rw": true,
"codex_rw": true
"codex_rw": true,
"portal_gitops_dashboard": true,
"human_shortlinks_login_required": true
},
"version": "V1R1C0",
"version": "V1R1C4",
"baseline_tag": "V1R1C0-baseline-20260706",
"source": "AIOS_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_PROD_FULL_DEPLOY_20260706.zip",
"source_package": "AIOS_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_PROD_FULL_DEPLOY_20260706.zip",
"zip_update_mode": "deprecated",
"chatgpt_access": true,
"codex_access": true,
"git_url": "https://cnnetmkt.top/git/aios/aios-v23.git",
"git_url": "https://g.cnnetmkt.top/git/aios/aios-v23.git",
"short_links": [
"/git",
"/base",
@@ -44,5 +46,113 @@
"/api",
"/hook"
],
"updated_at": "2026-07-06T07:16:01.842497+00:00"
"updated_at": "2026-07-06T13:24:28Z",
"auth": {
"primary_domain": "https://auth.cnnetmkt.top",
"backup_domain": "https://auth.h.imruyi.top:8443",
"primary_user_entry": "https://cnnetmkt.top/",
"primary_callback": "https://cnnetmkt.top/casdoor/callback/",
"casdoor_application": "admin/aios-portal",
"backup_domain_role": "standby-only"
},
"previous_version": "V1R1C3",
"codex_gitops": {
"config": "config/codex/gitops-online.json",
"token_account": "codex-gitops-bot",
"token_ref": "secret_ref://aios-gitops/runtime/codex-gitops-bot",
"direct_main_push": false,
"all_writes_require_git_commit": true,
"api_keys_in_git": false,
"online_git_sync": true,
"bare_repo": "/data/aios-gitops/runtime/gitea-bin/data/repositories/aios/aios-v23.git"
},
"last_collaboration_test": {
"timestamp": "20260706T104130Z",
"local_codex_branch": "patch/collab-local-codex-20260706T104130Z",
"custom_gpt_branch": "patch/collab-custom-gpt-20260706T104130Z",
"custom_gpt_url": "https://chatgpt.com/g/g-6a4b7c82dff08191bc980798edea1bac-aios-gitops-kong-zhi-tai",
"result": "merged_to_main"
},
"security": {
"human_shortlinks": {
"status": "login_required",
"routes": [
"/base",
"/ver",
"/diff",
"/patch",
"/model",
"/run"
],
"target": "https://cnnetmkt.top/portal/me/#gitops",
"machine_api": "https://cnnetmkt.top/api"
},
"gitea": {
"web": "https://g.cnnetmkt.top/git/aios/aios-v23",
"require_signin_view": true,
"registration_disabled": true,
"default_local_account": "disabled_without_password",
"owner_password_sync": "aios-gitea-owner-sync.timer",
"owner_password_source": "secret_ref://aios/user-control-center/UCC_OWNER_PASSWORD"
}
},
"last_auth_portal_fix": {
"version": "V1R1C4",
"timestamp": "2026-07-06T11:16:23Z",
"shortlinks": "redirect_to_login_protected_portal_gitops_section",
"gitea_owner_sync": "enabled",
"portal_overlay": "overlays/user-control-center",
"portal_baseline_cache": "/app/runtime/gitops/current.json",
"sync_log_secret_hygiene": "runuser_without_sudo_command_logging"
},
"actions_write_auth": {
"account": "gitops-write-bot",
"token_ref": "secret_ref://aios-gitops/runtime/gitops-write-bot",
"header": "Authorization: Bearer <token>",
"scopes": [
"baseline:read",
"version:read",
"diff:read",
"patch:read",
"model:read",
"runtime:read",
"branch:create",
"file:write",
"commit:create",
"patch:create",
"rollback:request",
"model:write",
"model:priority",
"nl:plan",
"runtime:test"
],
"direct_main_push": false,
"delete_tag": false,
"owner_approval_required": true,
"updated_at": "2026-07-06T11:50:41Z",
"bridge_gpts": [
{
"name": "codex-chartgpt-bridge",
"gpt_id": "g-6a4ba1b4ea588191bab125d53ce83612",
"url": "https://chatgpt.com/g/g-6a4ba1b4ea588191bab125d53ce83612-codex-chartgpt-bridge",
"token_ref": "secret_ref://aios-gitops/runtime/gitops-write-bot",
"schema_url": "https://cnnetmkt.top/api/openapi-chatgpt-actions.yaml",
"auth_binding_status": "requires_gpt_builder_bearer_secret_binding",
"server_side_smoke": "pass",
"updated_at": "2026-07-06T13:24:28Z"
}
],
"external_actions_binding_note": "ChatGPT GPT Builder must configure Bearer auth with the runtime token behind token_ref; server cannot inject the token into an external GPT by URL alone.",
"gpt_builder_api_key_field": "raw token only, without Bearer prefix",
"do_not_use_as_api_key": "secret_ref://aios-gitops/runtime/gitops-write-bot",
"last_client_response_error_fix": {
"updated_at": "2026-07-06T13:41:34Z",
"server_changes": [
"normalize duplicated Bearer/Authorization prefixes",
"return auth_state diagnostics",
"flatten branch/rev/sync_status on write success"
],
"external_builder_requirement": "paste raw token into Bearer API key field"
}
}
}
@@ -0,0 +1,36 @@
# 20260706 Auth GitOps Portal Hardening
Version: V1R1C4
Previous: V1R1C3
Timestamp: 2026-07-06T11:05:30Z
## User-visible fixes
- Human shortlinks no longer expose baseline/model/run JSON without portal login.
- The cnnetmkt portal has a visible GitOps section for owner/admin users.
- Gitea is served from `https://g.cnnetmkt.top/git/` and repository browsing requires login.
- Gitea `owner` password is synchronized from the cnnetmkt owner account secret.
- The default local Gitea account password is removed and direct login is disabled.
## Files
- `deploy/nginx/aios-gitops-locations.conf`
- `deploy/gitea-binary/app.ini.template`
- `deploy/systemd/aios-gitea-owner-sync.service`
- `deploy/systemd/aios-gitea-owner-sync.timer`
- `ops/gitops-migration/sync_gitea_owner_password.sh`
- `ops/gitops-migration/apply_user_portal_gitops_overlay.sh`
- `overlays/user-control-center/control/portal_views.py`
- `overlays/user-control-center/templates/portal/me.html`
- `baseline/current.json`
- `config/codex/gitops-online.json`
- `versions/V1R1C4.json`
- `docs/release/AIOS_V1R1C4_AUTH_GITOPS_PORTAL.md`
## Notes
Docker image rebuild was not required for the live fix. The runtime source and running container were patched, and the exact overlay has been added to GitOps so a later image rebuild can reproduce the change.
## Secret Log Hygiene
The Gitea owner password sync script uses `runuser` instead of `sudo -u ... --password ...`, strips CRLF from sourced owner fields, and does not print passwords or tokens. Archived journal files were rotated/vacuumed after the live script was corrected.
@@ -0,0 +1,25 @@
# 20260706 ChatGPT Actions GitOps Access
## Summary
Implemented a controlled ChatGPT Actions adapter for the AIOS online GitOps baseline under https://cnnetmkt.top/api.
## Access Model
- Public read APIs: /api/base, /api/ver, /api/diff, /api/patch, /api/model, /api/run.
- Bearer-token write APIs: /api/branch, /api/file, /api/commit, /api/patch, /api/tag, /api/rollback, /api/model, /api/model-priority, /api/nl.
- chatgpt-gitops-bot is scoped for branch/file/commit/patch/model/rollback-request/NL plan operations.
- Direct main writes are rejected by the adapter.
- V-level owner/admin actions are represented as approval request commits unless an owner executes them.
- Custom LLM API keys are stored outside Git under runtime secrets; committed model records store only secret_ref.
## Files
- ctive-runtime/app/aios_chatgpt_actions_api.py
- docs/gitops/openapi-chatgpt-actions.yaml
- deploy/systemd/aios-gitops-model-api.service
- scripts/package_contract_check_v1r1c0.sh
## Verification
See /data/aios-gitops/artifacts/CHATGPT_ACCESS_ACCEPTANCE.md after live deployment.
@@ -0,0 +1,34 @@
# 20260706 cnnetmkt auth primary route fix
## Problem
`https://cnnetmkt.top/` could be opened, but login redirected users to `https://auth.h.imruyi.top:8443`. Casdoor then rejected the OAuth request because `https://cnnetmkt.top/casdoor/callback/` was not in the application allow-list.
`*.h.imruyi.top:8443` is retained as a backup domain only and must not be exposed as the primary public login URL.
## Runtime Changes
- Added `auth.cnnetmkt.top` Nginx server block on `hw.imruyi.top` using the wildcard `cnnetmkt.top` certificate.
- Proxied `auth.cnnetmkt.top` to the existing backup/home Casdoor upstream `auth.h.imruyi.top:8443` while preserving the public host as `auth.cnnetmkt.top`.
- Recreated `user-control-web`, `user-control-worker`, and `user-control-beat` with:
- `CASDOOR_PUBLIC_BASE_URL=https://auth.cnnetmkt.top`
- `CASDOOR_INTERNAL_BASE_URL=https://auth.cnnetmkt.top`
- `CASDOOR_INVITE_SYNC_BASE_URL=https://auth.cnnetmkt.top`
- `UCC_PUBLIC_BASE_URL=https://cnnetmkt.top`
- Updated Casdoor application `admin/aios-portal` on `aihost` to include `https://cnnetmkt.top/casdoor/callback/` and keep user/portal/backup callbacks.
## Backups
- Casdoor DB application table backup: `/data/aios/backups/casdoor-cnnetmkt-primary-20260706T085445Z/application-before.sql`
- HW UCC/Nginx backup directory: `/data/aios-gitops/backups/auth-cnnetmkt-primary-*`
## Verification
- `https://cnnetmkt.top/` redirects to `/portal/login/` and then to `https://auth.cnnetmkt.top/login/oauth/authorize?...redirect_uri=https%3A%2F%2Fcnnetmkt.top%2Fcasdoor%2Fcallback%2F...`.
- `https://auth.cnnetmkt.top/login/oauth/authorize?...` returns HTTP 200 from the Casdoor SPA through the HW Nginx proxy.
- `user-control-web` health returned `healthy` after recreation.
- Casdoor DB readback contains `https://cnnetmkt.top/casdoor/callback/` in `application.redirect_uris`.
## Follow-up
Run `ops/gitops-migration/check_cnnetmkt_auth_primary.sh` after any domain, Nginx, or Casdoor application change.
@@ -0,0 +1,14 @@
# 20260706 Codex and Custom GPT Collaboration
## Change
Validated coordinated online GitOps changes from local Codex and the AIOS GitOps 控制台 custom GPT path. Promoted the merged result to `V1R1C3`.
## Evidence Branches
- `patch/collab-local-codex-20260706T104130Z` -> `a62d4ce` (codex-gitops-bot, sync=synced)
- `patch/collab-custom-gpt-20260706T104130Z` -> `c393931` (chatgpt-gitops-bot, sync=synced)
## Result
`/api/base` should report `V1R1C3` after this release commit is synchronized.
@@ -0,0 +1,16 @@
# 20260706 Codex Online Git Branch Sync
## Change
Promoted the online GitOps configuration to `V1R1C2` and enabled automatic synchronization of committed non-main API/Codex branches to the Gitea bare repository.
## Reason
Codex writes must be visible through the online Git service, not only in the local workspace clone.
## Guardrails
- `main` and `master` are skipped by API branch sync.
- Direct main writes remain forbidden by branch validation.
- Tags are synchronized only in the owner direct tag execution path.
- Tokens and LLM API keys remain outside Git.
@@ -0,0 +1,23 @@
# 20260706 Codex Online GitOps Version Management
## Change
Configured Codex to use the AIOS online Git baseline for version management and promoted the current controlled access changes to `V1R1C1`.
## Files
- `config/codex/gitops-online.json`
- `docs/gitops/CODEX_ONLINE_VERSION_MANAGEMENT.md`
- `docs/gitops/CHATGPT_CODEX_ACCESS.md`
- `docs/release/AIOS_V1R1C1_CODEX_GITOPS_RELEASE.md`
- `versions/V1R1C1.json`
- `baseline/current.json`
- `scripts/codex_gitops_online_check.sh`
## Merge Policy
Production `main` includes the ChatGPT/Codex GitOps API, schema, service configuration, contract checks, and secret-ref fix. Acceptance dummy model branches are retained as audit branches and not merged into production.
## Secret Policy
`codex-gitops-bot` plaintext token is stored only under `/data/aios-gitops/runtime/secrets/`; Git stores only `secret_ref` and token account metadata.
@@ -0,0 +1,59 @@
{
"name": "codex-chartgpt-bridge",
"gpt_id": "g-6a4ba1b4ea588191bab125d53ce83612",
"gpt_url": "https://chatgpt.com/g/g-6a4ba1b4ea588191bab125d53ce83612-codex-chartgpt-bridge",
"api_base": "https://cnnetmkt.top/api",
"openapi_schema": "https://cnnetmkt.top/api/openapi-chatgpt-actions.yaml",
"auth": {
"type": "bearer",
"header": "Authorization: Bearer <token>",
"token_account": "gitops-write-bot",
"token_ref": "secret_ref://aios-gitops/runtime/gitops-write-bot",
"runtime_token_file": "/data/aios-gitops/runtime/secrets/gitops-write-bot.token",
"plaintext_token_in_git": false
},
"scopes": {
"read": [
"baseline:read",
"version:read",
"diff:read",
"patch:read",
"model:read",
"runtime:read"
],
"write": [
"branch:create",
"file:write",
"commit:create",
"patch:create",
"rollback:request",
"model:write",
"model:priority",
"nl:plan",
"runtime:test"
]
},
"policy": {
"direct_main_push": false,
"delete_tag": false,
"owner_approval_required": true,
"v_level_owner_admin_approval_required": true,
"api_keys_in_git": false
},
"status": {
"server_side_smoke": "pass",
"external_gpt_builder_auth_binding": "required_in_gpt_builder_ui",
"last_updated_at": "2026-07-06T13:24:28Z"
},
"gpt_builder_api_key_field": "raw token only, without Bearer prefix",
"do_not_use_as_api_key": "secret_ref://aios-gitops/runtime/gitops-write-bot",
"last_client_response_error_fix": {
"updated_at": "2026-07-06T13:41:34Z",
"server_changes": [
"normalize duplicated Bearer/Authorization prefixes",
"return auth_state diagnostics",
"flatten branch/rev/sync_status on write success"
],
"external_builder_requirement": "paste raw token into Bearer API key field"
}
}
+128
View File
@@ -0,0 +1,128 @@
{
"profile": "aios-v23-online-gitops",
"mode": "gitops-online",
"version": "V1R1C4",
"previous_version": "V1R1C3",
"repository": {
"workspace": "/data/aios-gitops/workspace",
"remote": "origin",
"url": "https://g.cnnetmkt.top/git/aios/aios-v23.git",
"web": "https://g.cnnetmkt.top/git/aios/aios-v23",
"protected_branches": [
"main"
],
"patch_branch_prefixes": [
"patch/",
"codex/patch/",
"request/"
],
"bare_repo": "/data/aios-gitops/runtime/gitea-bin/data/repositories/aios/aios-v23.git",
"sync_mode": "workspace_commit_to_online_bare_repo",
"public_portal": "https://cnnetmkt.top/portal/me/#gitops"
},
"api": {
"base_url": "https://cnnetmkt.top/api",
"openapi_schema": "https://cnnetmkt.top/api/openapi-chatgpt-actions.yaml",
"local_schema": "/data/aios-gitops/workspace/docs/gitops/openapi-chatgpt-actions.yaml",
"token_account": "codex-gitops-bot",
"token_ref": "secret_ref://aios-gitops/runtime/codex-gitops-bot"
},
"policy": {
"zip_update_deprecated": true,
"update_mode": "git_patch_only",
"all_writes_require_git_commit": true,
"direct_main_push": false,
"delete_tag": false,
"v_level_owner_admin_approval_required": true,
"api_keys_in_git": false,
"api_keys_in_logs": false,
"custom_llm_secret_storage": "runtime_secret_ref_only",
"online_git_sync": true,
"human_shortlinks_login_required": true,
"gitea_require_signin_view": true,
"gitea_default_local_account_disabled": true,
"owner_password_sync_secret_ref_only": true
},
"workflow": [
"create patch branch from main",
"commit every file/config/script change",
"push or sync branch to online Git service",
"review diff and run smoke checks",
"merge to main only after owner/admin approval",
"tag VxRxCx release and update baseline/current.json"
],
"created_at": "2026-07-06T10:21:08Z",
"base_commit_before_release": "43ddb91",
"updated_at": "2026-07-06T13:24:28Z",
"last_collaboration_test": {
"timestamp": "20260706T104130Z",
"result": "local_codex_and_custom_gpt_patches_merged"
},
"last_auth_portal_fix": {
"version": "V1R1C4",
"timestamp": "2026-07-06T11:16:23Z",
"shortlink_routes": [
"/base",
"/ver",
"/diff",
"/patch",
"/model",
"/run"
],
"gitea_sync_service": "aios-gitea-owner-sync.service",
"gitea_sync_timer": "aios-gitea-owner-sync.timer",
"portal_overlay": "overlays/user-control-center",
"portal_baseline_cache": "/app/runtime/gitops/current.json",
"sync_log_secret_hygiene": "runuser_without_sudo_command_logging"
},
"actions_write_auth": {
"account": "gitops-write-bot",
"token_ref": "secret_ref://aios-gitops/runtime/gitops-write-bot",
"header": "Authorization: Bearer <token>",
"scopes": [
"baseline:read",
"version:read",
"diff:read",
"patch:read",
"model:read",
"runtime:read",
"branch:create",
"file:write",
"commit:create",
"patch:create",
"rollback:request",
"model:write",
"model:priority",
"nl:plan",
"runtime:test"
],
"direct_main_push": false,
"delete_tag": false,
"owner_approval_required": true,
"updated_at": "2026-07-06T11:50:41Z",
"bridge_gpts": [
{
"name": "codex-chartgpt-bridge",
"gpt_id": "g-6a4ba1b4ea588191bab125d53ce83612",
"url": "https://chatgpt.com/g/g-6a4ba1b4ea588191bab125d53ce83612-codex-chartgpt-bridge",
"token_ref": "secret_ref://aios-gitops/runtime/gitops-write-bot",
"schema_url": "https://cnnetmkt.top/api/openapi-chatgpt-actions.yaml",
"auth_binding_status": "requires_gpt_builder_bearer_secret_binding",
"server_side_smoke": "pass",
"updated_at": "2026-07-06T13:24:28Z"
}
],
"external_actions_binding_note": "ChatGPT GPT Builder must configure Bearer auth with the runtime token behind token_ref; server cannot inject the token into an external GPT by URL alone.",
"gpt_builder_api_key_field": "raw token only, without Bearer prefix",
"do_not_use_as_api_key": "secret_ref://aios-gitops/runtime/gitops-write-bot",
"last_client_response_error_fix": {
"updated_at": "2026-07-06T13:41:34Z",
"server_changes": [
"normalize duplicated Bearer/Authorization prefixes",
"return auth_state diagnostics",
"flatten branch/rev/sync_status on write success"
],
"external_builder_requirement": "paste raw token into Bearer API key field"
}
}
}
+4
View File
@@ -1,4 +1,8 @@
AIOS_PUBLIC_DOMAIN=https://cnnetmkt.top
AIOS_USER_DOMAIN=https://user.cnnetmkt.top
AIOS_AUTH_DOMAIN=https://auth.cnnetmkt.top
AIOS_AUTH_BACKUP_DOMAIN=https://auth.h.imruyi.top:8443
AIOS_CASDOOR_PRIMARY_CALLBACK=https://cnnetmkt.top/casdoor/callback/
AIOS_WEBOPS_DOMAIN=https://ops.cnnetmkt.top
AIOS_API_DOMAIN=https://api.cnnetmkt.top
AIOS_WECHAT_CALLBACK_DOMAIN=https://wechat.cnnetmkt.top
+4 -3
View File
@@ -6,8 +6,8 @@ WORK_PATH = /data/aios-gitops/runtime/gitea-bin
PROTOCOL = http
HTTP_ADDR = 127.0.0.1
HTTP_PORT = 3000
DOMAIN = cnnetmkt.top
ROOT_URL = https://cnnetmkt.top/git/
DOMAIN = g.cnnetmkt.top
ROOT_URL = https://g.cnnetmkt.top/git/
SERVE_FROM_SUB_PATH = true
DISABLE_SSH = true
OFFLINE_MODE = false
@@ -27,7 +27,8 @@ INTERNAL_TOKEN = __INTERNAL_TOKEN__
[service]
DISABLE_REGISTRATION = true
REQUIRE_SIGNIN_VIEW = false
REQUIRE_SIGNIN_VIEW = true
SHOW_REGISTRATION_BUTTON = false
[webhook]
ALLOWED_HOST_LIST = *
+6 -6
View File
@@ -24,12 +24,12 @@
proxy_set_header X-Forwarded-Proto $scheme;
}
location = /base { proxy_pass http://127.0.0.1:18890/base; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; }
location = /ver { proxy_pass http://127.0.0.1:18890/ver; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; }
location = /diff { proxy_pass http://127.0.0.1:18890/diff; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; }
location = /patch { proxy_pass http://127.0.0.1:18890/patch; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; }
location = /model { proxy_pass http://127.0.0.1:18890/model; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; }
location = /run { proxy_pass http://127.0.0.1:18890/run; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; }
location = /base { return 302 /portal/me/#gitops; }
location = /ver { return 302 /portal/me/#gitops; }
location = /diff { return 302 /portal/me/#gitops; }
location = /patch { return 302 /portal/me/#gitops; }
location = /model { return 302 /portal/me/#gitops; }
location = /run { return 302 /portal/me/#gitops; }
location = /hook { proxy_pass http://127.0.0.1:18890/hook; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; }
location /hook/ {
proxy_pass http://127.0.0.1:18890/hook/;
+40
View File
@@ -0,0 +1,40 @@
# AIOS primary public Casdoor entry for cnnetmkt.top.
# The upstream remains the existing backup/home Casdoor service; public clients should see auth.cnnetmkt.top only.
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name auth.cnnetmkt.top;
access_log /var/log/nginx/aios-auth-cnnetmkt.access.log;
error_log /var/log/nginx/aios-auth-cnnetmkt.error.log;
ssl_certificate /etc/letsencrypt/live/cnnetmkt-wildcard/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/cnnetmkt-wildcard/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
proxy_pass https://auth.h.imruyi.top:8443;
proxy_http_version 1.1;
proxy_ssl_server_name on;
proxy_ssl_name auth.h.imruyi.top;
proxy_ssl_verify off;
proxy_set_header Host auth.h.imruyi.top;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port 443;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_redirect https://auth.h.imruyi.top:8443/ https://auth.cnnetmkt.top/;
proxy_redirect https://auth.h.imruyi.top/ https://auth.cnnetmkt.top/;
}
}
server {
listen 80;
listen [::]:80;
server_name auth.cnnetmkt.top;
return 301 https://$host$request_uri;
}
@@ -0,0 +1,7 @@
[Unit]
Description=Sync AIOS Gitea owner password from UCC owner account
After=aios-gitea.service
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/aios-sync-gitea-owner-password.sh
@@ -0,0 +1,10 @@
[Unit]
Description=Periodic AIOS Gitea owner password sync
[Timer]
OnBootSec=2min
OnUnitActiveSec=15min
Persistent=true
[Install]
WantedBy=timers.target
+2 -1
View File
@@ -10,9 +10,10 @@ Environment=AIOS_GITOPS_ROOT=/data/aios-gitops
Environment=AIOS_GITOPS_WORKSPACE=/data/aios-gitops/workspace
Environment=AIOS_GITOPS_DB=/data/aios-gitops/runtime/gitops_model.sqlite3
Environment=AIOS_GITOPS_TOKEN_FILE=/data/aios-gitops/runtime/secrets/gitops.tokens
Environment=AIOS_GITOPS_BARE_REPO=/data/aios-gitops/runtime/gitea-bin/data/repositories/aios/aios-v23.git
Environment=AIOS_GITOPS_HOST=127.0.0.1
Environment=AIOS_GITOPS_PORT=18890
ExecStart=/usr/bin/python3 /data/aios-gitops/workspace/active-runtime/app/aios_gitops_model_api.py
ExecStart=/usr/bin/python3 /data/aios-gitops/workspace/active-runtime/app/aios_chatgpt_actions_api.py
Restart=always
RestartSec=3
+15
View File
@@ -12,3 +12,18 @@ Forbidden:
- API keys in Git
- API keys in plaintext logs
- bypassing owner/admin approval for V-level changes
## Codex Online Version Management
Codex uses `/data/aios-gitops/workspace` and `https://cnnetmkt.top/git/aios/aios-v23.git` as the online Git version baseline. Runtime write access uses `codex-gitops-bot`; its token is stored outside Git and referenced as `secret_ref://aios-gitops/runtime/codex-gitops-bot`.
Current production release after this merge: `V1R1C3`. Acceptance-only branches are retained for audit and are not automatically merged into `main`.
## Online Git Sync
Starting in `V1R1C3`, committed API/Codex patch branches are synchronized from the workspace clone into the online Gitea bare repository. Protected production branches remain excluded from API sync.
## Codex and Custom GPT Collaboration Test
`V1R1C3` confirms local Codex and the custom GPT Actions path can independently create patch branches, commit files, sync to online Git, and then merge into the latest baseline version.
@@ -0,0 +1,81 @@
# Codex ChatGPT Bridge Actions Setup
Date: 2026-07-06T13:24:28Z
GPT: codex-chartgpt-bridge
URL: https://chatgpt.com/g/g-6a4ba1b4ea588191bab125d53ce83612-codex-chartgpt-bridge
## Current Server-Side State
The AIOS GitOps API write path is ready for this GPT bridge.
- API base: `https://cnnetmkt.top/api`
- OpenAPI schema: `https://cnnetmkt.top/api/openapi-chatgpt-actions.yaml`
- Auth type: Bearer token
- Required header: `Authorization: Bearer <token>`
- Token account: `gitops-write-bot`
- Token source: `secret_ref://aios-gitops/runtime/gitops-write-bot`
- Runtime token file: `/data/aios-gitops/runtime/secrets/gitops-write-bot.token`
The token value must not be copied into Git, docs, logs, prompts, or issue comments.
## Required GPT Builder Action Binding
The GPT URL alone cannot make the GPT send a Bearer header. In the GPT Builder Actions configuration for `codex-chartgpt-bridge`, bind authentication as:
```text
Authentication type: API Key / Bearer
Auth header: Authorization
Auth value / API key field: <raw runtime token value only>
Schema URL: https://cnnetmkt.top/api/openapi-chatgpt-actions.yaml
Secret source: secret_ref://aios-gitops/runtime/gitops-write-bot
```
If the builder asks for the token value, read it only from the runtime secret file on the server and paste it into the GPT Builder secret field. Do not paste it into chat.
## Server-Side Smoke Already Passed
- no-token `createPatch`: `401 unauthorized`
- `gitops-write-bot` `createPatch`: `200`, branch synced
- `gitops-write-bot` `commitFile`: `200`, branch synced
- `codex-gitops-bot` `createPatch`: `200`, branch synced
- `codex-gitops-bot` `commitFile`: `200`, branch synced
## Latest ClientResponseError Fix
Current public API and auth smoke status:
- `GET /api/base`, `GET /api/run`, `GET /api/status`, and the OpenAPI schema URL return `200`.
- no-token writes return `401` with `auth_state=missing_bearer`.
- using the public `secret_ref://...` string as the API key returns `401` with `auth_state=secret_ref_literal_not_token`.
- duplicated `Bearer ` prefixes are normalized server-side and accepted when the underlying token is valid.
- valid raw-token `createPatch` and `commitFile` return `branch`, `rev`, and `sync_status` at top level.
- `HEAD /api/base`, `HEAD /api/run`, and `HEAD /api/openapi-chatgpt-actions.yaml` return `200`, preventing endpoint validators from treating public reads as unsupported.
The GPT Builder API key field must contain only the raw token value.
If Actions returns `auth_state=invalid_token`, the GPT did send a Bearer value, but it is not the current runtime token. Re-open the GPT Builder Action authentication dialog, replace the hidden API key field with the current raw value from `/data/aios-gitops/runtime/secrets/gitops-write-bot.token`, save the Action, publish/update the GPT, and start a fresh GPT conversation before retesting.
## External GPT Retest
After binding the Bearer token in GPT Builder, ask the GPT to run:
1. `createPatch` with a branch under `patch/`.
2. `commitFile` on that same branch.
3. `getDiff` for the new branch versus `main`.
Expected result:
- `createPatch` returns `200`.
- `commitFile` returns `200`.
- branch sync status is `synced`.
- no direct `main` write occurs.
## Security Boundary
- `direct_main_push=false`
- `delete_tag=false`
- `owner_approval_required=true`
- V-level operations still require owner/admin approval.
- Custom LLM API keys must use secret refs only.
Important: because the GPT Builder screenshot has Bearer selected, the API key field must contain only the raw token value. Do not type `Bearer ` in that field. Do not type `secret_ref://aios-gitops/runtime/gitops-write-bot`; that string is a public reference, not a credential.
@@ -0,0 +1,38 @@
# Codex Online GitOps Version Management
Codex is configured to treat the online Git baseline as the version source of truth.
- Workspace: `/data/aios-gitops/workspace`
- Git web: `https://cnnetmkt.top/git/aios/aios-v23`
- API base: `https://cnnetmkt.top/api`
- OpenAPI schema: `https://cnnetmkt.top/api/openapi-chatgpt-actions.yaml`
- Runtime token account: `codex-gitops-bot`
- Runtime token ref: `secret_ref://aios-gitops/runtime/codex-gitops-bot`
## Rules
1. Codex updates must use Git patch branches; ZIP upload is deprecated.
2. Every Codex write must become a Git commit.
3. Codex must not push directly to `main`; merge to `main` requires owner/admin approval.
4. V-level release/tag/rollback requests must not bypass owner/admin approval.
5. User custom LLM API keys must stay in runtime secrets and enter Git only as `secret_ref`.
6. Test or acceptance branches can remain as audit evidence, but dummy model/provider configs must not be merged into production `main`.
## Standard Flow
```text
main -> patch/<name> -> commit -> diff review -> smoke test -> owner/admin approval -> merge main -> VxRxCx tag -> baseline/current.json
```
## Current Merge
`V1R1C1` merges the current GitOps/ChatGPT/Codex access changes into the latest online baseline while excluding acceptance dummy model configuration.
## Online Git Sync
Starting in `V1R1C3`, committed API/Codex patch branches are synchronized from the workspace clone into the online Gitea bare repository. Protected production branches remain excluded from API sync.
## Codex and Custom GPT Collaboration Test
`V1R1C3` confirms local Codex and the custom GPT Actions path can independently create patch branches, commit files, sync to online Git, and then merge into the latest baseline version.
+57
View File
@@ -0,0 +1,57 @@
# GitOps Write Auth Fix
Date: 2026-07-06T11:50:41Z
Branch: `patch/gitops-write-auth-fix`
## Root Cause
Read operations were working because they are public at the server layer. Write operations returned `401 unauthorized` when ChatGPT Actions did not inject `Authorization: Bearer ...` for write calls.
Runtime token records already supported write scopes for bot accounts, so the API write path itself was not broken. A bearer-authenticated smoke test proved `createPatch` and `commitFile` succeed when the write token is supplied.
## Fix
- Added/normalized runtime bot account: `gitops-write-bot`.
- Runtime token file: `/data/aios-gitops/runtime/secrets/gitops-write-bot.token`.
- Git-visible credential reference only: `secret_ref://aios-gitops/runtime/gitops-write-bot`.
- Added token hash and scopes to `/data/aios-gitops/runtime/secrets/gitops.tokens`.
- Updated OpenAPI schema to declare bearer auth at top level and on write operations.
- Updated API 401/403 responses to include required scope and `token_secret_ref`, without exposing secrets.
- Added smoke test: `scripts/smoke_gitops_write_auth.py`.
## Required Actions Auth Binding
Configure ChatGPT Actions / Codex Actions auth with:
- Auth type: Bearer token
- Header: `Authorization: Bearer <token>`
- Secret source: `secret_ref://aios-gitops/runtime/gitops-write-bot`
Do not paste the token into Git, docs, logs, prompts, or issue comments.
## Smoke
```bash
python3 scripts/smoke_gitops_write_auth.py --api https://cnnetmkt.top/api --token-file /data/aios-gitops/runtime/secrets/gitops-write-bot.token
```
Expected:
- no-token `createPatch` returns `401`
- authenticated `createPatch` returns `200`
- authenticated `commitFile` returns `200`
## Rollback
1. Remove or disable the `gitops-write-bot` JSON record in `/data/aios-gitops/runtime/secrets/gitops.tokens`.
2. Remove `/data/aios-gitops/runtime/secrets/gitops-write-bot.token` if rotation is required.
3. Revert this patch branch or restore OpenAPI/API files from `main`.
4. Restart the API service:
```bash
sudo systemctl restart aios-gitops-model-api.service
```
5. Verify unauthenticated writes still return `401`.
Important: because the GPT Builder screenshot has Bearer selected, the API key field must contain only the raw token value. Do not type `Bearer ` in that field. Do not type `secret_ref://aios-gitops/runtime/gitops-write-bot`; that string is a public reference, not a credential.
@@ -0,0 +1,135 @@
# GitOps Write Auth Fix Acceptance
Time: 2026-07-06T13:26:26Z
Branch: `patch/gitops-write-auth-fix`
Patch commit: see `git rev-parse --short patch/gitops-write-auth-fix`
Bridge GPT: `codex-chartgpt-bridge`
Bridge URL: https://chatgpt.com/g/g-6a4ba1b4ea588191bab125d53ce83612-codex-chartgpt-bridge
## Result
SERVER-SIDE PASS. The GitOps API write path accepts valid Bearer tokens and rejects no-token writes. The external GPT still must have Bearer authentication configured in GPT Builder Actions; a GPT URL alone cannot inject the header.
## Root Cause Confirmed
- Read endpoints were healthy without auth.
- Write endpoints returned `401 unauthorized` when Actions did not send `Authorization: Bearer ...`.
- Runtime token registry supports write scopes; the broken boundary was Actions auth header injection / write token binding.
## Fix Applied
- Added dedicated runtime account `gitops-write-bot`.
- Runtime token file: `/data/aios-gitops/runtime/secrets/gitops-write-bot.token`, mode `600`.
- Token registry stores hash/scopes only in `/data/aios-gitops/runtime/secrets/gitops.tokens`.
- Git-visible token reference only: `secret_ref://aios-gitops/runtime/gitops-write-bot`.
- OpenAPI now advertises Bearer auth, write operation scopes, and the bridge GPT URL.
- `/api/status` returns `bridge_gpts` with the `codex-chartgpt-bridge` URL and auth-binding status.
- Added bridge setup doc: `docs/gitops/CODEX_CHATGPT_BRIDGE_ACTIONS_SETUP.md`.
- Added smoke script: `scripts/smoke_gitops_write_auth.py`.
## Smoke Results
### gitops-write-bot
```json
{
"noauth": {"operation": "createPatch without token", "http": 401, "status": "unauthorized"},
"createPatch": {"operation": "createPatch", "http": 200, "status": "patch_branch_created", "branch": "patch/gitops-write-auth-smoke-20260706T115230Z", "rev": "6d3a050", "sync_status": "synced"},
"commitFile": {"operation": "commitFile", "http": 200, "status": "committed", "branch": "patch/gitops-write-auth-smoke-20260706T115230Z", "rev": "17344f7", "sync_status": "synced"}
}
```
### codex-gitops-bot
```json
{
"noauth": {"operation": "createPatch without token", "http": 401, "status": "unauthorized"},
"createPatch": {"operation": "createPatch", "http": 200, "status": "patch_branch_created", "branch": "patch/codex-write-auth-smoke-20260706T115256Z", "rev": "d684df2", "sync_status": "synced"},
"commitFile": {"operation": "commitFile", "http": 200, "status": "committed", "branch": "patch/codex-write-auth-smoke-20260706T115256Z", "rev": "884a5ec", "sync_status": "synced"}
}
```
### codex-chartgpt-bridge binding smoke
```json
{
"noauth": {"operation": "createPatch without token", "http": 401, "status": "unauthorized"},
"createPatch": {"operation": "createPatch", "http": 200, "status": "patch_branch_created", "branch": "patch/bridge-write-auth-smoke-20260706T132506Z", "rev": "f4ef8e5", "sync_status": "synced"},
"commitFile": {"operation": "commitFile", "http": 200, "status": "committed", "branch": "patch/bridge-write-auth-smoke-20260706T132506Z", "rev": "0ed0816", "sync_status": "synced"}
}
```
## ClientResponseError Retest (2026-07-06T13:47:06Z)
The external GPT Actions failure was reproduced as a write-auth boundary issue, not a public endpoint outage. Public reads reached the API and returned typed responses, while writes without a valid Bearer credential returned structured `401` responses.
```text
GET /api/base: 200 application/json
GET /api/run: 200 application/json
GET /api/status: 200 application/json
GET /api/openapi-chatgpt-actions.yaml: 200 application/yaml
POST /api/patch without token: 401 auth_state=missing_bearer
POST /api/patch with literal secret_ref as token: 401 auth_state=secret_ref_literal_not_token
POST /api/patch with duplicated Bearer prefix: 200 branch=patch/actions-double-bearer-20260706T134646Z rev=ddd8711 sync_status=synced
POST /api/patch with raw runtime token: 200 branch=patch/actions-raw-token-smoke-20260706T134706Z rev=0e48e1d sync_status=synced
POST /api/file with raw runtime token: 200 branch=patch/actions-raw-token-smoke-20260706T134706Z rev=6ab67e9 sync_status=synced
```
Server-side compatibility added in this patch:
- Normalize accidental `Authorization:` and repeated `Bearer ` prefixes before token verification.
- Reject literal `secret_ref://...` as a token and return `auth_state=secret_ref_literal_not_token`.
- Return `auth_state`, `required_scope`, and GPT Builder guidance on `401`.
- Return top-level `branch`, `rev`, and `sync_status` on write success so Actions can make deterministic pass/fail decisions.
- Support `HEAD` probes for public read/schema endpoints so GPT Builder or gateway validators do not see `501` during endpoint discovery.
GPT Builder still must store the raw runtime token in the hidden API key field. The public `secret_ref://aios-gitops/runtime/gitops-write-bot` value is a pointer only and must not be used as the API key.
HEAD compatibility retest:
```text
HEAD /api/base: 200 application/json
HEAD /api/run: 200 application/json
HEAD /api/openapi-chatgpt-actions.yaml: 200 application/yaml
```
## External GPT Retest (2026-07-06T13:59:43Z)
The custom GPT retest reached the server:
```text
GET /api/run: 200
GET /api/base: 200
POST /api/branch: 401
POST /api/patch: 401
```
This confirms that routing, TLS, schema loading, and read access are healthy. The remaining failure is the external GPT Builder secret binding: the write request does not carry the runtime `gitops-write-bot` raw token value. If a token-like value is present but does not match the token registry, the API now reports `auth_state=invalid_token` and `rebind_required=true`.
## External GPT Builder Binding Required
Configure the GPT Actions auth for `codex-chartgpt-bridge` as:
```text
Schema URL: https://cnnetmkt.top/api/openapi-chatgpt-actions.yaml
Authentication type: API Key / Bearer
Header: Authorization
Value / API key field: <raw runtime token value only>
Secret source: secret_ref://aios-gitops/runtime/gitops-write-bot
```
The actual token value must be read from `/data/aios-gitops/runtime/secrets/gitops-write-bot.token` and entered only into the GPT Builder secret/auth field. Do not paste it into chat or Git.
## Exit Criteria
- ChatGPT Actions write capability: PASS once the GPT Builder auth field is bound to the runtime token.
- Server-side authenticated write path: PASS.
- Codex API commit capability: PASS.
- No-token write requests remain rejected: PASS, `401 unauthorized`.
- No plaintext token committed to Git: PASS by workspace secret pattern scan.
## Security Notes
The patch does not allow direct `main` writes, does not permit tag deletion, and does not bypass owner/admin approval for V-level actions. Credentials stay in runtime secrets; Git stores only secret refs and token hashes.
Important: because the GPT Builder screenshot has Bearer selected, the API key field must contain only the raw token value. Do not type `Bearer ` in that field. Do not type `secret_ref://aios-gitops/runtime/gitops-write-bot`; that string is a public reference, not a credential.
@@ -0,0 +1,7 @@
# Custom GPT Collaboration Patch
Timestamp: 20260706T104130Z
Actor: chatgpt-gitops-bot
Created through chatgpt-gitops-bot to simulate the AIOS GitOps 控制台 GPT Actions path: https://chatgpt.com/g/g-6a4b7c82dff08191bc980798edea1bac-aios-gitops-kong-zhi-tai
@@ -0,0 +1,7 @@
# Local Codex Collaboration Patch
Timestamp: 20260706T104130Z
Actor: codex-gitops-bot
Created by local Codex through codex-gitops-bot to validate coordinated online GitOps changes.
+285
View File
@@ -0,0 +1,285 @@
openapi: 3.1.0
info:
title: AIOS GitOps Controlled Access API
version: V1R1C0-chatgpt-actions
description: Controlled ChatGPT Actions API for AIOS online GitOps baseline. Read endpoints are public. Write endpoints require Bearer token and create Git commits on non-main branches.
x-aios-bridge-gpts:
- name: codex-chartgpt-bridge
url: https://chatgpt.com/g/g-6a4ba1b4ea588191bab125d53ce83612-codex-chartgpt-bridge
token_secret_ref: secret_ref://aios-gitops/runtime/gitops-write-bot
api_key_field: raw token only, without Bearer prefix
servers:
- url: https://cnnetmkt.top
security:
- bearerAuth: []
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: opaque-token
description: Use Bearer auth for write operations. In GPT Builder Bearer mode, paste only the raw runtime token into the API key field; do not include the word Bearer and do not paste the public secret_ref string. Read endpoints remain public at server level.
x-token-secret-ref: secret_ref://aios-gitops/runtime/gitops-write-bot
schemas:
BranchRequest:
type: object
required: [branch]
properties:
branch: {type: string, example: patch/chatgpt-actions-demo}
base: {type: string, default: main}
message: {type: string}
FileCommitRequest:
type: object
required: [branch, path, content]
properties:
branch: {type: string}
base: {type: string, default: main}
create_branch: {type: boolean, default: false}
path: {type: string, example: docs/gitops/chatgpt-change.md}
content: {type: string}
message: {type: string}
CommitRequest:
type: object
properties:
branch: {type: string}
paths:
type: array
items: {type: string}
message: {type: string}
PatchRequest:
type: object
required: [patch_name]
properties:
patch_name: {type: string}
branch: {type: string}
base: {type: string, default: main}
risk: {type: string, enum: [low, medium, high]}
description: {type: string}
message: {type: string}
TagRequest:
type: object
required: [version]
properties:
version: {type: string, example: V1R1C1}
message: {type: string}
execute:
type: boolean
description: Owner-approved calls only. Otherwise an approval request commit is created.
RollbackRequest:
type: object
required: [target_version]
properties:
target_version: {type: string}
reason: {type: string}
branch: {type: string}
ModelRequest:
type: object
required: [provider_name, base_url, model_name]
properties:
username: {type: string}
provider_name: {type: string}
base_url: {type: string}
model_name: {type: string}
display_name: {type: string}
api_key_secret_ref:
type: string
description: Preferred. Git stores this secret_ref only.
api_key:
type: string
writeOnly: true
description: Accepted only for runtime secret storage; never committed to Git or logs.
branch: {type: string}
message: {type: string}
ModelPriorityRequest:
type: object
required: [items]
properties:
username: {type: string}
branch: {type: string}
items:
type: array
items:
type: object
properties:
model_id: {type: string}
priority: {type: integer}
enabled: {type: boolean}
fallback_policy: {type: string}
ActionWriteResult:
type: object
properties:
status: {type: string}
branch: {type: string}
rev: {type: string}
sync_status: {type: string}
commit: {type: object}
patch: {type: object}
path: {type: string}
ErrorResponse:
type: object
properties:
error: {type: string}
hint: {type: string}
auth_state: {type: string}
required_scope: {type: string}
token_secret_ref: {type: string}
auth_header: {type: string}
gpt_builder_note: {type: string}
NLRequest:
type: object
required: [text]
properties:
text: {type: string}
branch: {type: string}
paths:
/api/base:
get:
operationId: getBaseline
summary: Read current AIOS baseline
responses: {'200': {description: OK}}
/api/ver:
get:
operationId: listVersions
summary: List GitOps versions
responses: {'200': {description: OK}}
/api/diff:
get:
operationId: getDiff
summary: Read Git diff between refs
parameters:
- in: query
name: from
schema: {type: string, default: HEAD~1}
- in: query
name: to
schema: {type: string, default: HEAD}
responses: {'200': {description: OK}}
/api/patch:
get:
operationId: listPatches
summary: List patch records
responses: {'200': {description: OK}}
post:
operationId: createPatch
x-required-scope: patch:create
summary: Create a patch branch and commit metadata
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/PatchRequest'}
responses:
'200': {description: Patch branch committed, content: {application/json: {schema: {$ref: '#/components/schemas/ActionWriteResult'}}}}
'401': {description: Unauthorized, content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}}
'403': {description: Forbidden, content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}}
/api/model:
get:
operationId: readModelRegistry
summary: Read visible model registry and priority
responses: {'200': {description: OK}}
post:
operationId: registerModel
x-required-scope: model:write
summary: Register a custom model; API keys are stored as runtime secrets and Git receives secret_ref only
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/ModelRequest'}
responses: {'200': {description: Model registered and committed}, '401': {description: Unauthorized}, '403': {description: Forbidden}}
/api/run:
get:
operationId: getRuntimeStatus
summary: Read runtime status
responses: {'200': {description: OK}}
/api/branch:
post:
operationId: createBranch
x-required-scope: branch:create
summary: Create a non-main branch and commit branch metadata
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/BranchRequest'}
responses:
'200': {description: Branch created, content: {application/json: {schema: {$ref: '#/components/schemas/ActionWriteResult'}}}}
'401': {description: Unauthorized, content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}}
'403': {description: Forbidden, content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}}
/api/file:
post:
operationId: commitFile
x-required-scope: file:write
summary: Write a file and create a Git commit on a non-main branch
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/FileCommitRequest'}
responses:
'200': {description: File committed, content: {application/json: {schema: {$ref: '#/components/schemas/ActionWriteResult'}}}}
'400': {description: Secret rejected or invalid path, content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}}
'401': {description: Unauthorized, content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}}
/api/commit:
post:
operationId: commitChanges
x-required-scope: commit:create
summary: Commit existing workspace changes on a non-main branch
security: [{bearerAuth: []}]
requestBody:
required: false
content:
application/json:
schema: {$ref: '#/components/schemas/CommitRequest'}
responses: {'200': {description: Commit result}, '401': {description: Unauthorized}}
/api/tag:
post:
operationId: requestOrCreateTag
summary: Request tag creation; owner execution is required for direct tag
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/TagRequest'}
responses: {'200': {description: Tag request committed or tag created}, '403': {description: Forbidden}}
/api/rollback:
post:
operationId: requestRollback
x-required-scope: rollback:request
summary: Commit a rollback request requiring owner/admin approval
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/RollbackRequest'}
responses: {'200': {description: Rollback request committed}, '401': {description: Unauthorized}}
/api/model-priority:
put:
operationId: setModelPriority
x-required-scope: model:priority
summary: Set model priority and commit policy file
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/ModelPriorityRequest'}
responses: {'200': {description: Priority updated and committed}, '401': {description: Unauthorized}}
/api/nl:
post:
operationId: planNaturalLanguageGitOpsAction
x-required-scope: nl:plan
summary: Convert natural language to a safe committed plan without executing V-level changes
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/NLRequest'}
responses: {'200': {description: Plan committed}, '401': {description: Unauthorized}}
+17
View File
@@ -0,0 +1,17 @@
# CNNETMKT apex DNS status
generated_at=2026-07-06T16:39:07+08:00
provider=Huawei Cloud DNS
zone=cnnetmkt.top.
record=cnnetmkt.top. A 1.95.164.183
status=created_and_resolved
credential_storage=/etc/letsencrypt/huaweicloud-cnnetmkt.env
secret_policy=not_tracked_not_logged
## verification
- authoritative NS returns 1.95.164.183
- system resolver returns 1.95.164.183
- https://cnnetmkt.top/ returns 302 /portal/login/
- https://cnnetmkt.top/git/ returns 200 AIOS GitOps
- wildcard certificate SAN covers cnnetmkt.top and *.cnnetmkt.top
+53
View File
@@ -0,0 +1,53 @@
# CNNETMKT Auth Primary Status
更新时间:2026-07-06T09:00:06Z
## 结论
`https://cnnetmkt.top/` 是用户主入口。登录授权主域为 `https://auth.cnnetmkt.top`。
`https://auth.h.imruyi.top:8443` 只作为备份/上游兼容入口保留,不应出现在普通用户的主登录跳转链路里。
## 当前链路
1. 用户访问 `https://cnnetmkt.top/`。
2. UCC 返回 `/portal/login/`。
3. UCC 生成 Casdoor 授权地址:
`https://auth.cnnetmkt.top/login/oauth/authorize?...redirect_uri=https%3A%2F%2Fcnnetmkt.top%2Fcasdoor%2Fcallback%2F...`
4. 华为云 Nginx `auth.cnnetmkt.top` server block 反代到现有 Casdoor 上游。
5. Casdoor 允许 `https://cnnetmkt.top/casdoor/callback/` 作为 `admin/aios-portal` 的 OAuth callback。
## 已调整配置
### 华为云 UCC 运行时
- `CASDOOR_PUBLIC_BASE_URL=https://auth.cnnetmkt.top`
- `CASDOOR_INTERNAL_BASE_URL=https://auth.cnnetmkt.top`
- `CASDOOR_INVITE_SYNC_BASE_URL=https://auth.cnnetmkt.top`
- `UCC_PUBLIC_BASE_URL=https://cnnetmkt.top`
- `ALLOWED_HOSTS` 增加 `auth.cnnetmkt.top`
- `CSRF_TRUSTED_ORIGINS` 增加 `https://cnnetmkt.top` 和 `https://auth.cnnetmkt.top`
### Casdoor 应用
应用:`admin/aios-portal`
新增/保留 callback:
- `https://cnnetmkt.top/casdoor/callback/`
- `https://cnnetmkt.top/api/auth/callback/casdoor`
- `https://user.cnnetmkt.top/casdoor/callback/`
- `https://portal.cnnetmkt.top/casdoor/callback/`
- `https://portal.h.imruyi.top:8443/casdoor/callback/`(备份)
## 验证命令
```bash
ops/gitops-migration/check_cnnetmkt_auth_primary.sh
```
关键验收点:
- 主链路不再出现 `auth.h.imruyi.top:8443`。
- `user-control-web` 为 `healthy`。
- Casdoor DB `application.redirect_uris` 包含 `https://cnnetmkt.top/casdoor/callback/`。
@@ -0,0 +1,27 @@
# AIOS V1R1C1 Codex GitOps Online Release
Date: 2026-07-06T10:21:08Z
## Purpose
Merge the current controlled ChatGPT/Codex GitOps access changes into the latest online baseline and make Git the operating version manager for Codex changes.
## Included
- ChatGPT Actions API adapter and OpenAPI schema.
- Secret-safe model registration using `secret_ref`.
- `codex-gitops-bot` runtime token account with scoped API permissions.
- Codex online GitOps configuration under `config/codex/gitops-online.json`.
- Baseline metadata updated from `V1R1C0` to `V1R1C1`.
## Excluded
- Acceptance dummy model registry entries from `patch/chatgpt-model-acceptance-*`.
- Plaintext API keys or bearer tokens.
## Verification
- `curl https://cnnetmkt.top/api/base` returns 200.
- `curl https://cnnetmkt.top/api/openapi-chatgpt-actions.yaml` returns 200.
- `bash scripts/package_contract_check_v1r1c0.sh` passes.
- `bash scripts/codex_gitops_online_check.sh` passes after tag publication.
@@ -0,0 +1,18 @@
# AIOS V1R1C2 Codex GitOps Online Sync Release
Date: 2026-07-06T10:26:26Z
## Purpose
Make Codex/API writes visible in the online Git service by syncing non-main patch branches from `/data/aios-gitops/workspace` into the Gitea bare repository after each committed write.
## Included
- `active-runtime/app/aios_chatgpt_actions_api.py` now syncs non-main branch commits to the online Git bare repository.
- `POST /api/tag` direct owner flow syncs tags when executed.
- `config/codex/gitops-online.json` records `online_git_sync=true`.
- Live and template systemd service configuration declares `AIOS_GITOPS_BARE_REPO`.
## Policy
`main` remains protected. API/Codex writes still commit to patch/request branches and owner/admin approval remains required for V-level production actions.
@@ -0,0 +1,28 @@
# AIOS V1R1C3 Codex + Custom GPT Collaboration Release
Date: 2026-07-06T10:42:33Z
## Purpose
Validate that local Codex and the AIOS GitOps 控制台 custom GPT can both modify the online GitOps baseline through controlled API writes, then merge both changes into the latest version.
## Custom GPT
https://chatgpt.com/g/g-6a4b7c82dff08191bc980798edea1bac-aios-gitops-kong-zhi-tai
## Merged Inputs
| Actor | Account | Branch | Commit | Online Sync |
|---|---|---|---|---|
| local_codex | codex-gitops-bot | `patch/collab-local-codex-20260706T104130Z` | `a62d4ce` | synced |
| custom_gpt | chatgpt-gitops-bot | `patch/collab-custom-gpt-20260706T104130Z` | `c393931` | synced |
## Result
Both branches were merged into `main`, baseline metadata was refreshed to `V1R1C3`, and the release tag `V1R1C3` was published to the online Git repository.
## Guardrails
- GPT and Codex writes entered Git as patch branch commits.
- Protected `main` was updated only by the release merge step.
- Bot tokens and custom LLM API keys remain outside Git.
@@ -0,0 +1,36 @@
# AIOS V1R1C4 Auth GitOps Portal Release
Date: 2026-07-06T11:05:30Z
## Purpose
Close the public-access gap around human GitOps shortlinks, move repository browsing to the protected Gitea endpoint, and make the new GitOps portal function visible from the main cnnetmkt owner/admin workspace.
## Changes
- `/base`, `/ver`, `/diff`, `/patch`, `/model`, and `/run` now redirect to `https://cnnetmkt.top/portal/me/#gitops` instead of exposing raw JSON to unauthenticated users.
- `/api/*` remains the controlled machine interface for ChatGPT Actions and Codex.
- Gitea now uses `https://g.cnnetmkt.top/git/`, requires sign-in for repository browsing, and hides registration.
- `aios-gitea-owner-sync.service` and timer synchronize the Gitea `owner` account password from the user-control owner secret without writing the password to Git or logs.
- The legacy local `aios` Gitea account has login prohibited and its password cleared; the row remains only to preserve repository ownership.
- The user portal now includes a GitOps dashboard section for admin/owner roles.
- The patched user-control-center source is tracked under `overlays/user-control-center/` with a replay script.
- The portal GitOps section reads `/app/runtime/gitops/current.json` before HTTP fallback, so the current version remains visible inside Docker.
## Verification Targets
- `https://cnnetmkt.top/model` returns `302` to `/portal/me/#gitops`.
- `https://cnnetmkt.top/api/model` returns the public machine-readable model metadata.
- `https://g.cnnetmkt.top/git/aios/aios-v23` returns a login redirect when unauthenticated.
- Owner login can open the Gitea repository; no secret is printed.
- Portal login state renders the GitOps section and links.
## Guardrails
- No ZIP update path is reintroduced.
- All replayable files are tracked in Git.
- Passwords, bearer tokens, Huawei keys, and custom LLM API keys remain outside Git.
## Secret Log Hygiene
The Gitea owner password sync script uses `runuser` instead of `sudo -u ... --password ...`, strips CRLF from sourced owner fields, and does not print passwords or tokens. Archived journal files were rotated/vacuumed after the live script was corrected.
@@ -14,6 +14,7 @@ log() {
cd "$WORKSPACE"
TOKEN_FILE="$ROOT/runtime/secrets/gitops.tokens"
BARE_REPO="$ROOT/runtime/gitea-bin/data/repositories/aios/aios-v23.git"
if [ ! -s "$TOKEN_FILE" ]; then
(openssl rand -hex 32 || python3 -c 'import secrets; print(secrets.token_hex(32))') > "$TOKEN_FILE"
chmod 600 "$TOKEN_FILE"
@@ -32,9 +33,10 @@ Environment=AIOS_GITOPS_ROOT=$ROOT
Environment=AIOS_GITOPS_WORKSPACE=$WORKSPACE
Environment=AIOS_GITOPS_DB=$ROOT/runtime/gitops_model.sqlite3
Environment=AIOS_GITOPS_TOKEN_FILE=$TOKEN_FILE
Environment=AIOS_GITOPS_BARE_REPO=$BARE_REPO
Environment=AIOS_GITOPS_HOST=127.0.0.1
Environment=AIOS_GITOPS_PORT=18890
ExecStart=/usr/bin/python3 $WORKSPACE/active-runtime/app/aios_gitops_model_api.py
ExecStart=/usr/bin/python3 $WORKSPACE/active-runtime/app/aios_chatgpt_actions_api.py
Restart=always
RestartSec=3
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -euo pipefail
WORKSPACE="${AIOS_GITOPS_WORKSPACE:-/data/aios-gitops/workspace}"
RUNTIME_ROOT="${AIOS_USER_CONTROL_ROOT:-/data/aios/aios_r13_v18_runtime_portable_silent_20260704/components/user-control-center}"
CONTAINER="${AIOS_USER_CONTROL_CONTAINER:-user-control-web}"
install -D -m 0644 "$WORKSPACE/overlays/user-control-center/control/portal_views.py" "$RUNTIME_ROOT/control/portal_views.py"
install -D -m 0644 "$WORKSPACE/overlays/user-control-center/templates/portal/me.html" "$RUNTIME_ROOT/templates/portal/me.html"
install -D -m 0644 "$WORKSPACE/baseline/current.json" "$RUNTIME_ROOT/runtime/gitops/current.json"
if command -v docker >/dev/null 2>&1 && docker ps --format '{{.Names}}' | grep -qx "$CONTAINER"; then
docker cp "$RUNTIME_ROOT/control/portal_views.py" "$CONTAINER:/app/control/portal_views.py"
docker cp "$RUNTIME_ROOT/templates/portal/me.html" "$CONTAINER:/app/templates/portal/me.html"
docker exec "$CONTAINER" mkdir -p /app/runtime/gitops
docker cp "$RUNTIME_ROOT/runtime/gitops/current.json" "$CONTAINER:/app/runtime/gitops/current.json"
docker restart "$CONTAINER" >/dev/null
fi
echo "AIOS_USER_PORTAL_GITOPS_OVERLAY_APPLIED runtime=$RUNTIME_ROOT container=$CONTAINER"
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env bash
set -euo pipefail
PUBLIC_ENTRY="${PUBLIC_ENTRY:-https://cnnetmkt.top/}"
AUTH_ENTRY="${AUTH_ENTRY:-https://auth.cnnetmkt.top}"
CLIENT_ID="${CASDOOR_CLIENT_ID:-aios8a810963c1dd368a}"
CALLBACK="${CASDOOR_CALLBACK:-https://cnnetmkt.top/casdoor/callback/}"
AIHOST_HOST="${AIHOST_HOST:-aihost}"
HW_HOST="${HW_HOST:-hw.imruyi.top}"
fail() {
printf 'FAIL: %s\n' "$*" >&2
exit 1
}
CALLBACK_ENCODED="$(python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$CALLBACK")"
AUTH_URL="$AUTH_ENTRY/login/oauth/authorize?client_id=$CLIENT_ID&response_type=code&redirect_uri=$CALLBACK_ENCODED&scope=read&state=aios-portal&language=zh"
printf '== public redirect chain ==\n'
chain="$(curl -k -sS -L -I --max-redirs 4 "$PUBLIC_ENTRY")"
printf '%s\n' "$chain"
printf '%s\n' "$chain" | grep -F "Location: $AUTH_ENTRY/login/oauth/authorize" >/dev/null \
|| fail "public entry did not redirect to $AUTH_ENTRY"
if printf '%s\n' "$chain" | grep -F 'auth.h.imruyi.top:8443' >/dev/null; then
fail 'public chain still exposes backup auth domain'
fi
printf '\n== auth authorize http ==\n'
auth_headers="$(curl -k -sS -D - -o /tmp/aios-auth-primary-body --max-time 20 "$AUTH_URL")"
printf '%s\n' "$auth_headers"
printf '%s\n' "$auth_headers" | grep -F 'HTTP/' >/dev/null || fail 'auth endpoint returned no HTTP headers'
if grep -E '许可跳转列表|redirect.*not|not.*redirect' /tmp/aios-auth-primary-body >/dev/null 2>&1; then
fail 'auth body still contains redirect allow-list error text'
fi
if command -v ssh >/dev/null 2>&1; then
printf '\n== casdoor db redirect allow-list ==\n'
redirects="$(ssh "$AIHOST_HOST" "docker exec casdoor-postgres psql -U casdoor -d casdoor -Atc \"select redirect_uris from application where owner='admin' and name='aios-portal';\"" 2>/dev/null || true)"
if [ -n "$redirects" ]; then
printf '%s\n' "$redirects"
printf '%s\n' "$redirects" | grep -F "$CALLBACK" >/dev/null \
|| fail "Casdoor DB does not include $CALLBACK"
else
printf 'WARN: skipped Casdoor DB readback; set AIHOST_HOST to a reachable SSH target for a strict DB check.\n'
fi
printf '\n== ucc runtime env ==\n'
runtime_env="$(docker inspect user-control-web --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null | grep -E 'UCC_PUBLIC_BASE_URL|CASDOOR_(PUBLIC|INTERNAL|INVITE_SYNC)_BASE_URL' || true)"
if [ -z "$runtime_env" ]; then
runtime_env="$(ssh "$HW_HOST" 'docker inspect user-control-web --format "{{range .Config.Env}}{{println .}}{{end}}" | grep -E "UCC_PUBLIC_BASE_URL|CASDOOR_(PUBLIC|INTERNAL|INVITE_SYNC)_BASE_URL"' 2>/dev/null || true)"
fi
if [ -n "$runtime_env" ]; then
printf '%s\n' "$runtime_env"
printf '%s\n' "$runtime_env" | grep -F "CASDOOR_PUBLIC_BASE_URL=$AUTH_ENTRY" >/dev/null \
|| fail 'UCC runtime CASDOOR_PUBLIC_BASE_URL is not primary auth domain'
else
printf 'WARN: skipped UCC runtime env readback; run on hw.imruyi.top or set HW_HOST to a reachable SSH target.\n'
fi
fi
printf '\nPASS: cnnetmkt primary auth route is canonical and callback is allowed.\n'
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env python3
from __future__ import annotations
import json
import os
from pathlib import Path
from huaweicloudsdkcore.auth.credentials import BasicCredentials
from huaweicloudsdkdns.v2 import (
CreateRecordSetRequest,
CreateRecordSetRequestBody,
DnsClient,
ListRecordSetsByZoneRequest,
ListPublicZonesRequest,
UpdateRecordSetRequest,
UpdateRecordSetReq,
)
from huaweicloudsdkdns.v2.region.dns_region import DnsRegion
ENV_FILE = Path("/etc/letsencrypt/huaweicloud-cnnetmkt.env")
ZONE_NAME = "cnnetmkt.top."
RECORD_NAME = "cnnetmkt.top."
TARGET_IP = "1.95.164.183"
def load_env() -> None:
for line in ENV_FILE.read_text().splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
os.environ.setdefault(key, value)
def make_client() -> DnsClient:
ak = os.environ["HUAWEICLOUD_ACCESS_KEY_ID"]
sk = os.environ["HUAWEICLOUD_SECRET_ACCESS_KEY"]
region = os.environ.get("HUAWEICLOUD_REGION", "cn-southwest-2")
credentials = BasicCredentials(ak, sk)
return DnsClient.new_builder().with_credentials(credentials).with_region(DnsRegion.value_of(region)).build()
def as_dict(obj):
if hasattr(obj, "to_json_object"):
return obj.to_json_object()
if hasattr(obj, "to_dict"):
return obj.to_dict()
return obj
def main() -> int:
load_env()
client = make_client()
zones_resp = client.list_public_zones(ListPublicZonesRequest(name=ZONE_NAME))
zones = getattr(zones_resp, "zones", None) or []
if not zones:
raise RuntimeError(f"zone not found: {ZONE_NAME}")
zone = zones[0]
zone_id = zone.id
rs_req = ListRecordSetsByZoneRequest(zone_id=zone_id, name=RECORD_NAME, type="A")
rs_resp = client.list_record_sets_by_zone(rs_req)
recordsets = getattr(rs_resp, "recordsets", None) or []
existing = None
for item in recordsets:
if item.name == RECORD_NAME and item.type == "A":
existing = item
break
if existing:
records = list(existing.records or [])
if records == [TARGET_IP] and existing.status != "DISABLE":
action = "unchanged"
result = existing
else:
body = UpdateRecordSetReq(
name=RECORD_NAME,
type="A",
ttl=300,
records=[TARGET_IP],
description="AIOS GitOps apex entry",
)
req = UpdateRecordSetRequest(zone_id=zone_id, recordset_id=existing.id, body=body)
result = client.update_record_set(req)
action = "updated"
else:
body = CreateRecordSetRequestBody(
name=RECORD_NAME,
type="A",
ttl=300,
records=[TARGET_IP],
description="AIOS GitOps apex entry",
)
req = CreateRecordSetRequest(zone_id=zone_id, body=body)
result = client.create_record_set(req)
action = "created"
print(json.dumps({
"action": action,
"zone_id": zone_id,
"record": RECORD_NAME,
"type": "A",
"records": [TARGET_IP],
"result": as_dict(result),
}, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+113
View File
@@ -0,0 +1,113 @@
#!/usr/bin/env bash
set -euo pipefail
# Run from an operator machine that can SSH to both hw.imruyi.top and aihost.
# No secrets are embedded; the script uses existing server-side Docker/env state.
HW_HOST="${HW_HOST:-hw.imruyi.top}"
AIHOST_HOST="${AIHOST_HOST:-aihost}"
PRIMARY_AUTH="${PRIMARY_AUTH:-https://auth.cnnetmkt.top}"
PRIMARY_ENTRY="${PRIMARY_ENTRY:-https://cnnetmkt.top}"
UCC_DEPLOY_DIR="${UCC_DEPLOY_DIR:-/data/aios/aios_r13_v18_runtime_portable_silent_20260704/components/user-control-center/deploy}"
printf '== update Casdoor application on %s ==\n' "$AIHOST_HOST"
ssh "$AIHOST_HOST" bash -s <<'AIHOST'
set -euo pipefail
TS=$(date -u +%Y%m%dT%H%M%SZ)
BACKUP_DIR="/data/aios/backups/casdoor-cnnetmkt-primary-$TS"
sudo mkdir -p "$BACKUP_DIR"
sudo chown "$USER":"$USER" "$BACKUP_DIR"
docker exec casdoor-postgres pg_dump -U casdoor -d casdoor -t application > "$BACKUP_DIR/application-before.sql"
docker exec -i casdoor-postgres psql -U casdoor -d casdoor -v ON_ERROR_STOP=1 <<'SQL'
UPDATE application
SET
homepage_url = 'https://cnnetmkt.top/',
signin_url = 'https://cnnetmkt.top/portal/login/',
signup_url = 'https://cnnetmkt.top/portal/register/',
redirect_uris = '["https://cnnetmkt.top/api/auth/callback/casdoor", "https://cnnetmkt.top/casdoor/callback/", "https://portal.cnnetmkt.top/api/auth/callback/casdoor", "https://portal.cnnetmkt.top/casdoor/callback/", "https://user.cnnetmkt.top/api/auth/callback/casdoor", "https://user.cnnetmkt.top/casdoor/callback/", "https://portal.h.imruyi.top:8443/api/auth/callback/casdoor", "https://portal.h.imruyi.top:8443/casdoor/callback/"]'
WHERE owner = 'admin' AND name = 'aios-portal';
SQL
docker exec casdoor-postgres psql -U casdoor -d casdoor -P pager=off -c "select owner,name,homepage_url,signin_url,signup_url,redirect_uris from application where owner='admin' and name='aios-portal';" > "$BACKUP_DIR/application-after.txt"
cat "$BACKUP_DIR/application-after.txt"
printf 'casdoor_backup_dir=%s\n' "$BACKUP_DIR"
AIHOST
printf '\n== update HW ingress and UCC runtime on %s ==\n' "$HW_HOST"
ssh "$HW_HOST" bash -s <<HW
set -euo pipefail
TS=\$(date -u +%Y%m%dT%H%M%SZ)
BACKUP_DIR="/data/aios-gitops/backups/auth-cnnetmkt-primary-\$TS"
ENV_FILE="$UCC_DEPLOY_DIR/.env"
NGINX_AUTH="/etc/nginx/sites-available/auth-cnnetmkt-casdoor.conf"
sudo mkdir -p "\$BACKUP_DIR"
sudo cp -a "\$ENV_FILE" "\$BACKUP_DIR/user-control.env.before"
sudo cp -a /etc/nginx/sites-available "\$BACKUP_DIR/nginx-sites-available.before"
set_env() {
local key="\$1" value="\$2"
if sudo grep -q "^\${key}=" "\$ENV_FILE"; then
sudo sed -i "s#^\${key}=.*#\${key}=\${value}#" "\$ENV_FILE"
else
printf '%s=%s\n' "\$key" "\$value" | sudo tee -a "\$ENV_FILE" >/dev/null
fi
}
set_env ALLOWED_HOSTS 'cnnetmkt.top,portal.cnnetmkt.top,user.cnnetmkt.top,auth.cnnetmkt.top,127.0.0.1,localhost,hw.imruyi.top,1.95.164.183,portal.h.imruyi.top'
set_env CSRF_TRUSTED_ORIGINS 'https://cnnetmkt.top,https://portal.cnnetmkt.top,https://user.cnnetmkt.top,https://auth.cnnetmkt.top,http://portal.cnnetmkt.top,http://hw.imruyi.top,http://1.95.164.183,https://portal.h.imruyi.top:8443,http://portal.h.imruyi.top,http://user.cnnetmkt.top'
set_env UCC_PUBLIC_BASE_URL '$PRIMARY_ENTRY'
set_env CASDOOR_PUBLIC_BASE_URL '$PRIMARY_AUTH'
set_env CASDOOR_INTERNAL_BASE_URL '$PRIMARY_AUTH'
set_env CASDOOR_INVITE_SYNC_BASE_URL '$PRIMARY_AUTH'
sudo tee "\$NGINX_AUTH" >/dev/null <<'NGINX'
# AIOS primary public Casdoor entry for cnnetmkt.top.
# The upstream remains the existing backup/home Casdoor service; public clients should see auth.cnnetmkt.top only.
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name auth.cnnetmkt.top;
access_log /var/log/nginx/aios-auth-cnnetmkt.access.log;
error_log /var/log/nginx/aios-auth-cnnetmkt.error.log;
ssl_certificate /etc/letsencrypt/live/cnnetmkt-wildcard/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/cnnetmkt-wildcard/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
proxy_pass https://auth.h.imruyi.top:8443;
proxy_http_version 1.1;
proxy_ssl_server_name on;
proxy_ssl_name auth.h.imruyi.top;
proxy_ssl_verify off;
proxy_set_header Host auth.h.imruyi.top;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Host \$host;
proxy_set_header X-Forwarded-Port 443;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection \$connection_upgrade;
proxy_redirect https://auth.h.imruyi.top:8443/ https://auth.cnnetmkt.top/;
proxy_redirect https://auth.h.imruyi.top/ https://auth.cnnetmkt.top/;
}
}
server {
listen 80;
listen [::]:80;
server_name auth.cnnetmkt.top;
return 301 https://\$host\$request_uri;
}
NGINX
sudo ln -sf "\$NGINX_AUTH" /etc/nginx/sites-enabled/auth-cnnetmkt-casdoor.conf
sudo nginx -t
sudo systemctl reload nginx
cd "$UCC_DEPLOY_DIR"
sudo docker compose up -d --force-recreate user-control-web user-control-worker user-control-beat
printf 'hw_backup_dir=%s\n' "\$BACKUP_DIR"
HW
"$(dirname "$0")/check_cnnetmkt_auth_primary.sh"
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
set -euo pipefail
ENV_FILE="${UCC_ENV_FILE:-/data/aios/aios_r13_v18_runtime_portable_silent_20260704/components/user-control-center/deploy/.env}"
GITEA_BIN="${GITEA_BIN:-/data/aios-gitops/runtime/bin/gitea}"
APP_INI="${GITEA_APP_INI:-/data/aios-gitops/runtime/gitea-bin/custom/conf/app.ini}"
WORK_PATH="${GITEA_WORK_PATH:-/data/aios-gitops/runtime/gitea-bin}"
DB="${GITEA_DB:-/data/aios-gitops/runtime/gitea-bin/data/gitea.db}"
set -a
# shellcheck disable=SC1090
. "$ENV_FILE"
set +a
strip_trailing_cr() {
printf '%s' "${1:-}" | tr -d '\r'
}
OWNER_USERNAME="$(strip_trailing_cr "${UCC_OWNER_USERNAME:-owner}")"
OWNER_EMAIL="$(strip_trailing_cr "${UCC_OWNER_EMAIL:-owner@cnnetmkt.top}")"
OWNER_PASSWORD="$(strip_trailing_cr "${UCC_OWNER_PASSWORD:?UCC_OWNER_PASSWORD is required}")"
GITEA_RUN_USER="${GITEA_RUN_USER:-aiman}"
run_gitea() {
runuser -u "$GITEA_RUN_USER" -- "$GITEA_BIN" --config "$APP_INI" --work-path "$WORK_PATH" "$@"
}
if run_gitea admin user list 2>/dev/null | awk '{print $2}' | grep -Fxq "$OWNER_USERNAME"; then
run_gitea admin user change-password --username "$OWNER_USERNAME" --password "$OWNER_PASSWORD" --must-change-password=false >/dev/null
else
run_gitea admin user create --username "$OWNER_USERNAME" --password "$OWNER_PASSWORD" --email "$OWNER_EMAIL" --admin --must-change-password=false >/dev/null
fi
python3 - <<PY
import sqlite3, time
con=sqlite3.connect('$DB')
cur=con.cursor()
cur.execute("update user set is_admin=1,is_active=1,prohibit_login=0,must_change_password=0,updated_unix=? where lower_name=lower(?)", (int(time.time()), '$OWNER_USERNAME'))
cur.execute("update user set prohibit_login=1,is_admin=0,passwd='',passwd_hash_algo='disabled',must_change_password=1,updated_unix=? where lower_name in ('aios','admin','gitea') and lower_name<>lower(?)", (int(time.time()), '$OWNER_USERNAME'))
con.commit()
PY
printf 'GITEA_OWNER_SYNC_OK user=%s default_local_login_disabled=1\n' "$OWNER_USERNAME"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,9 @@
{
"type": "branch",
"branch": "patch/collab-custom-gpt-20260706T104130Z",
"base": "main",
"created_by": "chatgpt-gitops-bot",
"created_at": "2026-07-06T10:41:30Z",
"direct_main_push": false,
"owner_approval_required": true
}
@@ -0,0 +1,9 @@
{
"type": "branch",
"branch": "patch/collab-local-codex-20260706T104130Z",
"base": "main",
"created_by": "codex-gitops-bot",
"created_at": "2026-07-06T10:41:30Z",
"direct_main_push": false,
"owner_approval_required": true
}
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -euo pipefail
cd "${AIOS_GITOPS_WORKSPACE:-/data/aios-gitops/workspace}"
expected="${1:-V1R1C4}"
test "$(git branch --show-current)" = "main"
test "$(cat VERSION | tr -d '\n')" = "$expected"
git rev-parse --verify "$expected" >/dev/null
grep -q 'git_patch_only' config/codex/gitops-online.json
grep -q 'codex-gitops-bot' config/codex/gitops-online.json
grep -q 'online_git_sync' config/codex/gitops-online.json
python3 -m py_compile active-runtime/app/aios_chatgpt_actions_api.py
curl -fsS https://cnnetmkt.top/api/base >/tmp/aios-codex-gitops-base.json
curl -fsS https://cnnetmkt.top/api/openapi-chatgpt-actions.yaml >/tmp/aios-codex-gitops-openapi.yaml
if git grep -I -E '(sk-[A-Za-z0-9_-]{20,}|SDC[A-Za-z0-9]{20,}|HPUA[A-Z0-9]{10,})' -- . ':!docs/gitops/openapi-chatgpt-actions.yaml' >/tmp/aios-codex-secret-scan.txt; then
echo 'SECRET_PATTERN_IN_GIT'
cat /tmp/aios-codex-secret-scan.txt
exit 1
fi
echo "CODEX_GITOPS_ONLINE_OK $expected"
@@ -56,6 +56,7 @@ fi
echo "== Token initialization =="
TOKEN_FILE="$AIOS_GITOPS_ROOT/runtime/secrets/gitops.tokens"
BARE_REPO="$AIOS_GITOPS_ROOT/runtime/gitea-bin/data/repositories/aios/aios-v23.git"
if [ ! -s "$TOKEN_FILE" ]; then
(openssl rand -hex 32 || python3 -c 'import secrets; print(secrets.token_hex(32))') > "$TOKEN_FILE"
chmod 600 "$TOKEN_FILE"
@@ -73,9 +74,10 @@ Environment=AIOS_GITOPS_ROOT=$AIOS_GITOPS_ROOT
Environment=AIOS_GITOPS_WORKSPACE=$WORKSPACE
Environment=AIOS_GITOPS_DB=$AIOS_GITOPS_ROOT/runtime/gitops_model.sqlite3
Environment=AIOS_GITOPS_TOKEN_FILE=$TOKEN_FILE
Environment=AIOS_GITOPS_BARE_REPO=$BARE_REPO
Environment=AIOS_GITOPS_HOST=127.0.0.1
Environment=AIOS_GITOPS_PORT=18890
ExecStart=/usr/bin/python3 $AIOS_GITOPS_ROOT/adapters/gitops/aios_gitops_model_api.py
ExecStart=/usr/bin/python3 $WORKSPACE/active-runtime/app/aios_chatgpt_actions_api.py
Restart=always
RestartSec=3
+22 -4
View File
@@ -1,8 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
required=("VERSION" "docs/10_ARCHITECTURE_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_CN.md" "docs/11_USER_REQUIREMENTS_V1R1C0_CN.md" "docs/12_INTERFACE_API_SPEC_V1R1C0_CN.md" "docs/CODEX_FULL_EXECUTION_PROMPT_V1R1C0.md" "active-runtime/app/aios_gitops_model_api.py" "migrations/004_gitops_custom_llm_runtime.sql" "scripts/install_v1r1c0_gitops_model_overlay.sh" "config/model_registry.yaml" "config/runtime_selector.yaml")
required=(
"VERSION"
"docs/10_ARCHITECTURE_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_CN.md"
"docs/11_USER_REQUIREMENTS_V1R1C0_CN.md"
"docs/12_INTERFACE_API_SPEC_V1R1C0_CN.md"
"docs/CODEX_FULL_EXECUTION_PROMPT_V1R1C0.md"
"active-runtime/app/aios_gitops_model_api.py"
"active-runtime/app/aios_chatgpt_actions_api.py"
"docs/gitops/openapi-chatgpt-actions.yaml"
"migrations/004_gitops_custom_llm_runtime.sql"
"scripts/install_v1r1c0_gitops_model_overlay.sh"
"config/model_registry.yaml"
"config/runtime_selector.yaml"
)
for f in "${required[@]}"; do test -f "$ROOT/$f" || { echo "MISSING $f"; exit 1; }; done
grep -q "V1R1C0" "$ROOT/VERSION"
python3 -m py_compile "$ROOT/active-runtime/app/aios_gitops_model_api.py" "$ROOT/adapters/openclaw/openclaw_gitops_commands.py" "$ROOT/adapters/hermes/hermes_gitops_nl_router.py"
echo "PACKAGE_CONTRACT_OK V1R1C0"
grep -Eq "^V1R1C[0-9]+$" "$ROOT/VERSION"
python3 -m py_compile \
"$ROOT/active-runtime/app/aios_gitops_model_api.py" \
"$ROOT/active-runtime/app/aios_chatgpt_actions_api.py" \
"$ROOT/adapters/openclaw/openclaw_gitops_commands.py" \
"$ROOT/adapters/hermes/hermes_gitops_nl_router.py"
grep -q "/api/branch" "$ROOT/docs/gitops/openapi-chatgpt-actions.yaml"
echo "PACKAGE_CONTRACT_OK $(cat "$ROOT/VERSION")"
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse, json, os, ssl, time, urllib.request
from pathlib import Path
def post_json(api: str, path: str, token: str | None, payload: dict) -> tuple[int, dict]:
data = json.dumps(payload, ensure_ascii=False).encode('utf-8')
req = urllib.request.Request(api.rstrip('/') + path, data=data, method='POST')
req.add_header('Content-Type', 'application/json')
if token:
req.add_header('Authorization', f'Bearer {token}')
ctx = ssl._create_unverified_context()
try:
with urllib.request.urlopen(req, timeout=30, context=ctx) as r:
return r.status, json.loads(r.read().decode('utf-8'))
except urllib.error.HTTPError as e:
body = e.read().decode('utf-8', errors='ignore')
try:
obj = json.loads(body)
except Exception:
obj = {'raw': body[:300]}
return e.code, obj
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument('--api', default=os.environ.get('AIOS_GITOPS_API', 'https://cnnetmkt.top/api'))
ap.add_argument('--token-file', default=os.environ.get('AIOS_GITOPS_WRITE_TOKEN_FILE', '/data/aios-gitops/runtime/secrets/gitops-write-bot.token'))
ap.add_argument('--token-secret-ref', default=os.environ.get('AIOS_GITOPS_WRITE_TOKEN_REF', 'secret_ref://aios-gitops/runtime/gitops-write-bot'))
ap.add_argument('--branch-prefix', default='patch/gitops-write-auth-smoke')
args = ap.parse_args()
token = Path(args.token_file).read_text(encoding='utf-8').strip()
stamp = time.strftime('%Y%m%dT%H%M%SZ', time.gmtime())
branch = f'{args.branch_prefix}-{stamp}'
patch_name = f'gitops-write-auth-smoke-{stamp}'
noauth_code, noauth_body = post_json(args.api, '/patch', None, {'patch_name': patch_name + '-noauth'})
if noauth_code != 401:
raise SystemExit(f'expected noauth 401, got {noauth_code}: {noauth_body}')
patch_code, patch_body = post_json(args.api, '/patch', token, {
'patch_name': patch_name,
'branch': branch,
'base': 'main',
'risk': 'low',
'description': 'GitOps write auth smoke using secret_ref token',
'message': f'test(gitops): create write auth smoke patch {stamp}',
})
if patch_code != 200 or patch_body.get('status') != 'patch_branch_created':
raise SystemExit(f'createPatch failed: {patch_code}: {patch_body}')
file_code, file_body = post_json(args.api, '/file', token, {
'branch': branch,
'base': 'main',
'create_branch': False,
'path': f'docs/gitops/write-auth-smoke-{stamp}.md',
'content': f'# GitOps Write Auth Smoke\n\nTime: {stamp}\nResult: createPatch and commitFile authenticated via secret_ref token.\n',
'message': f'test(gitops): commit write auth smoke {stamp}',
})
if file_code != 200 or file_body.get('status') != 'committed':
raise SystemExit(f'commitFile failed: {file_code}: {file_body}')
def summarize(name: str, code: int, body: dict) -> dict:
commit = body.get('commit') or {}
return {
'operation': name,
'http': code,
'status': body.get('status'),
'branch': body.get('branch') or (body.get('patch') or {}).get('branch'),
'rev': commit.get('rev') if isinstance(commit, dict) else None,
'sync_status': ((commit.get('sync') or {}).get('status') if isinstance(commit, dict) else None),
}
print(json.dumps({
'noauth': {'operation': 'createPatch without token', 'http': noauth_code, 'status': noauth_body.get('error')},
'createPatch': summarize('createPatch', patch_code, patch_body),
'commitFile': summarize('commitFile', file_code, file_body),
'token_secret_ref': args.token_secret_ref,
}, ensure_ascii=False, indent=2))
return 0
if __name__ == '__main__':
raise SystemExit(main())
+23
View File
@@ -0,0 +1,23 @@
{
"version": "V1R1C1",
"tag": "V1R1C1",
"type": "patch-release",
"name": "Codex GitOps Online Version Management",
"previous_version": "V1R1C0",
"source_version": "V1R1C0-baseline-20260706",
"update_mode": "git_patch_only",
"zip_update_mode": "deprecated",
"git_url": "https://cnnetmkt.top/git/aios/aios-v23.git",
"api_base": "https://cnnetmkt.top/api",
"openapi_schema": "https://cnnetmkt.top/api/openapi-chatgpt-actions.yaml",
"codex_config": "config/codex/gitops-online.json",
"included_commits": [
"96c00d8",
"0b0505a",
"43ddb91"
],
"excluded_branches": [
"patch/chatgpt-model-acceptance-*"
],
"updated_at": "2026-07-06T10:21:08Z"
}
+19
View File
@@ -0,0 +1,19 @@
{
"version": "V1R1C2",
"tag": "V1R1C2",
"type": "patch-release",
"name": "Codex GitOps Online Branch Sync",
"previous_version": "V1R1C1",
"source_version": "V1R1C1",
"update_mode": "git_patch_only",
"zip_update_mode": "deprecated",
"git_url": "https://cnnetmkt.top/git/aios/aios-v23.git",
"api_base": "https://cnnetmkt.top/api",
"openapi_schema": "https://cnnetmkt.top/api/openapi-chatgpt-actions.yaml",
"codex_config": "config/codex/gitops-online.json",
"online_git_sync": true,
"included_commits": [
"ecf6735"
],
"updated_at": "2026-07-06T10:26:26Z"
}
+47
View File
@@ -0,0 +1,47 @@
{
"version": "V1R1C3",
"tag": "V1R1C3",
"type": "collaboration-release",
"name": "Local Codex and Custom GPT GitOps Collaboration",
"previous_version": "V1R1C2",
"update_mode": "git_patch_only",
"zip_update_mode": "deprecated",
"git_url": "https://cnnetmkt.top/git/aios/aios-v23.git",
"api_base": "https://cnnetmkt.top/api",
"custom_gpt_url": "https://chatgpt.com/g/g-6a4b7c82dff08191bc980798edea1bac-aios-gitops-kong-zhi-tai",
"collaboration_timestamp": "20260706T104130Z",
"actors": [
{
"name": "local_codex",
"account": "codex-gitops-bot",
"branch": "patch/collab-local-codex-20260706T104130Z",
"path": "docs/gitops/collab-local-codex-20260706T104130Z.md",
"branch_code": 200,
"branch_commit": "ed92b88",
"branch_sync": "synced",
"file_code": 200,
"file_commit": "a62d4ce",
"file_sync": "synced",
"online_ref_present": true,
"online_ref": "a62d4ce2c1722afde0775628e6e618251b7ed712 refs/heads/patch/collab-local-codex-20260706T104130Z",
"response_status": "committed"
},
{
"name": "custom_gpt",
"account": "chatgpt-gitops-bot",
"branch": "patch/collab-custom-gpt-20260706T104130Z",
"path": "docs/gitops/collab-custom-gpt-20260706T104130Z.md",
"branch_code": 200,
"branch_commit": "5a21bd5",
"branch_sync": "synced",
"file_code": 200,
"file_commit": "c393931",
"file_sync": "synced",
"online_ref_present": true,
"online_ref": "c393931f64b3664d685a809c1aa4f496709a05f3 refs/heads/patch/collab-custom-gpt-20260706T104130Z",
"response_status": "committed"
}
],
"online_git_sync": true,
"updated_at": "2026-07-06T10:42:33Z"
}
+34
View File
@@ -0,0 +1,34 @@
{
"version": "V1R1C4",
"tag": "V1R1C4",
"type": "auth-portal-hardening-release",
"name": "GitOps portal auth hardening and Gitea owner sync",
"previous_version": "V1R1C3",
"update_mode": "git_patch_only",
"zip_update_mode": "deprecated",
"entry": "https://cnnetmkt.top/",
"git_web": "https://g.cnnetmkt.top/git/aios/aios-v23",
"api_base": "https://cnnetmkt.top/api",
"changes": [
"Human shortlinks /base /ver /diff /patch /model /run now redirect to the login-protected portal GitOps section.",
"Machine API remains under /api for ChatGPT Actions and Codex automation.",
"Gitea requires sign-in for repository browsing and registration UI is hidden.",
"Gitea owner password sync service/timer copies the user-control owner password without logging secrets.",
"Default local Gitea account password is cleared and login is prohibited.",
"User portal includes a GitOps dashboard section for admin/owner users.",
"Patched user-control-center files are preserved under overlays/user-control-center for replayable releases.",
"Portal GitOps dashboard reads /app/runtime/gitops/current.json before HTTP fallback so the version is visible inside Docker.",
"Gitea owner sync uses runuser instead of sudo command logging and strips CRLF from sourced owner fields."
],
"artifacts": [
"deploy/nginx/aios-gitops-locations.conf",
"deploy/gitea-binary/app.ini.template",
"deploy/systemd/aios-gitea-owner-sync.service",
"deploy/systemd/aios-gitea-owner-sync.timer",
"ops/gitops-migration/sync_gitea_owner_password.sh",
"ops/gitops-migration/apply_user_portal_gitops_overlay.sh",
"overlays/user-control-center/control/portal_views.py",
"overlays/user-control-center/templates/portal/me.html"
],
"updated_at": "2026-07-06T11:16:23Z"
}