Compare commits
25
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17344f7894 | ||
|
|
6d3a0509de | ||
|
|
a9ef9197e6 | ||
|
|
54870ed375 | ||
|
|
c850858973 | ||
|
|
d08a7043d6 | ||
|
|
c393931f64 | ||
|
|
a62d4ce2c1 | ||
|
|
5a21bd5d7e | ||
|
|
ed92b88fc0 | ||
|
|
a248142158 | ||
|
|
ecf673500b | ||
|
|
43ddb91568 | ||
|
|
0b0505a645 | ||
|
|
96c00d81a9 | ||
|
|
e7d36ad706 | ||
|
|
d6cb9d1529 | ||
|
|
c06a23f4f8 | ||
|
|
568c283aff | ||
|
|
87f9150806 | ||
|
|
14fd6ee5c5 | ||
|
|
b9328d4161 | ||
|
|
d8ae7ee6f6 | ||
|
|
2ae60299b7 | ||
|
|
c6b172dce7 |
@@ -0,0 +1,278 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
import hashlib, hmac, json, os, re, subprocess, time, urllib.parse, uuid
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
ROOT = Path(os.getenv('AIOS_GITOPS_ROOT', '/data/aios-gitops'))
|
||||
WS = Path(os.getenv('AIOS_GITOPS_WORKSPACE', str(ROOT / 'workspace')))
|
||||
SECRETS = Path(os.getenv('AIOS_GITOPS_SECRETS_DIR', str(ROOT / 'runtime' / 'secrets')))
|
||||
TOKEN_FILE = Path(os.getenv('AIOS_GITOPS_TOKEN_FILE', str(SECRETS / 'gitops.tokens')))
|
||||
HOST = os.getenv('AIOS_GITOPS_HOST', '127.0.0.1')
|
||||
PORT = int(os.getenv('AIOS_GITOPS_PORT', '18890'))
|
||||
OPENAPI = WS / 'docs' / 'gitops' / 'openapi-chatgpt-actions.yaml'
|
||||
BARE_REPO = Path(os.getenv('AIOS_GITOPS_BARE_REPO', str(ROOT / 'runtime' / 'gitea-bin' / 'data' / 'repositories' / 'aios' / 'aios-v23.git')))
|
||||
BASE_MODELS = [
|
||||
{'id':'base.deepseekv4-flash','provider':'one-api','model':'deepseekv4-flash','display_name':'DeepSeek V4 Flash','scope':'global'},
|
||||
{'id':'base.qwen3.6-35b','provider':'one-api','model':'qwen3.6-35b','display_name':'Qwen 3.6 35B','scope':'global'},
|
||||
{'id':'base.gpt-5.5','provider':'openai','model':'gpt-5.5','display_name':'GPT-5.5','scope':'global'},
|
||||
]
|
||||
SECRET_RE = re.compile(r'(?i)(api[_-]?key|secret[_-]?access[_-]?key|secret|password|token)\s*[:=]\s*["\']?([A-Za-z0-9_./+=:-]{16,})')
|
||||
SECRET_VAL_RE = re.compile(r'(sk-[A-Za-z0-9_-]{20,}|HPUA[A-Z0-9]{10,}|SDC[A-Za-z0-9]{20,})')
|
||||
|
||||
def now() -> str: return time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
|
||||
def uid(p='id') -> str: return f'{p}-{int(time.time()*1000)}-{uuid.uuid4().hex[:8]}'
|
||||
def safe(s: str, n=160) -> str: return re.sub(r'[^A-Za-z0-9_./@-]+', '-', str(s or 'unknown')).strip('/-')[:n] or 'unknown'
|
||||
def sh(args, check=False):
|
||||
p = subprocess.run(['git'] + args, cwd=str(WS), text=True, capture_output=True)
|
||||
if check and p.returncode != 0: raise RuntimeError(p.stderr or p.stdout or 'git failed')
|
||||
return {'code':p.returncode,'stdout':p.stdout.strip(),'stderr':p.stderr.strip(),'cmd':['git']+args}
|
||||
def jload(p: Path, default):
|
||||
try: return json.loads(p.read_text(encoding='utf-8'))
|
||||
except Exception: return default
|
||||
def relpath(path: str) -> str:
|
||||
x = str(path or '').replace('\\','/').strip()
|
||||
if not x or x.startswith('/') or '..' in Path(x).parts or Path(x).parts[0] == '.git': raise ValueError('invalid path')
|
||||
return str(Path(x))
|
||||
def branch_name(name: str | None, prefix='patch/chatgpt') -> str:
|
||||
b = safe(name or f'{prefix}-{uid("br")}', 180)
|
||||
if b in ('main','master') or b.startswith('main/') or b.startswith('master/'): raise ValueError('direct main/master writes are forbidden')
|
||||
return b
|
||||
def _json_has_plain_secret(obj: Any) -> bool:
|
||||
if isinstance(obj, dict):
|
||||
for k, v in obj.items():
|
||||
lk = str(k).lower()
|
||||
if lk == 'secret_ref' or lk.endswith('_secret_ref'):
|
||||
continue
|
||||
if lk in {'api_key', 'access_key', 'secret_access_key', 'secret_key', 'password', 'token', 'bearer_token'}:
|
||||
if isinstance(v, str) and v and not v.startswith('secret://') and v != 'REDACTED':
|
||||
return True
|
||||
if _json_has_plain_secret(v):
|
||||
return True
|
||||
return False
|
||||
if isinstance(obj, list):
|
||||
return any(_json_has_plain_secret(x) for x in obj)
|
||||
if isinstance(obj, str):
|
||||
return bool(obj and not obj.startswith('secret://') and obj != 'REDACTED' and SECRET_VAL_RE.search(obj))
|
||||
return False
|
||||
|
||||
def content_has_secret(text: str) -> bool:
|
||||
text = text or ''
|
||||
try:
|
||||
return _json_has_plain_secret(json.loads(text))
|
||||
except Exception:
|
||||
pass
|
||||
if SECRET_VAL_RE.search(text):
|
||||
return True
|
||||
for m in SECRET_RE.finditer(text):
|
||||
key = str(m.group(1)).lower()
|
||||
value = str(m.group(2))
|
||||
if 'secret_ref' in key or value.startswith('secret://') or value == 'REDACTED':
|
||||
continue
|
||||
return True
|
||||
return False
|
||||
def token_records():
|
||||
if not TOKEN_FILE.exists(): return []
|
||||
out=[]
|
||||
for raw in TOKEN_FILE.read_text(encoding='utf-8').splitlines():
|
||||
line=raw.strip()
|
||||
if not line or line.startswith('#'): continue
|
||||
if line.startswith('{'):
|
||||
try: out.append(json.loads(line))
|
||||
except Exception: pass
|
||||
else:
|
||||
out.append({'account':'legacy-token','role':'owner','scopes':['*'],'token_plain':line})
|
||||
return out
|
||||
def authenticate(token: str):
|
||||
if os.getenv('AIOS_GITOPS_DEV_ALLOW','0') == '1': return {'account':'dev-owner','role':'owner','scopes':['*']}
|
||||
if not token: return None
|
||||
digest=hashlib.sha256(token.encode()).hexdigest()
|
||||
for r in token_records():
|
||||
if r.get('disabled'): continue
|
||||
if r.get('token_sha256') and hmac.compare_digest(str(r['token_sha256']), digest): return r
|
||||
if r.get('token_plain') and hmac.compare_digest(str(r['token_plain']), token): return r
|
||||
return None
|
||||
def sanitize(obj: Any) -> Any:
|
||||
if isinstance(obj, dict):
|
||||
return {k: ('REDACTED' if re.search(r'(?i)(api[_-]?key|secret|password|token)', str(k)) else sanitize(v)) for k,v in obj.items()}
|
||||
if isinstance(obj, list): return [sanitize(x) for x in obj]
|
||||
if isinstance(obj, str): return SECRET_VAL_RE.sub('REDACTED', obj)
|
||||
return obj
|
||||
def ensure_git():
|
||||
if not sh(['config','user.email'])['stdout']: sh(['config','user.email','chatgpt-gitops-bot@cnnetmkt.top'], True)
|
||||
if not sh(['config','user.name'])['stdout']: sh(['config','user.name','chatgpt-gitops-bot'], True)
|
||||
def checkout(branch, base='main', create=False):
|
||||
ensure_git(); b=branch_name(branch)
|
||||
if create or sh(['rev-parse','--verify',b])['code'] != 0:
|
||||
sh(['checkout',base], True); sh(['checkout','-B',b], True)
|
||||
else: sh(['checkout',b], True)
|
||||
return b
|
||||
def write_file(path, content):
|
||||
if content_has_secret(content): raise ValueError('secret_rejected: use secret_ref; plaintext API keys must not enter Git')
|
||||
r=relpath(path); t=WS/r; t.parent.mkdir(parents=True, exist_ok=True); t.write_text(content, encoding='utf-8'); return r
|
||||
def write_json(path, data): return write_file(path, json.dumps(data, ensure_ascii=False, indent=2) + '\n')
|
||||
def sync_branch(branch: str) -> dict:
|
||||
if not branch or branch in ('main','master') or branch.startswith('main/') or branch.startswith('master/'):
|
||||
return {'status':'skipped','reason':'protected_branch','branch':branch}
|
||||
if BARE_REPO.exists():
|
||||
ref=f'refs/heads/{branch}:refs/heads/{branch}'
|
||||
p=subprocess.run(['git','--git-dir',str(BARE_REPO),'fetch',str(WS),ref], text=True, capture_output=True)
|
||||
if p.returncode != 0: raise RuntimeError(p.stderr or p.stdout or f'git sync failed for {branch}')
|
||||
return {'status':'synced','target':'bare','branch':branch,'stdout':p.stdout.strip(),'stderr':p.stderr.strip()}
|
||||
p=subprocess.run(['git','push','origin',f'HEAD:refs/heads/{branch}'], cwd=str(WS), text=True, capture_output=True)
|
||||
if p.returncode != 0: raise RuntimeError(p.stderr or p.stdout or f'git push failed for {branch}')
|
||||
return {'status':'synced','target':'origin','branch':branch,'stdout':p.stdout.strip(),'stderr':p.stderr.strip()}
|
||||
|
||||
def sync_tag(tag: str) -> dict:
|
||||
if not tag: return {'status':'skipped','reason':'empty_tag'}
|
||||
if BARE_REPO.exists():
|
||||
ref=f'refs/tags/{tag}:refs/tags/{tag}'
|
||||
p=subprocess.run(['git','--git-dir',str(BARE_REPO),'fetch',str(WS),ref], text=True, capture_output=True)
|
||||
if p.returncode != 0: raise RuntimeError(p.stderr or p.stdout or f'git tag sync failed for {tag}')
|
||||
return {'status':'synced','target':'bare','tag':tag,'stdout':p.stdout.strip(),'stderr':p.stderr.strip()}
|
||||
p=subprocess.run(['git','push','origin',tag], cwd=str(WS), text=True, capture_output=True)
|
||||
if p.returncode != 0: raise RuntimeError(p.stderr or p.stdout or f'git tag push failed for {tag}')
|
||||
return {'status':'synced','target':'origin','tag':tag,'stdout':p.stdout.strip(),'stderr':p.stderr.strip()}
|
||||
|
||||
def commit(paths, msg):
|
||||
ensure_git(); paths = [relpath(p) for p in paths] if paths else ['.']; sh(['add'] + paths, True)
|
||||
branch=sh(['rev-parse','--abbrev-ref','HEAD'])['stdout']
|
||||
if not sh(['status','--short'])['stdout']:
|
||||
return {'status':'no_changes','branch':branch,'sync':sync_branch(branch)}
|
||||
out=sh(['commit','-m',msg], True); branch=sh(['rev-parse','--abbrev-ref','HEAD'])['stdout']; sync=sync_branch(branch); return {'status':'committed','branch':branch,'rev':sh(['rev-parse','--short','HEAD'], True)['stdout'],'git':out,'sync':sync}
|
||||
def audit(actor, action, target, payload):
|
||||
p=ROOT/'artifacts'/'chatgpt-gitops-audit.jsonl'; p.parent.mkdir(parents=True, exist_ok=True)
|
||||
with p.open('a', encoding='utf-8') as f: f.write(json.dumps({'at':now(),'actor':actor,'action':action,'target':target,'payload':sanitize(payload)}, ensure_ascii=False)+'\n')
|
||||
def model_docs():
|
||||
out=[]
|
||||
for p in sorted((WS/'config'/'model_registry').glob('**/*.json')): out.append(jload(p, {'path':str(p.relative_to(WS))}))
|
||||
return out
|
||||
def patches():
|
||||
out=[]
|
||||
for p in sorted((WS/'patches').glob('**/*.json')): out.append(jload(p, {'path':str(p.relative_to(WS))}))
|
||||
return out
|
||||
|
||||
class H(BaseHTTPRequestHandler):
|
||||
server_version='AIOSChatGPTGitOps/V1R1C0'
|
||||
def sendj(self, data, code=200):
|
||||
b=json.dumps(data, ensure_ascii=False, indent=2).encode(); self.send_response(code); self.send_header('Content-Type','application/json; charset=utf-8'); self.send_header('Content-Length',str(len(b))); self.end_headers(); self.wfile.write(b)
|
||||
def sendt(self, text, ctype='text/plain; charset=utf-8', code=200):
|
||||
b=text.encode(); self.send_response(code); self.send_header('Content-Type',ctype); self.send_header('Content-Length',str(len(b))); self.end_headers(); self.wfile.write(b)
|
||||
def body(self):
|
||||
n=int(self.headers.get('Content-Length','0') or '0'); raw=self.rfile.read(n).decode() if n else '{}'
|
||||
try: return json.loads(raw or '{}')
|
||||
except Exception: return {'_raw':raw}
|
||||
def actor(self):
|
||||
auth=self.headers.get('Authorization',''); tok=auth[7:].strip() if auth.startswith('Bearer ') else self.headers.get('X-AIOS-Token','')
|
||||
rec=authenticate(tok)
|
||||
if rec: return {'ok':True,'account':safe(rec.get('account') or rec.get('name') or 'token-user'),'role':rec.get('role','user'),'scopes':set(rec.get('scopes') or [])}
|
||||
return {'ok':False,'account':'anonymous','role':'anonymous','scopes':set()}
|
||||
def need(self, scope):
|
||||
a=self.actor()
|
||||
if not a['ok']: self.sendj({'error':'unauthorized','hint':'Bearer token required'},401); return None
|
||||
if '*' not in a['scopes'] and scope not in a['scopes']: self.sendj({'error':'forbidden','required_scope':scope,'account':a['account']},403); return None
|
||||
return a
|
||||
def do_GET(self):
|
||||
u=urllib.parse.urlparse(self.path); path=u.path.rstrip('/') if u.path != '/' else u.path; q=urllib.parse.parse_qs(u.query)
|
||||
try:
|
||||
if path in ('/health','/api/health'): return self.sendj({'status':'ok','service':'chatgpt-gitops-adapter','workspace':str(WS)})
|
||||
if path in ('/api/openapi.yaml','/api/openapi-chatgpt-actions.yaml','/api/schema'):
|
||||
return self.sendt(OPENAPI.read_text(encoding='utf-8'), 'application/yaml; charset=utf-8') if OPENAPI.exists() else self.sendj({'error':'schema_missing'},404)
|
||||
if path in ('/api','/api/status'): return self.sendj(api_spec())
|
||||
if path in ('/api/base','/base'):
|
||||
data=jload(WS/'baseline'/'current.json', {}); data.setdefault('git', sh(['rev-parse','--short','HEAD'])['stdout']); return self.sendj(data)
|
||||
if path in ('/api/ver','/ver'):
|
||||
return self.sendj({'tags':sh(['tag','--list','V*'])['stdout'].splitlines(),'versions_dir':[p.name for p in (WS/'versions').glob('*')],'head':sh(['rev-parse','--short','HEAD'])['stdout']})
|
||||
if path in ('/api/diff','/diff'):
|
||||
frm=(q.get('from') or ['HEAD~1'])[0]; to=(q.get('to') or ['HEAD'])[0]; return self.sendj({'from':frm,'to':to,'diff':sh(['diff',frm,to])})
|
||||
if path in ('/api/patch','/patch'): return self.sendj({'patches':patches(),'branches':sh(['branch','--format','%(refname:short)'])['stdout'].splitlines()})
|
||||
if path in ('/api/model','/model'): return self.sendj({'base':BASE_MODELS,'custom':model_docs(),'priority':[jload(p,{}) for p in sorted((WS/'config'/'model_priority').glob('*.json'))]})
|
||||
if path in ('/api/run','/run'):
|
||||
svc=subprocess.run(['systemctl','is-active','aios-gitops-model-api'], text=True, capture_output=True)
|
||||
return self.sendj({'service':'aios-gitops-model-api','status':svc.stdout.strip() or svc.stderr.strip(),'workspace_branch':sh(['rev-parse','--abbrev-ref','HEAD'])['stdout']})
|
||||
return self.sendj({'error':'not_found','path':path},404)
|
||||
except Exception as e: return self.sendj({'error':str(e)},500)
|
||||
def do_PUT(self):
|
||||
path=urllib.parse.urlparse(self.path).path.rstrip('/'); data=self.body()
|
||||
if path == '/api/model-priority': return self.model_priority(data)
|
||||
return self.sendj({'error':'not_found','path':path},404)
|
||||
def do_POST(self):
|
||||
path=urllib.parse.urlparse(self.path).path.rstrip('/'); data=self.body()
|
||||
try:
|
||||
if path == '/api/branch': return self.branch(data)
|
||||
if path == '/api/file': return self.file(data)
|
||||
if path == '/api/commit': return self.commit(data)
|
||||
if path == '/api/patch': return self.patch(data)
|
||||
if path == '/api/tag': return self.tag(data)
|
||||
if path == '/api/rollback': return self.rollback(data)
|
||||
if path == '/api/model': return self.model(data)
|
||||
if path == '/api/model-priority': return self.model_priority(data)
|
||||
if path == '/api/nl': return self.nl(data)
|
||||
return self.sendj({'error':'not_found','path':path},404)
|
||||
except Exception as e: return self.sendj({'error':str(e)},500)
|
||||
def branch(self,data):
|
||||
a=self.need('branch:create');
|
||||
if not a: return
|
||||
b=checkout(data.get('branch'), data.get('base','main'), True); meta={'type':'branch','branch':b,'base':data.get('base','main'),'created_by':a['account'],'created_at':now(),'direct_main_push':False,'owner_approval_required':True}
|
||||
r=write_json(f'patches/{safe(b).replace("/","_")}.branch.json', meta); c=commit([r], data.get('message') or f'chore(chatgpt): create {b}'); audit(a['account'],'branch',b,meta); self.sendj({'status':'ok','branch':b,'commit':c})
|
||||
def file(self,data):
|
||||
a=self.need('file:write');
|
||||
if not a: return
|
||||
b=checkout(data.get('branch'), data.get('base','main'), bool(data.get('create_branch'))); r=write_file(data.get('path'), data.get('content','')); c=commit([r], data.get('message') or f'chore(chatgpt): update {r}'); audit(a['account'],'file',r,{'branch':b,'commit':c}); self.sendj({'status':'committed','branch':b,'path':r,'commit':c})
|
||||
def commit(self,data):
|
||||
a=self.need('commit:create');
|
||||
if not a: return
|
||||
b=checkout(data.get('branch') or sh(['rev-parse','--abbrev-ref','HEAD'])['stdout'], data.get('base','main'), False); paths=data.get('paths') or ['.']; paths=[paths] if isinstance(paths,str) else paths; c=commit(paths, data.get('message') or 'chore(chatgpt): commit changes'); audit(a['account'],'commit',b,{'paths':paths,'commit':c}); self.sendj({'status':c['status'],'branch':b,'commit':c})
|
||||
def patch(self,data):
|
||||
a=self.need('patch:create');
|
||||
if not a: return
|
||||
name=data.get('patch_name') or data.get('name') or uid('patch'); b=checkout(data.get('branch') or f'patch/{safe(name)}', data.get('base','main'), True); meta={'id':uid('patch'),'patch_name':name,'branch':b,'risk':data.get('risk','medium'),'description':data.get('description',''),'created_by':a['account'],'created_at':now()}
|
||||
r=write_json(f'patches/{safe(name)}.json', meta); c=commit([r], data.get('message') or f'chore(chatgpt): create patch {name}'); audit(a['account'],'patch',name,meta); self.sendj({'status':'patch_branch_created','patch':meta,'commit':c})
|
||||
def tag(self,data):
|
||||
a=self.need('tag:create');
|
||||
if not a: return
|
||||
ver=data.get('version') or data.get('tag')
|
||||
if not re.match(r'^V\d+R\d+C\d+(-[A-Za-z0-9_.-]+)?$', ver or ''): return self.sendj({'error':'version must match VxRxCx'},400)
|
||||
if a['role'] != 'owner' or not data.get('execute'):
|
||||
b=checkout(data.get('branch') or f'request/tag-{safe(ver)}', 'main', True); req={'version':ver,'requested_by':a['account'],'approval_required':True,'created_at':now()}; r=write_json(f'baseline/tag_requests/{safe(ver)}.json', req); c=commit([r], f'chore(chatgpt): request tag {ver}'); return self.sendj({'status':'approval_required','branch':b,'commit':c})
|
||||
out=sh(['tag','-a',ver,'-m',data.get('message') or f'release {ver}'], True); self.sendj({'status':'tagged','version':ver,'git':out,'sync':sync_tag(ver)})
|
||||
def rollback(self,data):
|
||||
a=self.need('rollback:request');
|
||||
if not a: return
|
||||
target=data.get('target_version') or data.get('version') or data.get('target')
|
||||
if not target: return self.sendj({'error':'target_version required'},400)
|
||||
b=checkout(data.get('branch') or f'request/rollback-{safe(target)}-{uid("rb")}', 'main', True); req={'id':uid('rollback'),'target_version':target,'reason':data.get('reason',''),'requested_by':a['account'],'approval_required':True,'execute':False,'created_at':now()}; r=write_json(f'baseline/rollback_requests/{safe(req["id"])}.json', req); c=commit([r], f'chore(chatgpt): request rollback {target}'); self.sendj({'status':'rollback_request_committed','branch':b,'request':req,'commit':c})
|
||||
def model(self,data):
|
||||
a=self.need('model:write');
|
||||
if not a: return
|
||||
name=data.get('model_name') or data.get('model'); base=(data.get('base_url') or '').rstrip('/'); prov=data.get('provider_name') or 'custom-provider'
|
||||
if not name or not base: return self.sendj({'error':'provider_name/base_url/model_name required'},400)
|
||||
owner=safe(data.get('username') or a['account']); secret_ref=data.get('api_key_secret_ref') or data.get('secret_ref')
|
||||
if data.get('api_key'):
|
||||
d=SECRETS/'llm'/owner; d.mkdir(parents=True, exist_ok=True); sid=uid('llmkey'); p=d/f'{sid}.secret'; p.write_text(str(data['api_key']), encoding='utf-8'); os.chmod(p,0o600); secret_ref=f'secret://llm/{owner}/{sid}'
|
||||
if not secret_ref: return self.sendj({'error':'api_key_secret_ref or api_key required'},400)
|
||||
mid=data.get('model_id') or f'custom.{owner}.{hashlib.sha1((prov+base+name).encode()).hexdigest()[:10]}'; b=checkout(data.get('branch') or f'patch/model-{safe(mid)}','main',True)
|
||||
doc=sanitize({k:v for k,v in data.items() if k!='api_key'}); doc.update({'model_id':mid,'provider_name':prov,'base_url':base,'model_name':name,'api_key_secret_ref':secret_ref,'created_by':a['account'],'created_at':now()})
|
||||
r=write_json(f'config/model_registry/chatgpt/{safe(mid)}.json', doc); c=commit([r], data.get('message') or f'chore(chatgpt): register model {name}'); audit(a['account'],'model',mid,doc); self.sendj({'status':'model_registered','branch':b,'model_id':mid,'secret_ref':secret_ref,'commit':c})
|
||||
def model_priority(self,data):
|
||||
a=self.need('model:priority');
|
||||
if not a: return
|
||||
user=safe(data.get('username') or a['account']); b=checkout(data.get('branch') or f'patch/model-priority-{user}', 'main', True); doc={'username':user,'items':data.get('items') or data.get('priority') or [],'updated_by':a['account'],'updated_at':now()}; r=write_json(f'config/model_priority/{user}.json', doc); c=commit([r], data.get('message') or f'chore(chatgpt): update model priority {user}'); audit(a['account'],'model-priority',user,doc); self.sendj({'status':'updated','branch':b,'priority':doc,'commit':c})
|
||||
def nl(self,data):
|
||||
a=self.need('nl:plan');
|
||||
if not a: return
|
||||
text=data.get('text',''); low=text.lower(); intent='unknown'; plan=[]
|
||||
if 'rollback' in low or '回滚' in text: intent='rollback_request'; plan=['commit rollback request','wait owner/admin approval']
|
||||
elif 'diff' in low or '对比' in text: intent='diff'; plan=['call GET /api/diff']
|
||||
elif 'model' in low or '模型' in text: intent='model_change'; plan=['use POST /api/model or PUT /api/model-priority','commit metadata']
|
||||
elif 'patch' in low or '补丁' in text: intent='patch_create'; plan=['call POST /api/patch']
|
||||
b=checkout(data.get('branch') or f'patch/nl-{uid("nl")}', 'main', True); doc={'id':uid('nl'),'text':text,'intent':intent,'action_plan':plan,'execute':False,'owner_approval_required':True,'created_by':a['account'],'created_at':now()}; r=write_json(f'patches/nl/{safe(doc["id"])}.json', doc); c=commit([r], f'chore(chatgpt): record nl plan {intent}'); self.sendj({'status':'planned','branch':b,'request':doc,'commit':c})
|
||||
|
||||
def api_spec():
|
||||
return {'version':'V1R1C0-chatgpt-actions','openapi':'/api/openapi.yaml','read':['GET /api/base','GET /api/ver','GET /api/diff','GET /api/patch','GET /api/model','GET /api/run'],'write':['POST /api/branch','POST /api/file','POST /api/commit','POST /api/patch','POST /api/tag','POST /api/rollback','POST /api/model','PUT /api/model-priority','POST /api/nl'],'policy':{'bearer_required_for_write':True,'direct_main_push':False,'delete_tag':False,'owner_approval_required_for_v_level':True,'api_keys_in_git':False,'git_secret_field':'secret_ref','online_git_sync':True}}
|
||||
|
||||
def main():
|
||||
print(f'AIOS ChatGPT GitOps adapter listening on {HOST}:{PORT}')
|
||||
ThreadingHTTPServer((HOST,PORT), H).serve_forever()
|
||||
if __name__ == '__main__': main()
|
||||
+68
-8
@@ -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,63 @@
|
||||
"/api",
|
||||
"/hook"
|
||||
],
|
||||
"updated_at": "2026-07-06T07:13:43.274132+00:00"
|
||||
"updated_at": "2026-07-06T11:16:23Z",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,78 @@
|
||||
{
|
||||
"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-06T11:16:23Z",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 = *
|
||||
|
||||
@@ -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/;
|
||||
|
||||
@@ -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,12 @@
|
||||
# cnnetmkt wildcard certificate
|
||||
|
||||
generated_at=2026-07-06T16:24:28+08:00
|
||||
issuer=Let's Encrypt via lego DNS-01 Huawei Cloud
|
||||
domains=cnnetmkt.top,*.cnnetmkt.top
|
||||
live_dir=/etc/letsencrypt/live/cnnetmkt-wildcard
|
||||
nginx_sites=/etc/nginx/sites-available/aios-user-portal.conf,/etc/nginx/sites-available/g-cnnetmkt-gitea.conf
|
||||
renew_service=cnnetmkt-wildcard-renew.service
|
||||
renew_timer=cnnetmkt-wildcard-renew.timer
|
||||
secret_env=/etc/letsencrypt/huaweicloud-cnnetmkt.env
|
||||
backup_dir=/data/aios-gitops/backups/nginx-wildcard-20260706-162202
|
||||
status=issued
|
||||
@@ -0,0 +1,52 @@
|
||||
# AIOS GitOps alias for https://g.cnnetmkt.top/
|
||||
# Note: this server block intentionally keeps /git/ as the Gitea app prefix,
|
||||
# matching the current Gitea ROOT_URL and avoiding a disruptive app.ini rewrite.
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
server_name g.cnnetmkt.top;
|
||||
|
||||
access_log /var/log/nginx/aios-g-cnnetmkt.access.log;
|
||||
error_log /var/log/nginx/aios-g-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 = / {
|
||||
return 302 /git/;
|
||||
}
|
||||
|
||||
location = /git {
|
||||
return 302 /git/;
|
||||
}
|
||||
|
||||
location /git/ {
|
||||
proxy_pass http://127.0.0.1:3000/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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 $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Prefix /git;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_redirect off;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name g.cnnetmkt.top;
|
||||
|
||||
location = / {
|
||||
return 302 https://$host/git/;
|
||||
}
|
||||
|
||||
location / {
|
||||
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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,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.
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,240 @@
|
||||
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.
|
||||
servers:
|
||||
- url: https://cnnetmkt.top
|
||||
security: []
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: opaque-token
|
||||
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}
|
||||
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
|
||||
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}, '401': {description: Unauthorized}, '403': {description: Forbidden}}
|
||||
/api/model:
|
||||
get:
|
||||
operationId: readModelRegistry
|
||||
summary: Read visible model registry and priority
|
||||
responses: {'200': {description: OK}}
|
||||
post:
|
||||
operationId: registerModel
|
||||
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
|
||||
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}, '401': {description: Unauthorized}, '403': {description: Forbidden}}
|
||||
/api/file:
|
||||
post:
|
||||
operationId: commitFile
|
||||
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}, '400': {description: Secret rejected or invalid path}, '401': {description: Unauthorized}}
|
||||
/api/commit:
|
||||
post:
|
||||
operationId: commitChanges
|
||||
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
|
||||
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
|
||||
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
|
||||
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}}
|
||||
@@ -0,0 +1,4 @@
|
||||
# GitOps Write Auth Smoke
|
||||
|
||||
Time: 20260706T115230Z
|
||||
Result: createPatch and commitFile authenticated via secret_ref token.
|
||||
@@ -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
|
||||
@@ -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,83 @@
|
||||
# cnnetmkt SSL ingress status
|
||||
|
||||
generated_at=2026-07-06T15:48:12+08:00
|
||||
g_route_status=installed
|
||||
certbot_status=issued-g-user-portal
|
||||
apex_dns_status=blocked_until_cnnetmkt.top_has_A_record_to_1.95.164.183
|
||||
wildcard_status=blocked_until_dns01_provider_credentials_available
|
||||
backup_dir=/data/aios-gitops/backups/nginx-g-cnnetmkt-20260706-154743
|
||||
tracked_nginx=/data/aios-gitops/workspace/deploy/nginx/g-cnnetmkt-gitea-server.conf
|
||||
|
||||
## validation
|
||||
### g.cnnetmkt.top
|
||||
HTTP/1.1 302 Moved Temporarily
|
||||
Server: nginx/1.24.0 (Ubuntu)
|
||||
Date: Mon, 06 Jul 2026 07:48:12 GMT
|
||||
Content-Type: text/html
|
||||
Content-Length: 154
|
||||
Location: https://g.cnnetmkt.top/git/
|
||||
Connection: keep-alive
|
||||
|
||||
<title>302 Found
|
||||
|
||||
subject=CN = user.cnnetmkt.top
|
||||
issuer=C = US, O = Let's Encrypt, CN = YE2
|
||||
notBefore=Jul 6 06:49:39 2026 GMT
|
||||
notAfter=Oct 4 06:49:38 2026 GMT
|
||||
X509v3 Subject Alternative Name:
|
||||
DNS:g.cnnetmkt.top, DNS:portal.cnnetmkt.top, DNS:user.cnnetmkt.top
|
||||
|
||||
### cnnetmkt.top
|
||||
HTTP/1.1 302 Found
|
||||
Server: nginx/1.24.0 (Ubuntu)
|
||||
Date: Mon, 06 Jul 2026 07:48:12 GMT
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Content-Length: 0
|
||||
Connection: keep-alive
|
||||
Location: /portal/login/
|
||||
X-Request-ID: 7c649abe-a2de-4b3e-87fe-325f4297738f
|
||||
X-Frame-Options: DENY
|
||||
X-Content-Type-Options: nosniff
|
||||
Referrer-Policy: same-origin
|
||||
Cross-Origin-Opener-Policy: same-origin
|
||||
|
||||
subject=CN = user.cnnetmkt.top
|
||||
issuer=C = US, O = Let's Encrypt, CN = YE2
|
||||
notBefore=Jul 6 06:49:39 2026 GMT
|
||||
notAfter=Oct 4 06:49:38 2026 GMT
|
||||
X509v3 Subject Alternative Name:
|
||||
DNS:g.cnnetmkt.top, DNS:portal.cnnetmkt.top, DNS:user.cnnetmkt.top
|
||||
|
||||
### user.cnnetmkt.top
|
||||
HTTP/1.1 302 Found
|
||||
Server: nginx/1.24.0 (Ubuntu)
|
||||
Date: Mon, 06 Jul 2026 07:48:12 GMT
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Content-Length: 0
|
||||
Connection: keep-alive
|
||||
Location: /portal/login/
|
||||
X-Request-ID: e3d70907-1ab7-456c-a725-7cb60647514d
|
||||
X-Frame-Options: DENY
|
||||
X-Content-Type-Options: nosniff
|
||||
Referrer-Policy: same-origin
|
||||
Cross-Origin-Opener-Policy: same-origin
|
||||
|
||||
subject=CN = user.cnnetmkt.top
|
||||
issuer=C = US, O = Let's Encrypt, CN = YE2
|
||||
notBefore=Jul 6 06:49:39 2026 GMT
|
||||
notAfter=Oct 4 06:49:38 2026 GMT
|
||||
X509v3 Subject Alternative Name:
|
||||
DNS:g.cnnetmkt.top, DNS:portal.cnnetmkt.top, DNS:user.cnnetmkt.top
|
||||
|
||||
## certbot
|
||||
|
||||
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
Found the following certs:
|
||||
Certificate Name: user.cnnetmkt.top
|
||||
Serial Number: 529feebc03b37f2866c25b1465f37e14565
|
||||
Key Type: ECDSA
|
||||
Domains: user.cnnetmkt.top g.cnnetmkt.top portal.cnnetmkt.top
|
||||
Expiry Date: 2026-10-04 06:49:38+00:00 (VALID: 89 days)
|
||||
Certificate Path: /etc/letsencrypt/live/user.cnnetmkt.top/fullchain.pem
|
||||
Private Key Path: /etc/letsencrypt/live/user.cnnetmkt.top/privkey.pem
|
||||
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
@@ -0,0 +1,28 @@
|
||||
# CNNETMKT wildcard certificate status
|
||||
|
||||
generated_at=2026-07-06T16:24:28+08:00
|
||||
status=issued
|
||||
staging_status=ok
|
||||
dns_provider=huaweicloud
|
||||
secret_storage=/etc/letsencrypt/huaweicloud-cnnetmkt.env
|
||||
certificate_live_dir=/etc/letsencrypt/live/cnnetmkt-wildcard
|
||||
nginx_status=loaded
|
||||
backup_dir=/data/aios-gitops/backups/nginx-wildcard-20260706-162202
|
||||
renew_timer=cnnetmkt-wildcard-renew.timer
|
||||
|
||||
## certificate
|
||||
|
||||
subject=CN = *.cnnetmkt.top
|
||||
issuer=C = US, O = Let's Encrypt, CN = YE2
|
||||
notBefore=Jul 6 07:25:55 2026 GMT
|
||||
notAfter=Oct 4 07:25:54 2026 GMT
|
||||
X509v3 Subject Alternative Name:
|
||||
DNS:*.cnnetmkt.top, DNS:cnnetmkt.top
|
||||
|
||||
## routes
|
||||
|
||||
- https://cnnetmkt.top/
|
||||
- https://cnnetmkt.top/git/
|
||||
- https://g.cnnetmkt.top/
|
||||
- https://g.cnnetmkt.top/git/
|
||||
- https://user.cnnetmkt.top/
|
||||
@@ -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.
|
||||
+363
@@ -0,0 +1,363 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="/data/aios-gitops"
|
||||
ZIP_NAME="AIOS_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_PROD_FULL_DEPLOY_20260706.zip"
|
||||
ZIP_PATH="$ROOT/imports/$ZIP_NAME"
|
||||
WORKSPACE="$ROOT/workspace"
|
||||
LOG_DIR="$ROOT/logs"
|
||||
ARTIFACT_DIR="$ROOT/artifacts"
|
||||
PRECHECK="$LOG_DIR/preflight-v1r1c0.md"
|
||||
RUN_LOG="$LOG_DIR/gitops-v1r1c0-migration.log"
|
||||
FAILED_REPORT="$ARTIFACT_DIR/AIOS_V1R1C0_ONLINE_BASELINE_FAILED.md"
|
||||
ACCEPT_REPORT="$ARTIFACT_DIR/AIOS_V1R1C0_ONLINE_BASELINE_ACCEPTANCE.md"
|
||||
BASELINE_TAG="V1R1C0-baseline-20260706"
|
||||
|
||||
mkdir -p "$ROOT"/{imports,workspace,repos,scripts,logs,backups,runtime,artifacts,adapters}
|
||||
touch "$RUN_LOG"
|
||||
|
||||
log() {
|
||||
printf '[%s] %s\n' "$(date -Is)" "$*" | tee -a "$RUN_LOG"
|
||||
}
|
||||
|
||||
run_section() {
|
||||
local title="$1"
|
||||
shift
|
||||
{
|
||||
printf '\n## %s\n\n' "$title"
|
||||
printf '```text\n'
|
||||
"$@" 2>&1 || true
|
||||
printf '```\n'
|
||||
} >> "$PRECHECK"
|
||||
}
|
||||
|
||||
write_failed_report() {
|
||||
local step="$1"
|
||||
local message="$2"
|
||||
cat > "$FAILED_REPORT" <<EOF
|
||||
# AIOS V1R1C0 Online Baseline Failed
|
||||
|
||||
- status: FAILED
|
||||
- failed_step: $step
|
||||
- error: $message
|
||||
- package: $ZIP_PATH
|
||||
- workspace: $WORKSPACE
|
||||
- production_impact: none-intended
|
||||
- npm_modified: NO
|
||||
- webops_modified: NO
|
||||
- git_modified: PARTIAL
|
||||
- openclaw_modified: NO
|
||||
- generated_at: $(date -Is)
|
||||
|
||||
## Completed Actions
|
||||
|
||||
- Created /data/aios-gitops directory structure.
|
||||
- Generated preflight report when possible.
|
||||
- Preserved original ZIP if present.
|
||||
- Stopped before production routing changes.
|
||||
|
||||
## Rollback Advice
|
||||
|
||||
The active production stack was not switched by this script. Keep the existing
|
||||
WebOps/NPM/Casdoor/OpenClaw/Hermes services unchanged. Remove only the new
|
||||
/data/aios-gitops workspace if an operator explicitly wants to discard this
|
||||
failed import attempt.
|
||||
|
||||
## Next Step
|
||||
|
||||
Fix the failed step, then rerun the migration script. Do not treat this state
|
||||
as an online baseline.
|
||||
EOF
|
||||
}
|
||||
|
||||
preflight() {
|
||||
log "preflight start"
|
||||
cat > "$PRECHECK" <<EOF
|
||||
# AIOS V1R1C0 GitOps Preflight
|
||||
|
||||
- generated_at: $(date -Is)
|
||||
- hostname: $(hostname)
|
||||
- user: $(whoami)
|
||||
- root: $ROOT
|
||||
- package: $ZIP_PATH
|
||||
|
||||
## Reuse Summary
|
||||
|
||||
- NPM_REUSED: UNKNOWN
|
||||
- WEBOPS_REUSED: UNKNOWN
|
||||
- CASDOOR_REUSED: UNKNOWN
|
||||
- OPENCLAW_REUSED: UNKNOWN
|
||||
- HERMES_REUSED: UNKNOWN
|
||||
- ONE_API_REUSED: UNKNOWN
|
||||
- GITEA_REUSED: UNKNOWN
|
||||
|
||||
EOF
|
||||
run_section "hostname" hostname
|
||||
run_section "date" date -Is
|
||||
run_section "whoami" whoami
|
||||
run_section "pwd" pwd
|
||||
run_section "df -h" df -h
|
||||
run_section "free -h" free -h
|
||||
run_section "docker ps" docker ps
|
||||
run_section "docker compose version" bash -lc 'docker compose version || docker-compose version || true'
|
||||
run_section "ss -lntp" ss -lntp
|
||||
run_section "ls /data" ls -lah /data
|
||||
run_section "ls /opt" ls -lah /opt
|
||||
run_section "component containers" bash -lc "docker ps | grep -Ei 'npm|nginx-proxy-manager|gitea|casdoor|openclaw|hermes|webops|one-api|postgres|redis|nats' || true"
|
||||
run_section "component paths" bash -lc "find /data /opt -maxdepth 4 \\( -iname '*npm*' -o -iname '*gitea*' -o -iname '*openclaw*' -o -iname '*webops*' -o -iname '*casdoor*' -o -iname '*one-api*' \\) 2>/dev/null || true"
|
||||
run_section "component services" bash -lc "systemctl list-units --type=service --no-pager | grep -Ei 'openclaw|hermes|aios|webops|npm|gitea|casdoor' || true"
|
||||
log "preflight done: $PRECHECK"
|
||||
}
|
||||
|
||||
backup_state() {
|
||||
log "backup start"
|
||||
local backup_dir="$ROOT/backups/pre-v1r1c0-$(date +%Y%m%d-%H%M%S)"
|
||||
mkdir -p "$backup_dir"
|
||||
cp -a /data/agent "$backup_dir/data-agent" 2>/dev/null || true
|
||||
cp -a /data/aios "$backup_dir/data-aios" 2>/dev/null || true
|
||||
cp -a /opt/aios "$backup_dir/opt-aios" 2>/dev/null || true
|
||||
cp -a /etc/systemd/system "$backup_dir/systemd-system" 2>/dev/null || true
|
||||
docker ps > "$backup_dir/docker-ps.txt" 2>/dev/null || true
|
||||
ss -lntp > "$backup_dir/ports.txt" 2>/dev/null || true
|
||||
find /data /opt -maxdepth 4 \( -iname '*npm*' -o -iname '*nginx-proxy-manager*' \) 2>/dev/null > "$backup_dir/npm-paths.txt" || true
|
||||
while IFS= read -r p; do
|
||||
[ -e "$p" ] || continue
|
||||
safe_name="$(printf '%s' "$p" | sed 's#^/##; s#[^A-Za-z0-9._-]#_#g')"
|
||||
cp -a "$p" "$backup_dir/$safe_name" 2>/dev/null || true
|
||||
done < "$backup_dir/npm-paths.txt"
|
||||
printf '%s\n' "$backup_dir" > "$ROOT/runtime/latest-backup-dir.txt"
|
||||
log "backup done: $backup_dir"
|
||||
}
|
||||
|
||||
import_zip() {
|
||||
log "import start"
|
||||
if [ ! -f "$ZIP_PATH" ]; then
|
||||
write_failed_report "locate_zip" "BLOCKED: missing $ZIP_NAME"
|
||||
log "BLOCKED: missing $ZIP_NAME"
|
||||
exit 2
|
||||
fi
|
||||
mkdir -p "$WORKSPACE"
|
||||
resolved="$(readlink -f "$WORKSPACE")"
|
||||
if [ "$resolved" != "$WORKSPACE" ]; then
|
||||
write_failed_report "workspace_resolve" "Unexpected workspace path: $resolved"
|
||||
exit 3
|
||||
fi
|
||||
find "$WORKSPACE" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +
|
||||
unzip -q -o "$ZIP_PATH" -d "$WORKSPACE"
|
||||
shopt -s nullglob dotglob
|
||||
entries=("$WORKSPACE"/*)
|
||||
if [ "${#entries[@]}" -eq 1 ] && [ -d "${entries[0]}" ]; then
|
||||
tmp_dir="$WORKSPACE/.flatten-tmp"
|
||||
mv "${entries[0]}" "$tmp_dir"
|
||||
shopt -s dotglob
|
||||
mv "$tmp_dir"/* "$WORKSPACE"/
|
||||
rmdir "$tmp_dir"
|
||||
fi
|
||||
chmod +x "$WORKSPACE"/package_verify_full.sh 2>/dev/null || true
|
||||
chmod +x "$WORKSPACE"/scripts/*.sh "$WORKSPACE"/tests/*.sh 2>/dev/null || true
|
||||
log "import done"
|
||||
}
|
||||
|
||||
run_package_checks() {
|
||||
log "package checks start"
|
||||
cd "$WORKSPACE"
|
||||
if ! bash scripts/package_contract_check_v1r1c0.sh > "$LOG_DIR/package_contract_check_v1r1c0.log" 2>&1; then
|
||||
write_failed_report "package_contract_check_v1r1c0" "scripts/package_contract_check_v1r1c0.sh failed; see $LOG_DIR/package_contract_check_v1r1c0.log"
|
||||
exit 4
|
||||
fi
|
||||
if ! bash tests/run_v1r1c0_acceptance.sh > "$LOG_DIR/run_v1r1c0_acceptance.log" 2>&1; then
|
||||
write_failed_report "run_v1r1c0_acceptance" "tests/run_v1r1c0_acceptance.sh failed; see $LOG_DIR/run_v1r1c0_acceptance.log"
|
||||
exit 5
|
||||
fi
|
||||
if ! ./package_verify_full.sh > "$LOG_DIR/package_verify_full.log" 2>&1; then
|
||||
write_failed_report "package_verify_full" "package_verify_full.sh failed; see $LOG_DIR/package_verify_full.log"
|
||||
exit 6
|
||||
fi
|
||||
log "package checks done"
|
||||
}
|
||||
|
||||
write_baseline_files() {
|
||||
log "write baseline files"
|
||||
cd "$WORKSPACE"
|
||||
mkdir -p baseline versions patches architecture evolution deploy ops tests docs/gitops docs/release
|
||||
cat > baseline/current.json <<'EOF'
|
||||
{
|
||||
"active_version": "V1R1C0",
|
||||
"active_tag": "V1R1C0-baseline-20260706",
|
||||
"mode": "gitops-online-baseline",
|
||||
"version_policy": "VxRxCx",
|
||||
"update_mode": "git_patch_only",
|
||||
"zip_update_deprecated": true,
|
||||
"entry": "https://cnnetmkt.top",
|
||||
"links": {
|
||||
"git": "/git",
|
||||
"baseline": "/base",
|
||||
"versions": "/ver",
|
||||
"diff": "/diff",
|
||||
"patch": "/patch",
|
||||
"model": "/model",
|
||||
"runtime": "/run",
|
||||
"api": "/api",
|
||||
"hook": "/hook"
|
||||
},
|
||||
"features": {
|
||||
"multiuser_runtime": true,
|
||||
"shared_dedicated_runtime": true,
|
||||
"custom_llm": true,
|
||||
"user_model_priority": true,
|
||||
"chatgpt_rw": true,
|
||||
"codex_rw": true
|
||||
}
|
||||
}
|
||||
EOF
|
||||
cat > versions/V1R1C0.json <<'EOF'
|
||||
{
|
||||
"version": "V1R1C0",
|
||||
"tag": "V1R1C0-baseline-20260706",
|
||||
"type": "baseline",
|
||||
"name": "MultiUser GitOps Custom LLM Baseline",
|
||||
"source": "AIOS_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_PROD_FULL_DEPLOY_20260706.zip",
|
||||
"update_mode": "git_patch_only",
|
||||
"zip_update_mode": "deprecated_after_import"
|
||||
}
|
||||
EOF
|
||||
cat > architecture/current.json <<'EOF'
|
||||
{
|
||||
"version": "V1R1C0",
|
||||
"architecture_version": "V1",
|
||||
"release_version": "R1",
|
||||
"change_version": "C0",
|
||||
"components": [
|
||||
"webops",
|
||||
"owner-admin-user",
|
||||
"npm",
|
||||
"casdoor",
|
||||
"openclaw",
|
||||
"hermes",
|
||||
"one-api",
|
||||
"gitops-control",
|
||||
"baseline-registry",
|
||||
"custom-llm-registry",
|
||||
"runtime-selector"
|
||||
],
|
||||
"entry": "https://cnnetmkt.top",
|
||||
"proxy": "npm",
|
||||
"runtime_sync": "openclaw"
|
||||
}
|
||||
EOF
|
||||
cat > docs/release/VxRxCx_RULES.md <<'EOF'
|
||||
# AIOS VxRxCx Versioning Rules
|
||||
|
||||
VxRxCx is the only supported online baseline version format.
|
||||
|
||||
- V: architecture version
|
||||
- R: release version
|
||||
- C: change version
|
||||
|
||||
V-level changes require owner/admin approval and blue-green or equivalent safe switching.
|
||||
R-level changes require canary or test-environment validation.
|
||||
C-level changes require smoke test and automatic rollback on failure.
|
||||
|
||||
Initial imported online baseline for this package: V1R1C0-baseline-20260706.
|
||||
EOF
|
||||
cat > docs/gitops/CHATGPT_CODEX_ACCESS.md <<'EOF'
|
||||
# ChatGPT / Codex GitOps Access
|
||||
|
||||
Entry: https://cnnetmkt.top
|
||||
|
||||
Short links: /git, /base, /ver, /diff, /patch, /model, /run, /api, /hook.
|
||||
|
||||
All writes must become Git commits. Fixes must use patch branches. Releases must use VxRxCx tags.
|
||||
|
||||
Forbidden:
|
||||
|
||||
- direct production edits without Git history
|
||||
- API keys in Git
|
||||
- API keys in plaintext logs
|
||||
- bypassing owner/admin approval for V-level changes
|
||||
EOF
|
||||
}
|
||||
|
||||
init_git() {
|
||||
log "git init start"
|
||||
cd "$WORKSPACE"
|
||||
git init
|
||||
git config user.name "AIOS GitOps Bot"
|
||||
git config user.email "gitops-bot@cnnetmkt.top"
|
||||
if [ ! -f .gitignore ]; then
|
||||
cat > .gitignore <<'EOF'
|
||||
.env
|
||||
*.secret
|
||||
*.key
|
||||
*.pem
|
||||
*.log
|
||||
__pycache__/
|
||||
node_modules/
|
||||
.venv/
|
||||
.DS_Store
|
||||
runtime/tmp/
|
||||
runtime/cache/
|
||||
logs/
|
||||
EOF
|
||||
fi
|
||||
git add .
|
||||
git commit -m "init(baseline): import AIOS V1R1C0 multiuser gitops custom llm baseline" || true
|
||||
git branch -M main
|
||||
git tag -a "$BASELINE_TAG" -m "AIOS V1R1C0 online baseline: multiuser gitops custom LLM" 2>/dev/null || true
|
||||
log "git init done"
|
||||
}
|
||||
|
||||
write_acceptance() {
|
||||
log "write acceptance report"
|
||||
cd "$WORKSPACE"
|
||||
cat > "$ACCEPT_REPORT" <<EOF
|
||||
# AIOS V1R1C0 Online Baseline Acceptance
|
||||
|
||||
1. server: $(hostname)
|
||||
2. zip_path: $ZIP_PATH
|
||||
3. npm_reused: PARTIAL_UNVERIFIED
|
||||
4. webops_reused: YES_DETECTED
|
||||
5. casdoor_reused: PARTIAL_UNVERIFIED
|
||||
6. openclaw_reused: YES_DETECTED
|
||||
7. hermes_reused: YES_DETECTED
|
||||
8. one_api_reused: YES_DETECTED
|
||||
9. git_service: LOCAL_GIT_READY_REMOTE_UI_PENDING
|
||||
10. git_url: PENDING_GITEA_OR_EXISTING_GIT_SERVICE
|
||||
11. short_links: /git,/base,/ver,/diff,/patch,/model,/run,/api,/hook
|
||||
12. baseline_version: V1R1C0
|
||||
13. baseline_tag: $BASELINE_TAG
|
||||
14. custom_llm_api: PACKAGE_UAT_PASS_ONLINE_ROUTE_PENDING
|
||||
15. model_priority: PACKAGE_UAT_PASS_ONLINE_ROUTE_PENDING
|
||||
16. runtime_selector: PACKAGE_UAT_PASS_ONLINE_ROUTE_PENDING
|
||||
17. gitops_patch: BASELINE_COMMIT_AND_TAG_READY
|
||||
18. smoke_test: PACKAGE_CHECKS_PASS
|
||||
19. known_issues:
|
||||
- NPM custom locations were not modified by this script.
|
||||
- Gitea/Web Git UI push was not completed by this script.
|
||||
- Spark sub-agent quota was unavailable during orchestration.
|
||||
20. next_update_mode: git_patch_only
|
||||
|
||||
## Git Evidence
|
||||
EOF
|
||||
{
|
||||
printf '\n### git status --short\n\n'
|
||||
git status --short
|
||||
printf '\n### git log --oneline -10\n\n'
|
||||
git log --oneline -10
|
||||
printf '\n### git tag grep V1R1C0\n\n'
|
||||
git tag | grep V1R1C0 || true
|
||||
} >> "$ACCEPT_REPORT"
|
||||
}
|
||||
|
||||
main() {
|
||||
preflight
|
||||
backup_state
|
||||
import_zip
|
||||
run_package_checks
|
||||
write_baseline_files
|
||||
init_git
|
||||
write_acceptance
|
||||
log "MIGRATION_LOCAL_BASELINE_OK"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="/data/aios-gitops"
|
||||
WORKSPACE="$ROOT/workspace"
|
||||
LOG="$ROOT/logs/runtime-services-v1r1c0.log"
|
||||
mkdir -p "$ROOT/logs" "$ROOT/runtime/secrets" "$WORKSPACE/deploy/systemd" "$WORKSPACE/deploy/gitea"
|
||||
touch "$LOG"
|
||||
|
||||
log() {
|
||||
printf '[%s] %s\n' "$(date -Is)" "$*" | tee -a "$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"
|
||||
fi
|
||||
|
||||
cat > deploy/systemd/aios-gitops-model-api.service <<EOF
|
||||
[Unit]
|
||||
Description=AIOS V1R1C0 GitOps Model API
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=aiman
|
||||
Group=aiman
|
||||
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_chatgpt_actions_api.py
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
cat > deploy/gitea/docker-compose.yml <<'EOF'
|
||||
services:
|
||||
gitea:
|
||||
image: gitea/gitea:1.22
|
||||
container_name: aios-gitea
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- USER_UID=1000
|
||||
- USER_GID=1000
|
||||
- GITEA__server__DOMAIN=cnnetmkt.top
|
||||
- GITEA__server__ROOT_URL=https://cnnetmkt.top/git/
|
||||
- GITEA__server__SERVE_FROM_SUB_PATH=true
|
||||
- GITEA__server__HTTP_PORT=3000
|
||||
- GITEA__server__SSH_DOMAIN=cnnetmkt.top
|
||||
- GITEA__server__SSH_PORT=2222
|
||||
- GITEA__service__DISABLE_REGISTRATION=true
|
||||
- GITEA__webhook__ALLOWED_HOST_LIST=*
|
||||
- GITEA__database__DB_TYPE=sqlite3
|
||||
volumes:
|
||||
- /data/aios-gitops/runtime/gitea:/data
|
||||
ports:
|
||||
- "127.0.0.1:3000:3000"
|
||||
- "127.0.0.1:2222:22"
|
||||
EOF
|
||||
|
||||
git add deploy/systemd/aios-gitops-model-api.service deploy/gitea/docker-compose.yml
|
||||
git commit -m "chore(gitops): add V1R1C0 runtime API and Gitea deployment manifests" || true
|
||||
|
||||
log "install API systemd unit"
|
||||
sudo cp deploy/systemd/aios-gitops-model-api.service /etc/systemd/system/aios-gitops-model-api.service
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now aios-gitops-model-api.service
|
||||
sleep 2
|
||||
systemctl is-active aios-gitops-model-api.service | tee -a "$LOG"
|
||||
curl -fsS http://127.0.0.1:18890/health | tee -a "$LOG"
|
||||
|
||||
log "start gitea"
|
||||
mkdir -p "$ROOT/adapters/gitea"
|
||||
cp deploy/gitea/docker-compose.yml "$ROOT/adapters/gitea/docker-compose.yml"
|
||||
(cd "$ROOT/adapters/gitea" && docker compose up -d)
|
||||
|
||||
for i in $(seq 1 60); do
|
||||
if curl -fsS http://127.0.0.1:3000/git/api/healthz >/dev/null 2>&1 || curl -fsS http://127.0.0.1:3000/git/ >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
curl -fsS http://127.0.0.1:3000/git/ >/dev/null || true
|
||||
docker ps --format '{{.Names}} {{.Image}} {{.Status}} {{.Ports}}' | grep -E '^aios-gitea ' | tee -a "$LOG" || true
|
||||
|
||||
log "runtime manifests committed and services attempted"
|
||||
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="/data/aios-gitops"
|
||||
WORKSPACE="$ROOT/workspace"
|
||||
|
||||
cd "$WORKSPACE"
|
||||
git add active-runtime/app/aios_gitops_model_api.py scripts/hermes_gitops_parallel_agents.py
|
||||
git commit -m "fix(gitops): align baseline tag and secret scan" || true
|
||||
|
||||
python3 - <<'PY'
|
||||
import datetime
|
||||
import sqlite3
|
||||
|
||||
p = "/data/aios-gitops/runtime/gitops_model.sqlite3"
|
||||
con = sqlite3.connect(p)
|
||||
con.execute(
|
||||
"UPDATE gitops_baseline SET active_version=?, active_tag=?, source_package=?, update_mode=?, updated_at=? WHERE id=?",
|
||||
(
|
||||
"V1R1C0",
|
||||
"V1R1C0-baseline-20260706",
|
||||
"AIOS_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_PROD_FULL_DEPLOY_20260706.zip",
|
||||
"git_patch_only",
|
||||
datetime.datetime.utcnow().isoformat() + "Z",
|
||||
"current",
|
||||
),
|
||||
)
|
||||
con.commit()
|
||||
con.close()
|
||||
PY
|
||||
|
||||
sudo chown -R aiman:aiman "$ROOT/runtime" "$WORKSPACE" "$ROOT/logs" "$ROOT/artifacts"
|
||||
sudo systemctl restart aios-gitops-model-api.service
|
||||
sleep 2
|
||||
curl -fsS http://127.0.0.1:18890/api/status
|
||||
echo
|
||||
python3 scripts/hermes_gitops_parallel_agents.py
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="/data/aios-gitops"
|
||||
WORKSPACE="$ROOT/workspace"
|
||||
NGINX_SITE="/etc/nginx/sites-enabled/aios-user-portal.conf"
|
||||
BACKUP_DIR="$ROOT/backups/nginx-gitops-$(date +%Y%m%d-%H%M%S)"
|
||||
LOG="$ROOT/logs/nginx-gitops-routes.log"
|
||||
mkdir -p "$BACKUP_DIR" "$ROOT/logs" "$WORKSPACE/deploy/nginx"
|
||||
exec > >(tee -a "$LOG") 2>&1
|
||||
|
||||
cd "$WORKSPACE"
|
||||
|
||||
cat > deploy/nginx/aios-gitops-locations.conf <<'EOF'
|
||||
# BEGIN AIOS V1R1C0 GitOps short links
|
||||
location = /git { return 302 /git/; }
|
||||
location /git/ {
|
||||
proxy_pass http://127.0.0.1:3000/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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 $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Prefix /git;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_redirect off;
|
||||
}
|
||||
|
||||
location = /api { return 302 /api/; }
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:18890/api/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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 $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 = /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/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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 $scheme;
|
||||
}
|
||||
# END AIOS V1R1C0 GitOps short links
|
||||
EOF
|
||||
|
||||
git add deploy/nginx/aios-gitops-locations.conf active-runtime/app/aios_gitops_model_api.py
|
||||
git commit -m "feat(gitops): add nginx short links and webhook endpoint" || true
|
||||
|
||||
echo "== backup nginx site =="
|
||||
sudo cp -a "$NGINX_SITE" "$BACKUP_DIR/aios-user-portal.conf"
|
||||
|
||||
echo "== insert locations =="
|
||||
python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
|
||||
site = Path("/etc/nginx/sites-enabled/aios-user-portal.conf")
|
||||
snippet = Path("/data/aios-gitops/workspace/deploy/nginx/aios-gitops-locations.conf").read_text()
|
||||
text = site.read_text()
|
||||
start = " # BEGIN AIOS V1R1C0 GitOps short links"
|
||||
end = " # END AIOS V1R1C0 GitOps short links"
|
||||
if start in text:
|
||||
before = text.split(start)[0]
|
||||
after = text.split(end, 1)[1]
|
||||
text = before + snippet + after
|
||||
else:
|
||||
marker = " location / {\n"
|
||||
if marker not in text:
|
||||
raise SystemExit("location marker not found")
|
||||
text = text.replace(marker, snippet + "\n" + marker, 1)
|
||||
Path("/tmp/aios-user-portal.conf.gitops").write_text(text)
|
||||
PY
|
||||
sudo cp /tmp/aios-user-portal.conf.gitops "$NGINX_SITE"
|
||||
|
||||
echo "== nginx test and reload =="
|
||||
sudo nginx -t
|
||||
sudo systemctl reload nginx
|
||||
|
||||
echo "== local route checks =="
|
||||
for path in /git/ /base /ver /model /run /hook /api/status; do
|
||||
echo "== ${path} =="
|
||||
curl -k -sS -I --resolve cnnetmkt.top:443:127.0.0.1 "https://cnnetmkt.top${path}" | sed -n '1,12p' || true
|
||||
done
|
||||
echo "NGINX_GITOPS_ROUTES_OK backup=$BACKUP_DIR"
|
||||
@@ -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
@@ -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'
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
echo "== host =="
|
||||
hostname
|
||||
date -Is
|
||||
|
||||
echo "== /etc/hosts cnnetmkt =="
|
||||
grep -nE 'cnnetmkt|gitea|aios' /etc/hosts || true
|
||||
|
||||
echo "== local resolver =="
|
||||
for n in cnnetmkt.top g.cnnetmkt.top user.cnnetmkt.top portal.cnnetmkt.top; do
|
||||
echo "-- ${n}"
|
||||
getent ahosts "${n}" || true
|
||||
done
|
||||
|
||||
echo "== DoH public DNS =="
|
||||
for n in cnnetmkt.top g.cnnetmkt.top user.cnnetmkt.top portal.cnnetmkt.top; do
|
||||
echo "-- ${n}"
|
||||
curl -fsS --connect-timeout 10 --max-time 20 \
|
||||
-H 'accept: application/dns-json' \
|
||||
"https://cloudflare-dns.com/dns-query?name=${n}&type=A" || true
|
||||
echo
|
||||
done
|
||||
|
||||
echo "== local SNI HTTP/TLS =="
|
||||
for h in cnnetmkt.top g.cnnetmkt.top user.cnnetmkt.top portal.cnnetmkt.top; do
|
||||
echo "-- ${h} curl /"
|
||||
rm -f /tmp/aios-ingress-body
|
||||
curl -k -sS -D - -o /tmp/aios-ingress-body \
|
||||
--resolve "${h}:443:127.0.0.1" \
|
||||
"https://${h}/" | sed -n '1,20p' || true
|
||||
sed -n '1,16p' /tmp/aios-ingress-body 2>/dev/null || true
|
||||
echo "-- ${h} curl /git/"
|
||||
curl -k -sS -D - -o /tmp/aios-ingress-body \
|
||||
--resolve "${h}:443:127.0.0.1" \
|
||||
"https://${h}/git/" | sed -n '1,12p' || true
|
||||
grep -ao '<title>[^<]*' /tmp/aios-ingress-body 2>/dev/null | head -1 || true
|
||||
echo "-- ${h} cert"
|
||||
echo | openssl s_client -connect 127.0.0.1:443 -servername "${h}" 2>/dev/null \
|
||||
| openssl x509 -noout -subject -issuer -dates -ext subjectAltName 2>/dev/null || true
|
||||
done
|
||||
|
||||
echo "== certbot certificates =="
|
||||
sudo certbot certificates || true
|
||||
|
||||
echo "== nginx matched config =="
|
||||
sudo nginx -T 2>/dev/null | grep -nE 'server_name|ssl_certificate|location = /git|location /git|proxy_pass http://127.0.0.1:3000' || true
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
for path in /git/ /base /ver /model /run /hook /api/status; do
|
||||
echo "== ${path} =="
|
||||
curl -k -sS --max-time 10 --resolve cnnetmkt.top:443:127.0.0.1 "https://cnnetmkt.top${path}" | head -20 || true
|
||||
done
|
||||
|
||||
echo "== direct API =="
|
||||
for path in /base /ver /model /run /hook /api/status; do
|
||||
echo "== 18890${path} =="
|
||||
curl -sS --max-time 10 "http://127.0.0.1:18890${path}" | head -10 || true
|
||||
done
|
||||
+112
@@ -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())
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="/data/aios-gitops"
|
||||
WORKSPACE="$ROOT/workspace"
|
||||
ARTIFACT="$ROOT/artifacts/AIOS_V1R1C0_ONLINE_BASELINE_ACCEPTANCE.md"
|
||||
REPORT_IN_REPO="$WORKSPACE/docs/release/AIOS_V1R1C0_ONLINE_BASELINE_ACCEPTANCE.md"
|
||||
EVIDENCE="$ROOT/artifacts/AIOS_V1R1C0_FINAL_EVIDENCE.txt"
|
||||
VERSION="V1R1C0"
|
||||
TAG="V1R1C0-baseline-20260706"
|
||||
SOURCE="AIOS_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_PROD_FULL_DEPLOY_20260706.zip"
|
||||
GIT_URL="https://cnnetmkt.top/git/aios/aios-v23.git"
|
||||
|
||||
mkdir -p "$ROOT/artifacts" "$ROOT/logs" "$WORKSPACE/docs/release"
|
||||
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
import sqlite3
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
root = Path("/data/aios-gitops")
|
||||
workspace = root / "workspace"
|
||||
db = root / "runtime" / "gitops_model.sqlite3"
|
||||
version = "V1R1C0"
|
||||
tag = "V1R1C0-baseline-20260706"
|
||||
source = "AIOS_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_PROD_FULL_DEPLOY_20260706.zip"
|
||||
entry = "https://cnnetmkt.top"
|
||||
git_url = "https://cnnetmkt.top/git/aios/aios-v23.git"
|
||||
short_links = ["/git", "/base", "/ver", "/diff", "/patch", "/model", "/run", "/api", "/hook"]
|
||||
|
||||
current_path = workspace / "baseline" / "current.json"
|
||||
current = {}
|
||||
if current_path.exists():
|
||||
current = json.loads(current_path.read_text(encoding="utf-8"))
|
||||
current.update({
|
||||
"version": version,
|
||||
"active_version": version,
|
||||
"baseline_tag": tag,
|
||||
"active_tag": tag,
|
||||
"source": source,
|
||||
"source_package": source,
|
||||
"mode": "gitops-online",
|
||||
"update_mode": "git_patch_only",
|
||||
"zip_update_mode": "deprecated",
|
||||
"chatgpt_access": True,
|
||||
"codex_access": True,
|
||||
"git_url": git_url,
|
||||
"entry": entry,
|
||||
"short_links": short_links,
|
||||
"updated_at": datetime.now(timezone.utc).isoformat(),
|
||||
})
|
||||
current_path.write_text(json.dumps(current, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
version_path = workspace / "versions" / "V1R1C0.json"
|
||||
version_doc = {}
|
||||
if version_path.exists():
|
||||
version_doc = json.loads(version_path.read_text(encoding="utf-8"))
|
||||
version_doc.update({
|
||||
"version": version,
|
||||
"tag": tag,
|
||||
"baseline_tag": tag,
|
||||
"source": source,
|
||||
"update_mode": "git_patch_only",
|
||||
"zip_update_mode": "deprecated",
|
||||
"git_url": git_url,
|
||||
"updated_at": datetime.now(timezone.utc).isoformat(),
|
||||
})
|
||||
version_path.write_text(json.dumps(version_doc, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
if db.exists():
|
||||
con = sqlite3.connect(db)
|
||||
try:
|
||||
con.execute(
|
||||
"UPDATE gitops_baseline SET active_version=?, active_tag=?, source_package=?, entry=?, update_mode=?, status=?, updated_at=? WHERE id='current'",
|
||||
(version, tag, source, entry, "git_patch_only", "active", datetime.now(timezone.utc).isoformat()),
|
||||
)
|
||||
con.execute(
|
||||
"UPDATE gitops_versions SET tag=?, status=? WHERE version=?",
|
||||
(tag, "ready", version),
|
||||
)
|
||||
con.commit()
|
||||
finally:
|
||||
con.close()
|
||||
PY
|
||||
|
||||
cd "$WORKSPACE"
|
||||
|
||||
{
|
||||
echo "== services =="
|
||||
systemctl is-active aios-gitops-model-api aios-gitea aios-hermes-router aios-hermes-executor openclaw-gateway nginx || true
|
||||
echo
|
||||
echo "== gitea =="
|
||||
curl -fsS http://127.0.0.1:3000/api/v1/version || true
|
||||
echo
|
||||
echo "== git =="
|
||||
git status --short
|
||||
git --no-pager log --oneline -8
|
||||
git tag --list 'V1R1C0*'
|
||||
git remote -v
|
||||
echo
|
||||
echo "== api health =="
|
||||
curl -fsS http://127.0.0.1:18890/health
|
||||
echo
|
||||
curl -fsS http://127.0.0.1:18890/api/status
|
||||
echo
|
||||
echo "== short links =="
|
||||
for path in /git/ /base /ver /diff /patch /model /run /api /hook /api/status; do
|
||||
tmp="$(mktemp)"
|
||||
code="$(curl -k -sS --max-time 10 --resolve cnnetmkt.top:443:127.0.0.1 -o "$tmp" -w "%{http_code}" "https://cnnetmkt.top${path}" || true)"
|
||||
title="$(grep -ao '<title>[^<]*' "$tmp" | head -1 | sed 's/<title>//' || true)"
|
||||
bytes="$(wc -c < "$tmp" | tr -d ' ')"
|
||||
printf '%s code=%s bytes=%s title=%s\n' "$path" "$code" "$bytes" "$title"
|
||||
rm -f "$tmp"
|
||||
done
|
||||
echo
|
||||
echo "== webhook smoke =="
|
||||
payload="$(mktemp)"
|
||||
cat > "$payload" <<'JSON'
|
||||
{"event":"manual-smoke","ref":"refs/heads/main","repository":{"full_name":"aios/aios-v23"}}
|
||||
JSON
|
||||
curl -fsS -H 'Content-Type: application/json' --data-binary "@${payload}" http://127.0.0.1:18890/hook/git || true
|
||||
rm -f "$payload"
|
||||
echo
|
||||
echo "== package smoke =="
|
||||
bash "$WORKSPACE/scripts/package_contract_check_v1r1c0.sh" || true
|
||||
echo
|
||||
echo "== hermes =="
|
||||
curl -fsS http://127.0.0.1:18792/internal/hermes/health || true
|
||||
echo
|
||||
echo "== openclaw =="
|
||||
openclaw --version 2>/dev/null || true
|
||||
openclaw models status --json 2>/dev/null | head -80 || true
|
||||
echo
|
||||
echo "== containers =="
|
||||
docker ps --format '{{.Names}} {{.Image}} {{.Ports}}' | grep -Ei 'webops|one-api|casdoor|nginx-proxy-manager|npm' || true
|
||||
echo
|
||||
echo "== secret scan =="
|
||||
if grep -R --binary-files=without-match --exclude-dir=.git -E 'sk-[A-Za-z0-9_-]{20,}|AIza[0-9A-Za-z_-]{20,}|xox[baprs]-[0-9A-Za-z-]{20,}' -n "$WORKSPACE" "$ROOT/logs" "$ROOT/artifacts" 2>/dev/null | head -20; then
|
||||
echo "SECRET_SCAN=FAILED"
|
||||
else
|
||||
echo "SECRET_SCAN=OK"
|
||||
fi
|
||||
} | tee "$EVIDENCE"
|
||||
|
||||
cat > "$ARTIFACT" <<EOF_REPORT
|
||||
# AIOS V1R1C0 Online Baseline Acceptance
|
||||
|
||||
AIOS_V1R1C0_ONLINE_BASELINE_STATUS=PARTIAL
|
||||
DOMAIN=https://cnnetmkt.top
|
||||
SHORT_LINKS=/git,/base,/ver,/diff,/patch,/model,/run,/api,/hook
|
||||
PROXY=NGINX_TEMP (NPM_NOT_RUNNING)
|
||||
GIT_URL=$GIT_URL
|
||||
BASELINE_VERSION=$VERSION
|
||||
BASELINE_TAG=$TAG
|
||||
WORKSPACE=$WORKSPACE
|
||||
WEBOPS_REUSED=YES
|
||||
CASDOOR_REUSED=PARTIAL
|
||||
OPENCLAW_REUSED=PARTIAL
|
||||
HERMES_REUSED=YES
|
||||
ONE_API_REUSED=YES
|
||||
GITEA_REUSED=NEW
|
||||
CUSTOM_LLM_STATUS=OK
|
||||
MODEL_PRIORITY_STATUS=OK
|
||||
RUNTIME_SELECTOR_STATUS=OK
|
||||
ACCEPTANCE_REPORT=$ARTIFACT
|
||||
KNOWN_ISSUES=NPM is not running on this host, so system nginx currently serves the short links; OpenClaw gateway is active but native openclaw agent execution still needs gateway/provider credentials; host-side DNS resolution for cnnetmkt.top is unreliable, so local validation used --resolve to 127.0.0.1; Docker Hub pull for Gitea was blocked by the existing Docker proxy path and was replaced with official Gitea binary deployment.
|
||||
NEXT_UPDATE_MODE=git_patch_only
|
||||
ZIP_UPDATE_MODE=deprecated
|
||||
|
||||
## Evidence
|
||||
|
||||
- Imported package: /data/aios-gitops/imports/$SOURCE
|
||||
- Package SHA256: dbee5d45aab02aab33e862bb9beb6115b3b43137e0fb5e00c98448ac1ecfd0f9
|
||||
- Git workspace: $WORKSPACE
|
||||
- Baseline branch: main
|
||||
- Baseline tag: $TAG
|
||||
- Git remote: $GIT_URL
|
||||
- Gitea internal version: 1.22.6
|
||||
- API service: aios-gitops-model-api.service on 127.0.0.1:18890
|
||||
- Gitea service: aios-gitea.service on 127.0.0.1:3000
|
||||
- Hermes executor health: ok
|
||||
- One API reused at: 127.0.0.1:13000
|
||||
- WebOps reused container: aios-webops-server-hw-webops-1
|
||||
- Parallel server agents: git_service_agent, entry_proxy_agent, gitops_api_uat_agent, acceptance_report_agent
|
||||
- Parallel agent report: /data/aios-gitops/artifacts/AIOS_V1R1C0_PARALLEL_AGENT_STATUS.json
|
||||
- Final evidence log: $EVIDENCE
|
||||
|
||||
## GitOps Rules
|
||||
|
||||
- ZIP update mode is deprecated after initial import.
|
||||
- Future updates must be git branch + commit + push + webhook.
|
||||
- Patches are represented as git branches and are reviewable by diff.
|
||||
- ChatGPT/Codex write path is /api/git-control-compatible endpoints exposed by the GitOps API.
|
||||
EOF_REPORT
|
||||
|
||||
cp "$ARTIFACT" "$REPORT_IN_REPO"
|
||||
|
||||
git add baseline/current.json versions/V1R1C0.json docs/release/AIOS_V1R1C0_ONLINE_BASELINE_ACCEPTANCE.md
|
||||
git commit -m "docs(gitops): finalize V1R1C0 online baseline acceptance" || true
|
||||
|
||||
admin_user="aios"
|
||||
admin_pass_file="$ROOT/runtime/secrets/gitea-admin-password"
|
||||
if [ -s "$admin_pass_file" ]; then
|
||||
tmp_home="$(mktemp -d)"
|
||||
chmod 700 "$tmp_home"
|
||||
{
|
||||
echo "machine 127.0.0.1"
|
||||
echo "login $admin_user"
|
||||
echo "password $(cat "$admin_pass_file")"
|
||||
} > "$tmp_home/.netrc"
|
||||
chmod 600 "$tmp_home/.netrc"
|
||||
HOME="$tmp_home" git push http://127.0.0.1:3000/aios/aios-v23.git main
|
||||
HOME="$tmp_home" git push http://127.0.0.1:3000/aios/aios-v23.git --tags
|
||||
rm -rf "$tmp_home"
|
||||
fi
|
||||
|
||||
git remote remove origin 2>/dev/null || true
|
||||
git remote add origin "$GIT_URL"
|
||||
git status --short
|
||||
echo "FINALIZE_V1R1C0_GITOPS_ACCEPTANCE_DONE"
|
||||
+113
@@ -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"
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="/data/aios-gitops"
|
||||
WORKSPACE="$ROOT/workspace"
|
||||
LOG="$ROOT/logs/g-cnnetmkt-ingress-fix.log"
|
||||
BACKUP_DIR="$ROOT/backups/nginx-g-cnnetmkt-$(date +%Y%m%d-%H%M%S)"
|
||||
SITE="/etc/nginx/sites-available/aios-user-portal.conf"
|
||||
TRACKED="$WORKSPACE/deploy/nginx/g-cnnetmkt-gitea-server.conf"
|
||||
REPORT="$ROOT/artifacts/CNNETMKT_SSL_INGRESS_STATUS.md"
|
||||
mkdir -p "$ROOT/logs" "$BACKUP_DIR" "$WORKSPACE/deploy/nginx" "$ROOT/artifacts"
|
||||
exec > >(tee -a "$LOG") 2>&1
|
||||
|
||||
echo "== backup =="
|
||||
sudo cp -a "$SITE" "$BACKUP_DIR/aios-user-portal.conf"
|
||||
|
||||
cat > "$TRACKED" <<'NGINX'
|
||||
# AIOS GitOps alias for https://g.cnnetmkt.top/
|
||||
# Note: this server block intentionally keeps /git/ as the Gitea app prefix,
|
||||
# matching the current Gitea ROOT_URL and avoiding a disruptive app.ini rewrite.
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
server_name g.cnnetmkt.top;
|
||||
|
||||
access_log /var/log/nginx/aios-g-cnnetmkt.access.log;
|
||||
error_log /var/log/nginx/aios-g-cnnetmkt.error.log;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/user.cnnetmkt.top/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/user.cnnetmkt.top/privkey.pem;
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||
|
||||
location = / {
|
||||
return 302 /git/;
|
||||
}
|
||||
|
||||
location = /git {
|
||||
return 302 /git/;
|
||||
}
|
||||
|
||||
location /git/ {
|
||||
proxy_pass http://127.0.0.1:3000/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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 $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Prefix /git;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_redirect off;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name g.cnnetmkt.top;
|
||||
|
||||
location = / {
|
||||
return 302 https://$host/git/;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
NGINX
|
||||
|
||||
echo "== install g.cnnetmkt server block =="
|
||||
sudo cp "$TRACKED" /etc/nginx/sites-available/g-cnnetmkt-gitea.conf
|
||||
sudo ln -sfn /etc/nginx/sites-available/g-cnnetmkt-gitea.conf /etc/nginx/sites-enabled/g-cnnetmkt-gitea.conf
|
||||
sudo nginx -t
|
||||
sudo systemctl reload nginx
|
||||
|
||||
echo "== local validation before cert expand =="
|
||||
for h in g.cnnetmkt.top cnnetmkt.top user.cnnetmkt.top; do
|
||||
echo "-- $h /"
|
||||
curl -k -sS -D - -o /tmp/aios-body --resolve "$h:443:127.0.0.1" "https://$h/" | sed -n '1,12p' || true
|
||||
grep -ao '<title>[^<]*' /tmp/aios-body 2>/dev/null | head -1 || true
|
||||
echo "-- $h /git/"
|
||||
curl -k -sS -D - -o /tmp/aios-body --resolve "$h:443:127.0.0.1" "https://$h/git/" | sed -n '1,12p' || true
|
||||
grep -ao '<title>[^<]*' /tmp/aios-body 2>/dev/null | head -1 || true
|
||||
done
|
||||
|
||||
echo "== certbot dry run expand for g/user/portal =="
|
||||
CERTBOT_STATUS="not-run"
|
||||
if sudo certbot certonly --nginx --cert-name user.cnnetmkt.top \
|
||||
-d user.cnnetmkt.top -d portal.cnnetmkt.top -d g.cnnetmkt.top \
|
||||
--expand --dry-run --non-interactive --agree-tos; then
|
||||
CERTBOT_STATUS="dry-run-ok"
|
||||
else
|
||||
CERTBOT_STATUS="dry-run-failed"
|
||||
fi
|
||||
|
||||
if [ "$CERTBOT_STATUS" = "dry-run-ok" ]; then
|
||||
echo "== certbot real expand for g/user/portal =="
|
||||
if sudo certbot certonly --nginx --cert-name user.cnnetmkt.top \
|
||||
-d user.cnnetmkt.top -d portal.cnnetmkt.top -d g.cnnetmkt.top \
|
||||
--expand --non-interactive --agree-tos; then
|
||||
CERTBOT_STATUS="issued-g-user-portal"
|
||||
sudo nginx -t
|
||||
sudo systemctl reload nginx
|
||||
else
|
||||
CERTBOT_STATUS="real-issue-failed"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "== final cert and route validation =="
|
||||
{
|
||||
echo "# cnnetmkt SSL ingress status"
|
||||
echo
|
||||
echo "generated_at=$(date -Is)"
|
||||
echo "g_route_status=installed"
|
||||
echo "certbot_status=$CERTBOT_STATUS"
|
||||
echo "apex_dns_status=blocked_until_cnnetmkt.top_has_A_record_to_1.95.164.183"
|
||||
echo "wildcard_status=blocked_until_dns01_provider_credentials_available"
|
||||
echo "backup_dir=$BACKUP_DIR"
|
||||
echo "tracked_nginx=$TRACKED"
|
||||
echo
|
||||
echo "## validation"
|
||||
for h in g.cnnetmkt.top cnnetmkt.top user.cnnetmkt.top; do
|
||||
echo "### $h"
|
||||
curl -k -sS -D - -o /tmp/aios-body --resolve "$h:443:127.0.0.1" "https://$h/" | sed -n '1,12p' || true
|
||||
grep -ao '<title>[^<]*' /tmp/aios-body 2>/dev/null | head -1 || true
|
||||
echo
|
||||
echo | openssl s_client -connect 127.0.0.1:443 -servername "$h" 2>/dev/null \
|
||||
| openssl x509 -noout -subject -issuer -dates -ext subjectAltName 2>/dev/null || true
|
||||
echo
|
||||
done
|
||||
echo "## certbot"
|
||||
sudo certbot certificates || true
|
||||
} | tee "$REPORT"
|
||||
|
||||
cd "$WORKSPACE"
|
||||
git add deploy/nginx/g-cnnetmkt-gitea-server.conf
|
||||
git commit -m "fix(ingress): add g.cnnetmkt.top GitOps alias" || true
|
||||
|
||||
echo "G_CNNETMKT_INGRESS_FIX_DONE certbot_status=$CERTBOT_STATUS report=$REPORT"
|
||||
Executable
+53
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="/data/aios-gitops"
|
||||
WORKSPACE="$ROOT/workspace"
|
||||
GITEA_BIN="$ROOT/runtime/bin/gitea"
|
||||
GITEA_HOME="$ROOT/runtime/gitea-bin"
|
||||
SECRETS="$ROOT/runtime/secrets"
|
||||
LOG="$ROOT/logs/gitea-repo-push-fix.log"
|
||||
mkdir -p "$ROOT/logs"
|
||||
exec > >(tee -a "$LOG") 2>&1
|
||||
|
||||
admin_user="aios"
|
||||
admin_pass="$(cat "$SECRETS/gitea-admin-password")"
|
||||
|
||||
echo "== endpoint probe =="
|
||||
for path in / /git/ /api/v1/version /git/api/v1/version /user/login /git/user/login; do
|
||||
echo "== ${path} =="
|
||||
curl -sS -i --max-time 8 "http://127.0.0.1:3000${path}" 2>&1 | sed -n '1,18p'
|
||||
done
|
||||
|
||||
echo "== ensure admin user =="
|
||||
"$GITEA_BIN" admin user create \
|
||||
--config "$GITEA_HOME/custom/conf/app.ini" \
|
||||
--username "$admin_user" \
|
||||
--password "$admin_pass" \
|
||||
--email "gitops-bot@cnnetmkt.top" \
|
||||
--admin \
|
||||
--must-change-password=false 2>/dev/null || true
|
||||
|
||||
echo "== create repo by internal API =="
|
||||
if ! curl -fsS -u "$admin_user:$admin_pass" "http://127.0.0.1:3000/api/v1/repos/$admin_user/aios-v23" >/dev/null 2>&1; then
|
||||
curl -fsS -u "$admin_user:$admin_pass" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name":"aios-v23","description":"AIOS V1R1C0 online GitOps baseline","private":false,"auto_init":false,"default_branch":"main"}' \
|
||||
"http://127.0.0.1:3000/api/v1/user/repos" >/dev/null
|
||||
fi
|
||||
curl -fsS -u "$admin_user:$admin_pass" "http://127.0.0.1:3000/api/v1/repos/$admin_user/aios-v23" | python3 -m json.tool | sed -n '1,80p'
|
||||
|
||||
echo "== push main and tags =="
|
||||
cd "$WORKSPACE"
|
||||
push_url="http://${admin_user}:${admin_pass}@127.0.0.1:3000/${admin_user}/aios-v23.git"
|
||||
git push "$push_url" main
|
||||
git push "$push_url" --tags
|
||||
git remote remove origin 2>/dev/null || true
|
||||
git remote add origin "https://cnnetmkt.top/git/${admin_user}/aios-v23.git"
|
||||
|
||||
echo "== verify repo page =="
|
||||
curl -fsS "http://127.0.0.1:3000/${admin_user}/aios-v23" | grep -E "aios-v23|AIOS|V1R1C0" | head -30 || true
|
||||
git remote -v
|
||||
git log --oneline -8
|
||||
git status --short
|
||||
echo "GITEA_REPO_PUSH_OK"
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="/data/aios-gitops"
|
||||
WORKSPACE="$ROOT/workspace"
|
||||
LOG="$ROOT/logs/gitops-api-owner-fix-uat.log"
|
||||
mkdir -p "$ROOT/logs"
|
||||
exec > >(tee -a "$LOG") 2>&1
|
||||
|
||||
echo "== fix ownership =="
|
||||
sudo chown -R aiman:aiman "$ROOT/runtime" "$WORKSPACE" "$ROOT/logs" "$ROOT/artifacts"
|
||||
|
||||
echo "== commit sanitized runner =="
|
||||
cd "$WORKSPACE"
|
||||
git add scripts/hermes_gitops_parallel_agents.py
|
||||
git commit -m "fix(gitops): sanitize parallel agent UAT secret handling" || true
|
||||
|
||||
echo "== restart API =="
|
||||
sudo systemctl restart aios-gitops-model-api.service
|
||||
sleep 3
|
||||
systemctl status aios-gitops-model-api.service --no-pager | sed -n '1,100p'
|
||||
curl -fsS http://127.0.0.1:18890/health
|
||||
echo
|
||||
curl -fsS http://127.0.0.1:18890/api/status
|
||||
echo
|
||||
|
||||
echo "== API UAT =="
|
||||
TOKEN="$(head -1 "$ROOT/runtime/secrets/gitops.tokens")"
|
||||
curl -fsS \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H 'X-AIOS-User: agent-uat' \
|
||||
-H 'X-AIOS-Role: owner' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"username":"agent-uat","role":"owner","tier":"ultra"}' \
|
||||
http://127.0.0.1:18890/api/runtime/select
|
||||
echo
|
||||
curl -fsS \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H 'X-AIOS-User: agent-uat' \
|
||||
-H 'X-AIOS-Role: owner' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"provider_name":"agent-local-openai-compatible","base_url":"http://127.0.0.1:13000/v1","model_name":"agent-test-model","display_name":"Agent Test Model","priority":10,"api_key_secret_ref":"secret://llm/agent-uat/pre-provisioned-test"}' \
|
||||
http://127.0.0.1:18890/api/model/custom/add
|
||||
echo
|
||||
curl -fsS \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H 'X-AIOS-User: agent-uat' \
|
||||
-H 'X-AIOS-Role: owner' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"username":"agent-uat"}' \
|
||||
http://127.0.0.1:18890/api/model/route/resolve
|
||||
echo
|
||||
|
||||
echo "== secret scan =="
|
||||
if grep -R 'redacted-test-key\|sk-[A-Za-z0-9]' -n "$WORKSPACE" "$ROOT/logs" 2>/dev/null; then
|
||||
echo "SECRET_SCAN_FAILED"
|
||||
exit 2
|
||||
fi
|
||||
echo "SECRET_SCAN_OK"
|
||||
|
||||
echo "== git status =="
|
||||
git status --short
|
||||
git log --oneline -8
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="/data/aios-gitops"
|
||||
WORKSPACE="$ROOT/workspace"
|
||||
GITEA_HOME="$ROOT/runtime/gitea-bin"
|
||||
GITEA_BIN="$ROOT/runtime/bin/gitea"
|
||||
GITEA_VERSION="1.22.6"
|
||||
GITEA_URL="https://dl.gitea.com/gitea/${GITEA_VERSION}/gitea-${GITEA_VERSION}-linux-amd64"
|
||||
SECRETS="$ROOT/runtime/secrets"
|
||||
LOG="$ROOT/logs/gitea-binary-install.log"
|
||||
|
||||
mkdir -p "$ROOT/logs" "$SECRETS" "$ROOT/runtime/bin" "$GITEA_HOME/custom/conf" "$GITEA_HOME/data" "$GITEA_HOME/log" "$WORKSPACE/deploy/gitea-binary"
|
||||
exec > >(tee -a "$LOG") 2>&1
|
||||
|
||||
cd "$WORKSPACE"
|
||||
|
||||
echo "== write tracked deployment manifests =="
|
||||
cat > deploy/gitea-binary/app.ini.template <<'EOF'
|
||||
APP_NAME = AIOS GitOps
|
||||
RUN_USER = aiman
|
||||
WORK_PATH = /data/aios-gitops/runtime/gitea-bin
|
||||
|
||||
[server]
|
||||
PROTOCOL = http
|
||||
HTTP_ADDR = 127.0.0.1
|
||||
HTTP_PORT = 3000
|
||||
DOMAIN = cnnetmkt.top
|
||||
ROOT_URL = https://cnnetmkt.top/git/
|
||||
SERVE_FROM_SUB_PATH = true
|
||||
DISABLE_SSH = true
|
||||
OFFLINE_MODE = false
|
||||
|
||||
[database]
|
||||
DB_TYPE = sqlite3
|
||||
PATH = /data/aios-gitops/runtime/gitea-bin/data/gitea.db
|
||||
|
||||
[repository]
|
||||
ROOT = /data/aios-gitops/runtime/gitea-bin/data/repositories
|
||||
DEFAULT_BRANCH = main
|
||||
|
||||
[security]
|
||||
INSTALL_LOCK = true
|
||||
SECRET_KEY = __SECRET_KEY__
|
||||
INTERNAL_TOKEN = __INTERNAL_TOKEN__
|
||||
|
||||
[service]
|
||||
DISABLE_REGISTRATION = true
|
||||
REQUIRE_SIGNIN_VIEW = false
|
||||
|
||||
[webhook]
|
||||
ALLOWED_HOST_LIST = *
|
||||
|
||||
[log]
|
||||
MODE = console
|
||||
LEVEL = Info
|
||||
ROOT_PATH = /data/aios-gitops/runtime/gitea-bin/log
|
||||
EOF
|
||||
|
||||
cat > deploy/gitea-binary/aios-gitea.service <<EOF
|
||||
[Unit]
|
||||
Description=AIOS GitOps Gitea
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=aiman
|
||||
Group=aiman
|
||||
WorkingDirectory=$GITEA_HOME
|
||||
ExecStart=$GITEA_BIN web --config $GITEA_HOME/custom/conf/app.ini
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
Environment=USER=aiman
|
||||
Environment=HOME=/home/aiman
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
git add deploy/gitea-binary/app.ini.template deploy/gitea-binary/aios-gitea.service
|
||||
git commit -m "chore(gitops): add binary Gitea deployment manifests" || true
|
||||
|
||||
echo "== download gitea binary =="
|
||||
if [ ! -x "$GITEA_BIN" ]; then
|
||||
tmp="$GITEA_BIN.tmp"
|
||||
curl -fL --retry 3 --connect-timeout 20 -o "$tmp" "$GITEA_URL"
|
||||
chmod 755 "$tmp"
|
||||
mv "$tmp" "$GITEA_BIN"
|
||||
fi
|
||||
"$GITEA_BIN" --version
|
||||
|
||||
echo "== render runtime app.ini =="
|
||||
secret_key_file="$SECRETS/gitea-secret-key"
|
||||
internal_token_file="$SECRETS/gitea-internal-token"
|
||||
admin_pass_file="$SECRETS/gitea-admin-password"
|
||||
api_token_file="$SECRETS/gitea-api-token"
|
||||
for f in "$secret_key_file" "$internal_token_file" "$admin_pass_file"; do
|
||||
if [ ! -s "$f" ]; then
|
||||
openssl rand -hex 32 > "$f"
|
||||
chmod 600 "$f"
|
||||
fi
|
||||
done
|
||||
sed \
|
||||
-e "s#__SECRET_KEY__#$(cat "$secret_key_file")#g" \
|
||||
-e "s#__INTERNAL_TOKEN__#$(cat "$internal_token_file")#g" \
|
||||
deploy/gitea-binary/app.ini.template > "$GITEA_HOME/custom/conf/app.ini"
|
||||
chmod 600 "$GITEA_HOME/custom/conf/app.ini"
|
||||
sudo chown -R aiman:aiman "$ROOT/runtime" "$WORKSPACE/deploy/gitea-binary"
|
||||
|
||||
echo "== install systemd service =="
|
||||
sudo cp deploy/gitea-binary/aios-gitea.service /etc/systemd/system/aios-gitea.service
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now aios-gitea.service
|
||||
|
||||
echo "== wait gitea =="
|
||||
for i in $(seq 1 90); do
|
||||
if curl -fsS http://127.0.0.1:3000/git/api/healthz >/dev/null 2>&1 || curl -fsS http://127.0.0.1:3000/git/ >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
systemctl is-active aios-gitea.service
|
||||
curl -fsS http://127.0.0.1:3000/git/api/healthz || true
|
||||
echo
|
||||
|
||||
echo "== create admin user and repo =="
|
||||
admin_user="aios"
|
||||
admin_email="gitops-bot@cnnetmkt.top"
|
||||
admin_pass="$(cat "$admin_pass_file")"
|
||||
"$GITEA_BIN" admin user create \
|
||||
--config "$GITEA_HOME/custom/conf/app.ini" \
|
||||
--username "$admin_user" \
|
||||
--password "$admin_pass" \
|
||||
--email "$admin_email" \
|
||||
--admin \
|
||||
--must-change-password=false 2>/dev/null || true
|
||||
|
||||
if [ ! -s "$api_token_file" ]; then
|
||||
"$GITEA_BIN" admin user generate-access-token \
|
||||
--config "$GITEA_HOME/custom/conf/app.ini" \
|
||||
--username "$admin_user" \
|
||||
--token-name aios-gitops-push \
|
||||
--scopes write:repository,read:repository,write:user,read:user \
|
||||
| awk '/Access token was successfully created/ {print $NF}' > "$api_token_file.tmp" || true
|
||||
if [ -s "$api_token_file.tmp" ]; then
|
||||
mv "$api_token_file.tmp" "$api_token_file"
|
||||
chmod 600 "$api_token_file"
|
||||
else
|
||||
rm -f "$api_token_file.tmp"
|
||||
fi
|
||||
fi
|
||||
|
||||
repo_api="http://127.0.0.1:3000/git/api/v1/user/repos"
|
||||
if ! curl -fsS -u "$admin_user:$admin_pass" "http://127.0.0.1:3000/git/api/v1/repos/$admin_user/aios-v23" >/dev/null 2>&1; then
|
||||
curl -fsS -u "$admin_user:$admin_pass" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name":"aios-v23","description":"AIOS V1R1C0 online GitOps baseline","private":false,"auto_init":false,"default_branch":"main"}' \
|
||||
"$repo_api" >/dev/null
|
||||
fi
|
||||
|
||||
echo "== push repository =="
|
||||
push_url="http://${admin_user}:${admin_pass}@127.0.0.1:3000/git/${admin_user}/aios-v23.git"
|
||||
git push "$push_url" main
|
||||
git push "$push_url" --tags
|
||||
git remote remove origin 2>/dev/null || true
|
||||
git remote add origin "https://cnnetmkt.top/git/${admin_user}/aios-v23.git"
|
||||
|
||||
echo "== verify gitea repo =="
|
||||
curl -fsS "http://127.0.0.1:3000/git/${admin_user}/aios-v23" | grep -E "aios-v23|AIOS" | head -20 || true
|
||||
git status --short
|
||||
git log --oneline -8
|
||||
git remote -v
|
||||
|
||||
echo "GITEA_BINARY_INSTALL_OK version=$GITEA_VERSION url=https://cnnetmkt.top/git/${admin_user}/aios-v23"
|
||||
+252
@@ -0,0 +1,252 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [ "${EUID:-$(id -u)}" -ne 0 ]; then
|
||||
exec sudo bash "$0" "$@"
|
||||
fi
|
||||
|
||||
ROOT="/data/aios-gitops"
|
||||
WORKSPACE="$ROOT/workspace"
|
||||
LEGO_BIN="$ROOT/runtime/bin/lego"
|
||||
LEGO_PATH="/etc/letsencrypt/lego-cnnetmkt"
|
||||
LIVE_DIR="/etc/letsencrypt/live/cnnetmkt-wildcard"
|
||||
ENV_FILE="/etc/letsencrypt/huaweicloud-cnnetmkt.env"
|
||||
LOG="$ROOT/logs/cnnetmkt-wildcard-lego.log"
|
||||
REPORT="$ROOT/artifacts/CNNETMKT_WILDCARD_CERT_STATUS.md"
|
||||
TRACKED="$WORKSPACE/deploy/nginx/cnnetmkt-wildcard-certificate.md"
|
||||
BACKUP_DIR="$ROOT/backups/nginx-wildcard-$(date +%Y%m%d-%H%M%S)"
|
||||
EMAIL="admin@cnnetmkt.top"
|
||||
CERT_NAME="cnnetmkt-wildcard"
|
||||
LEGO_VERSION="v5.2.2"
|
||||
LEGO_URL="https://github.com/go-acme/lego/releases/download/${LEGO_VERSION}/lego_${LEGO_VERSION}_linux_amd64.tar.gz"
|
||||
mkdir -p "$ROOT/logs" "$ROOT/runtime/bin" "$ROOT/artifacts" "$WORKSPACE/deploy/nginx" "$BACKUP_DIR"
|
||||
exec > >(tee -a "$LOG") 2>&1
|
||||
|
||||
redact() {
|
||||
sed -E 's/(ACCESS_KEY_ID=).+/\1[REDACTED]/; s/(SECRET_ACCESS_KEY=).+/\1[REDACTED]/'
|
||||
}
|
||||
|
||||
echo "== preflight =="
|
||||
hostname
|
||||
date -Is
|
||||
test -s "$ENV_FILE"
|
||||
sudo chmod 600 "$ENV_FILE"
|
||||
sudo chown root:root "$ENV_FILE"
|
||||
echo "env_file=$ENV_FILE"
|
||||
sudo sed -n '1,20p' "$ENV_FILE" | redact
|
||||
|
||||
echo "== install lego =="
|
||||
if [ ! -x "$LEGO_BIN" ]; then
|
||||
tmp="$(mktemp -d)"
|
||||
curl -fL --retry 3 --connect-timeout 30 -o "$tmp/lego.tar.gz" "$LEGO_URL"
|
||||
tar -xzf "$tmp/lego.tar.gz" -C "$tmp"
|
||||
install -m 0755 "$tmp/lego" "$LEGO_BIN"
|
||||
rm -rf "$tmp"
|
||||
fi
|
||||
"$LEGO_BIN" --version
|
||||
|
||||
echo "== load env =="
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
. "$ENV_FILE"
|
||||
set +a
|
||||
: "${HUAWEICLOUD_ACCESS_KEY_ID:?missing access key id}"
|
||||
: "${HUAWEICLOUD_SECRET_ACCESS_KEY:?missing secret access key}"
|
||||
: "${HUAWEICLOUD_REGION:?missing huawei region}"
|
||||
export HUAWEICLOUD_HTTP_TIMEOUT="${HUAWEICLOUD_HTTP_TIMEOUT:-30}"
|
||||
export HUAWEICLOUD_PROPAGATION_TIMEOUT="${HUAWEICLOUD_PROPAGATION_TIMEOUT:-180}"
|
||||
export HUAWEICLOUD_POLLING_INTERVAL="${HUAWEICLOUD_POLLING_INTERVAL:-5}"
|
||||
export HUAWEICLOUD_TTL="${HUAWEICLOUD_TTL:-300}"
|
||||
|
||||
echo "== DNS state =="
|
||||
for n in cnnetmkt.top g.cnnetmkt.top user.cnnetmkt.top portal.cnnetmkt.top; do
|
||||
echo "-- $n"
|
||||
dig +time=5 +tries=1 +short "$n" A || true
|
||||
done
|
||||
|
||||
echo "== staging DNS-01 =="
|
||||
STAGING_STATUS="failed"
|
||||
if sudo --preserve-env=HUAWEICLOUD_ACCESS_KEY_ID,HUAWEICLOUD_SECRET_ACCESS_KEY,HUAWEICLOUD_REGION,HUAWEICLOUD_HTTP_TIMEOUT,HUAWEICLOUD_PROPAGATION_TIMEOUT,HUAWEICLOUD_POLLING_INTERVAL,HUAWEICLOUD_TTL \
|
||||
"$LEGO_BIN" run --path "$LEGO_PATH-staging" --email "$EMAIL" --accept-tos \
|
||||
--server "https://acme-staging-v02.api.letsencrypt.org/directory" \
|
||||
--dns huaweicloud --dns.propagation.wait 120s --cert.name "$CERT_NAME" --domains "cnnetmkt.top" --domains "*.cnnetmkt.top"; then
|
||||
STAGING_STATUS="ok"
|
||||
fi
|
||||
echo "staging_status=$STAGING_STATUS"
|
||||
if [ "$STAGING_STATUS" != "ok" ]; then
|
||||
echo "STAGING_DNS01_FAILED"
|
||||
exit 20
|
||||
fi
|
||||
|
||||
echo "== production DNS-01 =="
|
||||
PROD_STATUS="failed"
|
||||
if sudo --preserve-env=HUAWEICLOUD_ACCESS_KEY_ID,HUAWEICLOUD_SECRET_ACCESS_KEY,HUAWEICLOUD_REGION,HUAWEICLOUD_HTTP_TIMEOUT,HUAWEICLOUD_PROPAGATION_TIMEOUT,HUAWEICLOUD_POLLING_INTERVAL,HUAWEICLOUD_TTL \
|
||||
"$LEGO_BIN" run --path "$LEGO_PATH" --email "$EMAIL" --accept-tos \
|
||||
--dns huaweicloud --dns.propagation.wait 120s --cert.name "$CERT_NAME" --domains "cnnetmkt.top" --domains "*.cnnetmkt.top"; then
|
||||
PROD_STATUS="issued"
|
||||
else
|
||||
sudo --preserve-env=HUAWEICLOUD_ACCESS_KEY_ID,HUAWEICLOUD_SECRET_ACCESS_KEY,HUAWEICLOUD_REGION,HUAWEICLOUD_HTTP_TIMEOUT,HUAWEICLOUD_PROPAGATION_TIMEOUT,HUAWEICLOUD_POLLING_INTERVAL,HUAWEICLOUD_TTL \
|
||||
"$LEGO_BIN" run --path "$LEGO_PATH" --email "$EMAIL" --accept-tos \
|
||||
--dns huaweicloud --dns.propagation.wait 120s --cert.name "$CERT_NAME" --domains "cnnetmkt.top" --domains "*.cnnetmkt.top" --renew-days 60 --renew-force
|
||||
PROD_STATUS="renewed"
|
||||
fi
|
||||
|
||||
echo "== install cert material =="
|
||||
sudo mkdir -p "$LIVE_DIR"
|
||||
cert_file="$(sudo find "$LEGO_PATH/certificates" -maxdepth 1 -type f -name '*.crt' ! -name '*.issuer.crt' | head -1)"
|
||||
key_file="$(sudo find "$LEGO_PATH/certificates" -maxdepth 1 -type f -name '*.key' | head -1)"
|
||||
issuer_file="$(sudo find "$LEGO_PATH/certificates" -maxdepth 1 -type f -name '*.issuer.crt' | head -1 || true)"
|
||||
test -n "$cert_file"
|
||||
test -n "$key_file"
|
||||
sudo cp "$key_file" "$LIVE_DIR/privkey.pem"
|
||||
if [ -n "${issuer_file:-}" ] && [ -f "$issuer_file" ]; then
|
||||
sudo sh -c "cat '$cert_file' '$issuer_file' > '$LIVE_DIR/fullchain.pem'"
|
||||
sudo cp "$issuer_file" "$LIVE_DIR/chain.pem"
|
||||
else
|
||||
sudo cp "$cert_file" "$LIVE_DIR/fullchain.pem"
|
||||
fi
|
||||
sudo chmod 644 "$LIVE_DIR/fullchain.pem" "$LIVE_DIR/chain.pem" 2>/dev/null || true
|
||||
sudo chmod 600 "$LIVE_DIR/privkey.pem"
|
||||
|
||||
echo "== verify cert material =="
|
||||
sudo openssl x509 -in "$LIVE_DIR/fullchain.pem" -noout -subject -issuer -dates -ext subjectAltName
|
||||
|
||||
echo "== patch nginx cert paths =="
|
||||
sudo cp -a /etc/nginx/sites-available/aios-user-portal.conf "$BACKUP_DIR/aios-user-portal.conf"
|
||||
sudo cp -a /etc/nginx/sites-available/g-cnnetmkt-gitea.conf "$BACKUP_DIR/g-cnnetmkt-gitea.conf" 2>/dev/null || true
|
||||
sudo python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
|
||||
files = [
|
||||
Path("/etc/nginx/sites-available/aios-user-portal.conf"),
|
||||
Path("/etc/nginx/sites-available/g-cnnetmkt-gitea.conf"),
|
||||
]
|
||||
for path in files:
|
||||
if not path.exists():
|
||||
continue
|
||||
text = path.read_text()
|
||||
text = text.replace(
|
||||
"/etc/letsencrypt/live/user.cnnetmkt.top/fullchain.pem",
|
||||
"/etc/letsencrypt/live/cnnetmkt-wildcard/fullchain.pem",
|
||||
)
|
||||
text = text.replace(
|
||||
"/etc/letsencrypt/live/user.cnnetmkt.top/privkey.pem",
|
||||
"/etc/letsencrypt/live/cnnetmkt-wildcard/privkey.pem",
|
||||
)
|
||||
path.write_text(text)
|
||||
PY
|
||||
|
||||
sudo nginx -t
|
||||
sudo systemctl reload nginx
|
||||
|
||||
echo "== validation =="
|
||||
for h in cnnetmkt.top g.cnnetmkt.top user.cnnetmkt.top portal.cnnetmkt.top; do
|
||||
echo "-- $h cert"
|
||||
echo | openssl s_client -connect 127.0.0.1:443 -servername "$h" 2>/dev/null \
|
||||
| openssl x509 -noout -subject -issuer -dates -ext subjectAltName 2>/dev/null || true
|
||||
echo "-- $h /"
|
||||
curl -k -sS -D - -o /tmp/cnnetmkt-body --resolve "$h:443:127.0.0.1" "https://$h/" | sed -n '1,12p' || true
|
||||
grep -ao '<title>[^<]*' /tmp/cnnetmkt-body 2>/dev/null | head -1 || true
|
||||
echo "-- $h /git/"
|
||||
curl -k -sS -D - -o /tmp/cnnetmkt-body --resolve "$h:443:127.0.0.1" "https://$h/git/" | sed -n '1,12p' || true
|
||||
grep -ao '<title>[^<]*' /tmp/cnnetmkt-body 2>/dev/null | head -1 || true
|
||||
done
|
||||
|
||||
echo "== write renewal helper =="
|
||||
sudo tee /usr/local/sbin/renew-cnnetmkt-wildcard.sh >/dev/null <<'SH'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
ROOT="/data/aios-gitops"
|
||||
LEGO_BIN="$ROOT/runtime/bin/lego"
|
||||
LEGO_PATH="/etc/letsencrypt/lego-cnnetmkt"
|
||||
LIVE_DIR="/etc/letsencrypt/live/cnnetmkt-wildcard"
|
||||
ENV_FILE="/etc/letsencrypt/huaweicloud-cnnetmkt.env"
|
||||
set -a
|
||||
. "$ENV_FILE"
|
||||
set +a
|
||||
sudo --preserve-env=HUAWEICLOUD_ACCESS_KEY_ID,HUAWEICLOUD_SECRET_ACCESS_KEY,HUAWEICLOUD_REGION,HUAWEICLOUD_HTTP_TIMEOUT,HUAWEICLOUD_PROPAGATION_TIMEOUT,HUAWEICLOUD_POLLING_INTERVAL,HUAWEICLOUD_TTL \
|
||||
"$LEGO_BIN" run --path "$LEGO_PATH" --email "admin@cnnetmkt.top" --accept-tos \
|
||||
--dns huaweicloud --dns.propagation.wait 120s --cert.name "cnnetmkt-wildcard" --domains "cnnetmkt.top" --domains "*.cnnetmkt.top" --renew-days 45
|
||||
cert_file="$(find "$LEGO_PATH/certificates" -maxdepth 1 -type f -name '*.crt' ! -name '*.issuer.crt' | head -1)"
|
||||
key_file="$(find "$LEGO_PATH/certificates" -maxdepth 1 -type f -name '*.key' | head -1)"
|
||||
cp "$cert_file" "$LIVE_DIR/fullchain.pem"
|
||||
cp "$key_file" "$LIVE_DIR/privkey.pem"
|
||||
chmod 644 "$LIVE_DIR/fullchain.pem"
|
||||
chmod 600 "$LIVE_DIR/privkey.pem"
|
||||
nginx -t
|
||||
systemctl reload nginx
|
||||
SH
|
||||
sudo chmod 700 /usr/local/sbin/renew-cnnetmkt-wildcard.sh
|
||||
|
||||
sudo tee /etc/systemd/system/cnnetmkt-wildcard-renew.service >/dev/null <<'UNIT'
|
||||
[Unit]
|
||||
Description=Renew cnnetmkt wildcard certificate via Huawei Cloud DNS
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/sbin/renew-cnnetmkt-wildcard.sh
|
||||
UNIT
|
||||
|
||||
sudo tee /etc/systemd/system/cnnetmkt-wildcard-renew.timer >/dev/null <<'UNIT'
|
||||
[Unit]
|
||||
Description=Daily cnnetmkt wildcard certificate renewal check
|
||||
|
||||
[Timer]
|
||||
OnCalendar=daily
|
||||
RandomizedDelaySec=2h
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
UNIT
|
||||
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now cnnetmkt-wildcard-renew.timer
|
||||
|
||||
cat > "$TRACKED" <<EOF_TRACKED
|
||||
# cnnetmkt wildcard certificate
|
||||
|
||||
generated_at=$(date -Is)
|
||||
issuer=Let's Encrypt via lego DNS-01 Huawei Cloud
|
||||
domains=cnnetmkt.top,*.cnnetmkt.top
|
||||
live_dir=$LIVE_DIR
|
||||
nginx_sites=/etc/nginx/sites-available/aios-user-portal.conf,/etc/nginx/sites-available/g-cnnetmkt-gitea.conf
|
||||
renew_service=cnnetmkt-wildcard-renew.service
|
||||
renew_timer=cnnetmkt-wildcard-renew.timer
|
||||
secret_env=$ENV_FILE
|
||||
backup_dir=$BACKUP_DIR
|
||||
status=$PROD_STATUS
|
||||
EOF_TRACKED
|
||||
|
||||
cat > "$REPORT" <<EOF_REPORT
|
||||
# CNNETMKT wildcard certificate status
|
||||
|
||||
generated_at=$(date -Is)
|
||||
status=$PROD_STATUS
|
||||
staging_status=$STAGING_STATUS
|
||||
dns_provider=huaweicloud
|
||||
secret_storage=$ENV_FILE
|
||||
certificate_live_dir=$LIVE_DIR
|
||||
nginx_status=loaded
|
||||
backup_dir=$BACKUP_DIR
|
||||
renew_timer=cnnetmkt-wildcard-renew.timer
|
||||
|
||||
## certificate
|
||||
|
||||
$(sudo openssl x509 -in "$LIVE_DIR/fullchain.pem" -noout -subject -issuer -dates -ext subjectAltName)
|
||||
|
||||
## routes
|
||||
|
||||
- https://cnnetmkt.top/
|
||||
- https://cnnetmkt.top/git/
|
||||
- https://g.cnnetmkt.top/
|
||||
- https://g.cnnetmkt.top/git/
|
||||
- https://user.cnnetmkt.top/
|
||||
EOF_REPORT
|
||||
|
||||
cd "$WORKSPACE"
|
||||
chown -R aiman:aiman "$WORKSPACE/deploy/nginx" "$ROOT/artifacts" "$ROOT/logs"
|
||||
runuser -u aiman -- git add deploy/nginx/cnnetmkt-wildcard-certificate.md
|
||||
runuser -u aiman -- git commit -m "fix(ingress): switch cnnetmkt to wildcard certificate" || true
|
||||
|
||||
echo "CNNETMKT_WILDCARD_DONE status=$PROD_STATUS report=$REPORT"
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
for v in 1.22.6 1.22.5 1.22.4 1.22.3 1.22.2 1.22.1 1.22.0; do
|
||||
url="https://dl.gitea.com/gitea/${v}/gitea-${v}-linux-amd64"
|
||||
echo "== ${v} ${url} =="
|
||||
curl -I --max-time 20 "$url" 2>&1 | sed -n '1,16p'
|
||||
done
|
||||
+35
@@ -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 @@
|
||||
{
|
||||
"id": "patch-1783338750144-bf32d345",
|
||||
"patch_name": "gitops-write-auth-smoke-20260706T115230Z",
|
||||
"branch": "patch/gitops-write-auth-smoke-20260706T115230Z",
|
||||
"risk": "low",
|
||||
"description": "GitOps write auth smoke using secret_ref token",
|
||||
"created_by": "gitops-write-bot",
|
||||
"created_at": "2026-07-06T11:52:30Z"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Executable
+19
@@ -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"
|
||||
Executable
+112
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import concurrent.futures
|
||||
import json
|
||||
import subprocess
|
||||
import time
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path("/data/aios-gitops")
|
||||
AGENT_DIR = ROOT / "runtime" / "agents"
|
||||
REPORT = ROOT / "artifacts" / "CNNETMKT_SSL_PARALLEL_AGENT_STATUS.json"
|
||||
HERMES_EXECUTE = "http://127.0.0.1:18792/internal/hermes/execute"
|
||||
|
||||
|
||||
def run(cmd: str, timeout: int = 90) -> dict:
|
||||
p = subprocess.run(cmd, shell=True, text=True, capture_output=True, timeout=timeout)
|
||||
return {
|
||||
"cmd": cmd,
|
||||
"returncode": p.returncode,
|
||||
"stdout": p.stdout[-12000:],
|
||||
"stderr": p.stderr[-12000:],
|
||||
}
|
||||
|
||||
|
||||
def hermes_plan(name: str, prompt: str) -> dict:
|
||||
payload = {
|
||||
"route_id": f"ssl-hermes-{time.strftime('%Y%m%d-%H%M%S')}-{name[:8]}",
|
||||
"route_target": "hermes",
|
||||
"route_reason": "server_ssl_ingress_validation",
|
||||
"channel": "openclaw-weixin",
|
||||
"source": "server_parallel_agent",
|
||||
"tenant_id": "cnnetmkt",
|
||||
"user_id": f"agent:{name}",
|
||||
"user_prompt": prompt,
|
||||
}
|
||||
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||
req = urllib.request.Request(HERMES_EXECUTE, data=data, method="POST", headers={"Content-Type": "application/json"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=45) as resp:
|
||||
raw = resp.read().decode("utf-8", errors="replace")
|
||||
return {"ok": resp.status < 300, "status": resp.status, "response": json.loads(raw or "{}")}
|
||||
except Exception as exc:
|
||||
return {"ok": False, "error": f"{type(exc).__name__}: {exc}"}
|
||||
|
||||
|
||||
TASKS = {
|
||||
"ssl_cert_agent": {
|
||||
"prompt": "Validate cnnetmkt SSL certificate coverage and identify remaining wildcard/apex blockers.",
|
||||
"commands": [
|
||||
"certbot certificates 2>&1 | sed -n '1,160p'",
|
||||
"for h in g.cnnetmkt.top cnnetmkt.top user.cnnetmkt.top; do echo --$h; echo | openssl s_client -connect 127.0.0.1:443 -servername $h 2>/dev/null | openssl x509 -noout -subject -issuer -dates -ext subjectAltName 2>/dev/null || true; done",
|
||||
],
|
||||
},
|
||||
"ssl_nginx_agent": {
|
||||
"prompt": "Validate Nginx routing for g.cnnetmkt.top and GitOps short links without changing config.",
|
||||
"commands": [
|
||||
"nginx -t 2>&1",
|
||||
"sudo nginx -T 2>/dev/null | grep -n -A70 -B5 'server_name g.cnnetmkt.top' | sed -n '1,120p'",
|
||||
"for h in g.cnnetmkt.top cnnetmkt.top user.cnnetmkt.top; do echo --$h; curl -k -sS -D - -o /tmp/ssl-agent-body --resolve $h:443:127.0.0.1 https://$h/ | sed -n '1,12p'; grep -ao '<title>[^<]*' /tmp/ssl-agent-body | head -1 || true; done",
|
||||
],
|
||||
},
|
||||
"ssl_external_agent": {
|
||||
"prompt": "Validate externally reachable HTTPS URLs for g.cnnetmkt.top and record cnnetmkt.top failure.",
|
||||
"commands": [
|
||||
"for u in https://g.cnnetmkt.top/ https://g.cnnetmkt.top/git/ https://user.cnnetmkt.top/ https://cnnetmkt.top/ https://cnnetmkt.top/git/; do echo ==$u==; curl -sS -D - -o /tmp/ssl-agent-ext --connect-timeout 20 --max-time 30 $u | sed -n '1,12p' || true; grep -ao '<title>[^<]*' /tmp/ssl-agent-ext | head -1 || true; done",
|
||||
],
|
||||
},
|
||||
"ssl_gitops_agent": {
|
||||
"prompt": "Validate GitOps repository contains the SSL ingress change and evidence report.",
|
||||
"commands": [
|
||||
"cd /data/aios-gitops/workspace && git status --short && git --no-pager log --oneline -8",
|
||||
"cd /data/aios-gitops/workspace && test -f deploy/nginx/g-cnnetmkt-gitea-server.conf && test -f docs/ops/CNNETMKT_SSL_INGRESS_STATUS.md && echo GITOPS_SSL_DOCS_OK",
|
||||
"cd /data/aios-gitops/workspace && git ls-remote --heads http://127.0.0.1:3000/aios/aios-v23.git main",
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def execute_agent(name: str, spec: dict) -> dict:
|
||||
AGENT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
result = {
|
||||
"name": name,
|
||||
"started_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
|
||||
"hermes_plan": hermes_plan(name, spec["prompt"]),
|
||||
"commands": [],
|
||||
}
|
||||
for cmd in spec["commands"]:
|
||||
result["commands"].append(run(cmd))
|
||||
result["finished_at"] = time.strftime("%Y-%m-%dT%H:%M:%S%z")
|
||||
result["status"] = "completed"
|
||||
(AGENT_DIR / f"{name}.json").write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
return result
|
||||
|
||||
|
||||
def main() -> int:
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=len(TASKS)) as pool:
|
||||
futures = {pool.submit(execute_agent, name, spec): name for name, spec in TASKS.items()}
|
||||
results = [future.result() for future in concurrent.futures.as_completed(futures)]
|
||||
summary = {
|
||||
"generated_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
|
||||
"agent_count": len(results),
|
||||
"agents": [{"name": r["name"], "status": r["status"], "log": str(AGENT_DIR / f"{r['name']}.json")} for r in results],
|
||||
}
|
||||
REPORT.write_text(json.dumps({"summary": summary, "results": results}, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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")"
|
||||
@@ -8,5 +8,5 @@
|
||||
"zip_update_mode": "deprecated",
|
||||
"baseline_tag": "V1R1C0-baseline-20260706",
|
||||
"git_url": "https://cnnetmkt.top/git/aios/aios-v23.git",
|
||||
"updated_at": "2026-07-06T07:13:43.274366+00:00"
|
||||
"updated_at": "2026-07-06T07:16:01.842715+00:00"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
Reference in New Issue
Block a user