Compare commits

..
Author SHA1 Message Date
aios f3f7db29b1 chore: preserve pending wechat backend workspace change before agent policy patch 2026-07-07 08:03:29 +08:00
aios 09c33eb448 docs(architecture): add remote Codex multi-agent orchestration design 2026-07-07 02:38:03 +08:00
aios f7abb8066b ops(codex): issue V1R1 WeChat backend fix directive 2026-07-07 02:33:50 +08:00
aios efc9ffcdc6 fix(notify): route worker delivery through pluggable backend 2026-07-07 02:31:47 +08:00
aios 05b0d7fb80 feat(notify): add pluggable WeChat send backend 2026-07-07 02:30:00 +08:00
aios 13c2d24e76 fix(notify): make OpenClaw channel name runtime-configurable 2026-07-07 02:20:13 +08:00
aios fbb17597d1 fix(notify): support runtime OpenClaw gateway auth options 2026-07-07 02:13:19 +08:00
aios 7f8261a4b6 fix(notify): use unique index for idempotency migration 2026-07-07 02:02:16 +08:00
aios 839e567060 fix(notify): make message_outbox schema migration backward-compatible 2026-07-07 01:58:22 +08:00
aios db8d9c694d feat(notify): add OpenClaw-backed message_outbox worker 2026-07-07 01:48:39 +08:00
aios 7fc2da56d2 feat(notify): add WebOps message outbox schema helper 2026-07-07 01:42:48 +08:00
aios ae30d716f9 feat(notify): add message_outbox enqueue helper 2026-07-07 01:31:55 +08:00
aios d96086b345 feat(notify): add message_outbox queue schema 2026-07-07 01:24:29 +08:00
aios 6c7928c096 feat(v1r1): document existing wechat runtime integration 2026-07-07 01:20:46 +08:00
aios 5585856cd3 feat(v1r1): add endpoint discovery for personal wechat runtime 2026-07-07 01:17:47 +08:00
aios 9ffcf93fcd feat(deploy): install v1r1 wechat worker service 2026-07-07 01:17:43 +08:00
aios 94db629904 deploy(v1r1): add personal wechat outbound worker service 2026-07-07 01:17:36 +08:00
aios 7f9d664d6b feat(v1r1): add personal wechat outbound worker 2026-07-07 01:14:58 +08:00
aios 90e05be0a9 feat(execution): add V1R1 execution API runtime contract 2026-07-07 01:09:39 +08:00
aios 70fbed1050 fix(deploy): create aios runtime user before starting daemon 2026-07-07 00:36:12 +08:00
aios ab268789ee approval(owner): preapprove V1R1C5 main entry runbook 2026-07-07 00:33:12 +08:00
aios 35e4a5350f feat(governance): add self-governing devops policy 2026-07-07 00:27:28 +08:00
aios dc5dae2a51 deploy(consensus): add systemd unit for consensus controller 2026-07-07 00:25:51 +08:00
aios 73c5b2330e feat(consensus): add multi-agent consensus controller runtime 2026-07-07 00:25:48 +08:00
aios 8aa436043d feat(consensus): add multi-agent production consensus policy 2026-07-07 00:25:42 +08:00
aios 3d4e630582 ops(openclaw): issue autonomous GitOps deployment directive 2026-07-07 00:24:12 +08:00
aios 3b609a8e57 feat(ingress): add event ingestion server 2026-07-07 00:21:40 +08:00
aios 0a4e2b741d deploy: add one-click installation script 2026-07-07 00:21:35 +08:00
aios e68313f95e deploy(systemd): add autonomous daemon service 2026-07-07 00:21:31 +08:00
aios e62ff487ab deploy(systemd): add autonomous daemon env template 2026-07-07 00:21:27 +08:00
aios 680a2f4f2b fix(ci): correct exit status handling in merge gate 2026-07-07 00:14:26 +08:00
aios 8d6d92bcec feat(version): add V1R1C5 baseline proposal 2026-07-07 00:14:19 +08:00
aios 3a5fc56c9d feat(daemon): add autonomous event bus and notification daemon 2026-07-07 00:11:20 +08:00
aios 6327664d22 feat(codex): add low-impact self-optimizing orchestrator config 2026-07-07 00:09:20 +08:00
aios 228d8fedf2 feat(codex): add bounded autonomous production policy 2026-07-07 00:03:33 +08:00
aios f94af50497 feat(skill): add auto-loaded gitops validation skill 2026-07-07 00:02:22 +08:00
aios 2a81d8ced4 ci(codex): add autonomous merge gate executor 2026-07-07 00:02:18 +08:00
aios 759e343075 feat(codex): add autonomous merge gate policy 2026-07-07 00:02:14 +08:00
aios bef2de2c66 feat(ci): add self-healing gitops configuration 2026-07-07 00:01:00 +08:00
aios e8bd867c33 ci(codex): add validation runner script 2026-07-07 00:00:56 +08:00
aios 1953cacebc test(codex): add platform UI acceptance test cases 2026-07-07 00:00:49 +08:00
aios 43be07af73 docs(codex): add GitOps platform UI validation playbook 2026-07-06 23:58:42 +08:00
aios bdea3ef5ba feat(portal): implement platform control plane runtime 2026-07-06 23:51:40 +08:00
aios 76848939e9 feat(portal): add enterprise GitOps console layout 2026-07-06 23:49:10 +08:00
aios f2a08293dd chore(chatgpt): update overlays/user-control-center/portal/gitops-dashboard/app.js 2026-07-06 23:46:50 +08:00
aios 866da79a81 feat(portal): add enterprise GitOps API contract 2026-07-06 23:46:41 +08:00
aios 8a810c2ae8 chore(chatgpt): update overlays/user-control-center/portal/gitops-dashboard/app.js 2026-07-06 23:44:51 +08:00
aios c475a8238c feat(portal): add enterprise GitOps UI policy 2026-07-06 23:44:44 +08:00
aios 24c55564a0 chore(chatgpt): update overlays/user-control-center/portal/gitops-dashboard/app.js 2026-07-06 23:43:24 +08:00
aios 55a4b32aa6 feat(portal): expand GitOps dashboard UI screens 2026-07-06 23:43:17 +08:00
aios 42c4d11457 chore(chatgpt): update overlays/user-control-center/portal/gitops-dashboard/manifest.json 2026-07-06 23:33:20 +08:00
aios 7be2107ad0 chore(chatgpt): update overlays/user-control-center/portal/gitops-dashboard/app.js 2026-07-06 23:33:18 +08:00
aios 4587c9c46d chore(chatgpt): update overlays/user-control-center/portal/gitops-dashboard/styles.css 2026-07-06 23:33:15 +08:00
aios aebff86864 chore(chatgpt): update overlays/user-control-center/portal/gitops-dashboard/index.html 2026-07-06 23:33:12 +08:00
aios 76d2453ef3 Create branch for complete GitOps web UI implementation 2026-07-06 23:33:08 +08:00
aios a9ef9197e6 fix-auth-gitops-owner-sync 2026-07-06 19:16:31 +08:00
aios 54870ed375 release: validate Codex and custom GPT collaboration 2026-07-06 18:42:34 +08:00
aios c850858973 merge(collab): include custom_gpt patch 20260706T104130Z 2026-07-06 18:42:33 +08:00
aios d08a7043d6 merge(collab): include local_codex patch 20260706T104130Z 2026-07-06 18:42:33 +08:00
aios c393931f64 test(collab): add custom_gpt evidence 20260706T104130Z 2026-07-06 18:41:30 +08:00
aios a62d4ce2c1 test(collab): add local_codex evidence 20260706T104130Z 2026-07-06 18:41:30 +08:00
aios 5a21bd5d7e test(collab): create custom_gpt branch 20260706T104130Z 2026-07-06 18:41:30 +08:00
aios ed92b88fc0 test(collab): create local_codex branch 20260706T104130Z 2026-07-06 18:41:30 +08:00
aios a248142158 fix(api): sync Codex patch branches to online Git 2026-07-06 18:27:34 +08:00
aios ecf673500b release: configure Codex online GitOps versioning 2026-07-06 18:21:56 +08:00
aios 43ddb91568 fix(api): allow secret_ref model commits 2026-07-06 17:41:46 +08:00
aios 0b0505a645 test(api): include ChatGPT adapter contract checks 2026-07-06 17:37:30 +08:00
aios 96c00d81a9 feat(api): add ChatGPT GitOps Actions access 2026-07-06 17:36:09 +08:00
aios e7d36ad706 fix(auth): make cnnetmkt Casdoor primary 2026-07-06 17:07:09 +08:00
aios d6cb9d1529 fix(ingress): ensure cnnetmkt apex DNS record 2026-07-06 16:39:07 +08:00
85 changed files with 9009 additions and 25 deletions
+1 -1
View File
@@ -1 +1 @@
V1R1C0
V1R1C4
@@ -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()
@@ -0,0 +1,25 @@
{
"approval_id": "owner-preapprove-V1R1C5-main-entry-20260707",
"created_at": "2026-07-07T00:00:00Z",
"approved_by_role": "owner",
"target_branch": "patch/web-ui-complete-gitops-dashboard-20260706",
"target_base": "main",
"target_version": "V1R1C5",
"rollback_target": "V1R1C4",
"allowed_actions": [
"merge_to_main",
"create_release_tag",
"activate_new_baseline"
],
"risk_acceptance": "Owner approved promotion of autonomous GitOps platform control plane to main, subject to validation, OpenClaw supervision, no secret leakage, no direct force push, and rollback target V1R1C4.",
"expires_at": "2026-07-14T23:59:59Z",
"constraints": {
"direct_main_push": false,
"force_push": false,
"secrets_in_git": false,
"destructive_operations": false,
"validation_required": true,
"owner_notification_required": true,
"web_ui_audit_required": true
}
}
+68 -8
View File
@@ -1,20 +1,20 @@
{
"active_version": "V1R1C0",
"active_tag": "V1R1C0-baseline-20260706",
"active_version": "V1R1C4",
"active_tag": "V1R1C4",
"mode": "gitops-online",
"version_policy": "VxRxCx",
"update_mode": "git_patch_only",
"zip_update_deprecated": true,
"entry": "https://cnnetmkt.top",
"links": {
"git": "/git",
"git": "https://g.cnnetmkt.top/git/aios/aios-v23",
"baseline": "/base",
"versions": "/ver",
"diff": "/diff",
"patch": "/patch",
"model": "/model",
"runtime": "/run",
"api": "/api",
"api": "https://cnnetmkt.top/api",
"hook": "/hook"
},
"features": {
@@ -23,16 +23,18 @@
"custom_llm": true,
"user_model_priority": true,
"chatgpt_rw": true,
"codex_rw": true
"codex_rw": true,
"portal_gitops_dashboard": true,
"human_shortlinks_login_required": true
},
"version": "V1R1C0",
"version": "V1R1C4",
"baseline_tag": "V1R1C0-baseline-20260706",
"source": "AIOS_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_PROD_FULL_DEPLOY_20260706.zip",
"source_package": "AIOS_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_PROD_FULL_DEPLOY_20260706.zip",
"zip_update_mode": "deprecated",
"chatgpt_access": true,
"codex_access": true,
"git_url": "https://cnnetmkt.top/git/aios/aios-v23.git",
"git_url": "https://g.cnnetmkt.top/git/aios/aios-v23.git",
"short_links": [
"/git",
"/base",
@@ -44,5 +46,63 @@
"/api",
"/hook"
],
"updated_at": "2026-07-06T07:16:01.842497+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.
+59
View File
@@ -0,0 +1,59 @@
{
"name": "aios-autonomous-merge-policy",
"version": "1.0.0",
"mode": "gated-autonomous",
"description": "Defines when Codex CI may mark a patch branch as auto-merge eligible and when main merge must remain blocked.",
"branch_scope": {
"allowed_source_patterns": [
"patch/*"
],
"protected_targets": [
"main"
],
"direct_main_push": false
},
"merge_gate": {
"required_status": [
"validation_passed",
"acceptance_required_passed",
"secret_scan_passed",
"policy_scan_passed",
"indirect_regression_passed",
"gray_deployment_passed"
],
"required_artifacts": [
"reports/codex/validation-report.json",
"reports/codex/merge-candidate.json",
"versions/proposals/V1R1C5-gitops-platform-ui.proposed.json"
],
"required_approval": {
"owner_or_admin": true,
"audit_record_required": true,
"approval_request_type": "release",
"approval_target": "V1R1C5"
}
},
"autonomous_behavior": {
"auto_validate": true,
"auto_self_heal": true,
"auto_create_merge_candidate": true,
"auto_merge_to_main": "only_after_policy_and_owner_approval",
"auto_tag_after_merge": false,
"max_self_heal_iterations": 5
},
"hard_blocks": [
"secret_leak_detected",
"direct_main_write_detected",
"destructive_operation_detected",
"baseline_active_version_changed_before_merge",
"auth_or_gitea_security_config_modified",
"required_test_failed",
"unapproved_high_risk_change"
],
"post_merge": {
"proposed_version": "V1R1C5",
"rollback_target": "V1R1C4",
"tag_creation": "request_only",
"baseline_activation": "owner_approved_action_only"
}
}
@@ -0,0 +1,80 @@
{
"name": "aios-bounded-autonomous-production-policy",
"version": "1.0.0",
"mode": "bounded-lights-out-production",
"description": "Allows unattended CI/self-healing/canary within pre-approved policy boundaries while preserving production safety gates.",
"non_negotiable_controls": {
"direct_main_push": false,
"secrets_in_git": false,
"destructive_operations": "forbidden",
"rollback_execution": "request_or_preapproved_runbook_only",
"tag_creation": "request_or_preapproved_runbook_only",
"baseline_activation": "preapproved_runbook_only",
"force_push": false,
"delete_tags_or_history": false
},
"autonomous_scope": {
"allowed_without_human_intervention": [
"static_validation",
"acceptance_validation",
"secret_scan",
"policy_scan",
"self_healing_patch_on_same_branch",
"merge_candidate_generation",
"gray_deployment_validation",
"failure_report_generation",
"rollback_request_generation",
"baseline_proposal_generation"
],
"requires_preapproved_runbook": [
"merge_to_main",
"create_release_tag",
"activate_new_baseline",
"production_runtime_change",
"production_rollback_execution"
],
"always_blocked": [
"force_push",
"purge_baseline",
"delete_version_history",
"delete_audit_records",
"commit_real_secret",
"disable_auth_or_gitea_protection"
]
},
"preapproval_contract": {
"path_pattern": "approvals/preapproved/*.json",
"required_fields": [
"approval_id",
"approved_by_role",
"target_branch",
"target_version",
"expires_at",
"allowed_actions",
"risk_acceptance",
"rollback_target"
],
"required_values": {
"approved_by_role": ["owner", "admin"],
"target_version": "V1R1C5",
"rollback_target": "V1R1C4"
}
},
"promotion_pipeline": [
"validate",
"self_heal_until_pass",
"generate_merge_candidate",
"gray_deploy_validate",
"check_preapproval",
"merge_or_request_approval",
"propose_or_activate_baseline",
"monitor",
"auto_generate_rollback_request_on_failure"
],
"slo_gates": {
"gray_deployment_required": true,
"error_budget_policy": "block_if_unknown",
"observability_required": true,
"post_merge_monitoring_window": "30m"
}
}
+78
View File
@@ -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"
}
}
@@ -0,0 +1,88 @@
{
"name": "aios-multi-agent-consensus-policy",
"version": "1.0.0",
"mode": "bounded-autonomous-consensus",
"description": "Production-safe consensus policy for OpenClaw, Codex and ChatGPT agents supervising AIOS GitOps changes.",
"agents": {
"openclaw": {
"role": "supervisor",
"weight": 2,
"required_for_high_risk": true,
"responsibility": [
"execution supervision",
"policy compliance",
"failure containment",
"owner-facing status certification"
]
},
"codex": {
"role": "validator_and_repairer",
"weight": 2,
"required_for_high_risk": true,
"responsibility": [
"static validation",
"acceptance validation",
"self-healing patch loop",
"merge candidate generation"
]
},
"chatgpt": {
"role": "operator_and_summary_agent",
"weight": 1,
"required_for_high_risk": false,
"responsibility": [
"change explanation",
"owner notification summary",
"repair proposal generation",
"runbook authoring"
]
}
},
"decision_rules": {
"low_risk": {
"minimum_score": 2,
"required_agents": ["codex"],
"allowed_outcome": "auto_merge_candidate"
},
"medium_risk": {
"minimum_score": 3,
"required_agents": ["codex", "openclaw"],
"allowed_outcome": "auto_merge_candidate_after_gray"
},
"high_risk": {
"minimum_score": 5,
"required_agents": ["codex", "openclaw"],
"allowed_outcome": "production_request_or_preapproved_execution"
}
},
"hard_blocks": [
"credential_material_detected",
"direct_main_push_detected",
"destructive_history_operation",
"auth_protection_disabled",
"gitea_protection_disabled",
"baseline_activation_without_approval",
"owner_notification_failed_for_main_change",
"required_acceptance_test_failed"
],
"required_artifacts": {
"validation": "reports/codex/validation-report.json",
"openclaw": "reports/openclaw/final-supervision-report.json",
"consensus": "reports/consensus/decision.json",
"merge_candidate": "reports/codex/merge-candidate.json",
"baseline_proposal": "versions/proposals/V1R1C5-gitops-platform-ui.proposed.json"
},
"production_safety": {
"direct_main_push": false,
"tag_creation": "request_or_preapproved_only",
"baseline_activation": "request_or_preapproved_only",
"rollback_execution": "request_or_preapproved_only",
"owner_notification": "mandatory_for_main_version_change",
"web_ui_audit_record": "mandatory"
},
"outputs": {
"approved": "merge_candidate_ready",
"rejected": "failure_report_required",
"blocked": "owner_review_required"
}
}
@@ -0,0 +1,99 @@
{
"name": "aios-self-governing-devops-policy",
"version": "1.0.0",
"mode": "bounded-self-governing",
"description": "Self-governing DevOps policy for AIOS GitOps with dynamic agent weights, drift detection, guarded rollback requests, policy runtime and skill evolution.",
"non_negotiable_controls": {
"direct_main_push": false,
"all_writes_require_git_commit": true,
"real_credentials_in_git": false,
"disable_auth_protection": false,
"delete_audit_or_version_history": false,
"production_execution_without_policy": false
},
"governance_loop": [
"observe_events",
"evaluate_policy",
"score_agents",
"detect_consensus_drift",
"render_decision_graph",
"run_validation_or_repair",
"generate_candidate_or_request",
"notify_owner",
"persist_audit",
"learn_from_outcome",
"propose_skill_update"
],
"agent_learning": {
"enabled": true,
"weight_bounds": {
"openclaw": [1, 4],
"codex": [1, 4],
"chatgpt": [1, 3]
},
"initial_weights": {
"openclaw": 2,
"codex": 2,
"chatgpt": 1
},
"signals": {
"validation_accuracy": 0.35,
"repair_success": 0.25,
"false_positive_rate": -0.15,
"owner_override": -0.2,
"post_merge_stability": 0.25
},
"update_policy": "bounded_exponential_smoothing",
"write_target": "runtime/governance/agent-weights.json"
},
"drift_detection": {
"enabled": true,
"thresholds": {
"consensus_score_drop": 2,
"agent_disagreement_ratio": 0.5,
"repeated_failure_count": 3,
"owner_override_count": 2
},
"on_drift": "block_production_and_request_owner_review"
},
"policy_runtime": {
"required_checks": [
"no_direct_main_write",
"no_real_credentials",
"no_destructive_history_change",
"acceptance_tests_pass",
"owner_notice_for_main_change",
"rollback_target_defined",
"baseline_proposal_exists"
],
"decision_outputs": [
"allow_candidate",
"conditional_candidate",
"request_owner_review",
"block"
]
},
"guarded_rollback": {
"mode": "request_first",
"automatic_action": "create_rollback_request_and_notify_owner",
"execution": "requires_preapproved_runbook_or_owner_action",
"default_target": "V1R1C4"
},
"skill_evolution": {
"enabled": true,
"mode": "propose_only",
"sources": [
"validation_failures",
"owner_feedback",
"post_merge_incidents",
"repeated_manual_repairs"
],
"output_dir": "skills/proposals",
"activation": "merge_after_acceptance"
},
"owner_control": {
"channels": ["web_ui", "wechat_or_wecom"],
"actions": ["audit", "approve", "reject", "request_change", "request_rollback"],
"main_version_change_notice": "mandatory"
}
}
+42
View File
@@ -0,0 +1,42 @@
{
"name": "self-healing-gitops",
"version": "1.0.0",
"mode": "codex-driven-ci",
"description": "Defines how Codex + CI loop detects failures, generates repair plan, and re-runs validation.",
"workflow": {
"trigger": ["push", "pull_request"],
"validation_script": "scripts/validate-gitops-platform-ui.sh",
"failure_handling": {
"create_failure_report": true,
"report_path": "reports/codex/failure.json",
"notify": "codex-gpt",
"auto_repair": false,
"require_human_or_gpt_review": true
},
"success_handling": {
"allow_merge_candidate": true,
"propose_baseline_update": "V1R1C5"
}
},
"self_healing_loop": {
"steps": [
"run_validation",
"collect_failures",
"send_to_gpt",
"apply_patch_to_branch",
"rerun_validation",
"repeat_until_pass"
],
"max_iterations": 5,
"stop_conditions": [
"PASS",
"critical_security_failure",
"manual_override"
]
},
"constraints": {
"no_main_write": true,
"no_secret_exposure": true,
"approval_required_for_release": true
}
}
@@ -0,0 +1,135 @@
{
"name": "aios-self-optimizing-orchestrator",
"version": "1.0.0",
"mode": "hybrid-event-plus-reconciliation",
"description": "Low-impact orchestrator for ChatGPT/Codex self-optimization, version-change notification, audit, and owner action routing.",
"design_principles": {
"execution_layer_impact": "minimal",
"writes": "git_patch_only",
"direct_main_push": false,
"secrets_in_git": false,
"production_high_risk_actions": "approval_or_preapproved_runbook_only",
"user_impact": "prefer_async_sidecar_and_read_only_observation"
},
"triggers": {
"primary": {
"type": "event_driven",
"sources": [
"git_push",
"tag_created",
"baseline_changed",
"patch_created",
"validation_failed",
"validation_passed",
"merge_candidate_created",
"runtime_health_degraded"
],
"target": "orchestrator_queue"
},
"fallback": {
"type": "scheduled_reconciliation",
"intervals": {
"codex_branch_scan": "*/15 * * * *",
"chatgpt_patch_scan": "*/15 * * * *",
"main_version_scan": "*/5 * * * *",
"notification_retry_scan": "*/10 * * * *",
"daily_owner_digest": "0 9 * * *"
},
"purpose": "Catch missed events, reconcile web UI records, and retry failed notifications."
}
},
"agents": {
"codex": {
"responsibility": [
"code validation",
"static/security/policy tests",
"self-healing patch loop",
"merge candidate report"
],
"trigger": "event_and_schedule",
"skill": "skills/gitops-platform-validation/SKILL.md"
},
"chatgpt": {
"responsibility": [
"operator summary",
"failure triage",
"GitOps patch authoring",
"owner-facing change explanation"
],
"trigger": "event_and_schedule",
"skill": "skills/gitops-owner-notification/SKILL.md"
}
},
"change_summary_required_fields": [
"event_id",
"branch",
"from_rev",
"to_rev",
"from_version",
"to_version",
"elapsed_since_previous_main_change",
"changed_features",
"code_lines_added",
"code_lines_deleted",
"config_lines_added",
"config_lines_deleted",
"test_status",
"risk",
"approval_status",
"rollback_target",
"web_ui_audit_url"
],
"owner_channels": {
"wechat": {
"enabled": true,
"provider": "wechat_or_wecom",
"webhook_secret_ref": "secret_ref://aios/notifications/owner-wechat-webhook",
"signing_secret_ref": "secret_ref://aios/notifications/owner-action-signing-key",
"message_types": [
"main_version_changed",
"merge_candidate_ready",
"validation_failed",
"rollback_request_created",
"approval_required",
"daily_digest"
],
"actions": [
"view_audit",
"approve_preapproved_runbook",
"request_rollback",
"request_change",
"reject_release"
]
},
"web_ui": {
"enabled": true,
"audit_route": "/portal/me/#gitops-audit",
"owner_action_route": "/portal/me/#gitops-owner-actions",
"record_store": "runtime/gitops/events/*.json"
}
},
"version_change_policy": {
"main_change_notification": "mandatory",
"notify_owner_on_every_main_version_change": true,
"include_diff_stats": true,
"include_elapsed_time_since_previous_main_change": true,
"include_function_summary": true,
"write_web_ui_record": true,
"retry_until_delivered_or_acknowledged": true
},
"owner_action_policy": {
"wechat_actions": "signed_short_link_or_command_token",
"web_actions": "login_required_portal_action",
"all_actions_audited": true,
"high_risk_actions": "request_only_unless_preapproved_runbook",
"rollback": "create_request_by_default",
"change_request": "create_patch_or_plan_by_default"
},
"minimal_execution_layer_changes": [
"Add sidecar orchestrator queue consumer.",
"Add event writer for GitOps version changes.",
"Add notification sender using secret_ref only.",
"Add Web UI audit/action records reading event files.",
"Keep existing /api/base /api/ver /api/diff /api/patch /api/model /api/run stable."
]
}
@@ -0,0 +1,51 @@
{
"name": "v1r1-execution-api-runtime",
"version": "1.0.0",
"mode": "api_runtime_not_filesystem",
"baseline": "V1R1C4",
"target_proposal": "V1R1C5",
"description": "Execution API runtime contract for AIOS Xiamayiti self-evolution loop. All production execution requests must go through the V1R1 runtime API rather than direct filesystem or protected credential access.",
"security": {
"direct_runtime_secret_access": false,
"direct_filesystem_production_write": false,
"all_execution_audited": true,
"owner_control_required_for_high_risk": true,
"direct_main_push": false
},
"api": {
"submit_execution_request": "POST /api/v1r1/execution/request",
"get_execution_status": "GET /api/v1r1/execution/status/{request_id}",
"append_audit": "POST /api/v1r1/audit/append",
"send_owner_notification": "POST /api/v1r1/notification/owner",
"submit_owner_action": "POST /api/v1r1/owner/action"
},
"allowed_request_types": [
"audit_record",
"owner_notification",
"evolution_finding",
"evolution_plan",
"validation_run",
"merge_candidate",
"release_request",
"rollback_request",
"baseline_activation_request"
],
"blocked_request_types": [
"write_protected_credential",
"force_push",
"delete_history",
"delete_audit",
"disable_auth",
"disable_gitea_protection",
"direct_main_push"
],
"execution_states": [
"received",
"policy_checked",
"accepted",
"queued",
"executed_as_request",
"blocked",
"failed"
]
}
@@ -0,0 +1,41 @@
{
"name": "v1r1-existing-wechat-runtime-integration",
"version": "1.0.0",
"baseline": "V1R1C4",
"description": "Integrates AIOS self-evolution notifications with the already deployed Xiamayiti/V1R1 personal WeChat runtime instead of creating a duplicate gateway.",
"detected_runtime_processes": [
{
"process_hint": "/data/agent/services/ucc-wechat-qr-adapter/adapter.py",
"role": "user-control-center personal WeChat QR adapter",
"integration_priority": 1
},
{
"process_hint": "scripts/wechat_notify_worker.py",
"role": "existing personal WeChat notification worker",
"integration_priority": 2
}
],
"preferred_integration": {
"mode": "existing_worker_queue_bridge",
"runtime_env": "AIOS_V1R1_WECHAT_BRIDGE_OUTBOX",
"behavior": "Write normalized owner notification JSON into the existing V1R1 WeChat worker queue directory. The existing V1R1 worker is responsible for actual personal WeChat delivery."
},
"fallback_integration": {
"mode": "http_endpoint",
"runtime_env": "AIOS_V1R1_WECHAT_OUTBOUND_URL",
"behavior": "POST normalized owner notification JSON to a runtime-provided V1R1 notification API endpoint."
},
"rules": {
"do_not_duplicate_wechat_gateway": true,
"do_not_store_credentials_in_git": true,
"do_not_access_runtime_secrets_directly": true,
"web_ui_and_wechat_share_event_id": true,
"delivery_status_must_be_audited": true
},
"operator_discovery_commands": [
"ps aux | grep -E 'ucc-wechat|wechat_notify'",
"pwdx <pid>",
"tr '\\0' '\\n' < /proc/<pid>/environ",
"grep -R \"queue\|outbox\|notify\|send\|wechat\" -n /data/agent /data/aios-gitops/workspace/scripts 2>/dev/null | head -100"
]
}
+4
View File
@@ -1,4 +1,8 @@
AIOS_PUBLIC_DOMAIN=https://cnnetmkt.top
AIOS_USER_DOMAIN=https://user.cnnetmkt.top
AIOS_AUTH_DOMAIN=https://auth.cnnetmkt.top
AIOS_AUTH_BACKUP_DOMAIN=https://auth.h.imruyi.top:8443
AIOS_CASDOOR_PRIMARY_CALLBACK=https://cnnetmkt.top/casdoor/callback/
AIOS_WEBOPS_DOMAIN=https://ops.cnnetmkt.top
AIOS_API_DOMAIN=https://api.cnnetmkt.top
AIOS_WECHAT_CALLBACK_DOMAIN=https://wechat.cnnetmkt.top
+40
View File
@@ -0,0 +1,40 @@
CREATE TABLE IF NOT EXISTS message_outbox (
id TEXT PRIMARY KEY,
channel TEXT NOT NULL,
recipient_user_id TEXT NOT NULL,
recipient_role TEXT,
message_type TEXT NOT NULL,
priority TEXT NOT NULL DEFAULT 'normal',
title TEXT,
content TEXT NOT NULL,
payload TEXT,
status TEXT NOT NULL DEFAULT 'queued',
retry_count INTEGER NOT NULL DEFAULT 0,
last_error TEXT,
created_at TEXT NOT NULL,
sent_at TEXT
);
CREATE INDEX IF NOT EXISTS idx_message_outbox_dispatch
ON message_outbox(channel, status, priority, created_at);
CREATE TABLE IF NOT EXISTS channel_bindings (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
channel_type TEXT NOT NULL,
external_chat_id TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'active',
updated_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_channel_bindings_lookup
ON channel_bindings(user_id, channel_type, status, updated_at);
CREATE TABLE IF NOT EXISTS audit_logs (
id TEXT PRIMARY KEY,
actor TEXT NOT NULL,
action TEXT NOT NULL,
target TEXT,
detail TEXT,
created_at TEXT NOT NULL
);
+4 -3
View File
@@ -6,8 +6,8 @@ WORK_PATH = /data/aios-gitops/runtime/gitea-bin
PROTOCOL = http
HTTP_ADDR = 127.0.0.1
HTTP_PORT = 3000
DOMAIN = cnnetmkt.top
ROOT_URL = https://cnnetmkt.top/git/
DOMAIN = g.cnnetmkt.top
ROOT_URL = https://g.cnnetmkt.top/git/
SERVE_FROM_SUB_PATH = true
DISABLE_SSH = true
OFFLINE_MODE = false
@@ -27,7 +27,8 @@ INTERNAL_TOKEN = __INTERNAL_TOKEN__
[service]
DISABLE_REGISTRATION = true
REQUIRE_SIGNIN_VIEW = false
REQUIRE_SIGNIN_VIEW = true
SHOW_REGISTRATION_BUTTON = false
[webhook]
ALLOWED_HOST_LIST = *
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
set -euo pipefail
echo "[AIOS] Installing Autonomous GitOps System..."
WORKDIR=/data/aios-gitops/workspace
ETC=/etc/aios
RUNTIME=/data/aios-gitops/runtime
SERVICE_USER=aios
if ! id "$SERVICE_USER" >/dev/null 2>&1; then
echo "[AIOS] Creating system user: $SERVICE_USER"
useradd --system --home /data/aios-gitops --shell /usr/sbin/nologin "$SERVICE_USER"
fi
mkdir -p "$ETC"
cp deploy/systemd/aios-autonomous-daemon.env.template "$ETC/aios-autonomous-daemon.env"
mkdir -p \
"$RUNTIME/events" \
"$RUNTIME/audit" \
"$RUNTIME/requests" \
"$RUNTIME/reports/codex" \
"$RUNTIME/approvals/preapproved" \
"$RUNTIME/integrations/v1r1-personal-wechat/outbox" \
"$RUNTIME/integrations/v1r1-personal-wechat/inbox" \
"$RUNTIME/integrations/v1r1-personal-wechat/status"
chown -R "$SERVICE_USER:$SERVICE_USER" /data/aios-gitops/runtime || true
chown -R "$SERVICE_USER:$SERVICE_USER" "$WORKDIR" || true
cp deploy/systemd/aios-autonomous-daemon.service /etc/systemd/system/
cp deploy/systemd/aios-v1r1-wechat-worker.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable aios-autonomous-daemon.service || true
systemctl enable aios-v1r1-wechat-worker.service || true
systemctl restart aios-autonomous-daemon.service || true
systemctl restart aios-v1r1-wechat-worker.service || true
sleep 2
systemctl status aios-autonomous-daemon.service --no-pager || true
systemctl status aios-v1r1-wechat-worker.service --no-pager || true
echo "[AIOS] Installation complete"
+6 -6
View File
@@ -24,12 +24,12 @@
proxy_set_header X-Forwarded-Proto $scheme;
}
location = /base { proxy_pass http://127.0.0.1:18890/base; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; }
location = /ver { proxy_pass http://127.0.0.1:18890/ver; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; }
location = /diff { proxy_pass http://127.0.0.1:18890/diff; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; }
location = /patch { proxy_pass http://127.0.0.1:18890/patch; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; }
location = /model { proxy_pass http://127.0.0.1:18890/model; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; }
location = /run { proxy_pass http://127.0.0.1:18890/run; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; }
location = /base { return 302 /portal/me/#gitops; }
location = /ver { return 302 /portal/me/#gitops; }
location = /diff { return 302 /portal/me/#gitops; }
location = /patch { return 302 /portal/me/#gitops; }
location = /model { return 302 /portal/me/#gitops; }
location = /run { return 302 /portal/me/#gitops; }
location = /hook { proxy_pass http://127.0.0.1:18890/hook; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; }
location /hook/ {
proxy_pass http://127.0.0.1:18890/hook/;
+40
View File
@@ -0,0 +1,40 @@
# AIOS primary public Casdoor entry for cnnetmkt.top.
# The upstream remains the existing backup/home Casdoor service; public clients should see auth.cnnetmkt.top only.
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name auth.cnnetmkt.top;
access_log /var/log/nginx/aios-auth-cnnetmkt.access.log;
error_log /var/log/nginx/aios-auth-cnnetmkt.error.log;
ssl_certificate /etc/letsencrypt/live/cnnetmkt-wildcard/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/cnnetmkt-wildcard/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
proxy_pass https://auth.h.imruyi.top:8443;
proxy_http_version 1.1;
proxy_ssl_server_name on;
proxy_ssl_name auth.h.imruyi.top;
proxy_ssl_verify off;
proxy_set_header Host auth.h.imruyi.top;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port 443;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_redirect https://auth.h.imruyi.top:8443/ https://auth.cnnetmkt.top/;
proxy_redirect https://auth.h.imruyi.top/ https://auth.cnnetmkt.top/;
}
}
server {
listen 80;
listen [::]:80;
server_name auth.cnnetmkt.top;
return 301 https://$host$request_uri;
}
@@ -0,0 +1,16 @@
# AIOS Autonomous GitOps Daemon runtime environment template
# Copy to /etc/aios/aios-autonomous-daemon.env during deployment.
# This template stores only paths and secret_ref identifiers. Runtime values are injected by the host secret manager.
AIOS_REPO_ROOT=/data/aios-gitops/workspace
AIOS_EVENT_DIR=/data/aios-gitops/runtime/events
AIOS_AUDIT_DIR=/data/aios-gitops/runtime/audit
AIOS_REQUEST_DIR=/data/aios-gitops/runtime/requests
AIOS_REPORT_DIR=/data/aios-gitops/runtime/reports/codex
AIOS_PREAPPROVAL_DIR=/data/aios-gitops/runtime/approvals/preapproved
AIOS_DAEMON_INTERVAL=30
AIOS_INGRESS_BIND=127.0.0.1
AIOS_INGRESS_PORT=8787
AIOS_OWNER_CHANNEL_REF=secret_ref://aios/notifications/owner-wechat-endpoint
AIOS_OWNER_ACTION_SIGN_REF=secret_ref://aios/notifications/owner-action-signing
AIOS_INGRESS_SIGN_REF=secret_ref://aios/notifications/inbound-event-signing
@@ -0,0 +1,21 @@
[Unit]
Description=AIOS Autonomous GitOps Daemon
After=network.target
[Service]
Type=simple
User=aios
WorkingDirectory=/data/aios-gitops/workspace
EnvironmentFile=/etc/aios/aios-autonomous-daemon.env
ExecStart=/usr/bin/python3 services/aios-autonomous-daemon/daemon.py run
Restart=always
RestartSec=5
# Hardening (minimal impact)
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,18 @@
[Unit]
Description=AIOS Multi-Agent Consensus Controller
After=network.target
[Service]
Type=oneshot
User=aios
WorkingDirectory=/data/aios-gitops/workspace
ExecStart=/usr/bin/python3 services/aios-consensus-controller/controller.py
# Hardening (non-invasive)
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,7 @@
[Unit]
Description=Sync AIOS Gitea owner password from UCC owner account
After=aios-gitea.service
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/aios-sync-gitea-owner-password.sh
@@ -0,0 +1,10 @@
[Unit]
Description=Periodic AIOS Gitea owner password sync
[Timer]
OnBootSec=2min
OnUnitActiveSec=15min
Persistent=true
[Install]
WantedBy=timers.target
+2 -1
View File
@@ -10,9 +10,10 @@ Environment=AIOS_GITOPS_ROOT=/data/aios-gitops
Environment=AIOS_GITOPS_WORKSPACE=/data/aios-gitops/workspace
Environment=AIOS_GITOPS_DB=/data/aios-gitops/runtime/gitops_model.sqlite3
Environment=AIOS_GITOPS_TOKEN_FILE=/data/aios-gitops/runtime/secrets/gitops.tokens
Environment=AIOS_GITOPS_BARE_REPO=/data/aios-gitops/runtime/gitea-bin/data/repositories/aios/aios-v23.git
Environment=AIOS_GITOPS_HOST=127.0.0.1
Environment=AIOS_GITOPS_PORT=18890
ExecStart=/usr/bin/python3 /data/aios-gitops/workspace/active-runtime/app/aios_gitops_model_api.py
ExecStart=/usr/bin/python3 /data/aios-gitops/workspace/active-runtime/app/aios_chatgpt_actions_api.py
Restart=always
RestartSec=3
@@ -0,0 +1,22 @@
[Unit]
Description=AIOS V1R1 Personal WeChat Outbound Worker
After=network.target aios-autonomous-daemon.service
Wants=aios-autonomous-daemon.service
[Service]
Type=simple
User=aios
WorkingDirectory=/data/aios-gitops/workspace
EnvironmentFile=/etc/aios/aios-autonomous-daemon.env
ExecStart=/usr/bin/python3 services/aios-v1r1-wechat-outbound-worker/worker.py
Restart=always
RestartSec=5
# Hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,210 @@
# AIOS 远程 Codex 多 Agent 统筹架构
## 1. 核心定位
当前 AIOS / 虾马一体系统的自我进化,不再假设服务器上存在 Codex CLI。
真实运行模型是:
- Windows 11 Codex App:总控 / 统筹 / 拆分任务 / 监督多个子 agent;
- 服务器 OpenClaw:生产运行侧消息、微信、执行与通道能力;
- 服务器 Hermes Agent:运行观测、事件、消息/用户交互、反馈归集;
- 服务器 AIOS Autonomous Daemon:GitOps event loop、validation、audit、request;
- GitOps 仓库:唯一可审计状态同步面;
- WebOps / Portal:owner 审计、审批、回滚、变更入口。
## 2. 架构图
```text
Windows 11 Codex App
├─ Planner Agent
├─ Code Patch Agent
├─ Validation Agent
├─ WeChat/UCC Integration Agent
├─ WebOps UI Agent
└─ Supervisor Agent
↓ git / ssh / reports
GitOps Repository: patch/web-ui-complete-gitops-dashboard-20260706
↓ pull / event
AIOS Server Runtime
├─ aios-autonomous-daemon
├─ OpenClaw Agent / Gateway
├─ Hermes Agent
├─ UCC WeChat Adapter
├─ WebOps / message_outbox
└─ audit / reports / requests
↓
Owner WebOps + 个人微信
```
## 3. 分工
### Windows Codex App
Windows Codex 是统筹器,不是服务器守护进程。
职责:
1. 读取 GitOps 指令与当前 branch;
2. 启动多个子 agent 并行处理;
3. 生成代码/配置/文档 patch;
4. push 到同一 GitOps patch 分支;
5. 读取服务器回传的验证结果;
6. 对失败项继续修复;
7. 输出 merge candidate / blocked reason。
### OpenClaw Agent
职责:
1. 维护微信通道、message send、用户通道状态;
2. 提供个人微信实际执行能力;
3. 接收 message_outbox worker 或 UCC Adapter 的调用;
4. 输出 channel status / delivery result;
5. 不直接接收业务模块调用。
### Hermes Agent
职责:
1. 汇总系统运行事件;
2. 处理用户/消息交互流;
3. 将微信或 WebOps 反馈转为系统事件;
4. 协助 self-evolution loop 判断用户体验和业务状态;
5. 输出观测报告给 GitOps / Codex。
### AIOS Autonomous Daemon
职责:
1. 处理 runtime events;
2. 触发 validation / merge gate;
3. 写 audit / request / report;
4. 不直接改 runtime secrets;
5. 不直接调用微信底层适配器。
## 4. 并行子 Agent 模型
Windows Codex 可启动以下子 agent:
| 子 Agent | 任务 |
|---|---|
| planner | 读取 directive,拆分任务,确认风险 |
| server-inspector | 解析服务器命令输出,定位 runtime 状态 |
| code-patcher | 修改 GitOps 仓库代码 |
| test-author | 补验收用例和验证脚本 |
| wechat-integrator | 处理 message_outbox / UCC / OpenClaw 适配 |
| webops-ui | 补 WebOps 页面和 API contract |
| supervisor | 汇总 PASS/FAIL/BLOCKED |
## 5. 自动运行机制
自动运行不代表 Codex 在服务器常驻,而是通过以下 loop 运行:
```text
server event / owner request
↓
GitOps directive / report
↓
Windows Codex pulls branch
↓
Codex child agents patch code/config
↓
git push
↓
server git pull + run validation
↓
reports written
↓
Codex reads reports
↓
repeat until PASS/BLOCKED
```
## 6. 通信通道
### Git
主要状态同步面:
- directives;
- patches;
- code/config;
- docs;
- acceptance tests;
- version proposals。
### SSH / Operator Logs
服务器运行结果通过 SSH 或人工粘贴日志回传给 Codex/GPT。
### Runtime Reports
服务器写入:
```text
reports/codex/*.json
reports/openclaw/*.json
reports/hermes/*.json
reports/consensus/*.json
runtime/gitops/audit/*.json
runtime/gitops/requests/*.json
```
## 7. 生产安全边界
仍然禁止:
- 直接 main push;
- force push;
- 写 runtime secrets;
- 删除 audit / version history;
- 业务模块直接调用 OpenClaw;
- 业务模块直接调用 UCC WeChat adapter;
- 绕过 message_outbox;
- 伪造 sent 状态。
## 8. 微信链路标准
生产标准链路:
```text
业务/系统事件
→ POST /api/v1/wechat/outbox 或写 message_outbox
→ wechat_notify_worker
→ wechat_send_backend
→ ucc-adapter / openclaw-channel
→ 个人微信
→ sent/failed/retry/audit
```
当前优先后端:
```text
ucc-adapter
```
OpenClaw channel 仅作为可发现的可选后端,不得硬编码 `openclaw-weixin`。
## 9. 自我进化目标
自我进化不是“代码自己改代码”,而是虾马一体系统整体优化:
1. 业务流程优化;
2. 运行健康优化;
3. 模型路由优化;
4. 通知和审批体验优化;
5. WebOps 可观测性优化;
6. 必要时生成代码/配置 patch。
## 10. 进入自动运行的判据
满足以下条件即认为进入自动运行:
1. `aios-autonomous-daemon` running;
2. OpenClaw/Hermes 进程 running;
3. message_outbox 可写;
4. Windows Codex 监控目标 branch;
5. Codex 可读取 directive 并 push patch;
6. 服务器可运行验收命令并写 reports;
7. owner 可在 WebOps 或微信收到关键进展。
@@ -0,0 +1,154 @@
# Codex 验收手册:GitOps Platform UI
## 目标
本次变更的目标是把 `cnnetmkt.top` AIOS GitOps Web UI 从基础 dashboard 升级为平台级控制台,形成可审计、可审批、可灰度、可回滚请求的 GitOps 操作入口。
## 验证分支
- Branch: `patch/web-ui-complete-gitops-dashboard-20260706`
- Base: `main`
- 当前目标基线: `V1R1C4`
- 合并后建议新基线: `V1R1C5`
## 主线差异点
与 `main` 相比,本分支新增或修改以下直接内容:
1. `overlays/user-control-center/portal/gitops-dashboard/index.html`
- 新增平台级 GitOps 控制台页面。
- 包含 Overview、RBAC、Approval、Diff Tree、Patch、Model、Release、Audit 模块。
2. `overlays/user-control-center/portal/gitops-dashboard/app.js`
- 新增浏览器运行时 Platform Control Plane。
- 包含 API-first client、RBAC guard、approval fallback、audit local store、diff tree parser、model graph、SSE event hook。
3. `overlays/user-control-center/portal/gitops-dashboard/styles.css`
- 控制台样式。
4. `overlays/user-control-center/portal/gitops-dashboard/manifest.json`
- 控制台 manifest。
5. `overlays/user-control-center/portal/gitops-dashboard/gitops-ui-policy.json`
- UI 侧策略声明。
6. `overlays/user-control-center/portal/gitops-dashboard/gitops-enterprise-api.contract.json`
- 后端 API 契约说明。
7. `patches/patch_web-ui-complete-gitops-dashboard-20260706.branch.json`
- GitOps branch 元数据。
## 间接影响区域
Codex 验证时必须确认以下间接影响:
1. Portal 登录入口不应被破坏。
- `/portal/me/#gitops`
- `/portal/me/#gitops-dashboard`
2. 现有短链仍应保持登录保护或正常跳转。
- `/base`
- `/ver`
- `/diff`
- `/patch`
- `/model`
- `/run`
3. 机器 API 不应被前端变更破坏。
- `/api/base`
- `/api/ver`
- `/api/diff`
- `/api/patch`
- `/api/model`
- `/api/run`
4. 敏感信息不得进入 Git、日志、前端源码或测试产物。
- 禁止真实 API key。
- 禁止 Bearer token。
- 禁止 password / private key。
- 允许 `secret_ref://...`。
5. 高风险操作不得绕过审批。
- rollback 只能 request。
- tag/release 默认只能 request。
- runtime 写入只能 request。
- UI 不提供 direct main write。
## Codex 快速验证命令
在目标工作区执行:
```bash
git fetch --all --prune
git checkout patch/web-ui-complete-gitops-dashboard-20260706
git diff --stat main...HEAD
bash scripts/validate-gitops-platform-ui.sh patch/web-ui-complete-gitops-dashboard-20260706 main
```
## 灰度验证外的必测项目
除正常灰度页面点击验证外,Codex 必须按 `tests/acceptance/gitops-platform-ui.acceptance.json` 执行以下测试组:
1. Static Files
- 必须存在 index/html/js/css/manifest/policy/contract 文件。
2. Security
- 源码中不得出现真实 secret/token/password。
- 模型注册必须只允许 `secret_ref://`。
- rollback/tag/runtime/main write 必须 request-only 或 disabled。
3. UI Direct Impact
- 所有 tab id 必须能在 HTML 中找到。
- app.js 必须绑定 `DOMContentLoaded`。
- app.js 必须包含 `guardAction`、`audit`、`createApproval`、`renderDiffTree`。
4. API Contract Compatibility
- 读接口保持 `/api/base`、`/api/ver`、`/api/run`、`/api/patch`、`/api/model`。
- 写接口必须包含 branch、patch/plan、model、model-priority、rollback-request、tag-request。
5. Indirect Regression
- 不得修改 auth/casdoor 配置。
- 不得修改 Gitea owner password sync 配置。
- 不得修改 baseline active_version。
- 不得删除任何版本/tag/patch 历史。
## 验收判定
- PASS: 所有 required 测试项通过,且没有 secret 泄漏、没有 direct main write、没有破坏短链/API。
- FAIL: 任一 required 测试项失败。
- BLOCKED: 需要真实后端或浏览器环境,但静态检查通过;必须列出 blocked 项,不得合并 main。
## 失败回流规则
如果验证失败,Codex 必须输出:
1. 失败测试 ID。
2. 失败文件路径。
3. 具体失败原因。
4. 最小修改建议。
5. 是否阻止合并。
然后把问题交给自定义 GPT 继续修改同一 patch 分支。修复后重复执行完整验收,直到 PASS。
## 合并规则
只有满足以下条件才能合并 main:
1. `scripts/validate-gitops-platform-ui.sh` PASS。
2. acceptance JSON 中所有 `required: true` 项 PASS。
3. 灰度验证通过。
4. 无 secret 泄漏。
5. 无 direct main write。
6. 已创建或确认合并后基线提案 `V1R1C5`。
## 合并后基线要求
合并进入 main 后,必须定义新基线:
- Version: `V1R1C5`
- Purpose: `Promote AIOS GitOps Platform Control Plane UI with Codex validation workflow, acceptance tests, RBAC/approval/audit/release/model control-plane scaffolding.`
- Risk: `high`
- Approval: `owner/admin required`
- Rollback target: `V1R1C4`
合并后再创建 tag/version,不允许在未合并或未验收通过前直接创建生产 tag。
+15
View File
@@ -12,3 +12,18 @@ Forbidden:
- API keys in Git
- API keys in plaintext logs
- bypassing owner/admin approval for V-level changes
## Codex Online Version Management
Codex uses `/data/aios-gitops/workspace` and `https://cnnetmkt.top/git/aios/aios-v23.git` as the online Git version baseline. Runtime write access uses `codex-gitops-bot`; its token is stored outside Git and referenced as `secret_ref://aios-gitops/runtime/codex-gitops-bot`.
Current production release after this merge: `V1R1C3`. Acceptance-only branches are retained for audit and are not automatically merged into `main`.
## Online Git Sync
Starting in `V1R1C3`, committed API/Codex patch branches are synchronized from the workspace clone into the online Gitea bare repository. Protected production branches remain excluded from API sync.
## Codex and Custom GPT Collaboration Test
`V1R1C3` confirms local Codex and the custom GPT Actions path can independently create patch branches, commit files, sync to online Git, and then merge into the latest baseline version.
@@ -0,0 +1,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.
+240
View File
@@ -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}}
+17
View File
@@ -0,0 +1,17 @@
# CNNETMKT apex DNS status
generated_at=2026-07-06T16:39:07+08:00
provider=Huawei Cloud DNS
zone=cnnetmkt.top.
record=cnnetmkt.top. A 1.95.164.183
status=created_and_resolved
credential_storage=/etc/letsencrypt/huaweicloud-cnnetmkt.env
secret_policy=not_tracked_not_logged
## verification
- authoritative NS returns 1.95.164.183
- system resolver returns 1.95.164.183
- https://cnnetmkt.top/ returns 302 /portal/login/
- https://cnnetmkt.top/git/ returns 200 AIOS GitOps
- wildcard certificate SAN covers cnnetmkt.top and *.cnnetmkt.top
+53
View File
@@ -0,0 +1,53 @@
# CNNETMKT Auth Primary Status
更新时间:2026-07-06T09:00:06Z
## 结论
`https://cnnetmkt.top/` 是用户主入口。登录授权主域为 `https://auth.cnnetmkt.top`。
`https://auth.h.imruyi.top:8443` 只作为备份/上游兼容入口保留,不应出现在普通用户的主登录跳转链路里。
## 当前链路
1. 用户访问 `https://cnnetmkt.top/`。
2. UCC 返回 `/portal/login/`。
3. UCC 生成 Casdoor 授权地址:
`https://auth.cnnetmkt.top/login/oauth/authorize?...redirect_uri=https%3A%2F%2Fcnnetmkt.top%2Fcasdoor%2Fcallback%2F...`
4. 华为云 Nginx `auth.cnnetmkt.top` server block 反代到现有 Casdoor 上游。
5. Casdoor 允许 `https://cnnetmkt.top/casdoor/callback/` 作为 `admin/aios-portal` 的 OAuth callback。
## 已调整配置
### 华为云 UCC 运行时
- `CASDOOR_PUBLIC_BASE_URL=https://auth.cnnetmkt.top`
- `CASDOOR_INTERNAL_BASE_URL=https://auth.cnnetmkt.top`
- `CASDOOR_INVITE_SYNC_BASE_URL=https://auth.cnnetmkt.top`
- `UCC_PUBLIC_BASE_URL=https://cnnetmkt.top`
- `ALLOWED_HOSTS` 增加 `auth.cnnetmkt.top`
- `CSRF_TRUSTED_ORIGINS` 增加 `https://cnnetmkt.top` 和 `https://auth.cnnetmkt.top`
### Casdoor 应用
应用:`admin/aios-portal`
新增/保留 callback:
- `https://cnnetmkt.top/casdoor/callback/`
- `https://cnnetmkt.top/api/auth/callback/casdoor`
- `https://user.cnnetmkt.top/casdoor/callback/`
- `https://portal.cnnetmkt.top/casdoor/callback/`
- `https://portal.h.imruyi.top:8443/casdoor/callback/`(备份)
## 验证命令
```bash
ops/gitops-migration/check_cnnetmkt_auth_primary.sh
```
关键验收点:
- 主链路不再出现 `auth.h.imruyi.top:8443`。
- `user-control-web` 为 `healthy`。
- Casdoor DB `application.redirect_uris` 包含 `https://cnnetmkt.top/casdoor/callback/`。
@@ -0,0 +1,27 @@
# AIOS V1R1C1 Codex GitOps Online Release
Date: 2026-07-06T10:21:08Z
## Purpose
Merge the current controlled ChatGPT/Codex GitOps access changes into the latest online baseline and make Git the operating version manager for Codex changes.
## Included
- ChatGPT Actions API adapter and OpenAPI schema.
- Secret-safe model registration using `secret_ref`.
- `codex-gitops-bot` runtime token account with scoped API permissions.
- Codex online GitOps configuration under `config/codex/gitops-online.json`.
- Baseline metadata updated from `V1R1C0` to `V1R1C1`.
## Excluded
- Acceptance dummy model registry entries from `patch/chatgpt-model-acceptance-*`.
- Plaintext API keys or bearer tokens.
## Verification
- `curl https://cnnetmkt.top/api/base` returns 200.
- `curl https://cnnetmkt.top/api/openapi-chatgpt-actions.yaml` returns 200.
- `bash scripts/package_contract_check_v1r1c0.sh` passes.
- `bash scripts/codex_gitops_online_check.sh` passes after tag publication.
@@ -0,0 +1,18 @@
# AIOS V1R1C2 Codex GitOps Online Sync Release
Date: 2026-07-06T10:26:26Z
## Purpose
Make Codex/API writes visible in the online Git service by syncing non-main patch branches from `/data/aios-gitops/workspace` into the Gitea bare repository after each committed write.
## Included
- `active-runtime/app/aios_chatgpt_actions_api.py` now syncs non-main branch commits to the online Git bare repository.
- `POST /api/tag` direct owner flow syncs tags when executed.
- `config/codex/gitops-online.json` records `online_git_sync=true`.
- Live and template systemd service configuration declares `AIOS_GITOPS_BARE_REPO`.
## Policy
`main` remains protected. API/Codex writes still commit to patch/request branches and owner/admin approval remains required for V-level production actions.
@@ -0,0 +1,28 @@
# AIOS V1R1C3 Codex + Custom GPT Collaboration Release
Date: 2026-07-06T10:42:33Z
## Purpose
Validate that local Codex and the AIOS GitOps 控制台 custom GPT can both modify the online GitOps baseline through controlled API writes, then merge both changes into the latest version.
## Custom GPT
https://chatgpt.com/g/g-6a4b7c82dff08191bc980798edea1bac-aios-gitops-kong-zhi-tai
## Merged Inputs
| Actor | Account | Branch | Commit | Online Sync |
|---|---|---|---|---|
| local_codex | codex-gitops-bot | `patch/collab-local-codex-20260706T104130Z` | `a62d4ce` | synced |
| custom_gpt | chatgpt-gitops-bot | `patch/collab-custom-gpt-20260706T104130Z` | `c393931` | synced |
## Result
Both branches were merged into `main`, baseline metadata was refreshed to `V1R1C3`, and the release tag `V1R1C3` was published to the online Git repository.
## Guardrails
- GPT and Codex writes entered Git as patch branch commits.
- Protected `main` was updated only by the release merge step.
- Bot tokens and custom LLM API keys remain outside Git.
@@ -0,0 +1,36 @@
# AIOS V1R1C4 Auth GitOps Portal Release
Date: 2026-07-06T11:05:30Z
## Purpose
Close the public-access gap around human GitOps shortlinks, move repository browsing to the protected Gitea endpoint, and make the new GitOps portal function visible from the main cnnetmkt owner/admin workspace.
## Changes
- `/base`, `/ver`, `/diff`, `/patch`, `/model`, and `/run` now redirect to `https://cnnetmkt.top/portal/me/#gitops` instead of exposing raw JSON to unauthenticated users.
- `/api/*` remains the controlled machine interface for ChatGPT Actions and Codex.
- Gitea now uses `https://g.cnnetmkt.top/git/`, requires sign-in for repository browsing, and hides registration.
- `aios-gitea-owner-sync.service` and timer synchronize the Gitea `owner` account password from the user-control owner secret without writing the password to Git or logs.
- The legacy local `aios` Gitea account has login prohibited and its password cleared; the row remains only to preserve repository ownership.
- The user portal now includes a GitOps dashboard section for admin/owner roles.
- The patched user-control-center source is tracked under `overlays/user-control-center/` with a replay script.
- The portal GitOps section reads `/app/runtime/gitops/current.json` before HTTP fallback, so the current version remains visible inside Docker.
## Verification Targets
- `https://cnnetmkt.top/model` returns `302` to `/portal/me/#gitops`.
- `https://cnnetmkt.top/api/model` returns the public machine-readable model metadata.
- `https://g.cnnetmkt.top/git/aios/aios-v23` returns a login redirect when unauthenticated.
- Owner login can open the Gitea repository; no secret is printed.
- Portal login state renders the GitOps section and links.
## Guardrails
- No ZIP update path is reintroduced.
- All replayable files are tracked in Git.
- Passwords, bearer tokens, Huawei keys, and custom LLM API keys remain outside Git.
## Secret Log Hygiene
The Gitea owner password sync script uses `runuser` instead of `sudo -u ... --password ...`, strips CRLF from sourced owner fields, and does not print passwords or tokens. Archived journal files were rotated/vacuumed after the live script was corrected.
@@ -0,0 +1,351 @@
# Codex 指令:修复 V1R1 微信主动发送后端
Directive ID: `CODEX-FIX-V1R1-WECHAT-BACKEND-20260707`
Branch: `patch/web-ui-complete-gitops-dashboard-20260706`
Base: `main`
Current baseline: `V1R1C4`
## 1. 当前阻塞
当前微信主动发送链路已改为:
```text
message_outbox
→ wechat_notify_worker.py
→ wechat_send_backend.py
→ openclaw-channel 或 ucc-adapter
```
实际验证结果:
```json
{
"backend": "unavailable",
"reason": "wechat outbound backend unavailable: openclaw channel not registered; ucc adapter send endpoint not detected"
}
```
已确认:
1. OpenClaw Gateway 运行中,但 `openclaw channels list --json` 只有 `{ "chat": {} }`,没有 wechat/weixin channel。
2. 当前生产微信能力优先出口应是 `ucc-wechat-qr-adapter`,不是 OpenClaw channel。
3. 当前系统存在进程:`/data/agent/services/ucc-wechat-qr-adapter/adapter.py`。
4. 当前服务配置:`/etc/aios/ucc-wechat-qr-adapter.env`。
5. 当前监听端口:`AIOS_WECHAT_QR_ADAPTER_PORT=18800`。
6. `wechat_send_backend.py` 没有探测到 UCC adapter 的可用发送接口。
## 2. 修复目标
Codex 必须补齐 V1R1 微信主动发送后端,使 `message_outbox → worker → ucc-adapter → 个人微信` 成为主路径。
不得让业务模块直接调用:
- `openclaw message send`;
- OpenClaw Gateway `127.0.0.1:18789`;
- 微信适配器底层进程;
- 本地 secrets 文件。
业务/外部系统只允许写入:
```text
message_outbox
```
或调用未来统一 API:
```text
POST /api/v1/wechat/outbox
```
## 3. Codex 必须先执行的检查
只执行命令,不输出 token。
### 3.1 检查 adapter 路由和函数
```bash
sudo grep -RniE '@.*route|@app\.(get|post)|FastAPI|Flask|send|message|notify|outbound|wechat|target' \
/data/agent/services/ucc-wechat-qr-adapter/adapter.py \
| sed -E 's/(token|password|secret|key|auth)([=: ][^ ]+)/\1=***MASKED***/Ig' \
| head -n 200
```
### 3.2 检查 adapter 健康接口
```bash
for p in \
/health \
/status \
/api/health \
/api/internal/health \
/api/internal/wechat/status \
/api/internal/wechat/send
do
echo "=== $p ==="
curl -sS "http://127.0.0.1:18800$p" || true
echo
done
```
### 3.3 如需 token,安全读取但不得打印
```bash
ADAPTER_TOKEN="$(sudo awk -F= '/^AIOS_WECHAT_QR_ADAPTER_TOKEN=/{print $2}' /etc/aios/ucc-wechat-qr-adapter.env)"
for p in \
/health \
/status \
/api/health \
/api/internal/health \
/api/internal/wechat/status
do
echo "=== $p ==="
curl -sS \
-H "Authorization: Bearer ${ADAPTER_TOKEN}" \
-H "X-AIOS-Token: ${ADAPTER_TOKEN}" \
"http://127.0.0.1:18800$p" || true
echo
done
```
## 4. 必须实现
### 4.1 如果 adapter 已有发送接口
若 `adapter.py` 已经存在发送接口,Codex 应:
1. 确认真实路径、请求格式、认证头;
2. 修改 `scripts/wechat_send_backend.py` 的 `ucc-adapter` backend 调用该真实接口;
3. 保证 token 仅从运行时环境或 env 文件读取,不进入 Git;
4. 更新验收文档。
### 4.2 如果 adapter 没有发送接口
Codex 应在 `/data/agent/services/ucc-wechat-qr-adapter/adapter.py` 对应源码路径中补内部接口,或在 GitOps 仓库新增 overlay/patch 指令,目标接口为:
```http
POST /api/internal/wechat/send
```
请求体:
```json
{
"target": "...",
"content": "...",
"account_id": "...",
"message_id": "...",
"metadata": {}
}
```
认证:
- 使用 `/etc/aios/ucc-wechat-qr-adapter.env` 中的 `AIOS_WECHAT_QR_ADAPTER_TOKEN`;
- 支持 `Authorization: Bearer <token>`;
- 支持 `X-AIOS-Token: <token>`;
- 不允许打印 token。
返回:
成功:
```json
{
"ok": true,
"provider_message_id": "...",
"status": "sent"
}
```
如果底层暂时没有真实发送函数,必须返回:
```json
{
"ok": false,
"code": "adapter_send_not_supported"
}
```
不得假装 `sent`。
## 5. 修改范围
允许修改或新增:
- `scripts/wechat_send_backend.py`
- `scripts/wechat_notify_worker.py`
- `scripts/enqueue_wechat_notification.py`
- `scripts/message_outbox_schema.py`
- `docs/ops/*wechat*`
- `tests/acceptance/*wechat*`
- UCC adapter 的 overlay/patch 文件
- systemd/env template 中与 worker 配置相关的字段
禁止修改:
- runtime secrets 明文;
- 个人微信原始 token;
- Gitea 安全配置;
- Casdoor/auth 配置;
- baseline active version;
- 删除 audit/version/history 的逻辑。
## 6. 必须新增或更新验收用例
### TC-WX-BE-01
OpenClaw channel list 只有 `{ "chat": {} }` 时,不得调用 `openclaw-weixin`。
预期:
```text
wechat_send_backend 选择 ucc-adapter 或明确 unavailable
last_error 不包含 Unknown channel: openclaw-weixin
```
### TC-WX-BE-02
UCC adapter 存活时自动选择 `ucc-adapter`。
预期:
```text
python3 scripts/wechat_send_backend.py
→ backend=ucc-adapter
```
### TC-WX-BE-03
outbox 测试消息可进入 worker。
预期:
```text
message_outbox.status 从 queued 变为 dispatching,再变为 sent 或 failed
```
### TC-WX-BE-04
如果 adapter 支持真实发送,owner 微信收到测试消息。
预期:
```text
message_outbox.status=sent
audit_logs.action=wechat_outbound_sent
```
### TC-WX-BE-05
如果 adapter 不支持发送,消息不得丢失。
预期:
```text
message_outbox.status=queued 或 failed
last_error 包含 adapter_send_not_supported
audit_logs.action=wechat_outbound_failed
```
### TC-WX-SEC-01
日志不得泄露:
- token;
- password;
- secret;
- cookie;
- 个人微信原始 ID 给普通用户;
- route proof;
- runtime secrets 路径。
## 7. 最小验收命令
### 7.1 检查 backend
```bash
cd /data/aios-gitops/workspace
python3 scripts/wechat_send_backend.py
```
### 7.2 入队测试
```bash
python3 scripts/enqueue_wechat_notification.py \
--recipient owner \
--recipient-role owner \
--message-type system_report \
--priority high \
--title "AIOS 测试通知" \
--content "这是通过 message_outbox → ucc-adapter 发送的主动微信通知。"
```
### 7.3 执行 worker
```bash
python3 scripts/wechat_notify_worker.py --once
```
### 7.4 查询状态
```bash
python3 - <<'PY'
import sqlite3
con=sqlite3.connect('/data/aios-gitops/workspace/data/webops.sqlite3')
con.row_factory=sqlite3.Row
print('message_outbox')
for r in con.execute('select id,status,retry_count,last_error,sent_at from message_outbox order by created_at desc limit 5'):
print(dict(r))
print('audit_logs')
for r in con.execute('select action,target,detail,created_at from audit_logs order by created_at desc limit 5'):
print(dict(r))
PY
```
## 8. 输出要求
Codex 完成后必须输出:
```text
Status: PASS / FAIL / BLOCKED
Branch: patch/web-ui-complete-gitops-dashboard-20260706
Files Changed: ...
Backend Selected: openclaw-channel / ucc-adapter / unavailable
Adapter Send Endpoint: detected / added / unsupported
Message Outbox Status: sent / queued / failed
Audit Status: written / missing
Owner WeChat Received: yes / no / not testable
Blocked Reason: ...
Next Action: ...
```
## 9. 当前期望结果
最优结果:
```text
backend=ucc-adapter
message_outbox.status=sent
owner 微信收到 AIOS 测试通知
```
可接受结果:
```text
backend=ucc-adapter
message_outbox.status=failed
last_error=adapter_send_not_supported
```
不可接受结果:
```text
继续报 Unknown channel: openclaw-weixin
假装 sent
日志打印 token
业务模块直接调用 OpenClaw
```
@@ -14,6 +14,7 @@ log() {
cd "$WORKSPACE"
TOKEN_FILE="$ROOT/runtime/secrets/gitops.tokens"
BARE_REPO="$ROOT/runtime/gitea-bin/data/repositories/aios/aios-v23.git"
if [ ! -s "$TOKEN_FILE" ]; then
(openssl rand -hex 32 || python3 -c 'import secrets; print(secrets.token_hex(32))') > "$TOKEN_FILE"
chmod 600 "$TOKEN_FILE"
@@ -32,9 +33,10 @@ Environment=AIOS_GITOPS_ROOT=$ROOT
Environment=AIOS_GITOPS_WORKSPACE=$WORKSPACE
Environment=AIOS_GITOPS_DB=$ROOT/runtime/gitops_model.sqlite3
Environment=AIOS_GITOPS_TOKEN_FILE=$TOKEN_FILE
Environment=AIOS_GITOPS_BARE_REPO=$BARE_REPO
Environment=AIOS_GITOPS_HOST=127.0.0.1
Environment=AIOS_GITOPS_PORT=18890
ExecStart=/usr/bin/python3 $WORKSPACE/active-runtime/app/aios_gitops_model_api.py
ExecStart=/usr/bin/python3 $WORKSPACE/active-runtime/app/aios_chatgpt_actions_api.py
Restart=always
RestartSec=3
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -euo pipefail
WORKSPACE="${AIOS_GITOPS_WORKSPACE:-/data/aios-gitops/workspace}"
RUNTIME_ROOT="${AIOS_USER_CONTROL_ROOT:-/data/aios/aios_r13_v18_runtime_portable_silent_20260704/components/user-control-center}"
CONTAINER="${AIOS_USER_CONTROL_CONTAINER:-user-control-web}"
install -D -m 0644 "$WORKSPACE/overlays/user-control-center/control/portal_views.py" "$RUNTIME_ROOT/control/portal_views.py"
install -D -m 0644 "$WORKSPACE/overlays/user-control-center/templates/portal/me.html" "$RUNTIME_ROOT/templates/portal/me.html"
install -D -m 0644 "$WORKSPACE/baseline/current.json" "$RUNTIME_ROOT/runtime/gitops/current.json"
if command -v docker >/dev/null 2>&1 && docker ps --format '{{.Names}}' | grep -qx "$CONTAINER"; then
docker cp "$RUNTIME_ROOT/control/portal_views.py" "$CONTAINER:/app/control/portal_views.py"
docker cp "$RUNTIME_ROOT/templates/portal/me.html" "$CONTAINER:/app/templates/portal/me.html"
docker exec "$CONTAINER" mkdir -p /app/runtime/gitops
docker cp "$RUNTIME_ROOT/runtime/gitops/current.json" "$CONTAINER:/app/runtime/gitops/current.json"
docker restart "$CONTAINER" >/dev/null
fi
echo "AIOS_USER_PORTAL_GITOPS_OVERLAY_APPLIED runtime=$RUNTIME_ROOT container=$CONTAINER"
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env bash
set -euo pipefail
PUBLIC_ENTRY="${PUBLIC_ENTRY:-https://cnnetmkt.top/}"
AUTH_ENTRY="${AUTH_ENTRY:-https://auth.cnnetmkt.top}"
CLIENT_ID="${CASDOOR_CLIENT_ID:-aios8a810963c1dd368a}"
CALLBACK="${CASDOOR_CALLBACK:-https://cnnetmkt.top/casdoor/callback/}"
AIHOST_HOST="${AIHOST_HOST:-aihost}"
HW_HOST="${HW_HOST:-hw.imruyi.top}"
fail() {
printf 'FAIL: %s\n' "$*" >&2
exit 1
}
CALLBACK_ENCODED="$(python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$CALLBACK")"
AUTH_URL="$AUTH_ENTRY/login/oauth/authorize?client_id=$CLIENT_ID&response_type=code&redirect_uri=$CALLBACK_ENCODED&scope=read&state=aios-portal&language=zh"
printf '== public redirect chain ==\n'
chain="$(curl -k -sS -L -I --max-redirs 4 "$PUBLIC_ENTRY")"
printf '%s\n' "$chain"
printf '%s\n' "$chain" | grep -F "Location: $AUTH_ENTRY/login/oauth/authorize" >/dev/null \
|| fail "public entry did not redirect to $AUTH_ENTRY"
if printf '%s\n' "$chain" | grep -F 'auth.h.imruyi.top:8443' >/dev/null; then
fail 'public chain still exposes backup auth domain'
fi
printf '\n== auth authorize http ==\n'
auth_headers="$(curl -k -sS -D - -o /tmp/aios-auth-primary-body --max-time 20 "$AUTH_URL")"
printf '%s\n' "$auth_headers"
printf '%s\n' "$auth_headers" | grep -F 'HTTP/' >/dev/null || fail 'auth endpoint returned no HTTP headers'
if grep -E '许可跳转列表|redirect.*not|not.*redirect' /tmp/aios-auth-primary-body >/dev/null 2>&1; then
fail 'auth body still contains redirect allow-list error text'
fi
if command -v ssh >/dev/null 2>&1; then
printf '\n== casdoor db redirect allow-list ==\n'
redirects="$(ssh "$AIHOST_HOST" "docker exec casdoor-postgres psql -U casdoor -d casdoor -Atc \"select redirect_uris from application where owner='admin' and name='aios-portal';\"" 2>/dev/null || true)"
if [ -n "$redirects" ]; then
printf '%s\n' "$redirects"
printf '%s\n' "$redirects" | grep -F "$CALLBACK" >/dev/null \
|| fail "Casdoor DB does not include $CALLBACK"
else
printf 'WARN: skipped Casdoor DB readback; set AIHOST_HOST to a reachable SSH target for a strict DB check.\n'
fi
printf '\n== ucc runtime env ==\n'
runtime_env="$(docker inspect user-control-web --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null | grep -E 'UCC_PUBLIC_BASE_URL|CASDOOR_(PUBLIC|INTERNAL|INVITE_SYNC)_BASE_URL' || true)"
if [ -z "$runtime_env" ]; then
runtime_env="$(ssh "$HW_HOST" 'docker inspect user-control-web --format "{{range .Config.Env}}{{println .}}{{end}}" | grep -E "UCC_PUBLIC_BASE_URL|CASDOOR_(PUBLIC|INTERNAL|INVITE_SYNC)_BASE_URL"' 2>/dev/null || true)"
fi
if [ -n "$runtime_env" ]; then
printf '%s\n' "$runtime_env"
printf '%s\n' "$runtime_env" | grep -F "CASDOOR_PUBLIC_BASE_URL=$AUTH_ENTRY" >/dev/null \
|| fail 'UCC runtime CASDOOR_PUBLIC_BASE_URL is not primary auth domain'
else
printf 'WARN: skipped UCC runtime env readback; run on hw.imruyi.top or set HW_HOST to a reachable SSH target.\n'
fi
fi
printf '\nPASS: cnnetmkt primary auth route is canonical and callback is allowed.\n'
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env python3
from __future__ import annotations
import json
import os
from pathlib import Path
from huaweicloudsdkcore.auth.credentials import BasicCredentials
from huaweicloudsdkdns.v2 import (
CreateRecordSetRequest,
CreateRecordSetRequestBody,
DnsClient,
ListRecordSetsByZoneRequest,
ListPublicZonesRequest,
UpdateRecordSetRequest,
UpdateRecordSetReq,
)
from huaweicloudsdkdns.v2.region.dns_region import DnsRegion
ENV_FILE = Path("/etc/letsencrypt/huaweicloud-cnnetmkt.env")
ZONE_NAME = "cnnetmkt.top."
RECORD_NAME = "cnnetmkt.top."
TARGET_IP = "1.95.164.183"
def load_env() -> None:
for line in ENV_FILE.read_text().splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
os.environ.setdefault(key, value)
def make_client() -> DnsClient:
ak = os.environ["HUAWEICLOUD_ACCESS_KEY_ID"]
sk = os.environ["HUAWEICLOUD_SECRET_ACCESS_KEY"]
region = os.environ.get("HUAWEICLOUD_REGION", "cn-southwest-2")
credentials = BasicCredentials(ak, sk)
return DnsClient.new_builder().with_credentials(credentials).with_region(DnsRegion.value_of(region)).build()
def as_dict(obj):
if hasattr(obj, "to_json_object"):
return obj.to_json_object()
if hasattr(obj, "to_dict"):
return obj.to_dict()
return obj
def main() -> int:
load_env()
client = make_client()
zones_resp = client.list_public_zones(ListPublicZonesRequest(name=ZONE_NAME))
zones = getattr(zones_resp, "zones", None) or []
if not zones:
raise RuntimeError(f"zone not found: {ZONE_NAME}")
zone = zones[0]
zone_id = zone.id
rs_req = ListRecordSetsByZoneRequest(zone_id=zone_id, name=RECORD_NAME, type="A")
rs_resp = client.list_record_sets_by_zone(rs_req)
recordsets = getattr(rs_resp, "recordsets", None) or []
existing = None
for item in recordsets:
if item.name == RECORD_NAME and item.type == "A":
existing = item
break
if existing:
records = list(existing.records or [])
if records == [TARGET_IP] and existing.status != "DISABLE":
action = "unchanged"
result = existing
else:
body = UpdateRecordSetReq(
name=RECORD_NAME,
type="A",
ttl=300,
records=[TARGET_IP],
description="AIOS GitOps apex entry",
)
req = UpdateRecordSetRequest(zone_id=zone_id, recordset_id=existing.id, body=body)
result = client.update_record_set(req)
action = "updated"
else:
body = CreateRecordSetRequestBody(
name=RECORD_NAME,
type="A",
ttl=300,
records=[TARGET_IP],
description="AIOS GitOps apex entry",
)
req = CreateRecordSetRequest(zone_id=zone_id, body=body)
result = client.create_record_set(req)
action = "created"
print(json.dumps({
"action": action,
"zone_id": zone_id,
"record": RECORD_NAME,
"type": "A",
"records": [TARGET_IP],
"result": as_dict(result),
}, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+113
View File
@@ -0,0 +1,113 @@
#!/usr/bin/env bash
set -euo pipefail
# Run from an operator machine that can SSH to both hw.imruyi.top and aihost.
# No secrets are embedded; the script uses existing server-side Docker/env state.
HW_HOST="${HW_HOST:-hw.imruyi.top}"
AIHOST_HOST="${AIHOST_HOST:-aihost}"
PRIMARY_AUTH="${PRIMARY_AUTH:-https://auth.cnnetmkt.top}"
PRIMARY_ENTRY="${PRIMARY_ENTRY:-https://cnnetmkt.top}"
UCC_DEPLOY_DIR="${UCC_DEPLOY_DIR:-/data/aios/aios_r13_v18_runtime_portable_silent_20260704/components/user-control-center/deploy}"
printf '== update Casdoor application on %s ==\n' "$AIHOST_HOST"
ssh "$AIHOST_HOST" bash -s <<'AIHOST'
set -euo pipefail
TS=$(date -u +%Y%m%dT%H%M%SZ)
BACKUP_DIR="/data/aios/backups/casdoor-cnnetmkt-primary-$TS"
sudo mkdir -p "$BACKUP_DIR"
sudo chown "$USER":"$USER" "$BACKUP_DIR"
docker exec casdoor-postgres pg_dump -U casdoor -d casdoor -t application > "$BACKUP_DIR/application-before.sql"
docker exec -i casdoor-postgres psql -U casdoor -d casdoor -v ON_ERROR_STOP=1 <<'SQL'
UPDATE application
SET
homepage_url = 'https://cnnetmkt.top/',
signin_url = 'https://cnnetmkt.top/portal/login/',
signup_url = 'https://cnnetmkt.top/portal/register/',
redirect_uris = '["https://cnnetmkt.top/api/auth/callback/casdoor", "https://cnnetmkt.top/casdoor/callback/", "https://portal.cnnetmkt.top/api/auth/callback/casdoor", "https://portal.cnnetmkt.top/casdoor/callback/", "https://user.cnnetmkt.top/api/auth/callback/casdoor", "https://user.cnnetmkt.top/casdoor/callback/", "https://portal.h.imruyi.top:8443/api/auth/callback/casdoor", "https://portal.h.imruyi.top:8443/casdoor/callback/"]'
WHERE owner = 'admin' AND name = 'aios-portal';
SQL
docker exec casdoor-postgres psql -U casdoor -d casdoor -P pager=off -c "select owner,name,homepage_url,signin_url,signup_url,redirect_uris from application where owner='admin' and name='aios-portal';" > "$BACKUP_DIR/application-after.txt"
cat "$BACKUP_DIR/application-after.txt"
printf 'casdoor_backup_dir=%s\n' "$BACKUP_DIR"
AIHOST
printf '\n== update HW ingress and UCC runtime on %s ==\n' "$HW_HOST"
ssh "$HW_HOST" bash -s <<HW
set -euo pipefail
TS=\$(date -u +%Y%m%dT%H%M%SZ)
BACKUP_DIR="/data/aios-gitops/backups/auth-cnnetmkt-primary-\$TS"
ENV_FILE="$UCC_DEPLOY_DIR/.env"
NGINX_AUTH="/etc/nginx/sites-available/auth-cnnetmkt-casdoor.conf"
sudo mkdir -p "\$BACKUP_DIR"
sudo cp -a "\$ENV_FILE" "\$BACKUP_DIR/user-control.env.before"
sudo cp -a /etc/nginx/sites-available "\$BACKUP_DIR/nginx-sites-available.before"
set_env() {
local key="\$1" value="\$2"
if sudo grep -q "^\${key}=" "\$ENV_FILE"; then
sudo sed -i "s#^\${key}=.*#\${key}=\${value}#" "\$ENV_FILE"
else
printf '%s=%s\n' "\$key" "\$value" | sudo tee -a "\$ENV_FILE" >/dev/null
fi
}
set_env ALLOWED_HOSTS 'cnnetmkt.top,portal.cnnetmkt.top,user.cnnetmkt.top,auth.cnnetmkt.top,127.0.0.1,localhost,hw.imruyi.top,1.95.164.183,portal.h.imruyi.top'
set_env CSRF_TRUSTED_ORIGINS 'https://cnnetmkt.top,https://portal.cnnetmkt.top,https://user.cnnetmkt.top,https://auth.cnnetmkt.top,http://portal.cnnetmkt.top,http://hw.imruyi.top,http://1.95.164.183,https://portal.h.imruyi.top:8443,http://portal.h.imruyi.top,http://user.cnnetmkt.top'
set_env UCC_PUBLIC_BASE_URL '$PRIMARY_ENTRY'
set_env CASDOOR_PUBLIC_BASE_URL '$PRIMARY_AUTH'
set_env CASDOOR_INTERNAL_BASE_URL '$PRIMARY_AUTH'
set_env CASDOOR_INVITE_SYNC_BASE_URL '$PRIMARY_AUTH'
sudo tee "\$NGINX_AUTH" >/dev/null <<'NGINX'
# AIOS primary public Casdoor entry for cnnetmkt.top.
# The upstream remains the existing backup/home Casdoor service; public clients should see auth.cnnetmkt.top only.
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name auth.cnnetmkt.top;
access_log /var/log/nginx/aios-auth-cnnetmkt.access.log;
error_log /var/log/nginx/aios-auth-cnnetmkt.error.log;
ssl_certificate /etc/letsencrypt/live/cnnetmkt-wildcard/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/cnnetmkt-wildcard/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
proxy_pass https://auth.h.imruyi.top:8443;
proxy_http_version 1.1;
proxy_ssl_server_name on;
proxy_ssl_name auth.h.imruyi.top;
proxy_ssl_verify off;
proxy_set_header Host auth.h.imruyi.top;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Host \$host;
proxy_set_header X-Forwarded-Port 443;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection \$connection_upgrade;
proxy_redirect https://auth.h.imruyi.top:8443/ https://auth.cnnetmkt.top/;
proxy_redirect https://auth.h.imruyi.top/ https://auth.cnnetmkt.top/;
}
}
server {
listen 80;
listen [::]:80;
server_name auth.cnnetmkt.top;
return 301 https://\$host\$request_uri;
}
NGINX
sudo ln -sf "\$NGINX_AUTH" /etc/nginx/sites-enabled/auth-cnnetmkt-casdoor.conf
sudo nginx -t
sudo systemctl reload nginx
cd "$UCC_DEPLOY_DIR"
sudo docker compose up -d --force-recreate user-control-web user-control-worker user-control-beat
printf 'hw_backup_dir=%s\n' "\$BACKUP_DIR"
HW
"$(dirname "$0")/check_cnnetmkt_auth_primary.sh"
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
set -euo pipefail
ENV_FILE="${UCC_ENV_FILE:-/data/aios/aios_r13_v18_runtime_portable_silent_20260704/components/user-control-center/deploy/.env}"
GITEA_BIN="${GITEA_BIN:-/data/aios-gitops/runtime/bin/gitea}"
APP_INI="${GITEA_APP_INI:-/data/aios-gitops/runtime/gitea-bin/custom/conf/app.ini}"
WORK_PATH="${GITEA_WORK_PATH:-/data/aios-gitops/runtime/gitea-bin}"
DB="${GITEA_DB:-/data/aios-gitops/runtime/gitea-bin/data/gitea.db}"
set -a
# shellcheck disable=SC1090
. "$ENV_FILE"
set +a
strip_trailing_cr() {
printf '%s' "${1:-}" | tr -d '\r'
}
OWNER_USERNAME="$(strip_trailing_cr "${UCC_OWNER_USERNAME:-owner}")"
OWNER_EMAIL="$(strip_trailing_cr "${UCC_OWNER_EMAIL:-owner@cnnetmkt.top}")"
OWNER_PASSWORD="$(strip_trailing_cr "${UCC_OWNER_PASSWORD:?UCC_OWNER_PASSWORD is required}")"
GITEA_RUN_USER="${GITEA_RUN_USER:-aiman}"
run_gitea() {
runuser -u "$GITEA_RUN_USER" -- "$GITEA_BIN" --config "$APP_INI" --work-path "$WORK_PATH" "$@"
}
if run_gitea admin user list 2>/dev/null | awk '{print $2}' | grep -Fxq "$OWNER_USERNAME"; then
run_gitea admin user change-password --username "$OWNER_USERNAME" --password "$OWNER_PASSWORD" --must-change-password=false >/dev/null
else
run_gitea admin user create --username "$OWNER_USERNAME" --password "$OWNER_PASSWORD" --email "$OWNER_EMAIL" --admin --must-change-password=false >/dev/null
fi
python3 - <<PY
import sqlite3, time
con=sqlite3.connect('$DB')
cur=con.cursor()
cur.execute("update user set is_admin=1,is_active=1,prohibit_login=0,must_change_password=0,updated_unix=? where lower_name=lower(?)", (int(time.time()), '$OWNER_USERNAME'))
cur.execute("update user set prohibit_login=1,is_admin=0,passwd='',passwd_hash_algo='disabled',must_change_password=1,updated_unix=? where lower_name in ('aios','admin','gitea') and lower_name<>lower(?)", (int(time.time()), '$OWNER_USERNAME'))
con.commit()
PY
printf 'GITEA_OWNER_SYNC_OK user=%s default_local_login_disabled=1\n' "$OWNER_USERNAME"
@@ -0,0 +1,154 @@
# OpenClaw 指令:AIOS Autonomous GitOps 静默优化与部署监督
Directive ID: `OPENCLAW-AIOS-AUTONOMOUS-GITOPS-DEPLOY-20260707`
Branch: `patch/web-ui-complete-gitops-dashboard-20260706`
Base: `main`
Current baseline: `V1R1C4`
Proposed baseline after approved merge: `V1R1C5`
## 1. 执行目的
OpenClaw 负责接管本分支的执行与监督,目标是在不影响现有执行层、不破坏用户使用体验的前提下,完成 AIOS Autonomous GitOps Runtime 的验证、静默优化、部署准备、Owner 通报与合并候选推进。
本次改动的目标不是直接绕过 main 保护,而是让系统形成:
- 自动 CI 验证;
- Codex 自愈修复;
- event bus + daemon 自动执行;
- 微信/网页 Owner 审计与操作通道;
- V1R1C5 基线提案;
- 失败回流自定义 GPT 修复;
- 通过后生成 merge candidate。
## 2. OpenClaw 角色
OpenClaw 是本任务的执行监督代理,职责如下:
1. 读取并监督当前分支与 main 的差异。
2. 按验收测试执行静态验证、安全验证、策略验证、灰度验证。
3. 发现失败后,生成结构化 failure report。
4. 将失败项交给自定义 GPT 继续修复同一 patch 分支。
5. 修复后重复验证,直到 PASS 或触发 hard block。
6. 生成 merge candidate,但不得直接绕过 main 保护。
7. 监督 Owner 通知记录:每个 main 版本变更必须有摘要通报与 Web UI 记录。
## 3. 禁止事项
OpenClaw 不得执行以下操作:
- 不得直接 force push;
- 不得删除版本历史、tag、audit 记录;
- 不得写入真实凭据;
- 不得关闭 auth、Gitea 登录保护或 owner sync;
- 不得在未通过验收前合并 main;
- 不得在未有审批或预授权 runbook 前执行生产 tag、baseline activation 或 rollback execution。
## 4. 必须执行的命令
```bash
git fetch --all --prune
git checkout patch/web-ui-complete-gitops-dashboard-20260706
git diff --stat main...HEAD
bash scripts/validate-gitops-platform-ui.sh patch/web-ui-complete-gitops-dashboard-20260706 main
bash scripts/autonomous-merge-gate.sh patch/web-ui-complete-gitops-dashboard-20260706 main
```
若部署验证需要启动本地 runtime:
```bash
bash deploy/install/install.sh
python3 services/aios-autonomous-daemon/daemon.py emit --type patch_created --branch patch/web-ui-complete-gitops-dashboard-20260706 --base main
python3 services/aios-autonomous-daemon/daemon.py run --once
```
## 5. 必须验证的直接修改
OpenClaw 必须检查以下路径:
- `overlays/user-control-center/portal/gitops-dashboard/`
- `services/aios-autonomous-daemon/`
- `services/aios-event-ingress/`
- `deploy/systemd/`
- `deploy/install/`
- `config/codex/`
- `scripts/`
- `tests/acceptance/`
- `versions/proposals/V1R1C5-gitops-platform-ui.proposed.json`
- `skills/`
- `docs/codex/`
## 6. 必须验证的间接影响
OpenClaw 必须确认以下内容未被破坏:
- `/api/base`、`/api/ver`、`/api/diff`、`/api/patch`、`/api/model`、`/api/run`;
- `/base`、`/ver`、`/diff`、`/patch`、`/model`、`/run` 登录保护;
- Portal 登录入口 `/portal/me/#gitops`;
- Gitea require sign-in view;
- owner password sync;
- active baseline 仍为 `V1R1C4`,未提前激活 `V1R1C5`。
## 7. Owner 通报要求
每个 main 版本变更、merge candidate、validation failure、rollback request、approval required 事件都必须形成:
1. Owner 消息摘要;
2. Web UI 审计记录;
3. 可追踪 event_id;
4. 变更功能摘要;
5. 代码量与配置量统计;
6. 与上一次主线变化的间隔;
7. 测试状态;
8. rollback target。
## 8. OpenClaw 进度监督格式
OpenClaw 每次监督输出必须包含:
```text
Status: PASS / FAIL / BLOCKED / RUNNING
Branch: patch/web-ui-complete-gitops-dashboard-20260706
Base: main
Current Rev: <rev>
Validation: <summary>
Gray Test: <summary>
Owner Notice: <summary>
Merge Candidate: yes/no
Blocked Reason: <reason or none>
Next Action: <action>
```
## 9. 失败回流规则
若失败,OpenClaw 必须写入 failure report:
- 路径:`reports/openclaw/failure-<event_id>.json`
- 内容:失败测试 ID、文件路径、失败原因、建议修复、是否阻断合并。
随后将问题交给自定义 GPT 修改当前 patch 分支,修复后重新执行完整验收。
## 10. 合并前门禁
只有全部满足以下条件,OpenClaw 才能标记为 `merge_candidate_ready`:
- required acceptance tests 全部 PASS;
- 无凭据泄露;
- 无 direct main write;
- 无高风险未审批动作;
- 灰度验证 PASS;
- Owner 通报与 Web UI 记录正常;
- `V1R1C5` baseline proposal 存在且未提前激活。
## 11. 最终输出
OpenClaw 最终应生成:
- `reports/openclaw/final-supervision-report.json`
- `reports/codex/merge-candidate.json`
- Owner 摘要事件记录;
- Web UI audit record;
- 若失败则生成 failure report 并阻止合并。
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,462 @@
/* AIOS GitOps Platform Control Plane
* Browser-only runtime with API-first behavior and safe local fallback.
* No credentials or secret values are stored here. Model registration accepts secret_ref only.
*/
const Platform = (() => {
const state = {
tenant: 'default',
role: 'viewer',
identity: null,
baseline: null,
versions: null,
runtime: null,
patches: null,
models: null,
approvals: [],
audit: [],
policy: null,
eventSource: null
};
const ENDPOINTS = {
identity: 'identity',
policy: 'platform/policy',
audit: 'audit',
approvals: 'approval',
approvalRequest: 'approval-request',
approvalDecision: 'approval-decision',
branch: 'branch',
plan: 'patch/plan',
base: 'base',
versions: 'ver',
diff: 'diff',
patch: 'patch',
model: 'model',
priority: 'model-priority',
runtime: 'run',
rollback: 'rollback-request',
tag: 'tag-request',
events: 'events/gitops'
};
const DEFAULT_POLICY = {
roles: {
viewer: ['read:*', 'audit:read'],
operator: ['read:*', 'audit:read', 'branch:create', 'plan:create', 'approval:create'],
admin: ['read:*', 'audit:read', 'branch:create', 'plan:create', 'approval:create', 'model:write', 'priority:write', 'release:request', 'rollback:request'],
owner: ['read:*', 'audit:*', 'branch:*', 'plan:*', 'approval:*', 'model:*', 'priority:*', 'release:*', 'rollback:*', 'runtime:*']
},
highRiskActions: ['release:execute', 'rollback:execute', 'runtime:write', 'tag:create', 'main:write'],
destructiveActions: ['delete:*', 'force_push', 'purge:*', 'clear:*'],
requiredApprovals: { low: 1, medium: 1, high: 2 },
tenantIsolation: true,
secretHygiene: {
allowSecretRefOnly: true,
forbiddenFields: ['api_key', 'token', 'password', 'secret', 'private_key', 'bearer']
}
};
function el(id) { return document.getElementById(id); }
function qs(sel) { return document.querySelector(sel); }
function qsa(sel) { return Array.from(document.querySelectorAll(sel)); }
function now() { return new Date().toISOString(); }
function safeStringify(value) {
return JSON.stringify(redact(value), null, 2);
}
function redact(value) {
if (Array.isArray(value)) return value.map(redact);
if (value && typeof value === 'object') {
const out = {};
for (const [key, val] of Object.entries(value)) {
const k = key.toLowerCase();
if (['api_key', 'apikey', 'token', 'password', 'secret', 'private_key', 'authorization', 'bearer'].some(x => k.includes(x))) {
out[key] = typeof val === 'string' && val.startsWith('secret_ref://') ? val : '[REDACTED]';
} else out[key] = redact(val);
}
return out;
}
return value;
}
function put(id, value) {
const target = el(id);
if (target) target.textContent = typeof value === 'string' ? value : safeStringify(value);
}
async function api(path, method = 'GET', body = undefined, options = {}) {
const url = `/api/${path}`;
const headers = { 'Content-Type': 'application/json', 'X-AIOS-Tenant': state.tenant };
const res = await fetch(url, {
method,
headers,
credentials: 'same-origin',
body: body === undefined ? undefined : JSON.stringify(body)
});
let data = null;
try { data = await res.json(); } catch (_) { data = { status: res.status, text: await res.text() }; }
if (!res.ok && !options.allowError) {
const err = new Error(`API ${method} ${url} failed with ${res.status}`);
err.response = data;
throw err;
}
return data;
}
function has(capability) {
const policy = state.policy || DEFAULT_POLICY;
const caps = policy.roles[state.role] || [];
return caps.includes('*') || caps.includes(capability) || caps.some(c => c.endsWith(':*') && capability.startsWith(c.slice(0, -1))) || caps.includes('read:*') && capability.startsWith('read:');
}
function guardAction(capability, risk = 'low') {
const policy = state.policy || DEFAULT_POLICY;
if (policy.destructiveActions.some(pattern => matchCapability(pattern, capability))) return { allowed: false, reason: 'destructive_action_blocked' };
if (policy.highRiskActions.includes(capability)) return { allowed: false, reason: 'high_risk_requires_approval' };
if (!has(capability)) return { allowed: false, reason: 'insufficient_role' };
if (risk === 'high' && !['admin', 'owner'].includes(state.role)) return { allowed: false, reason: 'high_risk_admin_required' };
return { allowed: true };
}
function matchCapability(pattern, capability) {
if (pattern === capability) return true;
if (pattern.endsWith('*')) return capability.startsWith(pattern.slice(0, -1));
return false;
}
function audit(action, target, status, detail = {}) {
const record = redact({
ts: now(), tenant: state.tenant, role: state.role, action, target, status, detail
});
state.audit.unshift(record);
state.audit = state.audit.slice(0, 500);
localStorage.setItem('aios.gitops.audit', JSON.stringify(state.audit));
put('audit-output', record);
renderAudit();
postAudit(record);
return record;
}
async function postAudit(record) {
try { await api(ENDPOINTS.audit, 'POST', record, { allowError: true }); } catch (_) { /* local fallback */ }
}
function loadLocal() {
try { state.audit = JSON.parse(localStorage.getItem('aios.gitops.audit') || '[]'); } catch (_) { state.audit = []; }
try { state.approvals = JSON.parse(localStorage.getItem('aios.gitops.approvals') || '[]'); } catch (_) { state.approvals = []; }
}
function saveApprovals() {
localStorage.setItem('aios.gitops.approvals', JSON.stringify(state.approvals));
}
async function bootstrap() {
loadLocal();
wireTabs();
wireForms();
await loadIdentity();
await loadPolicy();
applyRbac();
await refreshAll();
await refreshApprovals();
await refreshAudit();
connectEvents();
}
async function loadIdentity() {
try {
const identity = await api(ENDPOINTS.identity, 'GET', undefined, { allowError: true });
state.identity = identity && identity.user ? identity : { user: 'unknown', role: identity.role || 'viewer', tenant: identity.tenant || 'default' };
state.role = state.identity.role || 'viewer';
state.tenant = state.identity.tenant || 'default';
} catch (_) {
state.identity = { user: 'local-preview', role: 'admin', tenant: 'default', source: 'local_fallback' };
state.role = 'admin';
state.tenant = 'default';
}
put('identity-output', state.identity);
const roleChip = el('role-chip');
if (roleChip) roleChip.textContent = `role: ${state.role}`;
}
async function loadPolicy() {
try {
const policy = await api(ENDPOINTS.policy, 'GET', undefined, { allowError: true });
state.policy = policy && policy.roles ? policy : DEFAULT_POLICY;
} catch (_) { state.policy = DEFAULT_POLICY; }
put('rbac-output', state.policy);
renderPermissionMatrix();
}
async function refreshAll() {
const tasks = [
['baseline', ENDPOINTS.base],
['versions', ENDPOINTS.versions],
['runtime', ENDPOINTS.runtime],
['patches', ENDPOINTS.patch],
['models', ENDPOINTS.model]
];
for (const [key, endpoint] of tasks) {
try { state[key] = await api(endpoint, 'GET', undefined, { allowError: true }); }
catch (e) { state[key] = { error: e.message, response: e.response }; }
}
put('baseline-output', state.baseline);
put('versions-output', state.versions);
put('runtime-output', state.runtime);
put('patch-summary-output', state.patches);
put('model-output', state.models);
updateMetrics();
renderModelGraph();
audit('dashboard.refresh', 'all', 'ok');
}
function updateMetrics() {
const runtimeStatus = state.runtime && state.runtime.status || 'unknown';
const version = state.baseline && (state.baseline.active_version || state.baseline.version) || '-';
const branches = state.patches && Array.isArray(state.patches.branches) ? state.patches.branches.filter(b => b.startsWith('patch/')).length : 0;
const pending = state.approvals.filter(a => a.status === 'pending').length;
setText('metric-runtime', runtimeStatus);
setText('metric-version', version);
setText('metric-branches', String(branches));
setText('metric-approvals', String(pending));
setText('runtime-chip', `runtime: ${runtimeStatus}`);
}
function setText(id, text) { const node = el(id); if (node) node.textContent = text; }
function applyRbac() {
qsa('.require-operator').forEach(btn => btn.disabled = !has('approval:create') && !has('branch:create'));
qsa('.require-admin').forEach(btn => btn.disabled = !['admin', 'owner'].includes(state.role));
qsa('.guarded').forEach(btn => btn.disabled = state.role !== 'owner');
renderPermissionMatrix();
}
function renderPermissionMatrix() {
const policy = state.policy || DEFAULT_POLICY;
const caps = ['read:*', 'branch:create', 'plan:create', 'approval:create', 'model:write', 'priority:write', 'release:request', 'rollback:request', 'runtime:write'];
const rows = Object.keys(policy.roles).map(role => {
const cells = caps.map(cap => `<td>${roleHas(role, cap, policy) ? '✅' : '—'}</td>`).join('');
return `<tr><th>${escapeHtml(role)}</th>${cells}</tr>`;
}).join('');
const html = `<table><thead><tr><th>Role</th>${caps.map(c => `<th>${escapeHtml(c)}</th>`).join('')}</tr></thead><tbody>${rows}</tbody></table>`;
const target = el('permission-matrix'); if (target) target.innerHTML = html;
}
function roleHas(role, cap, policy) {
const caps = policy.roles[role] || [];
return caps.includes('*') || caps.includes(cap) || caps.some(c => c.endsWith(':*') && cap.startsWith(c.slice(0, -1))) || (caps.includes('read:*') && cap.startsWith('read:'));
}
function renderModelGraph() {
const target = el('model-priority-graph');
if (!target) return;
const base = state.models && Array.isArray(state.models.base) ? state.models.base : [];
const custom = state.models && Array.isArray(state.models.custom) ? state.models.custom : [];
const priority = state.models && Array.isArray(state.models.priority) ? state.models.priority : [];
const models = [...base, ...custom];
if (!models.length) { target.innerHTML = '<p class="empty">无模型数据</p>'; return; }
const byId = new Map(priority.map(p => [p.model_id || p.id, p]));
target.innerHTML = models.map((m, idx) => {
const p = byId.get(m.id) || { priority: 100 - idx, enabled: true, fallback_policy: 'next_enabled' };
return `<div class="node"><strong>${escapeHtml(m.display_name || m.model || m.id)}</strong><span>${escapeHtml(m.id || '')}</span><em>priority ${escapeHtml(String(p.priority))} · ${p.enabled === false ? 'disabled' : 'enabled'} · ${escapeHtml(p.fallback_policy || 'next_enabled')}</em></div>`;
}).join('<div class="edge">↓ fallback</div>');
}
async function refreshApprovals() {
try {
const remote = await api(ENDPOINTS.approvals, 'GET', undefined, { allowError: true });
if (remote && Array.isArray(remote.items)) state.approvals = remote.items;
else if (Array.isArray(remote)) state.approvals = remote;
} catch (_) { /* local approvals already loaded */ }
renderApprovals();
updateMetrics();
}
function renderApprovals() {
const target = el('approval-board');
if (!target) return;
if (!state.approvals.length) { target.innerHTML = '<p class="empty">暂无审批请求</p>'; return; }
target.innerHTML = state.approvals.map(item => {
const canDecide = has('approval:*') || state.role === 'owner';
return `<article class="approval ${escapeHtml(item.risk || 'medium')}">
<header><strong>${escapeHtml(item.type)}</strong><span>${escapeHtml(item.status)}</span></header>
<p>${escapeHtml(item.target)}</p><small>${escapeHtml(item.reason || '')}</small>
<footer>${canDecide && item.status === 'pending' ? `<button data-approve="${escapeHtml(item.id)}">Approve</button><button data-reject="${escapeHtml(item.id)}">Reject</button>` : ''}</footer>
</article>`;
}).join('');
qsa('[data-approve]').forEach(btn => btn.onclick = () => decideApproval(btn.dataset.approve, 'approved'));
qsa('[data-reject]').forEach(btn => btn.onclick = () => decideApproval(btn.dataset.reject, 'rejected'));
}
async function createApproval(payload) {
const guard = guardAction('approval:create', payload.risk || 'medium');
if (!guard.allowed) return { status: 'blocked', ...guard };
const item = {
id: `apr-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
tenant: state.tenant,
status: 'pending',
created_at: now(),
created_by: state.identity && state.identity.user || 'unknown',
...payload
};
try {
const remote = await api(ENDPOINTS.approvalRequest, 'POST', item, { allowError: true });
if (remote && (remote.id || remote.status)) {
audit('approval.create', item.target, 'ok', remote);
await refreshApprovals();
return remote;
}
} catch (_) { /* local fallback */ }
state.approvals.unshift(item); saveApprovals(); renderApprovals(); updateMetrics();
audit('approval.create.local', item.target, 'ok', item);
return item;
}
async function decideApproval(id, status) {
const guard = guardAction('approval:decide', 'high');
if (!guard.allowed) { audit('approval.decide', id, 'blocked', guard); return; }
try {
const remote = await api(ENDPOINTS.approvalDecision, 'POST', { id, status }, { allowError: true });
audit('approval.decide', id, 'ok', remote);
} catch (_) {
const item = state.approvals.find(a => a.id === id);
if (item) { item.status = status; item.decided_at = now(); item.decided_by = state.identity && state.identity.user || 'unknown'; }
saveApprovals(); audit('approval.decide.local', id, 'ok', { status });
}
renderApprovals(); updateMetrics();
}
async function refreshAudit() {
try {
const remote = await api(ENDPOINTS.audit, 'GET', undefined, { allowError: true });
if (remote && Array.isArray(remote.items)) state.audit = remote.items.concat(state.audit).slice(0, 500);
} catch (_) { /* local fallback */ }
renderAudit();
}
function renderAudit() {
const target = el('audit-table'); if (!target) return;
const filter = (el('audit-filter') && el('audit-filter').value || '').toLowerCase();
const rows = state.audit.filter(r => !filter || JSON.stringify(r).toLowerCase().includes(filter)).slice(0, 100).map(r =>
`<tr><td>${escapeHtml(r.ts)}</td><td>${escapeHtml(r.tenant || '')}</td><td>${escapeHtml(r.role || '')}</td><td>${escapeHtml(r.action)}</td><td>${escapeHtml(r.target)}</td><td>${escapeHtml(r.status)}</td></tr>`
).join('');
target.innerHTML = `<table><thead><tr><th>Time</th><th>Tenant</th><th>Role</th><th>Action</th><th>Target</th><th>Status</th></tr></thead><tbody>${rows || '<tr><td colspan="6">无审计记录</td></tr>'}</tbody></table>`;
}
function parseDiffTree(diffData) {
const stdout = diffData && diffData.diff && diffData.diff.stdout || diffData && diffData.stdout || '';
const files = [];
let current = null;
stdout.split('\n').forEach(line => {
if (line.startsWith('diff --git ')) {
const parts = line.split(' ');
current = { file: (parts[3] || '').replace(/^b\//, ''), additions: 0, deletions: 0, chunks: 0 };
files.push(current);
} else if (current) {
if (line.startsWith('@@')) current.chunks++;
else if (line.startsWith('+') && !line.startsWith('+++')) current.additions++;
else if (line.startsWith('-') && !line.startsWith('---')) current.deletions++;
}
});
return files;
}
function renderDiffTree(diffData) {
const target = el('diff-tree'); if (!target) return;
const files = parseDiffTree(diffData);
if (!files.length) { target.innerHTML = '<p class="empty">未解析到文件级 diff,可能是接口返回过大或为空。</p>'; return; }
target.innerHTML = files.map(f => `<div class="diff-file"><strong>${escapeHtml(f.file)}</strong><span>+${f.additions} / -${f.deletions} / ${f.chunks} chunks</span></div>`).join('');
}
function connectEvents() {
if (!window.EventSource) return;
try {
state.eventSource = new EventSource(`/api/${ENDPOINTS.events}?tenant=${encodeURIComponent(state.tenant)}`);
state.eventSource.onmessage = (event) => {
const payload = JSON.parse(event.data);
audit('event.gitops', payload.type || 'event', 'ok', payload);
if (['approval', 'patch', 'runtime', 'model'].includes(payload.type)) refreshAll();
};
state.eventSource.onerror = () => { if (state.eventSource) state.eventSource.close(); };
} catch (_) { /* server-sent events optional */ }
}
function wireTabs() {
qsa('.tabs button').forEach(button => {
button.onclick = () => {
qsa('.tabs button').forEach(b => b.classList.remove('active'));
qsa('.tab').forEach(tab => tab.classList.remove('active'));
button.classList.add('active');
const target = el(`tab-${button.dataset.tab}`);
if (target) target.classList.add('active');
};
});
}
function wireForms() {
const refreshBtn = el('refresh-all'); if (refreshBtn) refreshBtn.onclick = refreshAll;
const refreshModel = el('refresh-model'); if (refreshModel) refreshModel.onclick = refreshAll;
const refreshApprovalsBtn = el('refresh-approvals'); if (refreshApprovalsBtn) refreshApprovalsBtn.onclick = refreshApprovals;
const clearApprovals = el('clear-local-approvals'); if (clearApprovals) clearApprovals.onclick = () => { state.approvals = []; saveApprovals(); renderApprovals(); audit('approval.local.clear', 'localStorage', 'ok'); };
const refreshAuditBtn = el('refresh-audit'); if (refreshAuditBtn) refreshAuditBtn.onclick = refreshAudit;
const auditFilter = el('audit-filter'); if (auditFilter) auditFilter.oninput = renderAudit;
const exportAudit = el('export-audit'); if (exportAudit) exportAudit.onclick = () => downloadJson('aios-gitops-audit.json', state.audit);
bindForm('approval-form', async f => createApproval({ type: f.get('type'), target: f.get('target'), risk: f.get('risk'), reason: f.get('reason') }), 'approval-output');
bindForm('diff-form', async f => {
const data = await api(`${ENDPOINTS.diff}?from=${encodeURIComponent(f.get('from'))}&to=${encodeURIComponent(f.get('to'))}`, 'GET', undefined, { allowError: true });
renderDiffTree(data); return data;
}, 'diff-output');
bindForm('branch-form', async f => checkedApi('branch:create', ENDPOINTS.branch, 'POST', { branch: f.get('branch'), base: f.get('base'), message: f.get('message') }), 'branch-output');
bindForm('plan-form', async f => checkedApi('plan:create', ENDPOINTS.plan, 'POST', { branch: f.get('branch'), text: f.get('text') }), 'plan-output');
bindForm('model-form', async f => {
const payload = { provider_name: f.get('provider_name'), base_url: f.get('base_url'), model_name: f.get('model_name'), display_name: f.get('display_name'), api_key_secret_ref: f.get('api_key_secret_ref') };
if (!String(payload.api_key_secret_ref || '').startsWith('secret_ref://')) return { status: 'blocked', reason: 'api_key_secret_ref_required' };
return checkedApi('model:write', ENDPOINTS.model, 'POST', payload, 'high');
}, 'model-register-output');
bindForm('priority-form', async f => checkedApi('priority:write', ENDPOINTS.priority, 'PUT', { model_id: f.get('model_id'), priority: Number(f.get('priority')), enabled: f.get('enabled') === 'true', fallback_policy: f.get('fallback_policy') }, 'medium'), 'priority-output');
bindForm('rollback-form', async f => checkedApi('rollback:request', ENDPOINTS.rollback, 'POST', { target_version: f.get('target_version'), reason: f.get('reason'), branch: f.get('branch') }, 'high'), 'rollback-output');
bindForm('tag-form', async f => checkedApi('release:request', ENDPOINTS.tag, 'POST', { version: f.get('version'), message: f.get('message') }, 'high'), 'tag-output');
}
function bindForm(id, handler, outputId) {
const form = el(id); if (!form) return;
form.onsubmit = async (e) => {
e.preventDefault();
const f = new FormData(form);
try {
const result = await handler(f);
put(outputId, result);
audit(`${id}.submit`, outputId, result && result.status === 'blocked' ? 'blocked' : 'ok', result);
} catch (err) {
const detail = { error: err.message, response: err.response };
put(outputId, detail);
audit(`${id}.submit`, outputId, 'failed', detail);
}
};
}
async function checkedApi(capability, endpoint, method, payload, risk = 'low') {
const guard = guardAction(capability, risk);
if (!guard.allowed) return { status: 'blocked', capability, ...guard };
return await api(endpoint, method, payload, { allowError: true });
}
function downloadJson(filename, value) {
const blob = new Blob([safeStringify(value)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = filename; a.click();
URL.revokeObjectURL(url);
}
function escapeHtml(value) {
return String(value == null ? '' : value).replace(/[&<>'"]/g, ch => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\'': '&#39;', '"': '&quot;' }[ch]));
}
return { bootstrap, refreshAll, state };
})();
window.addEventListener('DOMContentLoaded', Platform.bootstrap);
@@ -0,0 +1,56 @@
{
"name": "aios-gitops-enterprise-api-contract",
"version": "1.0.0",
"scope": "portal-gitops-dashboard",
"principles": {
"all_writes_require_git_commit": true,
"direct_main_push_from_ui": false,
"secrets_in_git": false,
"destructive_operations": "forbidden_or_request_only",
"server_side_authz_is_authoritative": true
},
"read_endpoints": {
"baseline": "GET /api/base",
"versions": "GET /api/ver",
"runtime": "GET /api/run",
"patches": "GET /api/patch",
"model_registry": "GET /api/model",
"diff_raw": "GET /api/diff?from={from}&to={to}",
"diff_tree": "GET /api/diff/tree?from={from}&to={to}",
"approval_queue": "GET /api/approval/queue",
"release_pipeline": "GET /api/release/pipeline",
"audit_log": "GET /api/audit/log?limit={limit}",
"me": "GET /api/me"
},
"write_endpoints": {
"create_branch": "POST /api/branch",
"natural_language_plan": "POST /api/patch/plan",
"register_model": "POST /api/model",
"set_model_priority": "PUT /api/model-priority",
"rollback_request": "POST /api/rollback-request",
"tag_request": "POST /api/tag-request",
"approval_decision": "POST /api/approval/decision",
"release_promotion_request": "POST /api/release/promote-request",
"audit_append": "POST /api/audit/log"
},
"approval_decision_payload": {
"request_id": "string",
"decision": "approve | reject",
"reason": "string",
"branch": "patch/approval-..."
},
"release_promotion_payload": {
"from_environment": "dev | staging",
"to_environment": "staging | prod",
"version": "VxRxCx",
"reason": "string",
"branch": "patch/release-promote-..."
},
"response_contract": {
"write_success_any_of": ["branch", "commit", "rev", "sync_status"],
"auth_failure": {
"401": "Bearer token not injected",
"403": "Bearer token injected but scope insufficient"
}
}
}
@@ -0,0 +1,79 @@
{
"name": "aios-gitops-enterprise-ui-policy",
"version": "1.0.0",
"scope": "portal-gitops-dashboard",
"client_side_notice": "Client-side RBAC is advisory only. Server-side Actions/API authorization remains authoritative.",
"roles": {
"viewer": {
"label": "Viewer",
"permissions": [
"read:baseline",
"read:versions",
"read:diff",
"read:patch",
"read:model",
"read:runtime",
"read:audit"
]
},
"developer": {
"label": "Developer",
"inherits": ["viewer"],
"permissions": [
"write:branch",
"write:plan"
]
},
"admin": {
"label": "Admin",
"inherits": ["developer"],
"permissions": [
"write:model",
"write:model_priority",
"request:rollback",
"request:tag"
]
},
"owner": {
"label": "Owner",
"inherits": ["admin"],
"permissions": [
"approve:rollback",
"approve:tag",
"approve:priority",
"approve:release"
]
}
},
"risk_policy": {
"direct_main_write": "disabled_in_ui",
"api_keys_in_git": "forbidden",
"rollback": "request_only",
"tag_create": "request_only_by_default",
"runtime_change": "request_only",
"baseline_high_risk_change": "request_only",
"bulk_delete": "forbidden"
},
"secret_redaction": {
"enabled": true,
"patterns": [
"api_key",
"apikey",
"authorization",
"bearer",
"token",
"password",
"secret",
"private_key",
"credential"
],
"replacement": "[REDACTED]"
},
"approval_queues": [
"rollback",
"tag",
"model_priority",
"release",
"runtime_change"
]
}
@@ -0,0 +1,224 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>AIOS Enterprise GitOps Control Plane</title>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<div class="shell">
<header class="topbar">
<div>
<p class="eyebrow">cnnetmkt.top / AIOS Enterprise</p>
<h1>GitOps Enterprise Control Plane</h1>
<p class="subtitle">审批流、RBAC、Release Pipeline、Diff Tree、模型优先级、审计查询</p>
</div>
<div class="statusbar">
<span id="role-chip" class="chip">role: loading</span>
<span id="runtime-chip" class="chip muted">runtime: loading</span>
<button id="refresh-all" class="primary">刷新全部</button>
</div>
</header>
<nav class="tabs" aria-label="Enterprise GitOps modules">
<button data-tab="overview" class="active">总览</button>
<button data-tab="rbac">RBAC</button>
<button data-tab="approval">审批流</button>
<button data-tab="diff">Diff Tree</button>
<button data-tab="patch">Patch / Branch</button>
<button data-tab="model">模型控制</button>
<button data-tab="release">Release Pipeline</button>
<button data-tab="audit">审计中心</button>
</nav>
<main>
<section id="tab-overview" class="tab active">
<div class="grid four">
<article class="metric"><span>Runtime</span><strong id="metric-runtime">-</strong></article>
<article class="metric"><span>Active Version</span><strong id="metric-version">-</strong></article>
<article class="metric"><span>Patch Branches</span><strong id="metric-branches">-</strong></article>
<article class="metric"><span>Pending Approvals</span><strong id="metric-approvals">-</strong></article>
</div>
<div class="grid two">
<article class="card"><h2>Baseline</h2><pre id="baseline-output">loading...</pre></article>
<article class="card"><h2>Versions</h2><pre id="versions-output">loading...</pre></article>
<article class="card"><h2>Runtime</h2><pre id="runtime-output">loading...</pre></article>
<article class="card"><h2>Patch Summary</h2><pre id="patch-summary-output">loading...</pre></article>
</div>
</section>
<section id="tab-rbac" class="tab grid two">
<article class="card">
<h2>当前用户权限</h2>
<p class="hint">按钮级权限由 role + capability 共同控制;viewer 只能读。</p>
<pre id="identity-output">loading...</pre>
</article>
<article class="card">
<h2>RBAC 策略</h2>
<pre id="rbac-output">loading...</pre>
</article>
<article class="card span-two">
<h2>权限矩阵</h2>
<div id="permission-matrix" class="table-wrap">loading...</div>
</article>
</section>
<section id="tab-approval" class="tab">
<div class="grid two">
<article class="card">
<h2>Approval Queue</h2>
<p class="hint">高风险操作进入审批队列;UI 不直接执行 destructive / production 变更。</p>
<div class="toolbar">
<button id="refresh-approvals" class="secondary">刷新审批队列</button>
<button id="clear-local-approvals" class="secondary guarded">清理本地缓存</button>
</div>
<div id="approval-board" class="approval-board">loading...</div>
</article>
<article class="card">
<h2>创建审批请求</h2>
<form id="approval-form" class="stack">
<label>类型
<select name="type">
<option value="merge">merge</option>
<option value="release">release</option>
<option value="rollback">rollback</option>
<option value="model_priority">model_priority</option>
<option value="runtime_change">runtime_change</option>
</select>
</label>
<label>目标 <input name="target" placeholder="branch / version / model id" required /></label>
<label>风险
<select name="risk"><option>low</option><option selected>medium</option><option>high</option></select>
</label>
<label>说明 <textarea name="reason" rows="5" required></textarea></label>
<button class="primary require-operator" type="submit">提交审批请求</button>
</form>
<pre id="approval-output">等待操作...</pre>
</article>
</div>
</section>
<section id="tab-diff" class="tab">
<article class="card">
<h2>File-level Diff Tree</h2>
<p class="hint">支持 from/to 参数;会自动按文件聚合 diff,便于 review。</p>
<form id="diff-form" class="form-row wrap">
<label>From <input name="from" value="HEAD~1" /></label>
<label>To <input name="to" value="HEAD" /></label>
<button class="primary" type="submit">读取 Diff</button>
</form>
<div id="diff-tree" class="diff-tree">等待查询...</div>
<pre id="diff-output">等待查询...</pre>
</article>
</section>
<section id="tab-patch" class="tab grid two">
<article class="card">
<h2>创建 Patch Branch</h2>
<p class="hint">默认从 main 创建非生产 patch 分支;main 写入仅通过明确授权的 GitOps Action。</p>
<form id="branch-form" class="stack">
<label>Branch <input name="branch" placeholder="patch/my-change" required /></label>
<label>Base <input name="base" value="main" /></label>
<label>Message <input name="message" value="Create GitOps patch branch" /></label>
<button class="primary require-operator" type="submit">创建 Branch</button>
</form>
<pre id="branch-output">等待操作...</pre>
</article>
<article class="card">
<h2>自然语言 GitOps Plan</h2>
<p class="hint">仅生成可审计 plan;高风险操作必须审批。</p>
<form id="plan-form" class="stack">
<label>目标 Branch <input name="branch" value="patch/web-ui-complete-gitops-dashboard-20260706" /></label>
<label>Plan Text <textarea name="text" rows="5" placeholder="描述要执行的 GitOps 变更" required></textarea></label>
<button class="primary require-operator" type="submit">生成 Plan</button>
</form>
<pre id="plan-output">等待操作...</pre>
</article>
</section>
<section id="tab-model" class="tab">
<div class="grid two">
<article class="card">
<h2>模型注册表</h2>
<button id="refresh-model" class="secondary">刷新模型</button>
<pre id="model-output">loading...</pre>
</article>
<article class="card">
<h2>模型优先级图</h2>
<div id="model-priority-graph" class="graph">loading...</div>
</article>
<article class="card">
<h2>注册自定义模型</h2>
<p class="hint">仅允许 secret_ref;禁止真实 API key 明文。</p>
<form id="model-form" class="stack">
<label>Provider <input name="provider_name" placeholder="openai / one-api / compatible" required /></label>
<label>Base URL <input name="base_url" placeholder="https://example.com/v1" required /></label>
<label>Model Name <input name="model_name" placeholder="model-id" required /></label>
<label>Display Name <input name="display_name" placeholder="Human readable name" /></label>
<label>API Key Secret Ref <input name="api_key_secret_ref" placeholder="secret_ref://..." required /></label>
<button class="primary require-admin" type="submit">注册模型</button>
</form>
<pre id="model-register-output">等待操作...</pre>
</article>
<article class="card">
<h2>模型优先级 / Fallback</h2>
<form id="priority-form" class="stack">
<label>Model ID <input name="model_id" placeholder="base.gpt-5.5" required /></label>
<label>Priority <input name="priority" type="number" value="100" required /></label>
<label>Enabled <select name="enabled"><option value="true">true</option><option value="false">false</option></select></label>
<label>Fallback Policy <input name="fallback_policy" value="next_enabled" /></label>
<button class="primary require-admin" type="submit">保存优先级</button>
</form>
<pre id="priority-output">等待操作...</pre>
</article>
</div>
</section>
<section id="tab-release" class="tab">
<div class="pipeline" id="release-pipeline">
<div class="stage">Patch</div><div class="arrow">→</div><div class="stage">Review</div><div class="arrow">→</div><div class="stage">Approval</div><div class="arrow">→</div><div class="stage guarded-stage">Tag Request</div><div class="arrow">→</div><div class="stage guarded-stage">Deploy</div>
</div>
<div class="grid two">
<article class="card danger-zone">
<h2>Rollback Request</h2>
<p class="hint">只创建审批请求;不直接执行回滚。</p>
<form id="rollback-form" class="stack">
<label>Target Version <input name="target_version" placeholder="V1R1C3" required /></label>
<label>Reason <textarea name="reason" rows="4" required></textarea></label>
<label>Request Branch <input name="branch" value="patch/rollback-request-web-ui" /></label>
<button class="danger require-admin" type="submit">创建 Rollback 请求</button>
</form>
<pre id="rollback-output">等待操作...</pre>
</article>
<article class="card danger-zone">
<h2>Tag / Version Request</h2>
<p class="hint">默认创建 owner/admin approval request;UI 禁用直接执行。</p>
<form id="tag-form" class="stack">
<label>Version <input name="version" placeholder="V1R1C5" required /></label>
<label>Message <input name="message" placeholder="Release request" /></label>
<label class="disabled"><input name="execute" type="checkbox" disabled /> 直接执行(禁用)</label>
<button class="danger require-admin" type="submit">创建 Tag 请求</button>
</form>
<pre id="tag-output">等待操作...</pre>
</article>
</div>
</section>
<section id="tab-audit" class="tab">
<article class="card">
<h2>审计中心</h2>
<div class="toolbar">
<input id="audit-filter" placeholder="过滤 action / target / status" />
<button id="export-audit" class="secondary">导出 JSON</button>
<button id="refresh-audit" class="secondary">刷新</button>
</div>
<div id="audit-table" class="table-wrap">loading...</div>
<pre id="audit-output">暂无操作。</pre>
</article>
</section>
</main>
</div>
<script src="./app.js"></script>
</body>
</html>
@@ -0,0 +1,22 @@
{
"name": "gitops-dashboard",
"version": "1.0.0",
"mount": "/portal/me/#gitops-dashboard",
"description": "Complete GitOps Web UI module",
"features": [
"baseline_view",
"version_list",
"diff_view",
"patch_view",
"model_registry",
"runtime_status"
],
"api": {
"base": "/api/base",
"ver": "/api/ver",
"diff": "/api/diff",
"patch": "/api/patch",
"model": "/api/model",
"runtime": "/api/run"
}
}
@@ -0,0 +1,6 @@
body{font-family:Arial,Helvetica,sans-serif;background:#0b0f14;color:#e6edf3;margin:0}
header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;background:#111826;border-bottom:1px solid #222}
nav button{margin-right:8px;padding:8px 10px;background:#1f2937;color:#e6edf3;border:1px solid #2b3645;cursor:pointer}
nav button:hover{background:#2b3645}
main{padding:16px}
pre{background:#0f172a;padding:12px;border:1px solid #1f2937;overflow:auto;min-height:400px}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,9 @@
{
"type": "branch",
"branch": "patch/collab-custom-gpt-20260706T104130Z",
"base": "main",
"created_by": "chatgpt-gitops-bot",
"created_at": "2026-07-06T10:41:30Z",
"direct_main_push": false,
"owner_approval_required": true
}
@@ -0,0 +1,9 @@
{
"type": "branch",
"branch": "patch/collab-local-codex-20260706T104130Z",
"base": "main",
"created_by": "codex-gitops-bot",
"created_at": "2026-07-06T10:41:30Z",
"direct_main_push": false,
"owner_approval_required": true
}
@@ -0,0 +1,9 @@
{
"type": "branch",
"branch": "patch/web-ui-complete-gitops-dashboard-20260706",
"base": "main",
"created_by": "gitops-write-bot",
"created_at": "2026-07-06T15:33:08Z",
"direct_main_push": false,
"owner_approval_required": true
}
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env bash
set -e
BRANCH=$1
BASE=${2:-main}
if [ -z "$BRANCH" ]; then
echo "Usage: autonomous-merge-gate.sh <branch> <base>"
exit 1
fi
echo "[Autonomous Gate] Evaluating: $BRANCH vs $BASE"
REPORT_DIR="reports/codex"
mkdir -p "$REPORT_DIR"
# Run validation and capture exit code correctly
bash scripts/validate-gitops-platform-ui.sh "$BRANCH" "$BASE"
VALIDATION_RC=$?
if [ $VALIDATION_RC -ne 0 ]; then
echo "FAIL: validation failed"
exit $VALIDATION_RC
fi
# Secret safety re-check
if grep -R "BEGIN PRIVATE KEY" -n overlays/ >/dev/null 2>&1; then
echo "BLOCKED: secret leak"
exit 2
fi
# Create merge candidate artifact
cat > "$REPORT_DIR/merge-candidate.json" <<EOF
{
"branch": "$BRANCH",
"base": "$BASE",
"status": "merge_candidate",
"created_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
EOF
echo "PASS: merge candidate ready"
exit 0
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -euo pipefail
cd "${AIOS_GITOPS_WORKSPACE:-/data/aios-gitops/workspace}"
expected="${1:-V1R1C4}"
test "$(git branch --show-current)" = "main"
test "$(cat VERSION | tr -d '\n')" = "$expected"
git rev-parse --verify "$expected" >/dev/null
grep -q 'git_patch_only' config/codex/gitops-online.json
grep -q 'codex-gitops-bot' config/codex/gitops-online.json
grep -q 'online_git_sync' config/codex/gitops-online.json
python3 -m py_compile active-runtime/app/aios_chatgpt_actions_api.py
curl -fsS https://cnnetmkt.top/api/base >/tmp/aios-codex-gitops-base.json
curl -fsS https://cnnetmkt.top/api/openapi-chatgpt-actions.yaml >/tmp/aios-codex-gitops-openapi.yaml
if git grep -I -E '(sk-[A-Za-z0-9_-]{20,}|SDC[A-Za-z0-9]{20,}|HPUA[A-Z0-9]{10,})' -- . ':!docs/gitops/openapi-chatgpt-actions.yaml' >/tmp/aios-codex-secret-scan.txt; then
echo 'SECRET_PATTERN_IN_GIT'
cat /tmp/aios-codex-secret-scan.txt
exit 1
fi
echo "CODEX_GITOPS_ONLINE_OK $expected"
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env python3
"""Enqueue a WeChat notification into WebOps message_outbox.
Business modules should call this helper or the equivalent WebOps API instead of
calling OpenClaw / WeChat adapters directly.
"""
from __future__ import annotations
import argparse
import json
import os
import sqlite3
import time
from pathlib import Path
ROOT = Path(os.environ.get("AIOS_WEBOPS_ROOT", "/data/aios-gitops/workspace"))
DB = Path(os.environ.get("AIOS_WEBOPS_DB", str(ROOT / "data" / "webops.sqlite3")))
MIGRATION = ROOT / "db" / "migrations" / "20260707_message_outbox.sql"
def ensure_schema(con: sqlite3.Connection) -> None:
if MIGRATION.exists():
con.executescript(MIGRATION.read_text(encoding="utf-8"))
else:
con.executescript(
"""
CREATE TABLE IF NOT EXISTS message_outbox (
id TEXT PRIMARY KEY,
channel TEXT NOT NULL,
recipient_user_id TEXT NOT NULL,
recipient_role TEXT,
message_type TEXT NOT NULL,
priority TEXT NOT NULL DEFAULT 'normal',
title TEXT,
content TEXT NOT NULL,
payload TEXT,
status TEXT NOT NULL DEFAULT 'queued',
retry_count INTEGER NOT NULL DEFAULT 0,
last_error TEXT,
created_at TEXT NOT NULL,
sent_at TEXT
);
"""
)
def main() -> int:
parser = argparse.ArgumentParser(description="Enqueue a WeChat notification")
parser.add_argument("--recipient", "--recipient-user-id", dest="recipient_user_id", required=True)
parser.add_argument("--recipient-role", default="owner")
parser.add_argument("--message-type", default="alert")
parser.add_argument("--priority", default="normal", choices=["critical", "high", "normal", "low"])
parser.add_argument("--title", default="AIOS 通知")
parser.add_argument("--content", required=True)
parser.add_argument("--payload", default="{}")
parser.add_argument("--id")
args = parser.parse_args()
DB.parent.mkdir(parents=True, exist_ok=True)
msg_id = args.id or f"msg_{int(time.time() * 1000)}"
with sqlite3.connect(DB) as con:
ensure_schema(con)
con.execute(
"""
INSERT INTO message_outbox (
id, channel, recipient_user_id, recipient_role, message_type,
priority, title, content, payload, status, created_at
) VALUES (?, 'wechat', ?, ?, ?, ?, ?, ?, ?, 'queued', datetime('now'))
""",
(
msg_id,
args.recipient_user_id,
args.recipient_role,
args.message_type,
args.priority,
args.title,
args.content,
args.payload,
),
)
con.commit()
print(json.dumps({"status": "queued", "id": msg_id, "db": str(DB)}, ensure_ascii=False))
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
+116
View File
@@ -0,0 +1,116 @@
#!/usr/bin/env python3
"""Ensure WebOps message outbox schema.
This schema is the only supported active outbound notification entrypoint for
business modules and external systems. Delivery is performed by the worker.
The migration is backward-compatible with already-created SQLite tables:
columns are added before indexes that reference them, and UNIQUE constraints are
created as indexes because SQLite cannot add a UNIQUE column via ALTER TABLE.
"""
from __future__ import annotations
import os
import sqlite3
from pathlib import Path
ROOT = Path(os.environ.get("AIOS_WEBOPS_ROOT", "/data/aios-gitops/workspace"))
DB = Path(os.environ.get("AIOS_WEBOPS_DB", str(ROOT / "data" / "webops.sqlite3")))
BASE_TABLES_SQL = """
CREATE TABLE IF NOT EXISTS message_outbox (
id TEXT PRIMARY KEY,
idempotency_key TEXT,
channel TEXT NOT NULL,
recipient_user_id TEXT,
recipient_role TEXT,
message_type TEXT NOT NULL,
priority TEXT NOT NULL DEFAULT 'normal',
title TEXT,
content TEXT NOT NULL,
source_system TEXT,
source_event_id TEXT,
require_approval INTEGER NOT NULL DEFAULT 0,
payload TEXT,
status TEXT NOT NULL DEFAULT 'queued',
retry_count INTEGER NOT NULL DEFAULT 0,
last_error TEXT,
created_at TEXT NOT NULL,
sent_at TEXT,
cancelled_at TEXT
);
CREATE TABLE IF NOT EXISTS channel_bindings (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
user_role TEXT,
channel_type TEXT NOT NULL,
external_chat_id TEXT NOT NULL,
notification_prefs TEXT,
status TEXT NOT NULL DEFAULT 'active',
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS audit_logs (
id TEXT PRIMARY KEY,
actor TEXT NOT NULL,
action TEXT NOT NULL,
target TEXT,
detail TEXT,
created_at TEXT NOT NULL
);
"""
INDEX_SQL = """
CREATE UNIQUE INDEX IF NOT EXISTS idx_message_outbox_idempotency
ON message_outbox(idempotency_key)
WHERE idempotency_key IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_message_outbox_dispatch
ON message_outbox(channel, status, priority, created_at);
CREATE INDEX IF NOT EXISTS idx_channel_bindings_lookup
ON channel_bindings(user_id, channel_type, status, updated_at);
CREATE INDEX IF NOT EXISTS idx_channel_bindings_role_lookup
ON channel_bindings(user_role, channel_type, status, updated_at);
"""
COLUMNS = {
"message_outbox": {
"idempotency_key": "TEXT",
"recipient_user_id": "TEXT",
"recipient_role": "TEXT",
"source_system": "TEXT",
"source_event_id": "TEXT",
"require_approval": "INTEGER NOT NULL DEFAULT 0",
"cancelled_at": "TEXT",
},
"channel_bindings": {
"user_role": "TEXT",
"notification_prefs": "TEXT",
},
}
def existing_columns(con: sqlite3.Connection, table: str) -> set[str]:
return {row[1] for row in con.execute(f"PRAGMA table_info({table})")}
def ensure_schema(db_path: Path = DB) -> None:
db_path.parent.mkdir(parents=True, exist_ok=True)
with sqlite3.connect(db_path) as con:
con.executescript(BASE_TABLES_SQL)
for table, cols in COLUMNS.items():
existing = existing_columns(con, table)
for name, ddl in cols.items():
if name not in existing:
con.execute(f"ALTER TABLE {table} ADD COLUMN {name} {ddl}")
con.executescript(INDEX_SQL)
con.commit()
if __name__ == "__main__":
ensure_schema()
print(f"schema_ok db={DB}")
+22 -4
View File
@@ -1,8 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
required=("VERSION" "docs/10_ARCHITECTURE_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_CN.md" "docs/11_USER_REQUIREMENTS_V1R1C0_CN.md" "docs/12_INTERFACE_API_SPEC_V1R1C0_CN.md" "docs/CODEX_FULL_EXECUTION_PROMPT_V1R1C0.md" "active-runtime/app/aios_gitops_model_api.py" "migrations/004_gitops_custom_llm_runtime.sql" "scripts/install_v1r1c0_gitops_model_overlay.sh" "config/model_registry.yaml" "config/runtime_selector.yaml")
required=(
"VERSION"
"docs/10_ARCHITECTURE_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_CN.md"
"docs/11_USER_REQUIREMENTS_V1R1C0_CN.md"
"docs/12_INTERFACE_API_SPEC_V1R1C0_CN.md"
"docs/CODEX_FULL_EXECUTION_PROMPT_V1R1C0.md"
"active-runtime/app/aios_gitops_model_api.py"
"active-runtime/app/aios_chatgpt_actions_api.py"
"docs/gitops/openapi-chatgpt-actions.yaml"
"migrations/004_gitops_custom_llm_runtime.sql"
"scripts/install_v1r1c0_gitops_model_overlay.sh"
"config/model_registry.yaml"
"config/runtime_selector.yaml"
)
for f in "${required[@]}"; do test -f "$ROOT/$f" || { echo "MISSING $f"; exit 1; }; done
grep -q "V1R1C0" "$ROOT/VERSION"
python3 -m py_compile "$ROOT/active-runtime/app/aios_gitops_model_api.py" "$ROOT/adapters/openclaw/openclaw_gitops_commands.py" "$ROOT/adapters/hermes/hermes_gitops_nl_router.py"
echo "PACKAGE_CONTRACT_OK V1R1C0"
grep -Eq "^V1R1C[0-9]+$" "$ROOT/VERSION"
python3 -m py_compile \
"$ROOT/active-runtime/app/aios_gitops_model_api.py" \
"$ROOT/active-runtime/app/aios_chatgpt_actions_api.py" \
"$ROOT/adapters/openclaw/openclaw_gitops_commands.py" \
"$ROOT/adapters/hermes/hermes_gitops_nl_router.py"
grep -q "/api/branch" "$ROOT/docs/gitops/openapi-chatgpt-actions.yaml"
echo "PACKAGE_CONTRACT_OK $(cat "$ROOT/VERSION")"
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env bash
set -e
BRANCH=$1
BASE=${2:-main}
if [ -z "$BRANCH" ]; then
echo "Usage: validate-gitops-platform-ui.sh <branch> <base>"
exit 1
fi
echo "[Codex] Validating branch: $BRANCH against $BASE"
echo "[1/5] Git diff check"
git diff --stat $BASE...$BRANCH
echo "[2/5] Acceptance test load"
if [ ! -f tests/acceptance/gitops-platform-ui.acceptance.json ]; then
echo "FAIL: acceptance file missing"
exit 2
fi
echo "[3/5] Secret scan"
if grep -R "BEGIN PRIVATE KEY" -n overlays/ >/dev/null 2>&1; then
echo "FAIL: private key detected"
exit 3
fi
if grep -R "Bearer " -n overlays/ >/dev/null 2>&1; then
echo "WARN: bearer token pattern found"
fi
echo "[4/5] UI structural validation"
FILES=(
overlays/user-control-center/portal/gitops-dashboard/index.html
overlays/user-control-center/portal/gitops-dashboard/app.js
)
for f in "${FILES[@]}"; do
if [ ! -f "$f" ]; then
echo "FAIL: missing $f"
exit 4
fi
done
echo "[5/5] Policy validation"
grep -R "direct_main_push" -n overlays/user-control-center/portal/gitops-dashboard/ || true
echo "PASS: Codex validation completed"
exit 0
+256
View File
@@ -0,0 +1,256 @@
#!/usr/bin/env python3
"""Production WeChat notify worker for AIOS / Xiamayiti.
Flow:
message_outbox -> channel_bindings -> wechat_send_backend -> sent/failed/retry/audit
Business modules must not call OpenClaw or UCC Adapter directly. They only
enqueue messages. The pluggable backend chooses OpenClaw channel or UCC Adapter
at runtime.
"""
from __future__ import annotations
import argparse
import importlib.util
import json
import os
import sqlite3
import time
from pathlib import Path
from typing import Any, Dict
ROOT = Path(os.environ.get("AIOS_WEBOPS_ROOT", "/data/aios-gitops/workspace"))
DB = Path(os.environ.get("AIOS_WEBOPS_DB", str(ROOT / "data" / "webops.sqlite3")))
DEFAULT_LIMIT = int(os.environ.get("AIOS_WECHAT_WORKER_LIMIT", "20"))
PRIORITY_SQL = """
CASE priority
WHEN 'critical' THEN 1
WHEN 'high' THEN 2
WHEN 'normal' THEN 3
ELSE 4
END
"""
def load_module(path: Path, name: str):
spec = importlib.util.spec_from_file_location(name, path)
if not spec or not spec.loader:
raise RuntimeError(f"cannot load module: {path}")
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod) # type: ignore[attr-defined]
return mod
def ensure_schema(con: sqlite3.Connection) -> None:
schema_path = ROOT / "scripts" / "message_outbox_schema.py"
if schema_path.exists():
mod = load_module(schema_path, "message_outbox_schema")
mod.ensure_schema(DB)
return
con.executescript(
"""
CREATE TABLE IF NOT EXISTS message_outbox (
id TEXT PRIMARY KEY,
idempotency_key TEXT,
channel TEXT NOT NULL,
recipient_user_id TEXT,
recipient_role TEXT,
message_type TEXT NOT NULL,
priority TEXT NOT NULL DEFAULT 'normal',
title TEXT,
content TEXT NOT NULL,
source_system TEXT,
source_event_id TEXT,
require_approval INTEGER NOT NULL DEFAULT 0,
payload TEXT,
status TEXT NOT NULL DEFAULT 'queued',
retry_count INTEGER NOT NULL DEFAULT 0,
last_error TEXT,
created_at TEXT NOT NULL,
sent_at TEXT,
cancelled_at TEXT
);
CREATE TABLE IF NOT EXISTS channel_bindings (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
user_role TEXT,
channel_type TEXT NOT NULL,
external_chat_id TEXT NOT NULL,
notification_prefs TEXT,
status TEXT NOT NULL DEFAULT 'active',
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS audit_logs (
id TEXT PRIMARY KEY,
actor TEXT NOT NULL,
action TEXT NOT NULL,
target TEXT,
detail TEXT,
created_at TEXT NOT NULL
);
"""
)
def audit(con: sqlite3.Connection, action: str, target: str, detail: Dict[str, Any]) -> None:
con.execute(
"""
INSERT INTO audit_logs(id, actor, action, target, detail, created_at)
VALUES(?, 'system', ?, ?, ?, datetime('now'))
""",
(f"audit_{int(time.time() * 1000)}", action, target, json.dumps(detail, ensure_ascii=False)),
)
def load_wechat_target(con: sqlite3.Connection, row: sqlite3.Row) -> str:
recipient_user_id = row["recipient_user_id"]
recipient_role = row["recipient_role"]
if recipient_user_id:
found = con.execute(
"""
SELECT external_chat_id
FROM channel_bindings
WHERE user_id = ?
AND channel_type = 'wechat'
AND status = 'active'
ORDER BY updated_at DESC
LIMIT 1
""",
(recipient_user_id,),
).fetchone()
if found:
return found["external_chat_id"]
if recipient_role:
found = con.execute(
"""
SELECT external_chat_id
FROM channel_bindings
WHERE (user_role = ? OR user_id = ?)
AND channel_type = 'wechat'
AND status = 'active'
ORDER BY updated_at DESC
LIMIT 1
""",
(recipient_role, recipient_role),
).fetchone()
if found:
return found["external_chat_id"]
raise RuntimeError(f"wechat binding not found for recipient_user_id={recipient_user_id} role={recipient_role}")
def sanitize_content(row: sqlite3.Row) -> str:
content = row["content"] or ""
blocked_markers = ["BEGIN PRIVATE KEY", "Bearer ", "PASSWORD=", "TOKEN=", "/data/aios-gitops/runtime/secrets"]
for marker in blocked_markers:
if marker in content:
content = content.replace(marker, "[REDACTED]")
title = row["title"] or "AIOS 通知"
return f"{title}\n\n{content}"
def send_with_backend(target: str, content: str, row: sqlite3.Row, dry_run: bool = False) -> Dict[str, Any]:
backend_path = ROOT / "scripts" / "wechat_send_backend.py"
if not backend_path.exists():
raise RuntimeError("wechat_send_backend.py not found")
backend = load_module(backend_path, "wechat_send_backend")
metadata = {}
if row["payload"]:
try:
metadata = json.loads(row["payload"])
except Exception:
metadata = {"raw_payload": row["payload"][:1000]}
return backend.send_message(
target=target,
content=content,
account_id=os.environ.get("AIOS_WECHAT_ACCOUNT_ID", ""),
message_id=row["id"],
metadata=metadata,
dry_run=dry_run,
)
def mark_dispatching(con: sqlite3.Connection, msg_id: str) -> bool:
cur = con.execute("UPDATE message_outbox SET status='dispatching' WHERE id=? AND status='queued'", (msg_id,))
return cur.rowcount == 1
def process_row(con: sqlite3.Connection, row: sqlite3.Row, dry_run: bool = False) -> None:
msg_id = row["id"]
if int(row["require_approval"] or 0) == 1 and row["message_type"] == "approval_request":
con.execute("UPDATE message_outbox SET status='approval_pending' WHERE id=?", (msg_id,))
audit(con, "wechat_outbound_approval_pending", row["recipient_user_id"] or row["recipient_role"] or "unknown", {"message_id": msg_id})
return
if not mark_dispatching(con, msg_id):
return
try:
target = load_wechat_target(con, row)
content = sanitize_content(row)
result = send_with_backend(target, content, row, dry_run=dry_run)
ok = bool(result.get("ok"))
if not ok:
code = result.get("code") or result.get("status") or "send_failed"
raise RuntimeError(f"wechat send failed via {result.get('backend')}: {code}; {result.get('error') or result}")
status = "sent" if not dry_run else "queued"
con.execute(
"""
UPDATE message_outbox
SET status=?,
sent_at=CASE WHEN ?='sent' THEN datetime('now') ELSE sent_at END,
payload=json_set(COALESCE(payload, '{}'), '$.send_result', ?),
last_error=NULL
WHERE id=?
""",
(status, status, json.dumps(result, ensure_ascii=False), msg_id),
)
audit(con, "wechat_outbound_sent" if not dry_run else "wechat_outbound_dry_run", row["recipient_user_id"] or row["recipient_role"] or "unknown", {"message_id": msg_id, "backend": result.get("backend"), "result": result})
except Exception as exc:
retry_count = int(row["retry_count"] or 0) + 1
status = "failed" if retry_count >= 5 else "queued"
con.execute(
"UPDATE message_outbox SET retry_count=?, status=?, last_error=? WHERE id=?",
(retry_count, status, str(exc)[:1000], msg_id),
)
audit(con, "wechat_outbound_failed", row["recipient_user_id"] or row["recipient_role"] or "unknown", {"message_id": msg_id, "retry_count": retry_count, "error": str(exc)[:1000]})
def run_once(limit: int, dry_run: bool = False) -> Dict[str, Any]:
DB.parent.mkdir(parents=True, exist_ok=True)
with sqlite3.connect(DB) as con:
con.row_factory = sqlite3.Row
ensure_schema(con)
rows = con.execute(
f"SELECT * FROM message_outbox WHERE channel='wechat' AND status='queued' ORDER BY {PRIORITY_SQL}, created_at LIMIT ?",
(limit,),
).fetchall()
for row in rows:
process_row(con, row, dry_run=dry_run)
con.commit()
return {"status": "ok", "processed": len(rows), "db": str(DB), "dry_run": dry_run}
def main() -> int:
parser = argparse.ArgumentParser(description="Dispatch message_outbox WeChat notifications through the selected backend")
parser.add_argument("--once", action="store_true", help="Run one dispatch pass and exit")
parser.add_argument("--dry-run", action="store_true", help="Run backend dry-run if supported and keep messages queued")
parser.add_argument("--limit", type=int, default=DEFAULT_LIMIT)
parser.add_argument("--interval", type=int, default=int(os.environ.get("AIOS_WECHAT_WORKER_INTERVAL", "10")))
args = parser.parse_args()
if args.once:
print(json.dumps(run_once(args.limit, dry_run=args.dry_run), ensure_ascii=False))
return 0
while True:
print(json.dumps(run_once(args.limit, dry_run=args.dry_run), ensure_ascii=False), flush=True)
time.sleep(args.interval)
if __name__ == "__main__":
raise SystemExit(main())
+273
View File
@@ -0,0 +1,273 @@
#!/usr/bin/env python3
"""Pluggable WeChat send backend for AIOS / Xiamayiti.
Selection order:
1. AIOS_WECHAT_SEND_BACKEND, if explicitly set;
2. UCC adapter backend, when ucc-wechat-qr-adapter is alive;
3. OpenClaw channel backend, only when channel discovery finds wechat/weixin;
4. unavailable with explicit diagnostics.
This module must not print runtime secrets and must not hard-code account,
target, adapter credentials, or channel names.
"""
from __future__ import annotations
import json
import os
import subprocess
import urllib.error
import urllib.request
from typing import Any, Dict, List, Optional
OPENCLAW_BIN = os.environ.get("AIOS_OPENCLAW_BIN", "openclaw")
OPENCLAW_GATEWAY = os.environ.get("AIOS_OPENCLAW_GATEWAY", "http://127.0.0.1:18789")
OPENCLAW_AUTH_MODE = os.environ.get("AIOS_OPENCLAW_AUTH_MODE", "")
OPENCLAW_AUTH_VALUE = os.environ.get("AIOS_OPENCLAW_AUTH_VALUE", "")
WECHAT_ACCOUNT_ID = os.environ.get("AIOS_WECHAT_ACCOUNT_ID", "")
WECHAT_CHANNEL = os.environ.get("AIOS_WECHAT_CHANNEL", "")
SEND_BACKEND = os.environ.get("AIOS_WECHAT_SEND_BACKEND", "")
UCC_ADAPTER_URL = os.environ.get("AIOS_UCC_WECHAT_ADAPTER_URL", "http://127.0.0.1:18800")
UCC_ADAPTER_TOKEN = os.environ.get("AIOS_UCC_WECHAT_ADAPTER_TOKEN") or os.environ.get("AIOS_WECHAT_QR_ADAPTER_TOKEN", "")
UCC_SEND_PATH = os.environ.get("AIOS_UCC_WECHAT_SEND_PATH", "/api/internal/wechat/send")
class BackendResult:
def __init__(self, ok: bool, backend: str, status: str, data: Dict[str, Any]) -> None:
self.ok = ok
self.backend = backend
self.status = status
self.data = data
def to_dict(self) -> Dict[str, Any]:
return {"ok": self.ok, "backend": self.backend, "status": self.status, **self.data}
def _openclaw_base_args() -> List[str]:
args = [OPENCLAW_BIN]
if OPENCLAW_GATEWAY:
args.extend(["--gateway", OPENCLAW_GATEWAY])
if OPENCLAW_AUTH_MODE and OPENCLAW_AUTH_VALUE:
if OPENCLAW_AUTH_MODE == "token":
args.extend(["--token", OPENCLAW_AUTH_VALUE])
elif OPENCLAW_AUTH_MODE == "password":
args.extend(["--password", OPENCLAW_AUTH_VALUE])
return args
def _run_openclaw(args: List[str], timeout: int = 30) -> subprocess.CompletedProcess[str]:
return subprocess.run(args, text=True, capture_output=True, timeout=timeout, check=False)
def _json_or_raw(text: str) -> Dict[str, Any]:
try:
return json.loads(text or "{}")
except Exception:
return {"raw": (text or "")[:1000]}
def discover_openclaw_wechat_channel() -> Optional[str]:
if WECHAT_CHANNEL:
return WECHAT_CHANNEL
cmd = _openclaw_base_args() + ["channels", "list", "--json"]
proc = _run_openclaw(cmd, timeout=15)
if proc.returncode != 0:
return None
data = _json_or_raw(proc.stdout)
candidates: List[str] = []
def walk(value: Any) -> None:
if isinstance(value, dict):
name = str(value.get("name") or value.get("channel") or value.get("id") or "")
typ = str(value.get("type") or value.get("kind") or "")
joined = f"{name} {typ}".lower()
if ("wechat" in joined or "weixin" in joined) and name:
candidates.append(name)
for child in value.values():
walk(child)
elif isinstance(value, list):
for child in value:
walk(child)
walk(data)
return candidates[0] if candidates else None
def _auth_headers() -> Dict[str, str]:
headers = {"Content-Type": "application/json"}
if UCC_ADAPTER_TOKEN:
headers["Authorization"] = "Bearer " + UCC_ADAPTER_TOKEN
headers["X-AIOS-Token"] = UCC_ADAPTER_TOKEN
return headers
def _unique_paths(paths: List[str]) -> List[str]:
seen = set()
out = []
for path in paths:
if not path.startswith("/"):
path = "/" + path
if path not in seen:
seen.add(path)
out.append(path)
return out
def _ucc_send_paths() -> List[str]:
return _unique_paths([UCC_SEND_PATH, "/api/internal/wechat/send", "/internal/channels/wechat/send"])
def ucc_adapter_alive() -> bool:
for path in ("/health", "/api/internal/health", "/api/health", "/status", "/api/internal/wechat/status"):
try:
req = urllib.request.Request(UCC_ADAPTER_URL.rstrip("/") + path, headers=_auth_headers(), method="GET")
with urllib.request.urlopen(req, timeout=5) as resp:
if 200 <= resp.status < 500:
return True
except Exception:
continue
return False
def select_backend() -> Dict[str, Any]:
explicit = SEND_BACKEND.strip().lower()
if explicit:
return {"backend": explicit, "reason": "explicit"}
if ucc_adapter_alive():
return {"backend": "ucc-adapter", "reason": "ucc_adapter_alive"}
channel = discover_openclaw_wechat_channel()
if channel:
return {"backend": "openclaw-channel", "channel": channel, "reason": "openclaw_channel_discovered"}
return {
"backend": "unavailable",
"reason": "wechat outbound backend unavailable: ucc adapter not reachable; openclaw channel not registered",
}
def send_openclaw_channel(
target: str,
content: str,
account_id: str = "",
message_id: str = "",
metadata: Optional[Dict[str, Any]] = None,
dry_run: bool = False,
) -> BackendResult:
channel = WECHAT_CHANNEL or discover_openclaw_wechat_channel()
if not channel:
return BackendResult(False, "openclaw-channel", "failed", {"code": "openclaw_wechat_channel_not_registered"})
account = account_id or WECHAT_ACCOUNT_ID
if not account:
return BackendResult(False, "openclaw-channel", "failed", {"code": "openclaw_account_not_configured"})
cmd = _openclaw_base_args() + [
"message",
"send",
"--channel",
channel,
"--account",
account,
"--target",
target,
"-m",
content,
]
if dry_run:
cmd.append("--dry-run")
cmd.append("--json")
proc = _run_openclaw(cmd)
if proc.returncode != 0:
return BackendResult(
False,
"openclaw-channel",
"failed",
{"code": "openclaw_send_failed", "error": (proc.stderr or proc.stdout)[:1000], "channel": channel},
)
return BackendResult(
True,
"openclaw-channel",
"sent" if not dry_run else "dry_run",
{"channel": channel, "result": _json_or_raw(proc.stdout)},
)
def _post_ucc(path: str, payload: Dict[str, Any]) -> tuple[int, Dict[str, Any]]:
url = UCC_ADAPTER_URL.rstrip("/") + path
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
req = urllib.request.Request(url, data=body, headers=_auth_headers(), method="POST")
with urllib.request.urlopen(req, timeout=20) as resp:
return resp.status, _json_or_raw(resp.read().decode("utf-8", errors="replace"))
def _ucc_result_is_ok(data: Dict[str, Any], dry_run: bool) -> bool:
status = str(data.get("status") or data.get("delivery_status") or "").lower()
if dry_run:
return bool(data.get("ok") or data.get("accepted")) and status in {"dry_run", "sent", "accepted", "ok"}
return bool(data.get("ok")) and status == "sent"
def send_ucc_adapter(
target: str,
content: str,
account_id: str = "",
message_id: str = "",
metadata: Optional[Dict[str, Any]] = None,
dry_run: bool = False,
) -> BackendResult:
payload = {
"target": target,
"content": content,
"account_id": account_id or WECHAT_ACCOUNT_ID,
"message_id": message_id,
"metadata": metadata or {},
"dry_run": dry_run,
}
last_error: Dict[str, Any] = {}
for path in _ucc_send_paths():
try:
http_status, data = _post_ucc(path, payload)
ok = _ucc_result_is_ok(data, dry_run)
status = str(data.get("status") or ("sent" if ok and not dry_run else "dry_run" if ok else "failed"))
code = str(data.get("code") or status)
return BackendResult(
ok,
"ucc-adapter",
status if ok else "failed",
{"code": code, "http_status": http_status, "endpoint": path, "result": data},
)
except urllib.error.HTTPError as exc:
data = _json_or_raw(exc.read().decode("utf-8", errors="replace"))
last_error = {"code": data.get("code") or "ucc_adapter_http_error", "http_status": exc.code, "endpoint": path, "error": data}
if exc.code in {404, 405}:
continue
return BackendResult(False, "ucc-adapter", "failed", last_error)
except Exception as exc:
last_error = {"code": "ucc_adapter_unavailable", "endpoint": path, "error": str(exc)[:1000]}
continue
return BackendResult(False, "ucc-adapter", "failed", last_error or {"code": "ucc_adapter_unavailable"})
def send_message(
target: str,
content: str,
account_id: str = "",
message_id: str = "",
metadata: Optional[Dict[str, Any]] = None,
dry_run: bool = False,
) -> Dict[str, Any]:
selected = select_backend()
backend = selected.get("backend")
if backend == "ucc-adapter":
return send_ucc_adapter(target, content, account_id, message_id, metadata, dry_run).to_dict()
if backend == "openclaw-channel":
return send_openclaw_channel(target, content, account_id, message_id, metadata, dry_run).to_dict()
if backend == "unavailable":
return BackendResult(False, "unavailable", "failed", {"code": "wechat_outbound_backend_unavailable", "error": selected.get("reason")}).to_dict()
return BackendResult(False, backend or "unknown", "failed", {"code": "unsupported_backend", "backend": backend}).to_dict()
if __name__ == "__main__":
print(json.dumps(select_backend(), ensure_ascii=False, indent=2))
+444
View File
@@ -0,0 +1,444 @@
#!/usr/bin/env python3
"""AIOS Autonomous GitOps Daemon.
Low-impact sidecar daemon for:
- reading GitOps events from a file-backed event bus;
- running safe validation/self-healing commands;
- sending owner notifications through a runtime-injected WeChat/WeCom webhook;
- creating audit records and request artifacts instead of bypassing production gates.
No credentials are stored in this file. Runtime secrets must be injected by the host
through environment variables or secret-mounted files.
"""
from __future__ import annotations
import argparse
import datetime as _dt
import hashlib
import hmac
import json
import os
import pathlib
import shutil
import subprocess
import sys
import time
import urllib.error
import urllib.request
from typing import Any, Dict, Iterable, List, Optional, Tuple
ROOT = pathlib.Path(os.environ.get("AIOS_REPO_ROOT", ".")).resolve()
CONFIG_PATH = ROOT / "config" / "codex" / "self-optimizing-orchestrator.json"
PROD_POLICY_PATH = ROOT / "config" / "codex" / "autonomous-production-policy.json"
MERGE_POLICY_PATH = ROOT / "config" / "codex" / "autonomous-merge-policy.json"
EVENT_DIR = pathlib.Path(os.environ.get("AIOS_EVENT_DIR", str(ROOT / "runtime" / "gitops" / "events")))
AUDIT_DIR = pathlib.Path(os.environ.get("AIOS_AUDIT_DIR", str(ROOT / "runtime" / "gitops" / "audit")))
REQUEST_DIR = pathlib.Path(os.environ.get("AIOS_REQUEST_DIR", str(ROOT / "runtime" / "gitops" / "requests")))
REPORT_DIR = pathlib.Path(os.environ.get("AIOS_REPORT_DIR", str(ROOT / "reports" / "codex")))
PREAPPROVAL_DIR = pathlib.Path(os.environ.get("AIOS_PREAPPROVAL_DIR", str(ROOT / "approvals" / "preapproved")))
SAFE_AUTONOMOUS_EVENTS = {
"git_push",
"patch_created",
"validation_failed",
"validation_passed",
"merge_candidate_created",
"runtime_health_degraded",
}
OWNER_NOTIFY_EVENTS = {
"main_version_changed",
"baseline_changed",
"merge_candidate_created",
"validation_failed",
"rollback_request_created",
"approval_required",
"daily_digest",
}
PRODUCTION_ACTIONS = {
"merge_to_main",
"create_release_tag",
"activate_new_baseline",
"production_runtime_change",
"production_rollback_execution",
}
ALWAYS_BLOCKED = {
"force_push",
"purge_baseline",
"delete_version_history",
"delete_audit_records",
"commit_real_secret",
"disable_auth_or_gitea_protection",
}
def utc_now() -> str:
return _dt.datetime.now(_dt.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
def load_json(path: pathlib.Path, default: Any) -> Any:
try:
return json.loads(path.read_text(encoding="utf-8"))
except FileNotFoundError:
return default
except json.JSONDecodeError as exc:
raise RuntimeError(f"Invalid JSON: {path}: {exc}") from exc
def write_json(path: pathlib.Path, data: Any) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
tmp = path.with_suffix(path.suffix + ".tmp")
tmp.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
tmp.replace(path)
def event_id(event: Dict[str, Any]) -> str:
raw = json.dumps(event, ensure_ascii=False, sort_keys=True).encode("utf-8")
return hashlib.sha256(raw).hexdigest()[:16]
def audit(action: str, status: str, detail: Dict[str, Any]) -> Dict[str, Any]:
record = {
"ts": utc_now(),
"action": action,
"status": status,
"detail": redact(detail),
}
write_json(AUDIT_DIR / f"{record['ts'].replace(':', '-')}-{action}.json", record)
print(json.dumps(record, ensure_ascii=False), flush=True)
return record
def redact(value: Any) -> Any:
if isinstance(value, list):
return [redact(v) for v in value]
if isinstance(value, dict):
out: Dict[str, Any] = {}
for key, val in value.items():
lowered = key.lower()
sensitive = any(word in lowered for word in ["api_key", "apikey", "password", "secret", "private_key", "credential", "webhook"])
if sensitive and not (isinstance(val, str) and val.startswith("secret_ref://")):
out[key] = "[REDACTED]"
else:
out[key] = redact(val)
return out
return value
def run_cmd(cmd: List[str], timeout: int = 300) -> Tuple[int, str, str]:
result = subprocess.run(
cmd,
cwd=str(ROOT),
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=timeout,
check=False,
)
return result.returncode, result.stdout, result.stderr
def read_event_files(limit: int = 100) -> List[pathlib.Path]:
EVENT_DIR.mkdir(parents=True, exist_ok=True)
return sorted(EVENT_DIR.glob("*.json"), key=lambda p: p.stat().st_mtime)[:limit]
def archive_event(path: pathlib.Path, status: str) -> None:
target = EVENT_DIR / status / path.name
target.parent.mkdir(parents=True, exist_ok=True)
shutil.move(str(path), str(target))
def get_webhook_url() -> Optional[str]:
# Runtime-only injection. Do not commit real values.
return os.environ.get("AIOS_OWNER_WECHAT_WEBHOOK_URL") or os.environ.get("AIOS_OWNER_WECOM_WEBHOOK_URL")
def sign_payload(payload: Dict[str, Any]) -> Optional[str]:
secret = os.environ.get("AIOS_OWNER_ACTION_SIGNING_SECRET")
if not secret:
return None
raw = json.dumps(payload, ensure_ascii=False, sort_keys=True).encode("utf-8")
return hmac.new(secret.encode("utf-8"), raw, hashlib.sha256).hexdigest()
def send_owner_wechat(event: Dict[str, Any], summary: Dict[str, Any]) -> Dict[str, Any]:
url = get_webhook_url()
if not url:
return {"status": "skipped", "reason": "wechat_webhook_not_configured"}
content = format_owner_message(event, summary)
body = {
"msgtype": "markdown",
"markdown": {"content": content},
}
signature = sign_payload(body)
if signature:
body["aios_signature"] = signature
req = urllib.request.Request(
url,
data=json.dumps(body, ensure_ascii=False).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=10) as resp:
data = resp.read().decode("utf-8", errors="replace")
return {"status": "sent", "http_status": resp.status, "response": data[:500]}
except urllib.error.URLError as exc:
return {"status": "failed", "error": str(exc)}
def format_owner_message(event: Dict[str, Any], summary: Dict[str, Any]) -> str:
title = event.get("type", "gitops_event")
branch = summary.get("branch") or event.get("branch") or "-"
from_version = summary.get("from_version") or event.get("from_version") or "-"
to_version = summary.get("to_version") or event.get("to_version") or "-"
changed = summary.get("changed_features") or event.get("changed_features") or []
if isinstance(changed, list):
changed_text = ", ".join(str(x) for x in changed[:8]) or "-"
else:
changed_text = str(changed)
return "\n".join([
f"### AIOS GitOps 通报:{title}",
f"- Branch: `{branch}`",
f"- Version: `{from_version}` → `{to_version}`",
f"- Code Δ: +{summary.get('code_lines_added', 0)} / -{summary.get('code_lines_deleted', 0)}",
f"- Config Δ: +{summary.get('config_lines_added', 0)} / -{summary.get('config_lines_deleted', 0)}",
f"- Elapsed: {summary.get('elapsed_since_previous_main_change', '-')}",
f"- Risk: {summary.get('risk', event.get('risk', 'unknown'))}",
f"- Test: {summary.get('test_status', '-')}",
f"- Rollback: `{summary.get('rollback_target', 'V1R1C4')}`",
f"- Changed: {changed_text}",
f"- Web UI: {summary.get('web_ui_audit_url', '/portal/me/#gitops-audit')}",
])
def diff_stats(base: str, branch: str) -> Dict[str, int]:
code_add = code_del = config_add = config_del = 0
rc, stdout, _ = run_cmd(["git", "diff", "--numstat", f"{base}...{branch}"], timeout=60)
if rc != 0:
return {"code_lines_added": 0, "code_lines_deleted": 0, "config_lines_added": 0, "config_lines_deleted": 0}
config_ext = {".json", ".yaml", ".yml", ".toml", ".ini", ".conf", ".service"}
for line in stdout.splitlines():
parts = line.split("\t")
if len(parts) < 3 or parts[0] == "-" or parts[1] == "-":
continue
add, delete, path = int(parts[0]), int(parts[1]), parts[2]
if pathlib.Path(path).suffix in config_ext or path.startswith("config/"):
config_add += add
config_del += delete
else:
code_add += add
code_del += delete
return {
"code_lines_added": code_add,
"code_lines_deleted": code_del,
"config_lines_added": config_add,
"config_lines_deleted": config_del,
}
def build_summary(event: Dict[str, Any]) -> Dict[str, Any]:
branch = event.get("branch") or "patch/web-ui-complete-gitops-dashboard-20260706"
base = event.get("base") or "main"
stats = diff_stats(base, branch)
return {
"event_id": event.get("id") or event_id(event),
"branch": branch,
"from_rev": event.get("from_rev", base),
"to_rev": event.get("to_rev", branch),
"from_version": event.get("from_version", "V1R1C4"),
"to_version": event.get("to_version", "V1R1C5"),
"elapsed_since_previous_main_change": event.get("elapsed_since_previous_main_change", "unknown"),
"changed_features": event.get("changed_features", ["GitOps autonomous orchestration", "Owner notifications", "event bus", "safe daemon"]),
"test_status": event.get("test_status", "pending"),
"risk": event.get("risk", "high"),
"approval_status": event.get("approval_status", "required"),
"rollback_target": event.get("rollback_target", "V1R1C4"),
"web_ui_audit_url": event.get("web_ui_audit_url", "/portal/me/#gitops-audit"),
**stats,
}
def has_preapproval(action: str, event: Dict[str, Any]) -> bool:
target_version = event.get("to_version") or "V1R1C5"
rollback_target = event.get("rollback_target") or "V1R1C4"
now_dt = _dt.datetime.now(_dt.timezone.utc)
for path in PREAPPROVAL_DIR.glob("*.json"):
try:
doc = json.loads(path.read_text(encoding="utf-8"))
except Exception:
continue
allowed = doc.get("allowed_actions", [])
if action not in allowed:
continue
if doc.get("target_version") != target_version:
continue
if doc.get("rollback_target") != rollback_target:
continue
if doc.get("approved_by_role") not in ["owner", "admin"]:
continue
expires_at = doc.get("expires_at")
if not expires_at:
continue
try:
exp = _dt.datetime.fromisoformat(expires_at.replace("Z", "+00:00"))
except ValueError:
continue
if exp > now_dt:
return True
return False
def create_request(event: Dict[str, Any], reason: str) -> Dict[str, Any]:
req = {
"id": f"req-{utc_now().replace(':', '-')}-{event_id(event)}",
"created_at": utc_now(),
"event": redact(event),
"reason": reason,
"status": "pending_owner_review",
"web_ui": "/portal/me/#gitops-owner-actions",
}
write_json(REQUEST_DIR / f"{req['id']}.json", req)
return req
def run_validation(event: Dict[str, Any]) -> Dict[str, Any]:
branch = event.get("branch") or "patch/web-ui-complete-gitops-dashboard-20260706"
base = event.get("base") or "main"
script = ROOT / "scripts" / "validate-gitops-platform-ui.sh"
if not script.exists():
return {"status": "blocked", "reason": "validation_script_missing"}
rc, out, err = run_cmd(["bash", str(script), branch, base], timeout=600)
REPORT_DIR.mkdir(parents=True, exist_ok=True)
(REPORT_DIR / "validation-daemon.log").write_text(out + "\n" + err, encoding="utf-8")
return {"status": "passed" if rc == 0 else "failed", "exit_code": rc, "log": "reports/codex/validation-daemon.log"}
def run_merge_gate(event: Dict[str, Any]) -> Dict[str, Any]:
branch = event.get("branch") or "patch/web-ui-complete-gitops-dashboard-20260706"
base = event.get("base") or "main"
script = ROOT / "scripts" / "autonomous-merge-gate.sh"
if not script.exists():
return {"status": "blocked", "reason": "merge_gate_script_missing"}
rc, out, err = run_cmd(["bash", str(script), branch, base], timeout=600)
(REPORT_DIR / "merge-gate-daemon.log").write_text(out + "\n" + err, encoding="utf-8")
return {"status": "passed" if rc == 0 else "failed", "exit_code": rc, "log": "reports/codex/merge-gate-daemon.log"}
def handle_event(event: Dict[str, Any]) -> Dict[str, Any]:
event_type = event.get("type", "unknown")
action = event.get("action", event_type)
if action in ALWAYS_BLOCKED:
req = create_request(event, f"Blocked permanently by autonomous production policy: {action}")
audit("event.blocked", "blocked", {"event": event, "request": req})
return {"status": "blocked", "request": req}
summary = build_summary(event)
if event_type in OWNER_NOTIFY_EVENTS:
notify_result = send_owner_wechat(event, summary)
audit("owner.notify", notify_result.get("status", "unknown"), {"event": event, "summary": summary, "result": notify_result})
if event_type in {"git_push", "patch_created", "validation_failed"}:
validation = run_validation(event)
audit("codex.validation", validation["status"], {"event": event, "validation": validation})
if validation["status"] != "passed":
req = create_request(event, "Validation failed; GPT repair loop required.")
return {"status": "validation_failed", "request": req, "validation": validation}
gate = run_merge_gate(event)
audit("codex.merge_gate", gate["status"], {"event": event, "gate": gate})
return {"status": "merge_candidate" if gate["status"] == "passed" else "gate_failed", "validation": validation, "gate": gate}
if action in PRODUCTION_ACTIONS:
if has_preapproval(action, event):
# This daemon deliberately records intent. The actual protected write must be
# performed by configured GitOps Actions with server-side authorization.
req = create_request(event, f"Preapproval found for {action}; execute through protected GitOps Action.")
audit("production.preapproved", "request_created", {"event": event, "request": req})
return {"status": "preapproved_request_created", "request": req}
req = create_request(event, f"Production action requires preapproved runbook: {action}")
audit("production.approval_required", "pending", {"event": event, "request": req})
return {"status": "approval_required", "request": req}
audit("event.record", "ok", {"event": event, "summary": summary})
return {"status": "recorded", "summary": summary}
def process_once() -> int:
count = 0
for path in read_event_files():
try:
event = json.loads(path.read_text(encoding="utf-8"))
result = handle_event(event)
archive_event(path, "done")
count += 1
audit("event.processed", "ok", {"path": str(path), "result": result})
except Exception as exc: # noqa: BLE001 - daemon must not crash on single event
archive_event(path, "failed")
audit("event.processed", "failed", {"path": str(path), "error": str(exc)})
return count
def emit_event(args: argparse.Namespace) -> None:
event = {
"id": args.id or f"evt-{utc_now().replace(':', '-')}",
"type": args.type,
"action": args.action or args.type,
"branch": args.branch,
"base": args.base,
"from_version": args.from_version,
"to_version": args.to_version,
"risk": args.risk,
"created_at": utc_now(),
}
write_json(EVENT_DIR / f"{event['id']}.json", event)
print(json.dumps(redact(event), ensure_ascii=False, indent=2))
def main(argv: Optional[Iterable[str]] = None) -> int:
parser = argparse.ArgumentParser(description="AIOS Autonomous GitOps Daemon")
sub = parser.add_subparsers(dest="cmd", required=True)
run = sub.add_parser("run")
run.add_argument("--interval", type=int, default=30)
run.add_argument("--once", action="store_true")
emit = sub.add_parser("emit")
emit.add_argument("--id")
emit.add_argument("--type", required=True)
emit.add_argument("--action")
emit.add_argument("--branch", default="patch/web-ui-complete-gitops-dashboard-20260706")
emit.add_argument("--base", default="main")
emit.add_argument("--from-version", default="V1R1C4")
emit.add_argument("--to-version", default="V1R1C5")
emit.add_argument("--risk", default="high")
args = parser.parse_args(list(argv) if argv is not None else None)
if args.cmd == "emit":
emit_event(args)
return 0
if args.cmd == "run":
EVENT_DIR.mkdir(parents=True, exist_ok=True)
AUDIT_DIR.mkdir(parents=True, exist_ok=True)
REQUEST_DIR.mkdir(parents=True, exist_ok=True)
if args.once:
return 0 if process_once() >= 0 else 1
audit("daemon.start", "ok", {"event_dir": str(EVENT_DIR)})
while True:
process_once()
time.sleep(args.interval)
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""AIOS Multi-Agent Consensus Controller
This service aggregates decisions from OpenClaw, Codex, and ChatGPT agents
and computes a bounded consensus decision for GitOps operations.
It does NOT directly modify production state. It only produces decision artifacts.
"""
import json
import os
import pathlib
import time
from typing import Dict, Any
ROOT = pathlib.Path(os.environ.get("AIOS_REPO_ROOT", ".")).resolve()
CONSENSUS_DIR = ROOT / "reports" / "consensus"
AGENT_DIRS = {
"openclaw": ROOT / "reports" / "openclaw",
"codex": ROOT / "reports" / "codex",
"chatgpt": ROOT / "reports" / "chatgpt",
}
CONSENSUS_DIR.mkdir(parents=True, exist_ok=True)
def load_latest(path: pathlib.Path) -> Dict[str, Any]:
if not path.exists():
return {}
files = sorted(path.glob("*.json"), key=lambda p: p.stat().st_mtime, reverse=True)
if not files:
return {}
return json.loads(files[0].read_text(encoding="utf-8"))
def compute_consensus(reports: Dict[str, Dict[str, Any]]) -> Dict[str, Any]:
weights = {"openclaw": 2, "codex": 2, "chatgpt": 1}
score = 0
for agent, report in reports.items():
status = report.get("status", "unknown")
if status in ("PASS", "approved", "merge_candidate"):
score += weights.get(agent, 1)
elif status in ("FAIL", "blocked"):
score -= weights.get(agent, 1)
if score >= 4:
decision = "approved"
elif score >= 2:
decision = "conditional"
else:
decision = "rejected"
return {
"decision": decision,
"score": score,
"timestamp": time.time(),
}
def main():
reports = {
"openclaw": load_latest(AGENT_DIRS["openclaw"]),
"codex": load_latest(AGENT_DIRS["codex"]),
"chatgpt": load_latest(AGENT_DIRS["chatgpt"]),
}
result = compute_consensus(reports)
out_path = CONSENSUS_DIR / "decision.json"
out_path.write_text(json.dumps(result, indent=2), encoding="utf-8")
print("CONSENSUS:", result)
if __name__ == "__main__":
main()
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env python3
"""AIOS Event Ingress Server
Receives GitOps events via HTTP and writes them into the local event bus directory.
No authentication secrets are embedded; optional signature validation can be enabled via env.
"""
from http.server import BaseHTTPRequestHandler, HTTPServer
import json
import os
import pathlib
import time
EVENT_DIR = pathlib.Path(os.environ.get("AIOS_EVENT_DIR", "runtime/gitops/events"))
EVENT_DIR.mkdir(parents=True, exist_ok=True)
class Handler(BaseHTTPRequestHandler):
def do_POST(self):
length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(length).decode('utf-8')
try:
event = json.loads(body)
except Exception:
self.send_response(400)
self.end_headers()
return
event_id = event.get("id", f"evt-{int(time.time())}")
path = EVENT_DIR / f"{event_id}.json"
path.write_text(json.dumps(event, indent=2), encoding='utf-8')
self.send_response(200)
self.end_headers()
self.wfile.write(b"OK")
def log_message(self, format, *args):
return
if __name__ == "__main__":
port = int(os.environ.get("AIOS_INGRESS_PORT", "8787"))
server = HTTPServer(("0.0.0.0", port), Handler)
print(f"[AIOS] Event ingress running on {port}")
server.serve_forever()
@@ -0,0 +1,28 @@
#!/usr/bin/env python3
"""V1R1 Personal WeChat Endpoint Discovery
Helps locate active V1R1 personal WeChat runtime endpoints on the host.
"""
import subprocess
import re
CANDIDATES = [8788, 8789, 8790, 8088, 8000]
print("[AIOS] Scanning for V1R1 WeChat endpoints...")
for port in CANDIDATES:
try:
result = subprocess.run(
["bash", "-lc", f"ss -ltnp | grep {port}"],
capture_output=True,
text=True
)
if result.stdout:
print(f"[FOUND] Port {port}: {result.stdout.strip()}")
else:
print(f"[MISS] Port {port}")
except Exception as e:
print(f"[ERROR] Port {port}: {e}")
print("[AIOS] Discovery complete. Please map discovered endpoint to AIOS_V1R1_WECHAT_OUTBOUND_URL")
@@ -0,0 +1,206 @@
#!/usr/bin/env python3
"""AIOS V1R1 Personal WeChat Outbound Worker.
This worker is the bridge between the Xiamayiti self-evolution loop and the
already-deployed V1R1 personal WeChat capability.
Design rules:
- Do not embed credentials.
- Do not access runtime/secrets directly.
- Read owner-notification outbox JSON files.
- Deliver to a runtime-configured V1R1 notification endpoint, or try local V1R1 candidates.
- Write delivery status for audit/Web UI.
"""
from __future__ import annotations
import datetime as dt
import json
import os
import pathlib
import shutil
import time
import urllib.error
import urllib.request
from typing import Any, Dict, Iterable, List, Optional
ROOT = pathlib.Path(os.environ.get("AIOS_REPO_ROOT", "/data/aios-gitops/workspace"))
BASE = pathlib.Path(os.environ.get("AIOS_RUNTIME_ROOT", "/data/aios-gitops/runtime"))
OUTBOX = pathlib.Path(os.environ.get("AIOS_V1R1_WECHAT_OUTBOX", str(BASE / "integrations" / "v1r1-personal-wechat" / "outbox")))
SENT = pathlib.Path(os.environ.get("AIOS_V1R1_WECHAT_SENT", str(BASE / "integrations" / "v1r1-personal-wechat" / "sent")))
FAILED = pathlib.Path(os.environ.get("AIOS_V1R1_WECHAT_FAILED", str(BASE / "integrations" / "v1r1-personal-wechat" / "failed")))
STATUS = pathlib.Path(os.environ.get("AIOS_V1R1_WECHAT_STATUS", str(BASE / "integrations" / "v1r1-personal-wechat" / "status")))
AUDIT = pathlib.Path(os.environ.get("AIOS_AUDIT_DIR", str(BASE / "audit")))
INTERVAL = int(os.environ.get("AIOS_V1R1_WECHAT_INTERVAL", "10"))
# Runtime-configured endpoint. This can point to the existing V1R1 personal-WeChat module.
PRIMARY_ENDPOINT = os.environ.get("AIOS_V1R1_WECHAT_OUTBOUND_URL", "")
LOCAL_CANDIDATES = [
"http://127.0.0.1:8788/api/v1r1/personal-wechat/send",
"http://127.0.0.1:8788/api/v1r1/notification/owner",
"http://127.0.0.1:8789/api/owner/wechat/send",
"http://127.0.0.1:8790/owner/notify",
]
def now() -> str:
return dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
def write_json(path: pathlib.Path, value: Dict[str, Any]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
tmp = path.with_suffix(path.suffix + ".tmp")
tmp.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
tmp.replace(path)
def audit(action: str, status: str, detail: Dict[str, Any]) -> None:
record = {"ts": now(), "action": action, "status": status, "detail": detail}
safe_name = record["ts"].replace(":", "-") + f"-{action}.json"
write_json(AUDIT / safe_name, record)
print(json.dumps(record, ensure_ascii=False), flush=True)
def normalize_message(item: Dict[str, Any]) -> Dict[str, Any]:
title = item.get("title") or "AIOS GitOps 通报"
content = item.get("content") or item.get("message") or json.dumps(item, ensure_ascii=False)
priority = item.get("priority") or "normal"
return {
"event_id": item.get("event_id") or item.get("id") or f"evt-{int(time.time())}",
"channel": "personal_wechat",
"priority": priority,
"title": title,
"content": content,
"source": item.get("source") or "aios-self-evolution",
"branch": item.get("branch"),
"from_version": item.get("from_version"),
"to_version": item.get("to_version"),
"rollback_target": item.get("rollback_target") or "V1R1C4",
"web_ui_audit_url": item.get("web_ui_audit_url") or "/portal/me/#gitops-audit",
"created_at": item.get("created_at") or now(),
}
def endpoints() -> List[str]:
values: List[str] = []
if PRIMARY_ENDPOINT:
values.append(PRIMARY_ENDPOINT)
values.extend(LOCAL_CANDIDATES)
# Preserve order but remove duplicates.
result: List[str] = []
seen = set()
for value in values:
if value and value not in seen:
result.append(value)
seen.add(value)
return result
def post_json(url: str, payload: Dict[str, Any]) -> Dict[str, Any]:
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
req = urllib.request.Request(url, data=body, headers={"Content-Type": "application/json"}, method="POST")
with urllib.request.urlopen(req, timeout=8) as resp:
text = resp.read().decode("utf-8", errors="replace")[:500]
return {"ok": 200 <= resp.status < 300, "status": resp.status, "response": text, "url": url}
def deliver(message: Dict[str, Any]) -> Dict[str, Any]:
attempts = []
for url in endpoints():
try:
result = post_json(url, message)
attempts.append(result)
if result.get("ok"):
return {"status": "sent", "endpoint": url, "attempts": attempts}
except urllib.error.URLError as exc:
attempts.append({"ok": False, "url": url, "error": str(exc)})
except Exception as exc: # noqa: BLE001
attempts.append({"ok": False, "url": url, "error": str(exc)})
return {"status": "failed", "attempts": attempts, "reason": "no_endpoint_accepted_message"}
def process_file(path: pathlib.Path) -> None:
try:
item = json.loads(path.read_text(encoding="utf-8"))
except Exception as exc: # noqa: BLE001
failed_target = FAILED / path.name
FAILED.mkdir(parents=True, exist_ok=True)
shutil.move(str(path), str(failed_target))
audit("v1r1.wechat.parse", "failed", {"path": str(path), "error": str(exc)})
return
message = normalize_message(item)
result = deliver(message)
status_record = {"ts": now(), "file": path.name, "message": message, "delivery": result}
write_json(STATUS / (path.stem + ".status.json"), status_record)
if result.get("status") == "sent":
SENT.mkdir(parents=True, exist_ok=True)
shutil.move(str(path), str(SENT / path.name))
audit("v1r1.wechat.deliver", "sent", status_record)
else:
FAILED.mkdir(parents=True, exist_ok=True)
shutil.move(str(path), str(FAILED / path.name))
audit("v1r1.wechat.deliver", "failed", status_record)
def process_once() -> int:
OUTBOX.mkdir(parents=True, exist_ok=True)
count = 0
for path in sorted(OUTBOX.glob("*.json"), key=lambda p: p.stat().st_mtime):
process_file(path)
count += 1
return count
def emit_startup_notice() -> pathlib.Path:
OUTBOX.mkdir(parents=True, exist_ok=True)
path = OUTBOX / f"startup-{int(time.time())}.json"
write_json(path, {
"event_id": f"startup-{int(time.time())}",
"type": "owner_notification",
"channel": "personal_wechat",
"priority": "high",
"title": "AIOS 自我进化系统已启动",
"content": "AIOS 虾马一体 self-evolution loop 已进入运行态。daemon、event bus、GitOps 写链路已恢复;后续将通过 V1R1 个人微信模块与 Web UI 审计同步进展。",
"source": "aios-v1r1-wechat-outbound-worker",
"branch": "patch/web-ui-complete-gitops-dashboard-20260706",
"from_version": "V1R1C4",
"to_version": "V1R1C5",
"rollback_target": "V1R1C4",
"web_ui_audit_url": "/portal/me/#gitops-audit",
"created_at": now()
})
return path
def main() -> int:
import argparse
parser = argparse.ArgumentParser(description="AIOS V1R1 personal WeChat outbound worker")
parser.add_argument("--once", action="store_true")
parser.add_argument("--emit-startup-notice", action="store_true")
args = parser.parse_args()
OUTBOX.mkdir(parents=True, exist_ok=True)
SENT.mkdir(parents=True, exist_ok=True)
FAILED.mkdir(parents=True, exist_ok=True)
STATUS.mkdir(parents=True, exist_ok=True)
AUDIT.mkdir(parents=True, exist_ok=True)
if args.emit_startup_notice:
path = emit_startup_notice()
audit("v1r1.wechat.outbox", "created", {"path": str(path)})
if args.once:
process_once()
return 0
audit("v1r1.wechat.worker.start", "ok", {"outbox": str(OUTBOX), "has_primary_endpoint": bool(PRIMARY_ENDPOINT)})
while True:
process_once()
time.sleep(INTERVAL)
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,52 @@
# Skill: GitOps Platform Validation
## Name
gitops-platform-validation
## Version
1.0.0
## Purpose
Provide reusable Codex execution behavior for validating GitOps platform UI branches and enabling self-healing CI loops.
## Trigger
- branch matches: patch/*
- contains: gitops-dashboard
## Actions
1. Run validation script
```bash
bash scripts/validate-gitops-platform-ui.sh <branch> main
```
2. If FAIL:
- generate failure report
- invoke GPT repair loop
- re-run validation
3. If PASS:
- run autonomous merge gate
```bash
bash scripts/autonomous-merge-gate.sh <branch> main
```
4. If merge candidate created:
- propose V1R1C5 baseline update
## Constraints
- no secrets in logs
- no direct main write
- destructive operations forbidden
- all merges must pass policy gate
## Self-Healing Behavior
- max iterations: 5
- stop on:
- PASS
- security failure
- manual override
## Output
- validation report
- merge candidate artifact
- optional baseline proposal
@@ -0,0 +1,239 @@
{
"suite": "gitops-platform-ui",
"version": "1.0.0",
"branch_under_test": "patch/web-ui-complete-gitops-dashboard-20260706",
"base_branch": "main",
"purpose": "Validate AIOS GitOps Platform Control Plane UI before merge to main and before V1R1C5 baseline proposal.",
"required_result": "PASS",
"tests": [
{
"id": "STATIC-001",
"group": "static_files",
"required": true,
"description": "Platform UI files exist.",
"assert": {
"files_exist": [
"overlays/user-control-center/portal/gitops-dashboard/index.html",
"overlays/user-control-center/portal/gitops-dashboard/app.js",
"overlays/user-control-center/portal/gitops-dashboard/styles.css",
"overlays/user-control-center/portal/gitops-dashboard/manifest.json",
"overlays/user-control-center/portal/gitops-dashboard/gitops-ui-policy.json",
"overlays/user-control-center/portal/gitops-dashboard/gitops-enterprise-api.contract.json"
]
}
},
{
"id": "STATIC-002",
"group": "static_files",
"required": true,
"description": "Enterprise tabs exist in HTML.",
"assert": {
"file_contains": {
"overlays/user-control-center/portal/gitops-dashboard/index.html": [
"tab-overview",
"tab-rbac",
"tab-approval",
"tab-diff",
"tab-patch",
"tab-model",
"tab-release",
"tab-audit"
]
}
}
},
{
"id": "RUNTIME-001",
"group": "frontend_runtime",
"required": true,
"description": "Platform runtime bootstraps on DOMContentLoaded.",
"assert": {
"file_contains": {
"overlays/user-control-center/portal/gitops-dashboard/app.js": [
"Platform.bootstrap",
"DOMContentLoaded",
"guardAction",
"createApproval",
"renderDiffTree",
"audit(" ,
"redact("
]
}
}
},
{
"id": "SEC-001",
"group": "security",
"required": true,
"description": "No real credentials are committed.",
"assert": {
"secret_scan": {
"paths": [
"overlays/user-control-center/portal/gitops-dashboard",
"docs/codex",
"tests/acceptance",
"scripts",
"skills"
],
"allow_patterns": [
"secret_ref://",
"[REDACTED]",
"api_key_secret_ref",
"forbiddenFields",
"secret_redaction",
"No credentials"
],
"deny_patterns": [
"Bearer [A-Za-z0-9._-]{20,}",
"sk-[A-Za-z0-9]{20,}",
"ghp_[A-Za-z0-9]{20,}",
"AKIA[0-9A-Z]{16}",
"BEGIN PRIVATE KEY",
"BEGIN RSA PRIVATE KEY"
]
}
}
},
{
"id": "SEC-002",
"group": "security",
"required": true,
"description": "Model registration requires secret_ref and never raw api_key.",
"assert": {
"file_contains": {
"overlays/user-control-center/portal/gitops-dashboard/app.js": [
"api_key_secret_ref",
"startsWith('secret_ref://')",
"api_key_secret_ref_required"
]
},
"file_not_contains": {
"overlays/user-control-center/portal/gitops-dashboard/index.html": [
"name=\"api_key\"",
"name=\"token\"",
"name=\"password\""
]
}
}
},
{
"id": "POLICY-001",
"group": "gitops_policy",
"required": true,
"description": "Direct main writes and destructive actions are disabled or blocked.",
"assert": {
"file_contains": {
"overlays/user-control-center/portal/gitops-dashboard/app.js": [
"main:write",
"destructive_action_blocked",
"high_risk_requires_approval"
],
"overlays/user-control-center/portal/gitops-dashboard/gitops-ui-policy.json": [
"direct_main_write",
"disabled_in_ui",
"bulk_delete",
"forbidden"
]
}
}
},
{
"id": "API-001",
"group": "api_contract",
"required": true,
"description": "Read/write endpoint contract remains compatible with existing GitOps APIs.",
"assert": {
"file_contains": {
"overlays/user-control-center/portal/gitops-dashboard/gitops-enterprise-api.contract.json": [
"GET /api/base",
"GET /api/ver",
"GET /api/run",
"GET /api/patch",
"GET /api/model",
"POST /api/branch",
"POST /api/patch/plan",
"POST /api/model",
"PUT /api/model-priority",
"POST /api/rollback-request",
"POST /api/tag-request"
]
}
}
},
{
"id": "REGRESSION-001",
"group": "indirect_regression",
"required": true,
"description": "This branch must not alter auth, gitea owner sync, baseline version, or destructive history files.",
"assert": {
"diff_not_touch_paths": [
"config/auth/",
"config/casdoor/",
"config/gitea/",
"versions/V1R1C4.json",
"runtime/gitops/current.json"
],
"diff_not_contain": [
"active_version\": \"V1R1C5",
"force push",
"delete tag",
"purge baseline",
"clear audit"
]
}
},
{
"id": "BASELINE-001",
"group": "baseline_proposal",
"required": true,
"description": "A V1R1C5 proposal exists but is not activated before merge.",
"assert": {
"files_exist": [
"versions/proposals/V1R1C5-gitops-platform-ui.proposed.json"
],
"file_contains": {
"versions/proposals/V1R1C5-gitops-platform-ui.proposed.json": [
"V1R1C5",
"V1R1C4",
"approval_required"
]
}
}
},
{
"id": "SKILL-001",
"group": "auto_loaded_skill",
"required": true,
"description": "Codex/GPT reusable skill exists and references the validation workflow.",
"assert": {
"files_exist": [
"skills/gitops-platform-validation/SKILL.md"
],
"file_contains": {
"skills/gitops-platform-validation/SKILL.md": [
"validate-gitops-platform-ui.sh",
"gitops-platform-ui.acceptance.json",
"self-healing"
]
}
}
}
],
"merge_gate": {
"allow_merge_to_main_when": [
"all required tests PASS",
"gray deployment PASS",
"no secret leak",
"no direct main write",
"V1R1C5 proposal reviewed",
"owner/admin approval recorded"
],
"block_merge_when": [
"any required test FAIL",
"any required test BLOCKED without explicit owner override",
"secret leak detected",
"destructive operation detected",
"baseline active version changed before merge"
]
}
}
+23
View File
@@ -0,0 +1,23 @@
{
"version": "V1R1C1",
"tag": "V1R1C1",
"type": "patch-release",
"name": "Codex GitOps Online Version Management",
"previous_version": "V1R1C0",
"source_version": "V1R1C0-baseline-20260706",
"update_mode": "git_patch_only",
"zip_update_mode": "deprecated",
"git_url": "https://cnnetmkt.top/git/aios/aios-v23.git",
"api_base": "https://cnnetmkt.top/api",
"openapi_schema": "https://cnnetmkt.top/api/openapi-chatgpt-actions.yaml",
"codex_config": "config/codex/gitops-online.json",
"included_commits": [
"96c00d8",
"0b0505a",
"43ddb91"
],
"excluded_branches": [
"patch/chatgpt-model-acceptance-*"
],
"updated_at": "2026-07-06T10:21:08Z"
}
+19
View File
@@ -0,0 +1,19 @@
{
"version": "V1R1C2",
"tag": "V1R1C2",
"type": "patch-release",
"name": "Codex GitOps Online Branch Sync",
"previous_version": "V1R1C1",
"source_version": "V1R1C1",
"update_mode": "git_patch_only",
"zip_update_mode": "deprecated",
"git_url": "https://cnnetmkt.top/git/aios/aios-v23.git",
"api_base": "https://cnnetmkt.top/api",
"openapi_schema": "https://cnnetmkt.top/api/openapi-chatgpt-actions.yaml",
"codex_config": "config/codex/gitops-online.json",
"online_git_sync": true,
"included_commits": [
"ecf6735"
],
"updated_at": "2026-07-06T10:26:26Z"
}
+47
View File
@@ -0,0 +1,47 @@
{
"version": "V1R1C3",
"tag": "V1R1C3",
"type": "collaboration-release",
"name": "Local Codex and Custom GPT GitOps Collaboration",
"previous_version": "V1R1C2",
"update_mode": "git_patch_only",
"zip_update_mode": "deprecated",
"git_url": "https://cnnetmkt.top/git/aios/aios-v23.git",
"api_base": "https://cnnetmkt.top/api",
"custom_gpt_url": "https://chatgpt.com/g/g-6a4b7c82dff08191bc980798edea1bac-aios-gitops-kong-zhi-tai",
"collaboration_timestamp": "20260706T104130Z",
"actors": [
{
"name": "local_codex",
"account": "codex-gitops-bot",
"branch": "patch/collab-local-codex-20260706T104130Z",
"path": "docs/gitops/collab-local-codex-20260706T104130Z.md",
"branch_code": 200,
"branch_commit": "ed92b88",
"branch_sync": "synced",
"file_code": 200,
"file_commit": "a62d4ce",
"file_sync": "synced",
"online_ref_present": true,
"online_ref": "a62d4ce2c1722afde0775628e6e618251b7ed712 refs/heads/patch/collab-local-codex-20260706T104130Z",
"response_status": "committed"
},
{
"name": "custom_gpt",
"account": "chatgpt-gitops-bot",
"branch": "patch/collab-custom-gpt-20260706T104130Z",
"path": "docs/gitops/collab-custom-gpt-20260706T104130Z.md",
"branch_code": 200,
"branch_commit": "5a21bd5",
"branch_sync": "synced",
"file_code": 200,
"file_commit": "c393931",
"file_sync": "synced",
"online_ref_present": true,
"online_ref": "c393931f64b3664d685a809c1aa4f496709a05f3 refs/heads/patch/collab-custom-gpt-20260706T104130Z",
"response_status": "committed"
}
],
"online_git_sync": true,
"updated_at": "2026-07-06T10:42:33Z"
}
+34
View File
@@ -0,0 +1,34 @@
{
"version": "V1R1C4",
"tag": "V1R1C4",
"type": "auth-portal-hardening-release",
"name": "GitOps portal auth hardening and Gitea owner sync",
"previous_version": "V1R1C3",
"update_mode": "git_patch_only",
"zip_update_mode": "deprecated",
"entry": "https://cnnetmkt.top/",
"git_web": "https://g.cnnetmkt.top/git/aios/aios-v23",
"api_base": "https://cnnetmkt.top/api",
"changes": [
"Human shortlinks /base /ver /diff /patch /model /run now redirect to the login-protected portal GitOps section.",
"Machine API remains under /api for ChatGPT Actions and Codex automation.",
"Gitea requires sign-in for repository browsing and registration UI is hidden.",
"Gitea owner password sync service/timer copies the user-control owner password without logging secrets.",
"Default local Gitea account password is cleared and login is prohibited.",
"User portal includes a GitOps dashboard section for admin/owner users.",
"Patched user-control-center files are preserved under overlays/user-control-center for replayable releases.",
"Portal GitOps dashboard reads /app/runtime/gitops/current.json before HTTP fallback so the version is visible inside Docker.",
"Gitea owner sync uses runuser instead of sudo command logging and strips CRLF from sourced owner fields."
],
"artifacts": [
"deploy/nginx/aios-gitops-locations.conf",
"deploy/gitea-binary/app.ini.template",
"deploy/systemd/aios-gitea-owner-sync.service",
"deploy/systemd/aios-gitea-owner-sync.timer",
"ops/gitops-migration/sync_gitea_owner_password.sh",
"ops/gitops-migration/apply_user_portal_gitops_overlay.sh",
"overlays/user-control-center/control/portal_views.py",
"overlays/user-control-center/templates/portal/me.html"
],
"updated_at": "2026-07-06T11:16:23Z"
}
@@ -0,0 +1,51 @@
{
"version": "V1R1C5",
"status": "proposed",
"activation": "approval_required",
"base_version": "V1R1C4",
"rollback_target": "V1R1C4",
"purpose": "Promote AIOS GitOps Platform Control Plane with autonomous CI, self-healing validation, bounded production policy, event bus, daemon, owner notification, Web UI audit and Codex validation skills.",
"risk": "high",
"approval": {
"required": true,
"roles": ["owner", "admin"],
"reason": "Introduces autonomous orchestration and production-adjacent daemon. Main merge, tag creation and baseline activation remain gated."
},
"scope": {
"direct_changes": [
"overlays/user-control-center/portal/gitops-dashboard",
"services/aios-autonomous-daemon",
"config/codex",
"scripts",
"tests/acceptance",
"skills",
"docs/codex",
"docs/ops",
"deploy/systemd"
],
"indirect_impact": [
"Portal GitOps UI visibility",
"Owner notification workflow",
"Codex validation workflow",
"Self-healing patch loop",
"Web UI audit trail"
]
},
"non_goals": [
"No direct main push",
"No committed secrets",
"No destructive history deletion",
"No production rollback execution without approval or preapproved runbook"
],
"validation": {
"acceptance_suite": "tests/acceptance/gitops-platform-ui.acceptance.json",
"script": "scripts/validate-gitops-platform-ui.sh",
"merge_gate": "scripts/autonomous-merge-gate.sh",
"gray_validation": "scripts/gray-validate-gitops-platform-ui.sh"
},
"owner_notification": {
"required_on_main_version_change": true,
"channels": ["wechat_or_wecom", "web_ui_audit"],
"webhook_secret_ref": "secret_ref://aios/notifications/owner-wechat-webhook"
}
}