Compare commits
132
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0b6c9b78e | ||
|
|
187dcc3554 | ||
|
|
75fd3c6274 | ||
|
|
ac5518588f | ||
|
|
54a2b7ea41 | ||
|
|
3803f33d7b | ||
|
|
62fb0613a0 | ||
|
|
64e3e087f2 | ||
|
|
5c470b8833 | ||
|
|
89a0a26b3b | ||
|
|
0a6d50fff3 | ||
|
|
6c4f0a988d | ||
|
|
7d79f40324 | ||
|
|
1fec447ca2 | ||
|
|
c30d5aa12b | ||
|
|
90b50a853e | ||
|
|
a1de052ff8 | ||
|
|
b3f9bdb759 | ||
|
|
2d3061d825 | ||
|
|
9becff85ee | ||
|
|
b63a893d37 | ||
|
|
363a061cd3 | ||
|
|
9ce79275e0 | ||
|
|
087366bafb | ||
|
|
cd495c15fe | ||
|
|
f211fc1a96 | ||
|
|
6b879c7539 | ||
|
|
5ea27d7529 | ||
|
|
af01b48f11 | ||
|
|
84c327816b | ||
|
|
edbc1397c1 | ||
|
|
86e3bc351d | ||
|
|
60cefc1a60 | ||
|
|
81c9a3759a | ||
|
|
ab9a744d63 | ||
|
|
65f88f6a7f | ||
|
|
08a1c3f24d | ||
|
|
730fbb61ab | ||
|
|
c38c575d8e | ||
|
|
b1c1e49cc5 | ||
|
|
b9e2358901 | ||
|
|
a1874c90ae | ||
|
|
19a2ce8439 | ||
|
|
f8fc7ad73b | ||
|
|
e2b9cc6849 | ||
|
|
4f4c95ffce | ||
|
|
edd3bd239e | ||
|
|
7a1e862a03 | ||
|
|
dc0ac18aea | ||
|
|
9e3dbdf146 | ||
|
|
87b1a5357c | ||
|
|
128a7d9050 | ||
|
|
e8cf0f6348 | ||
|
|
786a3b6250 | ||
|
|
b9a739fd95 | ||
|
|
0fb71cd191 | ||
|
|
3b8e74bed4 | ||
|
|
9df07a5d7d | ||
|
|
54356c0fcc | ||
|
|
4b62b18a39 | ||
|
|
bb69d76a7b | ||
|
|
27eef25d12 | ||
|
|
7b563590b4 | ||
|
|
50bceb1d45 | ||
|
|
f89619f576 | ||
|
|
365ea5d06c | ||
|
|
f15a19d525 | ||
|
|
3bdb5f9d59 | ||
|
|
4a9f3fff1a | ||
|
|
ffbc1e0e01 | ||
|
|
8365afbeb8 | ||
|
|
d34ef72f5d | ||
|
|
7f118fa205 | ||
|
|
97867386a3 | ||
|
|
e17abc12be | ||
|
|
bec612c67d | ||
|
|
19bcb8aeed | ||
|
|
79e00ef089 | ||
|
|
4833b9f93a | ||
|
|
fd715457da | ||
|
|
4325f0b7d2 | ||
|
|
b2a4f67f93 | ||
|
|
c2e4a5b078 | ||
|
|
707cfe4d60 | ||
|
|
cf97cf697d | ||
|
|
82eecc27b4 | ||
|
|
bea425052f | ||
|
|
45c26a1a4a | ||
|
|
6ea5047dbb | ||
|
|
a5fdfbf057 | ||
|
|
8a5ce63069 | ||
|
|
bce254790c | ||
|
|
14a3b84fc6 | ||
|
|
39f23b0891 | ||
|
|
034d5f0faa | ||
|
|
cb6851b751 | ||
|
|
a79010f788 | ||
|
|
95b1d82738 | ||
|
|
afa5c0eb3e | ||
|
|
ca9d7a7424 | ||
|
|
890699efaa | ||
|
|
142f827feb | ||
|
|
32949d9dac | ||
|
|
4c505d2b6a | ||
|
|
d2bcf4d5af | ||
|
|
76b33c0c6a | ||
|
|
9babbcca43 | ||
|
|
c387c83c13 | ||
|
|
a9ef9197e6 | ||
|
|
54870ed375 | ||
|
|
c850858973 | ||
|
|
d08a7043d6 | ||
|
|
c393931f64 | ||
|
|
a62d4ce2c1 | ||
|
|
5a21bd5d7e | ||
|
|
ed92b88fc0 | ||
|
|
a248142158 | ||
|
|
ecf673500b | ||
|
|
43ddb91568 | ||
|
|
0b0505a645 | ||
|
|
96c00d81a9 | ||
|
|
e7d36ad706 | ||
|
|
d6cb9d1529 | ||
|
|
c06a23f4f8 | ||
|
|
568c283aff | ||
|
|
87f9150806 | ||
|
|
14fd6ee5c5 | ||
|
|
b9328d4161 | ||
|
|
d8ae7ee6f6 | ||
|
|
2ae60299b7 | ||
|
|
c6b172dce7 | ||
|
|
d3cfef4147 |
@@ -0,0 +1,93 @@
|
||||
"""Adaptive policy engine for self-loop optimization.
|
||||
|
||||
Optimizes retry, cooldown, and low-risk automation choices from failure history.
|
||||
It never disables owner gates or security controls.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass
|
||||
from typing import Any
|
||||
|
||||
from .failure_memory import FailureMemory
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AdaptiveDecision:
|
||||
retry_limit: int
|
||||
cooldown_seconds: int
|
||||
automation_level: str
|
||||
risk_level: str
|
||||
owner_approval_required: bool
|
||||
reasons: tuple[str, ...]
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
class AdaptivePolicy:
|
||||
def __init__(self, default_retry_limit: int = 3, default_cooldown_seconds: int = 30):
|
||||
self.default_retry_limit = default_retry_limit
|
||||
self.default_cooldown_seconds = default_cooldown_seconds
|
||||
|
||||
def evaluate(
|
||||
self,
|
||||
memory: FailureMemory,
|
||||
runtime: dict[str, Any],
|
||||
baseline: dict[str, Any],
|
||||
goal: str,
|
||||
) -> AdaptiveDecision:
|
||||
reasons: list[str] = []
|
||||
retry_limit = self.default_retry_limit
|
||||
cooldown = self.default_cooldown_seconds
|
||||
risk = "low"
|
||||
automation = "safe"
|
||||
|
||||
top = memory.top_failures(1)
|
||||
if top:
|
||||
score = float(top[0].get("recurrence_score", 0))
|
||||
if score >= 0.7:
|
||||
retry_limit = 1
|
||||
cooldown = 300
|
||||
risk = "high"
|
||||
automation = "analysis_only"
|
||||
reasons.append("recurrent failure detected; reduce automatic retries")
|
||||
elif score >= 0.4:
|
||||
cooldown = 120
|
||||
reasons.append("failure recurrence increasing; slow down loop")
|
||||
|
||||
if runtime.get("status") != "active":
|
||||
risk = "high"
|
||||
automation = "analysis_only"
|
||||
reasons.append("runtime not healthy")
|
||||
|
||||
if baseline.get("codex_gitops", {}).get("direct_main_push"):
|
||||
risk = "high"
|
||||
automation = "analysis_only"
|
||||
reasons.append("direct main push policy drift")
|
||||
|
||||
owner_gate = True
|
||||
reasons.append("owner gate preserved")
|
||||
|
||||
if not reasons:
|
||||
reasons.append(f"optimize policy for goal: {goal}")
|
||||
|
||||
return AdaptiveDecision(
|
||||
retry_limit=retry_limit,
|
||||
cooldown_seconds=cooldown,
|
||||
automation_level=automation,
|
||||
risk_level=risk,
|
||||
owner_approval_required=owner_gate,
|
||||
reasons=tuple(reasons),
|
||||
)
|
||||
|
||||
def apply_to_scheduler(self, decision: AdaptiveDecision) -> dict[str, Any]:
|
||||
return {
|
||||
"max_attempts": decision.retry_limit,
|
||||
"cooldown_seconds": decision.cooldown_seconds,
|
||||
"automation_level": decision.automation_level,
|
||||
"owner_approval_required": decision.owner_approval_required,
|
||||
}
|
||||
|
||||
|
||||
__all__ = ["AdaptiveDecision", "AdaptivePolicy"]
|
||||
@@ -0,0 +1,300 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
import hashlib, hmac, json, os, re, subprocess, sys, time, urllib.parse, uuid
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
sys.path.insert(0, str(Path(os.getenv('AIOS_GITOPS_WORKSPACE', '/data/aios-gitops/workspace')) / 'runtime' / 'agent-os'))
|
||||
from orchestrator.action_monitor import ActionMonitor
|
||||
|
||||
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')))
|
||||
monitor = ActionMonitor()
|
||||
|
||||
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','/api/baseline','/base'):
|
||||
data=jload(WS/'baseline'/'current.json', {}); data.setdefault('git', sh(['rev-parse','--short','HEAD'])['stdout']); return self.sendj(data)
|
||||
if path in ('/api/ver','/api/versions','/ver'):
|
||||
return self.sendj({'tags':sh(['tag','--list','V*'])['stdout'].splitlines(),'versions_dir':[p.name for p in (WS/'versions').glob('*')],'head':sh(['rev-parse','--short','HEAD'])['stdout']})
|
||||
if path in ('/api/diff','/diff'):
|
||||
frm=(q.get('from') or ['HEAD~1'])[0]; to=(q.get('to') or ['HEAD'])[0]; return self.sendj({'from':frm,'to':to,'diff':sh(['diff',frm,to])})
|
||||
if path in ('/api/patch','/api/patches','/api/branches','/patch'): return self.sendj({'patches':patches(),'branches':sh(['branch','--format','%(refname:short)'])['stdout'].splitlines()})
|
||||
if path in ('/api/model','/model'): return self.sendj({'base':BASE_MODELS,'custom':model_docs(),'priority':[jload(p,{}) for p in sorted((WS/'config'/'model_priority').glob('*.json'))]})
|
||||
if path in ('/api/run','/api/runtime-status','/run'):
|
||||
svc=subprocess.run(['systemctl','is-active','aios-gitops-model-api'], text=True, capture_output=True)
|
||||
return self.sendj({'service':'aios-gitops-model-api','status':svc.stdout.strip() or svc.stderr.strip(),'workspace_branch':sh(['rev-parse','--abbrev-ref','HEAD'])['stdout']})
|
||||
if path in ('/api/action-monitor','/action-monitor'):
|
||||
latest=monitor.latest()
|
||||
ldict={'event_id':latest.event_id,'action':latest.action,'error':latest.error,'context':latest.context,'created_at':latest.created_at} if latest else None
|
||||
return self.sendj({'events':[{"event_id":e.event_id,"action":e.action,"error":e.error,"context":e.context,"created_at":e.created_at} for e in monitor.list_events()],'latest':ldict})
|
||||
return self.sendj({'error':'not_found','path':path},404)
|
||||
except Exception as e:
|
||||
monitor.record_failure('do_GET', str(e), {'path':path})
|
||||
return self.sendj({'error':str(e)},500)
|
||||
def do_PUT(self):
|
||||
path=urllib.parse.urlparse(self.path).path.rstrip('/'); data=self.body()
|
||||
if path == '/api/model-priority': return self.model_priority(data)
|
||||
return self.sendj({'error':'not_found','path':path},404)
|
||||
def do_POST(self):
|
||||
path=urllib.parse.urlparse(self.path).path.rstrip('/'); data=self.body()
|
||||
try:
|
||||
if path in ('/api/branch','/api/create-branch'): return self.branch(data)
|
||||
if path in ('/api/file','/api/commit-file'): return self.file(data)
|
||||
if path == '/api/commit': return self.commit(data)
|
||||
if path in ('/api/patch','/api/create-patch'): return self.patch(data)
|
||||
if path in ('/api/tag','/api/create-tag'): return self.tag(data)
|
||||
if path == '/api/rollback': return self.rollback(data)
|
||||
if path == '/api/model': return self.model(data)
|
||||
if path == '/api/model-priority': return self.model_priority(data)
|
||||
if path == '/api/nl': return self.nl(data)
|
||||
return self.sendj({'error':'not_found','path':path},404)
|
||||
except Exception as e:
|
||||
monitor.record_failure('do_POST', str(e), {'path':path})
|
||||
return self.sendj({'error':str(e)},500)
|
||||
def branch(self,data):
|
||||
a=self.need('branch:create');
|
||||
if not a: return
|
||||
b=checkout(data.get('branch'), data.get('base','main'), True); meta={'type':'branch','branch':b,'base':data.get('base','main'),'created_by':a['account'],'created_at':now(),'direct_main_push':False,'owner_approval_required':True}
|
||||
r=write_json(f'patches/{safe(b).replace("/","_")}.branch.json', meta); c=commit([r], data.get('message') or f'chore(chatgpt): create {b}'); audit(a['account'],'branch',b,meta); self.sendj({'status':'ok','branch':b,'commit':c})
|
||||
def file(self,data):
|
||||
a=self.need('file:write');
|
||||
if not a: return
|
||||
b=checkout(data.get('branch'), data.get('base','main'), bool(data.get('create_branch'))); r=write_file(data.get('path'), data.get('content','')); c=commit([r], data.get('message') or f'chore(chatgpt): update {r}'); audit(a['account'],'file',r,{'branch':b,'commit':c}); self.sendj({'status':'committed','branch':b,'path':r,'commit':c})
|
||||
def commit(self,data):
|
||||
a=self.need('commit:create');
|
||||
if not a: return
|
||||
b=checkout(data.get('branch') or sh(['rev-parse','--abbrev-ref','HEAD'])['stdout'], data.get('base','main'), False); paths=data.get('paths') or ['.']; paths=[paths] if isinstance(paths,str) else paths; c=commit(paths, data.get('message') or 'chore(chatgpt): commit changes'); audit(a['account'],'commit',b,{'paths':paths,'commit':c}); self.sendj({'status':c['status'],'branch':b,'commit':c})
|
||||
def patch(self,data):
|
||||
a=self.need('patch:create');
|
||||
if not a: return
|
||||
name=data.get('patch_name') or data.get('name') or uid('patch'); b=checkout(data.get('branch') or f'patch/{safe(name)}', data.get('base','main'), True); meta={'id':uid('patch'),'patch_name':name,'branch':b,'risk':data.get('risk','medium'),'description':data.get('description',''),'created_by':a['account'],'created_at':now()}
|
||||
r=write_json(f'patches/{safe(name)}.json', meta); c=commit([r], data.get('message') or f'chore(chatgpt): create patch {name}'); audit(a['account'],'patch',name,meta); self.sendj({'status':'patch_branch_created','patch':meta,'commit':c})
|
||||
def tag(self,data):
|
||||
a=self.need('tag:create');
|
||||
if not a: return
|
||||
ver=data.get('version') or data.get('tag')
|
||||
if not re.match(r'^V\d+R\d+C\d+(-[A-Za-z0-9_.-]+)?$', ver or ''): return self.sendj({'error':'version must match VxRxCx'},400)
|
||||
if data.get('confirm') != True:
|
||||
self.sendj({'status':'confirm_required','operation':'tag','hint':'This operation creates a Git tag. Confirm to proceed.','x-openai-isConsequential':True}, 428)
|
||||
return
|
||||
if a['role'] != 'owner' or not data.get('execute'):
|
||||
b=checkout(data.get('branch') or f'request/tag-{safe(ver)}', 'main', True); req={'version':ver,'requested_by':a['account'],'approval_required':True,'created_at':now()}; r=write_json(f'baseline/tag_requests/{safe(ver)}.json', req); c=commit([r], f'chore(chatgpt): request tag {ver}'); return self.sendj({'status':'approval_required','branch':b,'commit':c})
|
||||
out=sh(['tag','-a',ver,'-m',data.get('message') or f'release {ver}'], True); self.sendj({'status':'tagged','version':ver,'git':out,'sync':sync_tag(ver)})
|
||||
def rollback(self,data):
|
||||
a=self.need('rollback:request');
|
||||
if not a: return
|
||||
target=data.get('target_version') or data.get('version') or data.get('target')
|
||||
if not target: return self.sendj({'error':'target_version required'},400)
|
||||
b=checkout(data.get('branch') or f'request/rollback-{safe(target)}-{uid("rb")}', 'main', True); req={'id':uid('rollback'),'target_version':target,'reason':data.get('reason',''),'requested_by':a['account'],'approval_required':True,'execute':False,'created_at':now()}; r=write_json(f'baseline/rollback_requests/{safe(req["id"])}.json', req); c=commit([r], f'chore(chatgpt): request rollback {target}'); self.sendj({'status':'rollback_request_committed','branch':b,'request':req,'commit':c})
|
||||
def model(self,data):
|
||||
a=self.need('model:write');
|
||||
if not a: return
|
||||
name=data.get('model_name') or data.get('model'); base=(data.get('base_url') or '').rstrip('/'); prov=data.get('provider_name') or 'custom-provider'
|
||||
if not name or not base: return self.sendj({'error':'provider_name/base_url/model_name required'},400)
|
||||
owner=safe(data.get('username') or a['account']); secret_ref=data.get('api_key_secret_ref') or data.get('secret_ref')
|
||||
if data.get('api_key'):
|
||||
d=SECRETS/'llm'/owner; d.mkdir(parents=True, exist_ok=True); sid=uid('llmkey'); p=d/f'{sid}.secret'; p.write_text(str(data['api_key']), encoding='utf-8'); os.chmod(p,0o600); secret_ref=f'secret://llm/{owner}/{sid}'
|
||||
if not secret_ref: return self.sendj({'error':'api_key_secret_ref or api_key required'},400)
|
||||
mid=data.get('model_id') or f'custom.{owner}.{hashlib.sha1((prov+base+name).encode()).hexdigest()[:10]}'; b=checkout(data.get('branch') or f'patch/model-{safe(mid)}','main',True)
|
||||
doc=sanitize({k:v for k,v in data.items() if k!='api_key'}); doc.update({'model_id':mid,'provider_name':prov,'base_url':base,'model_name':name,'api_key_secret_ref':secret_ref,'created_by':a['account'],'created_at':now()})
|
||||
if data.get('confirm') != True:
|
||||
self.sendj({'status':'confirm_required','operation':'model','hint':'This operation registers a new model. Confirm to proceed.','x-openai-isConsequential':True}, 428)
|
||||
return
|
||||
r=write_json(f'config/model_registry/chatgpt/{safe(mid)}.json', doc); c=commit([r], data.get('message') or f'chore(chatgpt): register model {name}'); audit(a['account'],'model',mid,doc); self.sendj({'status':'model_registered','branch':b,'model_id':mid,'secret_ref':secret_ref,'commit':c})
|
||||
def model_priority(self,data):
|
||||
a=self.need('model:priority');
|
||||
if not a: return
|
||||
if data.get('confirm') != True:
|
||||
self.sendj({'status':'confirm_required','operation':'model-priority','hint':'This operation updates model priority. Confirm to proceed.','x-openai-isConsequential':True}, 428)
|
||||
return
|
||||
user=safe(data.get('username') or a['account']); b=checkout(data.get('branch') or f'patch/model-priority-{user}', 'main', True); doc={'username':user,'items':data.get('items') or data.get('priority') or [],'updated_by':a['account'],'updated_at':now()}; r=write_json(f'config/model_priority/{user}.json', doc); c=commit([r], data.get('message') or f'chore(chatgpt): update model priority {user}'); audit(a['account'],'model-priority',user,doc); self.sendj({'status':'updated','branch':b,'priority':doc,'commit':c})
|
||||
def nl(self,data):
|
||||
a=self.need('nl:plan');
|
||||
if not a: return
|
||||
text=data.get('text',''); low=text.lower(); intent='unknown'; plan=[]
|
||||
if 'rollback' in low or '回滚' in text: intent='rollback_request'; plan=['commit rollback request','wait owner/admin approval']
|
||||
elif 'diff' in low or '对比' in text: intent='diff'; plan=['call GET /api/diff']
|
||||
elif 'model' in low or '模型' in text: intent='model_change'; plan=['use POST /api/model or PUT /api/model-priority','commit metadata']
|
||||
elif 'patch' in low or '补丁' in text: intent='patch_create'; plan=['call POST /api/patch']
|
||||
b=checkout(data.get('branch') or f'patch/nl-{uid("nl")}', 'main', True); doc={'id':uid('nl'),'text':text,'intent':intent,'action_plan':plan,'execute':False,'owner_approval_required':True,'created_by':a['account'],'created_at':now()}; r=write_json(f'patches/nl/{safe(doc["id"])}.json', doc); c=commit([r], f'chore(chatgpt): record nl plan {intent}'); self.sendj({'status':'planned','branch':b,'request':doc,'commit':c})
|
||||
|
||||
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,108 @@
|
||||
"""Autonomous self-loop orchestration primitives.
|
||||
|
||||
Goal: detect gaps, classify risk, plan remediation, run checks, and emit an
|
||||
approval-aware report without storing secrets or bypassing owner governance.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SelfLoopSignal:
|
||||
name: str
|
||||
status: str
|
||||
severity: str = "info"
|
||||
detail: str = ""
|
||||
|
||||
def ok(self) -> bool:
|
||||
return self.status in {"ok", "done", "pass"}
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RemediationStep:
|
||||
id: str
|
||||
title: str
|
||||
action: str
|
||||
risk: str = "low"
|
||||
requires_owner_approval: bool = False
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SelfLoopReport:
|
||||
id: str
|
||||
status: str
|
||||
signals: list[SelfLoopSignal] = field(default_factory=list)
|
||||
remediation: list[RemediationStep] = field(default_factory=list)
|
||||
created_at: str = field(default_factory=utc_now)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"id": self.id,
|
||||
"status": self.status,
|
||||
"signals": [s.to_dict() for s in self.signals],
|
||||
"remediation": [r.to_dict() for r in self.remediation],
|
||||
"created_at": self.created_at,
|
||||
}
|
||||
|
||||
|
||||
class AutonomousSelfLoop:
|
||||
def __init__(self, owner_approval_required: bool = True):
|
||||
self.owner_approval_required = owner_approval_required
|
||||
|
||||
def probe(self, runtime: dict[str, Any], baseline: dict[str, Any]) -> list[SelfLoopSignal]:
|
||||
return [
|
||||
SelfLoopSignal("runtime_active", "ok" if runtime.get("status") == "active" else "fail", "critical"),
|
||||
SelfLoopSignal("git_patch_only", "ok" if baseline.get("update_mode") == "git_patch_only" else "fail", "high"),
|
||||
SelfLoopSignal("no_direct_main_push", "ok" if not baseline.get("codex_gitops", {}).get("direct_main_push", True) else "fail", "high"),
|
||||
SelfLoopSignal("autonomous_recovery_loop", "ok" if baseline.get("features", {}).get("autonomous_recovery_loop") else "fail", "high"),
|
||||
SelfLoopSignal("portal_gitops_dashboard", "ok" if baseline.get("features", {}).get("portal_gitops_dashboard") else "fail", "medium"),
|
||||
]
|
||||
|
||||
def plan(self, signals: list[SelfLoopSignal]) -> list[RemediationStep]:
|
||||
steps: list[RemediationStep] = []
|
||||
failed = {s.name for s in signals if not s.ok()}
|
||||
if "runtime_active" in failed:
|
||||
steps.append(RemediationStep("restart-runtime", "恢复 GitOps Runtime", "request-runtime-recovery", "high", True))
|
||||
if "git_patch_only" in failed:
|
||||
steps.append(RemediationStep("enforce-git-patch", "恢复 git_patch_only 更新模式", "commit-policy-patch", "high", True))
|
||||
if "no_direct_main_push" in failed:
|
||||
steps.append(RemediationStep("disable-direct-main", "禁止直接推送 main", "commit-security-policy", "high", True))
|
||||
if "autonomous_recovery_loop" in failed:
|
||||
steps.append(RemediationStep("enable-recovery-loop", "启用自动恢复闭环", "commit-runtime-config", "medium", True))
|
||||
if "portal_gitops_dashboard" in failed:
|
||||
steps.append(RemediationStep("enable-portal-dashboard", "启用 Portal GitOps 驾驶舱", "commit-portal-config", "medium", True))
|
||||
if not steps:
|
||||
steps.append(RemediationStep("continue-implementation", "继续补齐 Router/UI/OpenAPI/验收", "create-low-risk-patch", "low", False))
|
||||
return steps
|
||||
|
||||
def evaluate(self, runtime: dict[str, Any], baseline: dict[str, Any]) -> SelfLoopReport:
|
||||
signals = self.probe(runtime, baseline)
|
||||
remediation = self.plan(signals)
|
||||
status = "done" if all(s.ok() for s in signals) and not any(r.requires_owner_approval for r in remediation) else "partial"
|
||||
return SelfLoopReport("self-loop-report", status, signals, remediation)
|
||||
|
||||
def acceptance(self, report: SelfLoopReport) -> dict[str, Any]:
|
||||
approval_blocked = any(step.requires_owner_approval for step in report.remediation)
|
||||
return {
|
||||
"status": report.status,
|
||||
"can_unattended_continue": report.status == "done" and not approval_blocked,
|
||||
"owner_approval_required": approval_blocked or self.owner_approval_required,
|
||||
"report": report.to_dict(),
|
||||
}
|
||||
|
||||
|
||||
__all__ = ["SelfLoopSignal", "RemediationStep", "SelfLoopReport", "AutonomousSelfLoop"]
|
||||
@@ -0,0 +1,192 @@
|
||||
"""Failure memory for autonomous self-loop learning.
|
||||
|
||||
The memory keeps sanitized failure fingerprints, recurrence counters, and
|
||||
remediation hints. It is intentionally in-memory and serializable; production
|
||||
adapters can persist the exported dict to SQL/object storage without changing the
|
||||
learning contract. Secrets and raw tokens must never be stored here.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from hashlib import sha256
|
||||
from typing import Any
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
def _clean(value: str | None) -> str:
|
||||
if not value:
|
||||
return "unknown"
|
||||
cleaned = value.replace("\n", " ").replace("\r", " ").strip().lower()
|
||||
secret_markers = ("token", "password", "secret", "api_key", "private key")
|
||||
for marker in secret_markers:
|
||||
cleaned = cleaned.replace(marker, "redacted")
|
||||
return cleaned[:500]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FailureSignature:
|
||||
component: str
|
||||
reason: str
|
||||
fingerprint: str
|
||||
severity: str = "medium"
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
@dataclass
|
||||
class FailureRecord:
|
||||
signature: FailureSignature
|
||||
count: int = 0
|
||||
first_seen: str = field(default_factory=utc_now)
|
||||
last_seen: str = field(default_factory=utc_now)
|
||||
last_job_id: str | None = None
|
||||
examples: list[str] = field(default_factory=list)
|
||||
remediation_hints: list[str] = field(default_factory=list)
|
||||
resolved_count: int = 0
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"signature": self.signature.to_dict(),
|
||||
"count": self.count,
|
||||
"first_seen": self.first_seen,
|
||||
"last_seen": self.last_seen,
|
||||
"last_job_id": self.last_job_id,
|
||||
"examples": list(self.examples),
|
||||
"remediation_hints": list(self.remediation_hints),
|
||||
"resolved_count": self.resolved_count,
|
||||
"recurrence_score": self.recurrence_score(),
|
||||
}
|
||||
|
||||
def recurrence_score(self) -> float:
|
||||
if self.count <= 1:
|
||||
return 0.0
|
||||
unresolved = max(self.count - self.resolved_count, 0)
|
||||
return min(1.0, unresolved / max(self.count, 1))
|
||||
|
||||
|
||||
class FailureMemory:
|
||||
def __init__(self) -> None:
|
||||
self.records: dict[str, FailureRecord] = {}
|
||||
|
||||
def fingerprint(self, component: str, reason: str, detail: str | None = None) -> str:
|
||||
base = "::".join([_clean(component), _clean(reason), _clean(detail)])
|
||||
return sha256(base.encode("utf-8")).hexdigest()[:16]
|
||||
|
||||
def signature(self, component: str, reason: str, detail: str | None = None, severity: str = "medium") -> FailureSignature:
|
||||
return FailureSignature(
|
||||
component=_clean(component),
|
||||
reason=_clean(reason),
|
||||
fingerprint=self.fingerprint(component, reason, detail),
|
||||
severity=severity,
|
||||
)
|
||||
|
||||
def record_failure(
|
||||
self,
|
||||
component: str,
|
||||
reason: str,
|
||||
detail: str | None = None,
|
||||
severity: str = "medium",
|
||||
job_id: str | None = None,
|
||||
remediation_hint: str | None = None,
|
||||
) -> FailureRecord:
|
||||
sig = self.signature(component, reason, detail, severity)
|
||||
record = self.records.get(sig.fingerprint)
|
||||
if record is None:
|
||||
record = FailureRecord(signature=sig)
|
||||
self.records[sig.fingerprint] = record
|
||||
record.count += 1
|
||||
record.last_seen = utc_now()
|
||||
record.last_job_id = job_id
|
||||
if detail:
|
||||
example = _clean(detail)
|
||||
if example not in record.examples:
|
||||
record.examples.append(example)
|
||||
record.examples = record.examples[-5:]
|
||||
if remediation_hint and remediation_hint not in record.remediation_hints:
|
||||
record.remediation_hints.append(remediation_hint)
|
||||
record.remediation_hints = record.remediation_hints[-5:]
|
||||
return record
|
||||
|
||||
def record_from_job(self, job: dict[str, Any]) -> FailureRecord | None:
|
||||
status = job.get("status")
|
||||
if status in {"completed", "waiting_owner"}:
|
||||
return None
|
||||
reason = job.get("last_error") or status or "unknown_failure"
|
||||
return self.record_failure(
|
||||
component="self_loop_job",
|
||||
reason=reason,
|
||||
detail=str(job.get("result") or job.get("last_error") or ""),
|
||||
severity="high" if status == "failed" else "medium",
|
||||
job_id=job.get("id"),
|
||||
remediation_hint="inspect acceptance report and regenerate patch plan",
|
||||
)
|
||||
|
||||
def mark_resolved(self, fingerprint: str) -> bool:
|
||||
record = self.records.get(fingerprint)
|
||||
if not record:
|
||||
return False
|
||||
record.resolved_count += 1
|
||||
record.last_seen = utc_now()
|
||||
return True
|
||||
|
||||
def recurrence_score(self, fingerprint: str) -> float:
|
||||
record = self.records.get(fingerprint)
|
||||
return record.recurrence_score() if record else 0.0
|
||||
|
||||
def top_failures(self, limit: int = 5) -> list[dict[str, Any]]:
|
||||
ranked = sorted(
|
||||
self.records.values(),
|
||||
key=lambda record: (record.recurrence_score(), record.count, record.last_seen),
|
||||
reverse=True,
|
||||
)
|
||||
return [record.to_dict() for record in ranked[:limit]]
|
||||
|
||||
def suggest_remediation(self, fingerprint: str) -> list[str]:
|
||||
record = self.records.get(fingerprint)
|
||||
if not record:
|
||||
return []
|
||||
if record.remediation_hints:
|
||||
return list(record.remediation_hints)
|
||||
reason = record.signature.reason
|
||||
if "runtime" in reason:
|
||||
return ["run runtime health probe", "request owner-gated runtime recovery if probe fails"]
|
||||
if "acceptance" in reason or "test" in reason:
|
||||
return ["rerun acceptance pipeline", "generate targeted low-risk patch"]
|
||||
if "permission" in reason or "forbidden" in reason:
|
||||
return ["verify GitOps branch policy", "request owner review for permission drift"]
|
||||
return ["classify failure", "generate remediation plan", "keep owner gate for non-low-risk changes"]
|
||||
|
||||
def export(self) -> dict[str, Any]:
|
||||
return {
|
||||
"records": {fingerprint: record.to_dict() for fingerprint, record in self.records.items()},
|
||||
"top_failures": self.top_failures(),
|
||||
}
|
||||
|
||||
def load(self, data: dict[str, Any]) -> None:
|
||||
self.records.clear()
|
||||
for fingerprint, raw in data.get("records", {}).items():
|
||||
sig_raw = raw.get("signature", {})
|
||||
signature = FailureSignature(
|
||||
component=sig_raw.get("component", "unknown"),
|
||||
reason=sig_raw.get("reason", "unknown"),
|
||||
fingerprint=sig_raw.get("fingerprint", fingerprint),
|
||||
severity=sig_raw.get("severity", "medium"),
|
||||
)
|
||||
record = FailureRecord(signature=signature)
|
||||
record.count = int(raw.get("count", 0))
|
||||
record.first_seen = raw.get("first_seen", utc_now())
|
||||
record.last_seen = raw.get("last_seen", utc_now())
|
||||
record.last_job_id = raw.get("last_job_id")
|
||||
record.examples = list(raw.get("examples", []))[-5:]
|
||||
record.remediation_hints = list(raw.get("remediation_hints", []))[-5:]
|
||||
record.resolved_count = int(raw.get("resolved_count", 0))
|
||||
self.records[fingerprint] = record
|
||||
|
||||
|
||||
__all__ = ["FailureSignature", "FailureRecord", "FailureMemory"]
|
||||
@@ -0,0 +1,98 @@
|
||||
"""Persistence adapters for FailureMemory.
|
||||
|
||||
Stores sanitized failure memory snapshots. The adapter only persists exported
|
||||
FailureMemory dictionaries; it does not store secrets or raw runtime credentials.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .failure_memory import FailureMemory
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MemoryStoreStatus:
|
||||
status: str
|
||||
path: str
|
||||
records: int
|
||||
updated_at: str = field(default_factory=utc_now)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
class JsonFailureMemoryStore:
|
||||
def __init__(self, path: str | Path):
|
||||
self.path = Path(path)
|
||||
|
||||
def save(self, memory: FailureMemory) -> MemoryStoreStatus:
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
data = memory.export()
|
||||
self.path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
return MemoryStoreStatus("saved", str(self.path), len(data.get("records", {})))
|
||||
|
||||
def load(self) -> FailureMemory:
|
||||
memory = FailureMemory()
|
||||
if not self.path.exists():
|
||||
return memory
|
||||
memory.load(json.loads(self.path.read_text(encoding="utf-8")))
|
||||
return memory
|
||||
|
||||
|
||||
class SQLiteFailureMemoryStore:
|
||||
def __init__(self, path: str | Path):
|
||||
self.path = Path(path)
|
||||
|
||||
def _connect(self):
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
conn = sqlite3.connect(str(self.path))
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS self_loop_failure_memory (
|
||||
fingerprint TEXT PRIMARY KEY,
|
||||
payload_json TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
return conn
|
||||
|
||||
def save(self, memory: FailureMemory) -> MemoryStoreStatus:
|
||||
data = memory.export().get("records", {})
|
||||
with self._connect() as conn:
|
||||
for fingerprint, payload in data.items():
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO self_loop_failure_memory (fingerprint, payload_json, updated_at)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT(fingerprint) DO UPDATE SET
|
||||
payload_json = excluded.payload_json,
|
||||
updated_at = excluded.updated_at
|
||||
""",
|
||||
(fingerprint, json.dumps(payload, ensure_ascii=False), utc_now()),
|
||||
)
|
||||
return MemoryStoreStatus("saved", str(self.path), len(data))
|
||||
|
||||
def load(self) -> FailureMemory:
|
||||
memory = FailureMemory()
|
||||
if not self.path.exists():
|
||||
return memory
|
||||
records: dict[str, Any] = {}
|
||||
with self._connect() as conn:
|
||||
for fingerprint, payload_json in conn.execute("SELECT fingerprint, payload_json FROM self_loop_failure_memory"):
|
||||
records[fingerprint] = json.loads(payload_json)
|
||||
memory.load({"records": records})
|
||||
return memory
|
||||
|
||||
|
||||
__all__ = ["MemoryStoreStatus", "JsonFailureMemoryStore", "SQLiteFailureMemoryStore"]
|
||||
@@ -0,0 +1,59 @@
|
||||
"""GitOps commit planning adapter for self-loop patches.
|
||||
|
||||
Creates commit intents and validates governance. Actual repository writes remain
|
||||
through the controlled GitOps Actions layer.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from .self_loop_patch_executor import PatchBundle
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CommitIntent:
|
||||
branch: str
|
||||
message: str
|
||||
files: tuple[str, ...]
|
||||
direct_main_push: bool
|
||||
owner_approval_required: bool
|
||||
created_at: str = field(default_factory=utc_now)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
class GitOpsCommitExecutor:
|
||||
"""Produces safe commit intents instead of bypassing GitOps controls."""
|
||||
|
||||
def prepare(self, bundle: PatchBundle) -> CommitIntent:
|
||||
if bundle.direct_main_push:
|
||||
raise ValueError("direct main push is forbidden")
|
||||
if not bundle.branch.startswith("patch/"):
|
||||
raise ValueError("only patch branches are allowed")
|
||||
return CommitIntent(
|
||||
branch=bundle.branch,
|
||||
message=f"Apply self-loop patch: {bundle.goal}",
|
||||
files=tuple(file.path for file in bundle.files),
|
||||
direct_main_push=False,
|
||||
owner_approval_required=bundle.owner_approval_required,
|
||||
)
|
||||
|
||||
def commit_result(self, intent: CommitIntent, revision: str | None = None) -> dict[str, Any]:
|
||||
return {
|
||||
"status": "ready" if not intent.owner_approval_required else "waiting_owner",
|
||||
"branch": intent.branch,
|
||||
"revision": revision,
|
||||
"commit": intent.to_dict(),
|
||||
"sync_required": True,
|
||||
}
|
||||
|
||||
|
||||
__all__ = ["CommitIntent", "GitOpsCommitExecutor"]
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Portal operations API domain layer.
|
||||
No secrets. Designed for user/group_admin/owner portal integration.
|
||||
"""
|
||||
|
||||
ROLES = ("user", "group_admin", "owner")
|
||||
|
||||
|
||||
class PortalOpsAPI:
|
||||
def dashboard(self, role):
|
||||
if role not in ROLES:
|
||||
raise ValueError("invalid role")
|
||||
return {
|
||||
"role": role,
|
||||
"modules": self.modules(role),
|
||||
"status": "ok"
|
||||
}
|
||||
|
||||
def modules(self, role):
|
||||
common = ["profile", "alerts", "approvals", "audit"]
|
||||
if role == "user":
|
||||
return common + ["tasks", "usage"]
|
||||
if role == "group_admin":
|
||||
return common + ["group_users", "group_quota"]
|
||||
return common + ["system", "gitops", "runtime", "security", "models"]
|
||||
|
||||
def approval(self, request_id, action):
|
||||
return {"request_id": request_id, "action": action, "state": "recorded"}
|
||||
|
||||
def audit_event(self, actor, action):
|
||||
return {"actor": actor, "action": action, "recorded": True}
|
||||
|
||||
def alert(self, alert_id, level):
|
||||
return {"id": alert_id, "level": level, "state": "open"}
|
||||
@@ -0,0 +1,493 @@
|
||||
"""Three-role portal RBAC, workflow, alert, audit, and owner cockpit services.
|
||||
|
||||
This module is intentionally framework-neutral so it can be used by FastAPI routes,
|
||||
unit tests, CLI checks, and future Hermes/OpenClaw agents without importing runtime
|
||||
secrets or web framework globals.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Iterable
|
||||
from uuid import uuid4
|
||||
|
||||
ROLE_USER = "user"
|
||||
ROLE_GROUP_ADMIN = "group_admin"
|
||||
ROLE_OWNER = "owner"
|
||||
ROLES = (ROLE_USER, ROLE_GROUP_ADMIN, ROLE_OWNER)
|
||||
|
||||
SCOPE_SELF = "self"
|
||||
SCOPE_GROUP = "group"
|
||||
SCOPE_GLOBAL = "global"
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PortalContext:
|
||||
user_id: str
|
||||
role: str = ROLE_USER
|
||||
group_id: str | None = None
|
||||
display_name: str | None = None
|
||||
|
||||
def natural_scope(self) -> str:
|
||||
if self.role == ROLE_OWNER:
|
||||
return SCOPE_GLOBAL
|
||||
if self.role == ROLE_GROUP_ADMIN:
|
||||
return SCOPE_GROUP
|
||||
return SCOPE_SELF
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PortalResource:
|
||||
scope: str
|
||||
owner_user_id: str | None = None
|
||||
group_id: str | None = None
|
||||
resource_type: str = "portal"
|
||||
resource_id: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RBACDecision:
|
||||
allowed: bool
|
||||
reason: str
|
||||
role: str
|
||||
scope: str
|
||||
action: str
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ApprovalRequest:
|
||||
id: str
|
||||
request_type: str
|
||||
requester_id: str
|
||||
scope: str
|
||||
group_id: str | None = None
|
||||
target: str | None = None
|
||||
payload: dict[str, Any] = field(default_factory=dict)
|
||||
state: str = "pending"
|
||||
decided_by: str | None = None
|
||||
decision_reason: str | None = None
|
||||
created_at: str = field(default_factory=utc_now)
|
||||
updated_at: str = field(default_factory=utc_now)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AlertRecord:
|
||||
id: str
|
||||
level: str
|
||||
message: str
|
||||
scope: str
|
||||
group_id: str | None = None
|
||||
owner_user_id: str | None = None
|
||||
source: str = "portal"
|
||||
state: str = "open"
|
||||
created_at: str = field(default_factory=utc_now)
|
||||
updated_at: str = field(default_factory=utc_now)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AuditEvent:
|
||||
id: str
|
||||
actor_id: str
|
||||
action: str
|
||||
scope: str
|
||||
group_id: str | None = None
|
||||
resource_type: str = "portal"
|
||||
resource_id: str | None = None
|
||||
outcome: str = "success"
|
||||
detail: dict[str, Any] = field(default_factory=dict)
|
||||
created_at: str = field(default_factory=utc_now)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PortalModule:
|
||||
key: str
|
||||
title: str
|
||||
route: str
|
||||
permission: str
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
ROLE_PERMISSIONS: dict[str, set[str]] = {
|
||||
ROLE_USER: {
|
||||
"portal.dashboard.read:self",
|
||||
"portal.profile.read:self",
|
||||
"portal.tasks.read:self",
|
||||
"portal.usage.read:self",
|
||||
"portal.alerts.read:self",
|
||||
"portal.approvals.create:self",
|
||||
"portal.approvals.read:self",
|
||||
"portal.audit.read:self",
|
||||
},
|
||||
ROLE_GROUP_ADMIN: {
|
||||
"portal.dashboard.read:self",
|
||||
"portal.dashboard.read:group",
|
||||
"portal.profile.read:self",
|
||||
"portal.tasks.read:self",
|
||||
"portal.usage.read:self",
|
||||
"portal.alerts.read:self",
|
||||
"portal.alerts.read:group",
|
||||
"portal.approvals.create:self",
|
||||
"portal.approvals.read:self",
|
||||
"portal.approvals.read:group",
|
||||
"portal.approvals.decide:group",
|
||||
"portal.audit.read:self",
|
||||
"portal.audit.read:group",
|
||||
"portal.group.users.read:group",
|
||||
"portal.group.quota.read:group",
|
||||
"portal.group.quota.write:group",
|
||||
},
|
||||
ROLE_OWNER: {
|
||||
"portal.*:*",
|
||||
},
|
||||
}
|
||||
|
||||
ROLE_MODULES: dict[str, tuple[PortalModule, ...]] = {
|
||||
ROLE_USER: (
|
||||
PortalModule("profile", "个人中心", "/portal/user/profile", "portal.profile.read:self"),
|
||||
PortalModule("tasks", "任务", "/portal/user/tasks", "portal.tasks.read:self"),
|
||||
PortalModule("usage", "用量", "/portal/user/usage", "portal.usage.read:self"),
|
||||
PortalModule("alerts", "告警", "/portal/user/alerts", "portal.alerts.read:self"),
|
||||
PortalModule("approvals", "审批", "/portal/user/approvals", "portal.approvals.read:self"),
|
||||
PortalModule("audit", "审计", "/portal/user/audit", "portal.audit.read:self"),
|
||||
),
|
||||
ROLE_GROUP_ADMIN: (
|
||||
PortalModule("group_users", "组用户", "/portal/group_admin/users", "portal.group.users.read:group"),
|
||||
PortalModule("group_quota", "组配额", "/portal/group_admin/quota", "portal.group.quota.read:group"),
|
||||
PortalModule("alerts", "组告警", "/portal/group_admin/alerts", "portal.alerts.read:group"),
|
||||
PortalModule("approvals", "组审批", "/portal/group_admin/approvals", "portal.approvals.read:group"),
|
||||
PortalModule("audit", "组审计", "/portal/group_admin/audit", "portal.audit.read:group"),
|
||||
),
|
||||
ROLE_OWNER: (
|
||||
PortalModule("system", "系统驾驶舱", "/portal/owner/system", "portal.system.read:global"),
|
||||
PortalModule("gitops", "GitOps", "/portal/owner/gitops", "portal.gitops.read:global"),
|
||||
PortalModule("runtime", "运行时", "/portal/owner/runtime", "portal.runtime.read:global"),
|
||||
PortalModule("security", "安全", "/portal/owner/security", "portal.security.read:global"),
|
||||
PortalModule("models", "模型", "/portal/owner/models", "portal.models.read:global"),
|
||||
PortalModule("approvals", "全局审批", "/portal/owner/approvals", "portal.approvals.read:global"),
|
||||
PortalModule("alerts", "全局告警", "/portal/owner/alerts", "portal.alerts.read:global"),
|
||||
PortalModule("audit", "全局审计", "/portal/owner/audit", "portal.audit.read:global"),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class PortalRBACService:
|
||||
"""Enforces user=self, group_admin=group, owner=global access."""
|
||||
|
||||
def __init__(self, permissions: dict[str, set[str]] | None = None):
|
||||
self.permissions = permissions or ROLE_PERMISSIONS
|
||||
|
||||
def authorize(self, ctx: PortalContext, action: str, resource: PortalResource | None = None) -> RBACDecision:
|
||||
if ctx.role not in ROLES:
|
||||
return RBACDecision(False, "unknown_role", ctx.role, SCOPE_SELF, action)
|
||||
|
||||
target = resource or PortalResource(scope=ctx.natural_scope(), owner_user_id=ctx.user_id, group_id=ctx.group_id)
|
||||
if ctx.role == ROLE_OWNER:
|
||||
return RBACDecision(True, "owner_global_allow", ctx.role, target.scope, action)
|
||||
|
||||
if target.scope == SCOPE_GLOBAL:
|
||||
return RBACDecision(False, "global_scope_requires_owner", ctx.role, target.scope, action)
|
||||
|
||||
if ctx.role == ROLE_USER:
|
||||
if target.scope != SCOPE_SELF or target.owner_user_id not in (None, ctx.user_id):
|
||||
return RBACDecision(False, "user_limited_to_self", ctx.role, target.scope, action)
|
||||
return self._permission_decision(ctx.role, action, SCOPE_SELF)
|
||||
|
||||
if ctx.role == ROLE_GROUP_ADMIN:
|
||||
if target.scope == SCOPE_SELF and target.owner_user_id in (None, ctx.user_id):
|
||||
return self._permission_decision(ctx.role, action, SCOPE_SELF)
|
||||
if target.scope == SCOPE_GROUP and ctx.group_id and target.group_id == ctx.group_id:
|
||||
return self._permission_decision(ctx.role, action, SCOPE_GROUP)
|
||||
return RBACDecision(False, "group_admin_limited_to_own_group", ctx.role, target.scope, action)
|
||||
|
||||
return RBACDecision(False, "deny_by_default", ctx.role, target.scope, action)
|
||||
|
||||
def visible_modules(self, ctx: PortalContext) -> list[dict[str, Any]]:
|
||||
modules = ROLE_MODULES.get(ctx.role, ())
|
||||
visible: list[dict[str, Any]] = []
|
||||
for module in modules:
|
||||
action, scope = module.permission.split(":", 1)
|
||||
resource = PortalResource(scope=scope, owner_user_id=ctx.user_id, group_id=ctx.group_id)
|
||||
if self.authorize(ctx, action, resource).allowed:
|
||||
visible.append(module.to_dict())
|
||||
return visible
|
||||
|
||||
def _permission_decision(self, role: str, action: str, scope: str) -> RBACDecision:
|
||||
permissions = self.permissions.get(role, set())
|
||||
if "portal.*:*" in permissions or f"{action}:{scope}" in permissions:
|
||||
return RBACDecision(True, "permission_match", role, scope, action)
|
||||
return RBACDecision(False, "permission_missing", role, scope, action)
|
||||
|
||||
|
||||
class PortalRepository:
|
||||
"""Small deterministic repository used for domain tests and route integration.
|
||||
|
||||
Production deployments can replace this repository with a SQL-backed adapter while
|
||||
preserving the service contract.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.approvals: dict[str, ApprovalRequest] = {}
|
||||
self.alerts: dict[str, AlertRecord] = {}
|
||||
self.audit_events: list[AuditEvent] = []
|
||||
|
||||
def create_approval(self, ctx: PortalContext, request_type: str, payload: dict[str, Any] | None = None, target: str | None = None) -> ApprovalRequest:
|
||||
approval = ApprovalRequest(
|
||||
id=f"apr_{uuid4().hex[:12]}",
|
||||
request_type=request_type,
|
||||
requester_id=ctx.user_id,
|
||||
scope=ctx.natural_scope(),
|
||||
group_id=ctx.group_id,
|
||||
target=target,
|
||||
payload=payload or {},
|
||||
)
|
||||
self.approvals[approval.id] = approval
|
||||
self.append_audit(ctx, "portal.approvals.create", approval.scope, approval.group_id, "approval", approval.id)
|
||||
return approval
|
||||
|
||||
def decide_approval(self, ctx: PortalContext, approval_id: str, decision: str, reason: str | None = None) -> ApprovalRequest:
|
||||
if decision not in {"approved", "rejected"}:
|
||||
raise ValueError("decision must be approved or rejected")
|
||||
approval = self.approvals[approval_id]
|
||||
approval.state = decision
|
||||
approval.decided_by = ctx.user_id
|
||||
approval.decision_reason = reason
|
||||
approval.updated_at = utc_now()
|
||||
self.append_audit(ctx, f"portal.approvals.{decision}", approval.scope, approval.group_id, "approval", approval.id)
|
||||
return approval
|
||||
|
||||
def create_alert(self, ctx: PortalContext, level: str, message: str, scope: str | None = None, source: str = "portal") -> AlertRecord:
|
||||
if level not in {"info", "warning", "high", "critical"}:
|
||||
raise ValueError("invalid alert level")
|
||||
alert_scope = scope or ctx.natural_scope()
|
||||
alert = AlertRecord(
|
||||
id=f"alt_{uuid4().hex[:12]}",
|
||||
level=level,
|
||||
message=message,
|
||||
scope=alert_scope,
|
||||
group_id=ctx.group_id if alert_scope == SCOPE_GROUP else None,
|
||||
owner_user_id=ctx.user_id if alert_scope == SCOPE_SELF else None,
|
||||
source=source,
|
||||
)
|
||||
self.alerts[alert.id] = alert
|
||||
self.append_audit(ctx, "portal.alerts.create", alert.scope, alert.group_id, "alert", alert.id)
|
||||
return alert
|
||||
|
||||
def resolve_alert(self, ctx: PortalContext, alert_id: str) -> AlertRecord:
|
||||
alert = self.alerts[alert_id]
|
||||
alert.state = "resolved"
|
||||
alert.updated_at = utc_now()
|
||||
self.append_audit(ctx, "portal.alerts.resolve", alert.scope, alert.group_id, "alert", alert.id)
|
||||
return alert
|
||||
|
||||
def append_audit(
|
||||
self,
|
||||
ctx: PortalContext,
|
||||
action: str,
|
||||
scope: str | None = None,
|
||||
group_id: str | None = None,
|
||||
resource_type: str = "portal",
|
||||
resource_id: str | None = None,
|
||||
outcome: str = "success",
|
||||
detail: dict[str, Any] | None = None,
|
||||
) -> AuditEvent:
|
||||
event = AuditEvent(
|
||||
id=f"aud_{uuid4().hex[:12]}",
|
||||
actor_id=ctx.user_id,
|
||||
action=action,
|
||||
scope=scope or ctx.natural_scope(),
|
||||
group_id=group_id or ctx.group_id,
|
||||
resource_type=resource_type,
|
||||
resource_id=resource_id,
|
||||
outcome=outcome,
|
||||
detail=detail or {},
|
||||
)
|
||||
self.audit_events.append(event)
|
||||
return event
|
||||
|
||||
def list_approvals(self, ctx: PortalContext) -> list[ApprovalRequest]:
|
||||
return [a for a in self.approvals.values() if self._visible(ctx, a.scope, a.requester_id, a.group_id)]
|
||||
|
||||
def list_alerts(self, ctx: PortalContext) -> list[AlertRecord]:
|
||||
return [a for a in self.alerts.values() if self._visible(ctx, a.scope, a.owner_user_id, a.group_id)]
|
||||
|
||||
def list_audit(self, ctx: PortalContext) -> list[AuditEvent]:
|
||||
return [a for a in self.audit_events if self._visible(ctx, a.scope, a.actor_id, a.group_id)]
|
||||
|
||||
def _visible(self, ctx: PortalContext, scope: str, owner_user_id: str | None, group_id: str | None) -> bool:
|
||||
if ctx.role == ROLE_OWNER:
|
||||
return True
|
||||
if scope == SCOPE_SELF:
|
||||
return owner_user_id in (None, ctx.user_id)
|
||||
if ctx.role == ROLE_GROUP_ADMIN and scope == SCOPE_GROUP:
|
||||
return bool(ctx.group_id and group_id == ctx.group_id)
|
||||
return False
|
||||
|
||||
|
||||
class PortalDashboardService:
|
||||
def __init__(self, repo: PortalRepository | None = None, rbac: PortalRBACService | None = None):
|
||||
self.repo = repo or PortalRepository()
|
||||
self.rbac = rbac or PortalRBACService()
|
||||
|
||||
def dashboard(self, ctx: PortalContext) -> dict[str, Any]:
|
||||
resource = PortalResource(scope=ctx.natural_scope(), owner_user_id=ctx.user_id, group_id=ctx.group_id)
|
||||
decision = self.rbac.authorize(ctx, "portal.dashboard.read", resource)
|
||||
if not decision.allowed:
|
||||
return {"status": "forbidden", "decision": decision.to_dict(), "modules": []}
|
||||
return {
|
||||
"status": "ok",
|
||||
"role": ctx.role,
|
||||
"scope": ctx.natural_scope(),
|
||||
"user_id": ctx.user_id,
|
||||
"group_id": ctx.group_id,
|
||||
"modules": self.rbac.visible_modules(ctx),
|
||||
"counters": {
|
||||
"approvals": len(self.repo.list_approvals(ctx)),
|
||||
"alerts": len([a for a in self.repo.list_alerts(ctx) if a.state == "open"]),
|
||||
"audit_events": len(self.repo.list_audit(ctx)),
|
||||
},
|
||||
}
|
||||
|
||||
def owner_cockpit(self, ctx: PortalContext, runtime: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
decision = self.rbac.authorize(ctx, "portal.system.read", PortalResource(scope=SCOPE_GLOBAL))
|
||||
if not decision.allowed:
|
||||
return {"status": "forbidden", "decision": decision.to_dict()}
|
||||
runtime = runtime or {}
|
||||
return {
|
||||
"status": "ok",
|
||||
"scope": SCOPE_GLOBAL,
|
||||
"runtime": {
|
||||
"service": runtime.get("service", "unknown"),
|
||||
"status": runtime.get("status", "unknown"),
|
||||
"workspace_branch": runtime.get("workspace_branch"),
|
||||
},
|
||||
"gitops": {
|
||||
"mode": runtime.get("mode", "git_patch_only"),
|
||||
"sync_status": runtime.get("sync_status", "unknown"),
|
||||
},
|
||||
"security": {
|
||||
"human_shortlinks": "login_required",
|
||||
"secrets_in_git": False,
|
||||
"direct_main_push": False,
|
||||
},
|
||||
"models": {
|
||||
"custom_llm": bool(runtime.get("custom_llm", True)),
|
||||
"user_priority": bool(runtime.get("user_model_priority", True)),
|
||||
},
|
||||
"ops": {
|
||||
"open_alerts": len([a for a in self.repo.alerts.values() if a.state == "open"]),
|
||||
"pending_approvals": len([a for a in self.repo.approvals.values() if a.state == "pending"]),
|
||||
"audit_events": len(self.repo.audit_events),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class PortalOpsService:
|
||||
def __init__(self, repo: PortalRepository | None = None, rbac: PortalRBACService | None = None):
|
||||
self.repo = repo or PortalRepository()
|
||||
self.rbac = rbac or PortalRBACService()
|
||||
self.dashboard_service = PortalDashboardService(self.repo, self.rbac)
|
||||
|
||||
def check_rbac(self, ctx: PortalContext, action: str, resource: PortalResource | None = None) -> dict[str, Any]:
|
||||
return self.rbac.authorize(ctx, action, resource).to_dict()
|
||||
|
||||
def dashboard(self, ctx: PortalContext) -> dict[str, Any]:
|
||||
return self.dashboard_service.dashboard(ctx)
|
||||
|
||||
def owner_cockpit(self, ctx: PortalContext, runtime: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
return self.dashboard_service.owner_cockpit(ctx, runtime)
|
||||
|
||||
def create_approval(self, ctx: PortalContext, request_type: str, payload: dict[str, Any] | None = None, target: str | None = None) -> dict[str, Any]:
|
||||
decision = self.rbac.authorize(ctx, "portal.approvals.create", PortalResource(scope=ctx.natural_scope(), owner_user_id=ctx.user_id, group_id=ctx.group_id))
|
||||
if not decision.allowed:
|
||||
return {"status": "forbidden", "decision": decision.to_dict()}
|
||||
approval = self.repo.create_approval(ctx, request_type, payload, target)
|
||||
return {"status": "ok", "approval": approval.to_dict()}
|
||||
|
||||
def decide_approval(self, ctx: PortalContext, approval_id: str, decision_value: str, reason: str | None = None) -> dict[str, Any]:
|
||||
approval = self.repo.approvals[approval_id]
|
||||
decision = self.rbac.authorize(ctx, "portal.approvals.decide", PortalResource(scope=approval.scope, group_id=approval.group_id, owner_user_id=approval.requester_id))
|
||||
if not decision.allowed:
|
||||
return {"status": "forbidden", "decision": decision.to_dict()}
|
||||
updated = self.repo.decide_approval(ctx, approval_id, decision_value, reason)
|
||||
return {"status": "ok", "approval": updated.to_dict()}
|
||||
|
||||
def create_alert(self, ctx: PortalContext, level: str, message: str, scope: str | None = None, source: str = "portal") -> dict[str, Any]:
|
||||
requested_scope = scope or ctx.natural_scope()
|
||||
decision = self.rbac.authorize(ctx, "portal.alerts.read", PortalResource(scope=requested_scope, group_id=ctx.group_id, owner_user_id=ctx.user_id))
|
||||
if not decision.allowed:
|
||||
return {"status": "forbidden", "decision": decision.to_dict()}
|
||||
alert = self.repo.create_alert(ctx, level, message, requested_scope, source)
|
||||
return {"status": "ok", "alert": alert.to_dict()}
|
||||
|
||||
def list_alerts(self, ctx: PortalContext) -> dict[str, Any]:
|
||||
return {"status": "ok", "alerts": [a.to_dict() for a in self.repo.list_alerts(ctx)]}
|
||||
|
||||
def list_approvals(self, ctx: PortalContext) -> dict[str, Any]:
|
||||
return {"status": "ok", "approvals": [a.to_dict() for a in self.repo.list_approvals(ctx)]}
|
||||
|
||||
def list_audit(self, ctx: PortalContext) -> dict[str, Any]:
|
||||
return {"status": "ok", "audit": [a.to_dict() for a in self.repo.list_audit(ctx)]}
|
||||
|
||||
def run_self_loop(self) -> dict[str, Any]:
|
||||
owner = PortalContext("owner", ROLE_OWNER)
|
||||
group_admin = PortalContext("admin", ROLE_GROUP_ADMIN, "g1")
|
||||
user = PortalContext("user", ROLE_USER, "g1")
|
||||
self.create_alert(owner, "info", "self-loop-started", SCOPE_GLOBAL, "self_loop")
|
||||
approval = self.create_approval(user, "model_priority_change", {"model": "default"}, "model_priority")
|
||||
approval_id = approval["approval"]["id"]
|
||||
decision = self.decide_approval(group_admin, approval_id, "approved", "self-loop group approval")
|
||||
return {
|
||||
"status": "ok",
|
||||
"checks": {
|
||||
"user_dashboard": self.dashboard(user)["status"] == "ok",
|
||||
"group_admin_dashboard": self.dashboard(group_admin)["status"] == "ok",
|
||||
"owner_cockpit": self.owner_cockpit(owner)["status"] == "ok",
|
||||
"approval_decision": decision["status"] == "ok",
|
||||
"audit_written": len(self.repo.audit_events) >= 3,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def build_context(user_id: str, role: str = ROLE_USER, group_id: str | None = None, display_name: str | None = None) -> PortalContext:
|
||||
return PortalContext(user_id=user_id, role=role, group_id=group_id, display_name=display_name)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ROLE_USER",
|
||||
"ROLE_GROUP_ADMIN",
|
||||
"ROLE_OWNER",
|
||||
"ROLES",
|
||||
"SCOPE_SELF",
|
||||
"SCOPE_GROUP",
|
||||
"SCOPE_GLOBAL",
|
||||
"PortalContext",
|
||||
"PortalResource",
|
||||
"RBACDecision",
|
||||
"ApprovalRequest",
|
||||
"AlertRecord",
|
||||
"AuditEvent",
|
||||
"PortalRBACService",
|
||||
"PortalRepository",
|
||||
"PortalDashboardService",
|
||||
"PortalOpsService",
|
||||
"build_context",
|
||||
]
|
||||
@@ -0,0 +1,239 @@
|
||||
"""HTTP routes for the three-role user portal.
|
||||
|
||||
The router is FastAPI-compatible but keeps handlers as plain functions so tests and
|
||||
agent self-checks can exercise the contract without booting an ASGI server.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
try: # pragma: no cover - optional runtime dependency
|
||||
from fastapi import APIRouter, Header, HTTPException
|
||||
except Exception: # pragma: no cover - test/runtime fallback
|
||||
APIRouter = None
|
||||
Header = None
|
||||
HTTPException = RuntimeError
|
||||
|
||||
from .portal_rbac_ops import (
|
||||
ROLE_GROUP_ADMIN,
|
||||
ROLE_OWNER,
|
||||
ROLE_USER,
|
||||
SCOPE_GLOBAL,
|
||||
PortalOpsService,
|
||||
PortalResource,
|
||||
build_context,
|
||||
)
|
||||
|
||||
portal_ops_service = PortalOpsService()
|
||||
|
||||
|
||||
def context_from_headers(
|
||||
user_id: str | None = None,
|
||||
role: str | None = None,
|
||||
group_id: str | None = None,
|
||||
display_name: str | None = None,
|
||||
):
|
||||
return build_context(
|
||||
user_id=user_id or "anonymous",
|
||||
role=role or ROLE_USER,
|
||||
group_id=group_id,
|
||||
display_name=display_name,
|
||||
)
|
||||
|
||||
|
||||
def get_portal_dashboard(role: str, user_id: str = "anonymous", group_id: str | None = None) -> dict[str, Any]:
|
||||
ctx = context_from_headers(user_id=user_id, role=role, group_id=group_id)
|
||||
return portal_ops_service.dashboard(ctx)
|
||||
|
||||
|
||||
def get_owner_system_cockpit(user_id: str = "owner", runtime: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
ctx = context_from_headers(user_id=user_id, role=ROLE_OWNER)
|
||||
return portal_ops_service.owner_cockpit(ctx, runtime)
|
||||
|
||||
|
||||
def check_portal_rbac(
|
||||
role: str,
|
||||
action: str,
|
||||
user_id: str = "anonymous",
|
||||
group_id: str | None = None,
|
||||
scope: str | None = None,
|
||||
owner_user_id: str | None = None,
|
||||
resource_group_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
ctx = context_from_headers(user_id=user_id, role=role, group_id=group_id)
|
||||
resource = PortalResource(
|
||||
scope=scope or ctx.natural_scope(),
|
||||
owner_user_id=owner_user_id or user_id,
|
||||
group_id=resource_group_id or group_id,
|
||||
)
|
||||
return portal_ops_service.check_rbac(ctx, action, resource)
|
||||
|
||||
|
||||
def create_portal_approval(
|
||||
role: str,
|
||||
request_type: str,
|
||||
user_id: str = "anonymous",
|
||||
group_id: str | None = None,
|
||||
payload: dict[str, Any] | None = None,
|
||||
target: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
ctx = context_from_headers(user_id=user_id, role=role, group_id=group_id)
|
||||
return portal_ops_service.create_approval(ctx, request_type, payload, target)
|
||||
|
||||
|
||||
def decide_portal_approval(
|
||||
approval_id: str,
|
||||
decision: str,
|
||||
role: str,
|
||||
user_id: str,
|
||||
group_id: str | None = None,
|
||||
reason: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
ctx = context_from_headers(user_id=user_id, role=role, group_id=group_id)
|
||||
return portal_ops_service.decide_approval(ctx, approval_id, decision, reason)
|
||||
|
||||
|
||||
def list_portal_alerts(role: str, user_id: str = "anonymous", group_id: str | None = None) -> dict[str, Any]:
|
||||
ctx = context_from_headers(user_id=user_id, role=role, group_id=group_id)
|
||||
return portal_ops_service.list_alerts(ctx)
|
||||
|
||||
|
||||
def create_portal_alert(
|
||||
role: str,
|
||||
level: str,
|
||||
message: str,
|
||||
user_id: str = "anonymous",
|
||||
group_id: str | None = None,
|
||||
scope: str | None = None,
|
||||
source: str = "portal",
|
||||
) -> dict[str, Any]:
|
||||
ctx = context_from_headers(user_id=user_id, role=role, group_id=group_id)
|
||||
return portal_ops_service.create_alert(ctx, level, message, scope, source)
|
||||
|
||||
|
||||
def list_portal_approvals(role: str, user_id: str = "anonymous", group_id: str | None = None) -> dict[str, Any]:
|
||||
ctx = context_from_headers(user_id=user_id, role=role, group_id=group_id)
|
||||
return portal_ops_service.list_approvals(ctx)
|
||||
|
||||
|
||||
def list_portal_audit_events(role: str, user_id: str = "anonymous", group_id: str | None = None) -> dict[str, Any]:
|
||||
ctx = context_from_headers(user_id=user_id, role=role, group_id=group_id)
|
||||
return portal_ops_service.list_audit(ctx)
|
||||
|
||||
|
||||
def run_portal_self_loop() -> dict[str, Any]:
|
||||
return portal_ops_service.run_self_loop()
|
||||
|
||||
|
||||
def build_router():
|
||||
if APIRouter is None:
|
||||
return None
|
||||
|
||||
router = APIRouter(prefix="/portal", tags=["portal"])
|
||||
|
||||
@router.get("/dashboard", operation_id="getPortalDashboard")
|
||||
def dashboard(
|
||||
x_user_id: str | None = Header(default=None),
|
||||
x_portal_role: str | None = Header(default=None),
|
||||
x_group_id: str | None = Header(default=None),
|
||||
):
|
||||
return get_portal_dashboard(x_portal_role or ROLE_USER, x_user_id or "anonymous", x_group_id)
|
||||
|
||||
@router.post("/rbac/check", operation_id="checkPortalRBAC")
|
||||
def rbac_check(payload: dict[str, Any]):
|
||||
return check_portal_rbac(
|
||||
role=payload.get("role", ROLE_USER),
|
||||
action=payload["action"],
|
||||
user_id=payload.get("user_id", "anonymous"),
|
||||
group_id=payload.get("group_id"),
|
||||
scope=payload.get("scope"),
|
||||
owner_user_id=payload.get("owner_user_id"),
|
||||
resource_group_id=payload.get("resource_group_id"),
|
||||
)
|
||||
|
||||
@router.post("/approvals", operation_id="createPortalApproval")
|
||||
def create_approval(payload: dict[str, Any]):
|
||||
return create_portal_approval(
|
||||
role=payload.get("role", ROLE_USER),
|
||||
request_type=payload["request_type"],
|
||||
user_id=payload.get("user_id", "anonymous"),
|
||||
group_id=payload.get("group_id"),
|
||||
payload=payload.get("payload"),
|
||||
target=payload.get("target"),
|
||||
)
|
||||
|
||||
@router.post("/approvals/{approval_id}/decision", operation_id="decidePortalApproval")
|
||||
def decide_approval(approval_id: str, payload: dict[str, Any]):
|
||||
return decide_portal_approval(
|
||||
approval_id=approval_id,
|
||||
decision=payload["decision"],
|
||||
role=payload.get("role", ROLE_GROUP_ADMIN),
|
||||
user_id=payload.get("user_id", "anonymous"),
|
||||
group_id=payload.get("group_id"),
|
||||
reason=payload.get("reason"),
|
||||
)
|
||||
|
||||
@router.get("/approvals", operation_id="listPortalApprovals")
|
||||
def approvals(
|
||||
x_user_id: str | None = Header(default=None),
|
||||
x_portal_role: str | None = Header(default=None),
|
||||
x_group_id: str | None = Header(default=None),
|
||||
):
|
||||
return list_portal_approvals(x_portal_role or ROLE_USER, x_user_id or "anonymous", x_group_id)
|
||||
|
||||
@router.get("/alerts", operation_id="listPortalAlerts")
|
||||
def alerts(
|
||||
x_user_id: str | None = Header(default=None),
|
||||
x_portal_role: str | None = Header(default=None),
|
||||
x_group_id: str | None = Header(default=None),
|
||||
):
|
||||
return list_portal_alerts(x_portal_role or ROLE_USER, x_user_id or "anonymous", x_group_id)
|
||||
|
||||
@router.post("/alerts", operation_id="createPortalAlert")
|
||||
def create_alert(payload: dict[str, Any]):
|
||||
return create_portal_alert(
|
||||
role=payload.get("role", ROLE_USER),
|
||||
level=payload["level"],
|
||||
message=payload["message"],
|
||||
user_id=payload.get("user_id", "anonymous"),
|
||||
group_id=payload.get("group_id"),
|
||||
scope=payload.get("scope"),
|
||||
source=payload.get("source", "portal"),
|
||||
)
|
||||
|
||||
@router.get("/audit", operation_id="listPortalAuditEvents")
|
||||
def audit(
|
||||
x_user_id: str | None = Header(default=None),
|
||||
x_portal_role: str | None = Header(default=None),
|
||||
x_group_id: str | None = Header(default=None),
|
||||
):
|
||||
return list_portal_audit_events(x_portal_role or ROLE_USER, x_user_id or "anonymous", x_group_id)
|
||||
|
||||
@router.get("/owner/system", operation_id="getOwnerSystemCockpit")
|
||||
def owner_system(x_user_id: str | None = Header(default=None)):
|
||||
return get_owner_system_cockpit(x_user_id or "owner")
|
||||
|
||||
@router.post("/self-loop", operation_id="runPortalSelfLoop")
|
||||
def self_loop():
|
||||
return run_portal_self_loop()
|
||||
|
||||
return router
|
||||
|
||||
|
||||
router = build_router()
|
||||
|
||||
__all__ = [
|
||||
"router",
|
||||
"build_router",
|
||||
"context_from_headers",
|
||||
"get_portal_dashboard",
|
||||
"get_owner_system_cockpit",
|
||||
"check_portal_rbac",
|
||||
"create_portal_approval",
|
||||
"decide_portal_approval",
|
||||
"list_portal_alerts",
|
||||
"list_portal_approvals",
|
||||
"list_portal_audit_events",
|
||||
"run_portal_self_loop",
|
||||
]
|
||||
@@ -0,0 +1,69 @@
|
||||
"""Production acceptance gate for self-loop promotion.
|
||||
|
||||
The gate verifies runtime, GitOps policy, portal readiness, and security rules
|
||||
before any promotion request can proceed.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GateCheck:
|
||||
name: str
|
||||
passed: bool
|
||||
detail: str
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProductionGateResult:
|
||||
status: str
|
||||
checks: tuple[GateCheck, ...]
|
||||
owner_approval_required: bool
|
||||
created_at: str = field(default_factory=utc_now)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"status": self.status,
|
||||
"checks": [c.to_dict() for c in self.checks],
|
||||
"owner_approval_required": self.owner_approval_required,
|
||||
"created_at": self.created_at,
|
||||
}
|
||||
|
||||
|
||||
class ProductionAcceptanceGate:
|
||||
def evaluate(
|
||||
self,
|
||||
runtime: dict[str, Any],
|
||||
baseline: dict[str, Any],
|
||||
acceptance: dict[str, Any],
|
||||
owner_approval_required: bool = True,
|
||||
) -> ProductionGateResult:
|
||||
checks = (
|
||||
GateCheck("runtime_active", runtime.get("status") == "active", "runtime status"),
|
||||
GateCheck("git_patch_only", baseline.get("update_mode") == "git_patch_only", "patch-only policy"),
|
||||
GateCheck("no_direct_main_push", baseline.get("codex_gitops", {}).get("direct_main_push") is False, "main push guard"),
|
||||
GateCheck("acceptance_passed", acceptance.get("status") in {"pass", "waiting_owner_gate"}, "acceptance pipeline"),
|
||||
GateCheck("no_secrets", baseline.get("codex_gitops", {}).get("api_keys_in_git") is False, "secret policy"),
|
||||
)
|
||||
passed = all(check.passed for check in checks)
|
||||
if not passed:
|
||||
status = "blocked"
|
||||
elif owner_approval_required:
|
||||
status = "waiting_owner_approval"
|
||||
else:
|
||||
status = "ready_for_promotion"
|
||||
return ProductionGateResult(status, checks, owner_approval_required)
|
||||
|
||||
|
||||
__all__ = ["GateCheck", "ProductionGateResult", "ProductionAcceptanceGate"]
|
||||
@@ -0,0 +1,59 @@
|
||||
"""Self-evolution reporting for release and owner review."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EvolutionReport:
|
||||
status: str
|
||||
improvements: list[str]
|
||||
risks: list[str]
|
||||
metrics: dict[str, Any]
|
||||
recommendations: list[str]
|
||||
created_at: str = utc_now()
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
class SelfEvolutionReporter:
|
||||
def generate(
|
||||
self,
|
||||
supervisor: dict[str, Any],
|
||||
failure_memory: dict[str, Any],
|
||||
adaptive_policy: dict[str, Any],
|
||||
) -> EvolutionReport:
|
||||
failures = failure_memory.get("top_failures", [])
|
||||
risks = []
|
||||
if failures:
|
||||
risks.append("recurring failures require owner-visible review")
|
||||
if adaptive_policy.get("automation_level") == "analysis_only":
|
||||
risks.append("automation reduced due to recurrence")
|
||||
return EvolutionReport(
|
||||
status="ready_for_review",
|
||||
improvements=[
|
||||
"self-loop supervisor enabled",
|
||||
"failure memory accumulated",
|
||||
"adaptive policy active",
|
||||
],
|
||||
risks=risks,
|
||||
metrics={
|
||||
"failure_records": len(failure_memory.get("records", {})),
|
||||
"supervisor_cycles": supervisor.get("state", {}).get("cycles", 0),
|
||||
},
|
||||
recommendations=[
|
||||
"continue low-risk autonomous patches",
|
||||
"review owner-gated promotion requests",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["EvolutionReport", "SelfEvolutionReporter"]
|
||||
@@ -0,0 +1,89 @@
|
||||
"""Event hooks that trigger autonomous self-loop jobs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from .adaptive_policy import AdaptivePolicy
|
||||
from .failure_memory import FailureMemory
|
||||
from .self_loop_scheduler import SelfLoopScheduler
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SelfLoopEvent:
|
||||
source: str
|
||||
event_type: str
|
||||
goal: str
|
||||
runtime: dict[str, Any] = field(default_factory=dict)
|
||||
baseline: dict[str, Any] = field(default_factory=dict)
|
||||
detail: str = ""
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
class SelfLoopEventHook:
|
||||
SUPPORTED_EVENTS = {
|
||||
"runtime_unhealthy",
|
||||
"gitops_drift",
|
||||
"test_failed",
|
||||
"portal_gap_detected",
|
||||
"acceptance_failed",
|
||||
}
|
||||
|
||||
def __init__(self, scheduler: SelfLoopScheduler | None = None, memory: FailureMemory | None = None, policy: AdaptivePolicy | None = None):
|
||||
self.scheduler = scheduler or SelfLoopScheduler()
|
||||
self.memory = memory or FailureMemory()
|
||||
self.policy = policy or AdaptivePolicy()
|
||||
|
||||
def parse(self, payload: dict[str, Any]) -> SelfLoopEvent:
|
||||
return SelfLoopEvent(
|
||||
source=str(payload.get("source", "unknown")),
|
||||
event_type=str(payload.get("event_type", "unknown")),
|
||||
goal=str(payload.get("goal", "investigate self-loop event")),
|
||||
runtime=dict(payload.get("runtime", {})),
|
||||
baseline=dict(payload.get("baseline", {})),
|
||||
detail=str(payload.get("detail", "")),
|
||||
)
|
||||
|
||||
def handle(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
event = self.parse(payload)
|
||||
if event.event_type not in self.SUPPORTED_EVENTS:
|
||||
return {
|
||||
"status": "ignored",
|
||||
"reason": "unsupported_event",
|
||||
"event": event.to_dict(),
|
||||
}
|
||||
|
||||
if event.event_type in {"test_failed", "acceptance_failed", "runtime_unhealthy"}:
|
||||
self.memory.record_failure(
|
||||
component=event.source,
|
||||
reason=event.event_type,
|
||||
detail=event.detail,
|
||||
severity="high",
|
||||
remediation_hint="run self-loop diagnosis and generate patch plan",
|
||||
)
|
||||
|
||||
decision = self.policy.evaluate(self.memory, event.runtime, event.baseline, event.goal)
|
||||
options = self.policy.apply_to_scheduler(decision)
|
||||
job = self.scheduler.enqueue(
|
||||
goal=event.goal,
|
||||
runtime=event.runtime,
|
||||
baseline=event.baseline,
|
||||
max_attempts=options["max_attempts"],
|
||||
)
|
||||
return {
|
||||
"status": "queued",
|
||||
"event": event.to_dict(),
|
||||
"policy": decision.to_dict(),
|
||||
"job": job.to_dict(),
|
||||
}
|
||||
|
||||
|
||||
__all__ = ["SelfLoopEvent", "SelfLoopEventHook"]
|
||||
@@ -0,0 +1,222 @@
|
||||
"""Autonomous self-loop executor.
|
||||
|
||||
This layer turns probe/plan reports into safe execution outcomes. It does not
|
||||
bypass owner governance: high-risk or restricted actions are blocked at the owner
|
||||
approval gate and emitted as explicit pending work.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Callable
|
||||
|
||||
from .autonomous_self_loop import AutonomousSelfLoop, RemediationStep, SelfLoopReport
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ExecutionResult:
|
||||
step_id: str
|
||||
action: str
|
||||
status: str
|
||||
reason: str
|
||||
risk: str
|
||||
requires_owner_approval: bool
|
||||
output: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestResult:
|
||||
name: str
|
||||
status: str
|
||||
detail: str = ""
|
||||
|
||||
def passed(self) -> bool:
|
||||
return self.status == "pass"
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExecutionReport:
|
||||
status: str
|
||||
report_id: str
|
||||
actions: list[ExecutionResult]
|
||||
tests: list[TestResult]
|
||||
owner_approval_required: bool
|
||||
created_at: str = field(default_factory=utc_now)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"status": self.status,
|
||||
"report_id": self.report_id,
|
||||
"actions": [a.to_dict() for a in self.actions],
|
||||
"tests": [t.to_dict() for t in self.tests],
|
||||
"owner_approval_required": self.owner_approval_required,
|
||||
"created_at": self.created_at,
|
||||
}
|
||||
|
||||
|
||||
class SelfLoopTestRunner:
|
||||
"""Deterministic self-loop checks that avoid shell execution and secrets."""
|
||||
|
||||
def run(self, runtime: dict[str, Any], baseline: dict[str, Any]) -> list[TestResult]:
|
||||
return [
|
||||
self._runtime_active(runtime),
|
||||
self._patch_only_policy(baseline),
|
||||
self._owner_gate_policy(baseline),
|
||||
self._portal_domain_smoke(),
|
||||
]
|
||||
|
||||
def _runtime_active(self, runtime: dict[str, Any]) -> TestResult:
|
||||
if runtime.get("status") == "active":
|
||||
return TestResult("runtime_active", "pass", "runtime reports active")
|
||||
return TestResult("runtime_active", "fail", "runtime is not active")
|
||||
|
||||
def _patch_only_policy(self, baseline: dict[str, Any]) -> TestResult:
|
||||
if baseline.get("update_mode") == "git_patch_only":
|
||||
return TestResult("git_patch_only", "pass", "patch-only updates enforced")
|
||||
return TestResult("git_patch_only", "fail", "update mode is not git_patch_only")
|
||||
|
||||
def _owner_gate_policy(self, baseline: dict[str, Any]) -> TestResult:
|
||||
codex_gitops = baseline.get("codex_gitops", {})
|
||||
if codex_gitops.get("direct_main_push") is False:
|
||||
return TestResult("owner_gate", "pass", "direct main push disabled")
|
||||
return TestResult("owner_gate", "fail", "direct main push must remain disabled")
|
||||
|
||||
def _portal_domain_smoke(self) -> TestResult:
|
||||
try:
|
||||
from .portal_rbac_ops import ROLE_OWNER, PortalOpsService, build_context
|
||||
|
||||
service = PortalOpsService()
|
||||
result = service.owner_cockpit(build_context("owner", ROLE_OWNER))
|
||||
if result.get("status") == "ok":
|
||||
return TestResult("portal_domain_smoke", "pass", "owner cockpit domain service responds")
|
||||
return TestResult("portal_domain_smoke", "fail", "owner cockpit returned non-ok")
|
||||
except Exception as exc: # pragma: no cover - defensive smoke output
|
||||
return TestResult("portal_domain_smoke", "fail", f"portal smoke failed: {type(exc).__name__}")
|
||||
|
||||
|
||||
class SelfLoopAgentExecutor:
|
||||
"""Executes safe self-loop steps and gates restricted ones."""
|
||||
|
||||
SAFE_ACTIONS = {
|
||||
"health_probe",
|
||||
"test_execution",
|
||||
"failure_classification",
|
||||
"patch_plan_generation",
|
||||
"report_generation",
|
||||
"create-low-risk-patch",
|
||||
}
|
||||
|
||||
RESTRICTED_ACTIONS = {
|
||||
"request-runtime-recovery",
|
||||
"commit-policy-patch",
|
||||
"commit-security-policy",
|
||||
"commit-runtime-config",
|
||||
"commit-portal-config",
|
||||
"production-promotion",
|
||||
"permission-escalation",
|
||||
"secret-rotation",
|
||||
}
|
||||
|
||||
def __init__(self, loop: AutonomousSelfLoop | None = None, test_runner: SelfLoopTestRunner | None = None):
|
||||
self.loop = loop or AutonomousSelfLoop(owner_approval_required=True)
|
||||
self.test_runner = test_runner or SelfLoopTestRunner()
|
||||
self.handlers: dict[str, Callable[[RemediationStep], dict[str, Any]]] = {
|
||||
"create-low-risk-patch": self._create_low_risk_patch_manifest,
|
||||
"patch_plan_generation": self._patch_plan_manifest,
|
||||
"report_generation": self._report_manifest,
|
||||
}
|
||||
|
||||
def execute_step(self, step: RemediationStep) -> ExecutionResult:
|
||||
if step.requires_owner_approval or step.action in self.RESTRICTED_ACTIONS:
|
||||
return ExecutionResult(
|
||||
step.id,
|
||||
step.action,
|
||||
"blocked_owner_gate",
|
||||
"restricted action requires owner approval",
|
||||
step.risk,
|
||||
True,
|
||||
{"title": step.title},
|
||||
)
|
||||
if step.risk not in {"low"}:
|
||||
return ExecutionResult(
|
||||
step.id,
|
||||
step.action,
|
||||
"blocked_risk_gate",
|
||||
"only low-risk autonomous actions are allowed",
|
||||
step.risk,
|
||||
True,
|
||||
{"title": step.title},
|
||||
)
|
||||
handler = self.handlers.get(step.action, self._safe_noop_manifest)
|
||||
return ExecutionResult(
|
||||
step.id,
|
||||
step.action,
|
||||
"executed",
|
||||
"safe autonomous action executed",
|
||||
step.risk,
|
||||
False,
|
||||
handler(step),
|
||||
)
|
||||
|
||||
def execute_report(self, report: SelfLoopReport, runtime: dict[str, Any], baseline: dict[str, Any]) -> ExecutionReport:
|
||||
actions = [self.execute_step(step) for step in report.remediation]
|
||||
tests = self.test_runner.run(runtime, baseline)
|
||||
owner_approval_required = self.loop.owner_approval_required or any(a.requires_owner_approval for a in actions)
|
||||
failed_tests = [t for t in tests if not t.passed()]
|
||||
if failed_tests:
|
||||
status = "failed_tests"
|
||||
elif owner_approval_required:
|
||||
status = "waiting_owner_gate"
|
||||
else:
|
||||
status = "completed"
|
||||
return ExecutionReport(status, report.id, actions, tests, owner_approval_required)
|
||||
|
||||
def run_once(self, runtime: dict[str, Any], baseline: dict[str, Any]) -> dict[str, Any]:
|
||||
report = self.loop.evaluate(runtime, baseline)
|
||||
execution = self.execute_report(report, runtime, baseline)
|
||||
acceptance = self.loop.acceptance(report)
|
||||
owner_approval_required = execution.owner_approval_required or acceptance["owner_approval_required"]
|
||||
return {
|
||||
"status": execution.status,
|
||||
"can_unattended_continue": execution.status == "completed" and not owner_approval_required,
|
||||
"owner_approval_required": owner_approval_required,
|
||||
"plan": report.to_dict(),
|
||||
"execution": execution.to_dict(),
|
||||
}
|
||||
|
||||
def _create_low_risk_patch_manifest(self, step: RemediationStep) -> dict[str, Any]:
|
||||
return {
|
||||
"patch_intent": step.title,
|
||||
"branch_policy": "patch-only",
|
||||
"direct_main_push": False,
|
||||
"secrets_in_git": False,
|
||||
}
|
||||
|
||||
def _patch_plan_manifest(self, step: RemediationStep) -> dict[str, Any]:
|
||||
return {"plan": step.title, "action": step.action}
|
||||
|
||||
def _report_manifest(self, step: RemediationStep) -> dict[str, Any]:
|
||||
return {"report": step.title, "generated": True}
|
||||
|
||||
def _safe_noop_manifest(self, step: RemediationStep) -> dict[str, Any]:
|
||||
return {"noop": True, "title": step.title}
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ExecutionResult",
|
||||
"TestResult",
|
||||
"ExecutionReport",
|
||||
"SelfLoopTestRunner",
|
||||
"SelfLoopAgentExecutor",
|
||||
]
|
||||
@@ -0,0 +1,58 @@
|
||||
"""Adapter manifest for controlled GitOps Actions execution.
|
||||
|
||||
The adapter produces an action plan consumed by the GitOps bridge. It does not
|
||||
embed credentials and does not perform direct repository writes.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from .self_loop_patch_executor import PatchBundle
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GitOpsActionPlan:
|
||||
action: str
|
||||
branch: str
|
||||
operations: tuple[dict[str, Any], ...]
|
||||
owner_approval_required: bool
|
||||
created_at: str = field(default_factory=utc_now)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
class SelfLoopGitOpsAdapter:
|
||||
def build_action_plan(self, bundle: PatchBundle) -> GitOpsActionPlan:
|
||||
operations = tuple(
|
||||
{
|
||||
"operation": "commitFile",
|
||||
"path": patch_file.path,
|
||||
"checksum": patch_file.checksum(),
|
||||
}
|
||||
for patch_file in bundle.files
|
||||
)
|
||||
return GitOpsActionPlan(
|
||||
action="gitops_patch_apply",
|
||||
branch=bundle.branch,
|
||||
operations=operations,
|
||||
owner_approval_required=bundle.owner_approval_required,
|
||||
)
|
||||
|
||||
def validate_action_plan(self, plan: GitOpsActionPlan) -> dict[str, Any]:
|
||||
return {
|
||||
"valid": plan.branch.startswith("patch/") and plan.action == "gitops_patch_apply",
|
||||
"branch": plan.branch,
|
||||
"owner_approval_required": plan.owner_approval_required,
|
||||
"operations": len(plan.operations),
|
||||
}
|
||||
|
||||
|
||||
__all__ = ["GitOpsActionPlan", "SelfLoopGitOpsAdapter"]
|
||||
@@ -0,0 +1,142 @@
|
||||
"""Multi-agent self-loop orchestration.
|
||||
|
||||
Coordinates planning, implementation proposals, and validation feedback from
|
||||
Hermes/OpenClaw/CodexBridge-style agents without storing credentials or bypassing
|
||||
GitOps governance.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from .self_loop_executor import SelfLoopAgentExecutor
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AgentProfile:
|
||||
name: str
|
||||
role: str
|
||||
weight: int
|
||||
capabilities: tuple[str, ...]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AgentTask:
|
||||
task_id: str
|
||||
goal: str
|
||||
task_type: str
|
||||
risk: str = "low"
|
||||
payload: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AgentResponse:
|
||||
agent: str
|
||||
status: str
|
||||
proposal: dict[str, Any]
|
||||
confidence: float
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AgentVote:
|
||||
agent: str
|
||||
approve: bool
|
||||
weight: int
|
||||
reason: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class PatchManifest:
|
||||
branch_policy: str
|
||||
files: list[str]
|
||||
tests: list[str]
|
||||
approvals_required: bool
|
||||
generated_at: str = field(default_factory=utc_now)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
class SelfLoopAgentOrchestrator:
|
||||
AGENTS = (
|
||||
AgentProfile("Hermes", "planner", 2, ("analysis", "planning", "risk")),
|
||||
AgentProfile("OpenClaw", "implementation", 2, ("code", "patch", "test")),
|
||||
AgentProfile("CodexBridge", "validation", 2, ("gitops", "validation", "acceptance")),
|
||||
)
|
||||
|
||||
def __init__(self, executor: SelfLoopAgentExecutor | None = None):
|
||||
self.executor = executor or SelfLoopAgentExecutor()
|
||||
|
||||
def dispatch(self, task: AgentTask) -> list[AgentResponse]:
|
||||
responses = []
|
||||
for agent in self.AGENTS:
|
||||
if agent.role == "planner":
|
||||
proposal = {"risk": task.risk, "plan": "classify issue and produce remediation steps"}
|
||||
elif agent.role == "implementation":
|
||||
proposal = {"patch": True, "branch_policy": "patch-only", "goal": task.goal}
|
||||
else:
|
||||
proposal = {"validation": ["tests", "gitops_policy", "acceptance_report"]}
|
||||
responses.append(AgentResponse(agent.name, "ready", proposal, 0.9))
|
||||
return responses
|
||||
|
||||
def vote(self, responses: list[AgentResponse]) -> dict[str, Any]:
|
||||
votes = []
|
||||
total = 0
|
||||
approved = 0
|
||||
for response in responses:
|
||||
profile = next(a for a in self.AGENTS if a.name == response.agent)
|
||||
approve = response.status == "ready"
|
||||
votes.append(AgentVote(response.agent, approve, profile.weight, "ready" if approve else "rejected"))
|
||||
total += profile.weight
|
||||
approved += profile.weight if approve else 0
|
||||
return {
|
||||
"approved": approved >= total / 2,
|
||||
"approval_weight": approved,
|
||||
"total_weight": total,
|
||||
"votes": [asdict(v) for v in votes],
|
||||
}
|
||||
|
||||
def create_patch_manifest(self, task: AgentTask, requires_approval: bool) -> PatchManifest:
|
||||
return PatchManifest(
|
||||
branch_policy="patch-only",
|
||||
files=[
|
||||
"active-runtime/app",
|
||||
"openapi",
|
||||
"tests",
|
||||
],
|
||||
tests=[
|
||||
"self_loop_executor",
|
||||
"portal_self_loop",
|
||||
"acceptance_pipeline",
|
||||
],
|
||||
approvals_required=requires_approval or task.risk != "low",
|
||||
)
|
||||
|
||||
def run_cycle(self, runtime: dict[str, Any], baseline: dict[str, Any], goal: str) -> dict[str, Any]:
|
||||
task = AgentTask("self-loop-cycle", goal, "remediation")
|
||||
executor_result = self.executor.run_once(runtime, baseline)
|
||||
responses = self.dispatch(task)
|
||||
decision = self.vote(responses)
|
||||
manifest = self.create_patch_manifest(task, executor_result["owner_approval_required"])
|
||||
return {
|
||||
"status": "ready_for_patch" if decision["approved"] else "blocked_agent_vote",
|
||||
"executor": executor_result,
|
||||
"agents": [asdict(r) for r in responses],
|
||||
"vote": decision,
|
||||
"patch_manifest": manifest.to_dict(),
|
||||
}
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AgentProfile",
|
||||
"AgentTask",
|
||||
"AgentResponse",
|
||||
"SelfLoopAgentOrchestrator",
|
||||
"PatchManifest",
|
||||
]
|
||||
@@ -0,0 +1,284 @@
|
||||
"""Self-loop patch executor and acceptance pipeline.
|
||||
|
||||
This module converts multi-agent self-loop decisions into GitOps-safe patch
|
||||
bundles, validates policy, runs deterministic acceptance checks, and prepares a
|
||||
promotion request. It intentionally does not push main, rotate secrets, or bypass
|
||||
owner approval.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from hashlib import sha256
|
||||
from typing import Any
|
||||
|
||||
from .self_loop_orchestrator import SelfLoopAgentOrchestrator
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
def stable_id(*parts: str) -> str:
|
||||
digest = sha256("::".join(parts).encode("utf-8")).hexdigest()[:12]
|
||||
return f"slp_{digest}"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PatchFile:
|
||||
path: str
|
||||
content: str
|
||||
purpose: str
|
||||
|
||||
def checksum(self) -> str:
|
||||
return sha256(self.content.encode("utf-8")).hexdigest()
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
data = asdict(self)
|
||||
data["sha256"] = self.checksum()
|
||||
return data
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PatchBundle:
|
||||
id: str
|
||||
branch: str
|
||||
risk: str
|
||||
goal: str
|
||||
files: tuple[PatchFile, ...]
|
||||
owner_approval_required: bool
|
||||
direct_main_push: bool = False
|
||||
secrets_in_git: bool = False
|
||||
created_at: str = field(default_factory=utc_now)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"id": self.id,
|
||||
"branch": self.branch,
|
||||
"risk": self.risk,
|
||||
"goal": self.goal,
|
||||
"files": [f.to_dict() for f in self.files],
|
||||
"owner_approval_required": self.owner_approval_required,
|
||||
"direct_main_push": self.direct_main_push,
|
||||
"secrets_in_git": self.secrets_in_git,
|
||||
"created_at": self.created_at,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AcceptanceCheck:
|
||||
name: str
|
||||
status: str
|
||||
detail: str
|
||||
required: bool = True
|
||||
|
||||
def passed(self) -> bool:
|
||||
return self.status == "pass" or not self.required
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AcceptanceReport:
|
||||
status: str
|
||||
patch_id: str
|
||||
checks: tuple[AcceptanceCheck, ...]
|
||||
owner_approval_required: bool
|
||||
created_at: str = field(default_factory=utc_now)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"status": self.status,
|
||||
"patch_id": self.patch_id,
|
||||
"checks": [c.to_dict() for c in self.checks],
|
||||
"owner_approval_required": self.owner_approval_required,
|
||||
"created_at": self.created_at,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PromotionRequest:
|
||||
status: str
|
||||
patch_id: str
|
||||
branch: str
|
||||
reason: str
|
||||
owner_approval_required: bool
|
||||
acceptance: AcceptanceReport
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"status": self.status,
|
||||
"patch_id": self.patch_id,
|
||||
"branch": self.branch,
|
||||
"reason": self.reason,
|
||||
"owner_approval_required": self.owner_approval_required,
|
||||
"acceptance": self.acceptance.to_dict(),
|
||||
}
|
||||
|
||||
|
||||
class PatchPolicyValidator:
|
||||
"""Validates GitOps safety constraints before any patch is promoted."""
|
||||
|
||||
FORBIDDEN_PATH_PREFIXES = (
|
||||
".git/",
|
||||
"secrets/",
|
||||
"private/",
|
||||
)
|
||||
SECRET_MARKERS = (
|
||||
"BEGIN PRIVATE KEY",
|
||||
"api_key=",
|
||||
"password=",
|
||||
"token=",
|
||||
"secret=",
|
||||
)
|
||||
|
||||
def validate(self, bundle: PatchBundle) -> tuple[AcceptanceCheck, ...]:
|
||||
checks: list[AcceptanceCheck] = []
|
||||
checks.append(self._patch_only(bundle))
|
||||
checks.append(self._no_direct_main_push(bundle))
|
||||
checks.append(self._no_secret_files(bundle))
|
||||
checks.append(self._file_paths_safe(bundle))
|
||||
checks.append(self._content_safe(bundle))
|
||||
return tuple(checks)
|
||||
|
||||
def _patch_only(self, bundle: PatchBundle) -> AcceptanceCheck:
|
||||
if bundle.branch.startswith("patch/"):
|
||||
return AcceptanceCheck("patch_branch_policy", "pass", "branch is patch-scoped")
|
||||
return AcceptanceCheck("patch_branch_policy", "fail", "branch must start with patch/")
|
||||
|
||||
def _no_direct_main_push(self, bundle: PatchBundle) -> AcceptanceCheck:
|
||||
if bundle.direct_main_push is False:
|
||||
return AcceptanceCheck("no_direct_main_push", "pass", "direct main push disabled")
|
||||
return AcceptanceCheck("no_direct_main_push", "fail", "direct main push is not allowed")
|
||||
|
||||
def _no_secret_files(self, bundle: PatchBundle) -> AcceptanceCheck:
|
||||
if bundle.secrets_in_git is False:
|
||||
return AcceptanceCheck("no_secrets_in_git", "pass", "bundle declares no secrets")
|
||||
return AcceptanceCheck("no_secrets_in_git", "fail", "secrets must not be stored in Git")
|
||||
|
||||
def _file_paths_safe(self, bundle: PatchBundle) -> AcceptanceCheck:
|
||||
unsafe = [f.path for f in bundle.files if f.path.startswith(self.FORBIDDEN_PATH_PREFIXES)]
|
||||
if not unsafe:
|
||||
return AcceptanceCheck("safe_file_paths", "pass", "all file paths are allowed")
|
||||
return AcceptanceCheck("safe_file_paths", "fail", f"unsafe paths: {unsafe}")
|
||||
|
||||
def _content_safe(self, bundle: PatchBundle) -> AcceptanceCheck:
|
||||
flagged: list[str] = []
|
||||
for patch_file in bundle.files:
|
||||
content_lower = patch_file.content.lower()
|
||||
for marker in self.SECRET_MARKERS:
|
||||
if marker.lower() in content_lower:
|
||||
flagged.append(patch_file.path)
|
||||
break
|
||||
if not flagged:
|
||||
return AcceptanceCheck("safe_file_content", "pass", "no secret markers detected")
|
||||
return AcceptanceCheck("safe_file_content", "fail", f"secret-like markers detected: {flagged}")
|
||||
|
||||
|
||||
class AcceptancePipeline:
|
||||
def __init__(self, validator: PatchPolicyValidator | None = None):
|
||||
self.validator = validator or PatchPolicyValidator()
|
||||
|
||||
def run(self, bundle: PatchBundle, executor_result: dict[str, Any]) -> AcceptanceReport:
|
||||
checks = list(self.validator.validate(bundle))
|
||||
checks.extend(self._executor_checks(executor_result))
|
||||
status = "pass" if all(c.passed() for c in checks) else "fail"
|
||||
if status == "pass" and bundle.owner_approval_required:
|
||||
status = "waiting_owner_gate"
|
||||
return AcceptanceReport(status, bundle.id, tuple(checks), bundle.owner_approval_required)
|
||||
|
||||
def _executor_checks(self, executor_result: dict[str, Any]) -> tuple[AcceptanceCheck, ...]:
|
||||
execution = executor_result.get("execution", {})
|
||||
tests = execution.get("tests", [])
|
||||
checks: list[AcceptanceCheck] = []
|
||||
for test in tests:
|
||||
checks.append(AcceptanceCheck(
|
||||
name=f"executor_{test.get('name', 'unknown')}",
|
||||
status="pass" if test.get("status") == "pass" else "fail",
|
||||
detail=test.get("detail", ""),
|
||||
))
|
||||
if not checks:
|
||||
checks.append(AcceptanceCheck("executor_tests_present", "fail", "no executor tests were reported"))
|
||||
return tuple(checks)
|
||||
|
||||
|
||||
class SelfLoopPatchExecutor:
|
||||
def __init__(self, orchestrator: SelfLoopAgentOrchestrator | None = None, pipeline: AcceptancePipeline | None = None):
|
||||
self.orchestrator = orchestrator or SelfLoopAgentOrchestrator()
|
||||
self.pipeline = pipeline or AcceptancePipeline()
|
||||
|
||||
def build_patch_bundle(self, cycle: dict[str, Any], goal: str, branch: str = "patch/autonomous-self-loop") -> PatchBundle:
|
||||
executor = cycle.get("executor", {})
|
||||
owner_approval_required = bool(executor.get("owner_approval_required"))
|
||||
manifest = cycle.get("patch_manifest", {})
|
||||
risk = "medium" if owner_approval_required else "low"
|
||||
patch_id = stable_id(branch, goal, cycle.get("status", "unknown"))
|
||||
files = (
|
||||
PatchFile(
|
||||
path=f"patches/{patch_id}.self-loop.json",
|
||||
purpose="self-loop patch manifest",
|
||||
content=self._manifest_content(patch_id, goal, cycle, manifest),
|
||||
),
|
||||
)
|
||||
return PatchBundle(
|
||||
id=patch_id,
|
||||
branch=branch,
|
||||
risk=risk,
|
||||
goal=goal,
|
||||
files=files,
|
||||
owner_approval_required=owner_approval_required or bool(manifest.get("approvals_required")),
|
||||
)
|
||||
|
||||
def run_cycle_to_promotion(
|
||||
self,
|
||||
runtime: dict[str, Any],
|
||||
baseline: dict[str, Any],
|
||||
goal: str,
|
||||
branch: str = "patch/autonomous-self-loop",
|
||||
) -> dict[str, Any]:
|
||||
cycle = self.orchestrator.run_cycle(runtime, baseline, goal)
|
||||
bundle = self.build_patch_bundle(cycle, goal, branch)
|
||||
acceptance = self.pipeline.run(bundle, cycle.get("executor", {}))
|
||||
promotion = self.prepare_promotion(bundle, acceptance)
|
||||
return {
|
||||
"status": promotion.status,
|
||||
"cycle": cycle,
|
||||
"patch_bundle": bundle.to_dict(),
|
||||
"acceptance": acceptance.to_dict(),
|
||||
"promotion": promotion.to_dict(),
|
||||
}
|
||||
|
||||
def prepare_promotion(self, bundle: PatchBundle, acceptance: AcceptanceReport) -> PromotionRequest:
|
||||
if acceptance.status == "fail":
|
||||
return PromotionRequest("blocked_failed_acceptance", bundle.id, bundle.branch, "acceptance failed", True, acceptance)
|
||||
if acceptance.owner_approval_required:
|
||||
return PromotionRequest("waiting_owner_approval", bundle.id, bundle.branch, "owner approval required", True, acceptance)
|
||||
return PromotionRequest("ready_for_auto_merge", bundle.id, bundle.branch, "low-risk patch passed acceptance", False, acceptance)
|
||||
|
||||
def _manifest_content(self, patch_id: str, goal: str, cycle: dict[str, Any], manifest: dict[str, Any]) -> str:
|
||||
lines = [
|
||||
"{",
|
||||
f' "id": "{patch_id}",',
|
||||
f' "goal": "{goal}",',
|
||||
f' "status": "{cycle.get("status", "unknown")}",',
|
||||
f' "branch_policy": "{manifest.get("branch_policy", "patch-only")}",',
|
||||
' "direct_main_push": false,',
|
||||
' "secrets_in_git": false,',
|
||||
f' "owner_approval_required": {str(bool(manifest.get("approvals_required"))).lower()}',
|
||||
"}",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"PatchFile",
|
||||
"PatchBundle",
|
||||
"AcceptanceCheck",
|
||||
"AcceptanceReport",
|
||||
"PromotionRequest",
|
||||
"PatchPolicyValidator",
|
||||
"AcceptancePipeline",
|
||||
"SelfLoopPatchExecutor",
|
||||
]
|
||||
@@ -0,0 +1,140 @@
|
||||
"""Runtime API wiring for autonomous self-loop jobs.
|
||||
|
||||
This module exposes plain functions and an optional FastAPI router for portal,
|
||||
webhook, and worker integrations. It keeps all writes behind patch-only GitOps
|
||||
plans and never stores secrets.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
try: # pragma: no cover - optional runtime dependency
|
||||
from fastapi import APIRouter, Header
|
||||
except Exception: # pragma: no cover - fallback for unit tests
|
||||
APIRouter = None
|
||||
Header = None
|
||||
|
||||
from .production_acceptance_gate import ProductionAcceptanceGate
|
||||
from .self_loop_scheduler import SelfLoopScheduler, TriggerType
|
||||
|
||||
scheduler = SelfLoopScheduler()
|
||||
production_gate = ProductionAcceptanceGate()
|
||||
|
||||
|
||||
def enqueue_self_loop_job(
|
||||
goal: str,
|
||||
runtime: dict[str, Any],
|
||||
baseline: dict[str, Any],
|
||||
trigger: str = "event",
|
||||
branch: str = "patch/autonomous-self-loop",
|
||||
) -> dict[str, Any]:
|
||||
job = scheduler.enqueue(goal, runtime, baseline, TriggerType(trigger), branch)
|
||||
return {"status": "queued", "job": job.to_dict()}
|
||||
|
||||
|
||||
def run_self_loop_job(job_id: str) -> dict[str, Any]:
|
||||
return {"status": "ok", "job": scheduler.run_job(job_id)}
|
||||
|
||||
|
||||
def tick_self_loop_jobs(limit: int = 1) -> dict[str, Any]:
|
||||
return {"status": "ok", "jobs": scheduler.tick(limit), "snapshot": scheduler.snapshot().to_dict()}
|
||||
|
||||
|
||||
def list_self_loop_jobs() -> dict[str, Any]:
|
||||
return {"status": "ok", "jobs": [job.to_dict() for job in scheduler.jobs.values()]}
|
||||
|
||||
|
||||
def get_self_loop_job(job_id: str) -> dict[str, Any]:
|
||||
job = scheduler.jobs.get(job_id)
|
||||
if not job:
|
||||
return {"status": "not_found", "job_id": job_id}
|
||||
return {"status": "ok", "job": job.to_dict()}
|
||||
|
||||
|
||||
def run_self_loop_once(
|
||||
goal: str,
|
||||
runtime: dict[str, Any],
|
||||
baseline: dict[str, Any],
|
||||
branch: str = "patch/autonomous-self-loop",
|
||||
) -> dict[str, Any]:
|
||||
job = scheduler.enqueue(goal, runtime, baseline, TriggerType.MANUAL, branch, max_attempts=1)
|
||||
return run_self_loop_job(job.id)
|
||||
|
||||
|
||||
def evaluate_self_loop_production_gate(
|
||||
runtime: dict[str, Any],
|
||||
baseline: dict[str, Any],
|
||||
acceptance: dict[str, Any],
|
||||
owner_approval_required: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
result = production_gate.evaluate(runtime, baseline, acceptance, owner_approval_required)
|
||||
return {"status": "ok", "gate": result.to_dict()}
|
||||
|
||||
|
||||
def build_router():
|
||||
if APIRouter is None:
|
||||
return None
|
||||
|
||||
router = APIRouter(prefix="/self-loop", tags=["self-loop"])
|
||||
|
||||
@router.post("/jobs", operation_id="enqueueSelfLoopJob")
|
||||
def enqueue_job(payload: dict[str, Any]):
|
||||
return enqueue_self_loop_job(
|
||||
goal=payload["goal"],
|
||||
runtime=payload.get("runtime", {}),
|
||||
baseline=payload.get("baseline", {}),
|
||||
trigger=payload.get("trigger", "event"),
|
||||
branch=payload.get("branch", "patch/autonomous-self-loop"),
|
||||
)
|
||||
|
||||
@router.get("/jobs", operation_id="listSelfLoopJobs")
|
||||
def jobs():
|
||||
return list_self_loop_jobs()
|
||||
|
||||
@router.get("/jobs/{job_id}", operation_id="getSelfLoopJob")
|
||||
def get_job(job_id: str):
|
||||
return get_self_loop_job(job_id)
|
||||
|
||||
@router.post("/jobs/{job_id}/run", operation_id="runSelfLoopJob")
|
||||
def run_job(job_id: str):
|
||||
return run_self_loop_job(job_id)
|
||||
|
||||
@router.post("/tick", operation_id="tickSelfLoopJobs")
|
||||
def tick(payload: dict[str, Any] | None = None):
|
||||
return tick_self_loop_jobs((payload or {}).get("limit", 1))
|
||||
|
||||
@router.post("/run-once", operation_id="runSelfLoopOnce")
|
||||
def run_once(payload: dict[str, Any]):
|
||||
return run_self_loop_once(
|
||||
goal=payload["goal"],
|
||||
runtime=payload.get("runtime", {}),
|
||||
baseline=payload.get("baseline", {}),
|
||||
branch=payload.get("branch", "patch/autonomous-self-loop"),
|
||||
)
|
||||
|
||||
@router.post("/production-gate", operation_id="evaluateSelfLoopProductionGate")
|
||||
def production_gate_route(payload: dict[str, Any]):
|
||||
return evaluate_self_loop_production_gate(
|
||||
runtime=payload.get("runtime", {}),
|
||||
baseline=payload.get("baseline", {}),
|
||||
acceptance=payload.get("acceptance", {}),
|
||||
owner_approval_required=payload.get("owner_approval_required", True),
|
||||
)
|
||||
|
||||
return router
|
||||
|
||||
|
||||
router = build_router()
|
||||
|
||||
__all__ = [
|
||||
"router",
|
||||
"build_router",
|
||||
"enqueue_self_loop_job",
|
||||
"run_self_loop_job",
|
||||
"tick_self_loop_jobs",
|
||||
"list_self_loop_jobs",
|
||||
"get_self_loop_job",
|
||||
"run_self_loop_once",
|
||||
"evaluate_self_loop_production_gate",
|
||||
]
|
||||
@@ -0,0 +1,163 @@
|
||||
"""Autonomous self-loop scheduler.
|
||||
|
||||
Schedules event-triggered and interval-style self-loop cycles. The scheduler is
|
||||
framework-neutral and deterministic: production code can invoke tick() from a
|
||||
cron, worker, or webhook handler without giving the module direct access to
|
||||
secrets, shell commands, or main-branch writes.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from .self_loop_patch_executor import SelfLoopPatchExecutor
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
class TriggerType(str, Enum):
|
||||
INTERVAL = "interval"
|
||||
EVENT = "event"
|
||||
MANUAL = "manual"
|
||||
|
||||
|
||||
class JobStatus(str, Enum):
|
||||
PENDING = "pending"
|
||||
RUNNING = "running"
|
||||
COMPLETED = "completed"
|
||||
WAITING_OWNER = "waiting_owner"
|
||||
FAILED = "failed"
|
||||
RETRY_PENDING = "retry_pending"
|
||||
|
||||
|
||||
@dataclass
|
||||
class SelfLoopJob:
|
||||
id: str
|
||||
goal: str
|
||||
trigger: TriggerType
|
||||
runtime: dict[str, Any]
|
||||
baseline: dict[str, Any]
|
||||
branch: str = "patch/autonomous-self-loop"
|
||||
status: JobStatus = JobStatus.PENDING
|
||||
attempts: int = 0
|
||||
max_attempts: int = 3
|
||||
last_error: str | None = None
|
||||
created_at: str = field(default_factory=utc_now)
|
||||
updated_at: str = field(default_factory=utc_now)
|
||||
result: dict[str, Any] | None = None
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
data = asdict(self)
|
||||
data["trigger"] = self.trigger.value
|
||||
data["status"] = self.status.value
|
||||
return data
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SchedulerSnapshot:
|
||||
pending: int
|
||||
running: int
|
||||
completed: int
|
||||
waiting_owner: int
|
||||
failed: int
|
||||
retry_pending: int
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
class SelfLoopScheduler:
|
||||
"""Small in-process scheduler used by runtime workers and tests."""
|
||||
|
||||
def __init__(self, executor: SelfLoopPatchExecutor | None = None):
|
||||
self.executor = executor or SelfLoopPatchExecutor()
|
||||
self.jobs: dict[str, SelfLoopJob] = {}
|
||||
|
||||
def enqueue(
|
||||
self,
|
||||
goal: str,
|
||||
runtime: dict[str, Any],
|
||||
baseline: dict[str, Any],
|
||||
trigger: TriggerType | str = TriggerType.EVENT,
|
||||
branch: str = "patch/autonomous-self-loop",
|
||||
max_attempts: int = 3,
|
||||
) -> SelfLoopJob:
|
||||
trigger_value = trigger if isinstance(trigger, TriggerType) else TriggerType(trigger)
|
||||
job = SelfLoopJob(
|
||||
id=f"job_{uuid4().hex[:12]}",
|
||||
goal=goal,
|
||||
trigger=trigger_value,
|
||||
runtime=runtime,
|
||||
baseline=baseline,
|
||||
branch=branch,
|
||||
max_attempts=max_attempts,
|
||||
)
|
||||
self.jobs[job.id] = job
|
||||
return job
|
||||
|
||||
def tick(self, limit: int = 1) -> list[dict[str, Any]]:
|
||||
runnable = [j for j in self.jobs.values() if j.status in {JobStatus.PENDING, JobStatus.RETRY_PENDING}]
|
||||
results: list[dict[str, Any]] = []
|
||||
for job in runnable[:limit]:
|
||||
results.append(self.run_job(job.id))
|
||||
return results
|
||||
|
||||
def run_job(self, job_id: str) -> dict[str, Any]:
|
||||
job = self.jobs[job_id]
|
||||
job.status = JobStatus.RUNNING
|
||||
job.attempts += 1
|
||||
job.updated_at = utc_now()
|
||||
try:
|
||||
result = self.executor.run_cycle_to_promotion(job.runtime, job.baseline, job.goal, job.branch)
|
||||
job.result = result
|
||||
promotion_status = result.get("promotion", {}).get("status")
|
||||
if promotion_status == "waiting_owner_approval":
|
||||
job.status = JobStatus.WAITING_OWNER
|
||||
elif promotion_status == "ready_for_auto_merge":
|
||||
job.status = JobStatus.COMPLETED
|
||||
elif job.attempts < job.max_attempts:
|
||||
job.status = JobStatus.RETRY_PENDING
|
||||
job.last_error = promotion_status or "promotion_not_ready"
|
||||
else:
|
||||
job.status = JobStatus.FAILED
|
||||
job.last_error = promotion_status or "promotion_failed"
|
||||
except Exception as exc: # pragma: no cover - defensive runtime guard
|
||||
job.last_error = f"{type(exc).__name__}: {exc}"
|
||||
job.status = JobStatus.RETRY_PENDING if job.attempts < job.max_attempts else JobStatus.FAILED
|
||||
job.updated_at = utc_now()
|
||||
return job.to_dict()
|
||||
|
||||
def retry_failed(self) -> list[str]:
|
||||
retry_ids: list[str] = []
|
||||
for job in self.jobs.values():
|
||||
if job.status == JobStatus.FAILED and job.attempts < job.max_attempts:
|
||||
job.status = JobStatus.RETRY_PENDING
|
||||
job.updated_at = utc_now()
|
||||
retry_ids.append(job.id)
|
||||
return retry_ids
|
||||
|
||||
def snapshot(self) -> SchedulerSnapshot:
|
||||
statuses = [job.status for job in self.jobs.values()]
|
||||
return SchedulerSnapshot(
|
||||
pending=statuses.count(JobStatus.PENDING),
|
||||
running=statuses.count(JobStatus.RUNNING),
|
||||
completed=statuses.count(JobStatus.COMPLETED),
|
||||
waiting_owner=statuses.count(JobStatus.WAITING_OWNER),
|
||||
failed=statuses.count(JobStatus.FAILED),
|
||||
retry_pending=statuses.count(JobStatus.RETRY_PENDING),
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"TriggerType",
|
||||
"JobStatus",
|
||||
"SelfLoopJob",
|
||||
"SchedulerSnapshot",
|
||||
"SelfLoopScheduler",
|
||||
]
|
||||
@@ -0,0 +1,77 @@
|
||||
"""Long-running supervisor model for autonomous self-loop runtime."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from .self_loop_event_hooks import SelfLoopEventHook
|
||||
from .failure_memory import FailureMemory
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
@dataclass
|
||||
class SupervisorState:
|
||||
running: bool = False
|
||||
cycles: int = 0
|
||||
last_cycle: str | None = None
|
||||
last_status: str = "idle"
|
||||
heartbeat: str = field(default_factory=utc_now)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
class SelfLoopSupervisor:
|
||||
def __init__(self, hook: SelfLoopEventHook | None = None, memory: FailureMemory | None = None):
|
||||
self.memory = memory or FailureMemory()
|
||||
self.hook = hook or SelfLoopEventHook(memory=self.memory)
|
||||
self.state = SupervisorState()
|
||||
|
||||
def start(self) -> dict[str, Any]:
|
||||
self.state.running = True
|
||||
self.state.last_status = "running"
|
||||
self.state.heartbeat = utc_now()
|
||||
return self.status()
|
||||
|
||||
def stop(self) -> dict[str, Any]:
|
||||
self.state.running = False
|
||||
self.state.last_status = "stopped"
|
||||
self.state.heartbeat = utc_now()
|
||||
return self.status()
|
||||
|
||||
def heartbeat(self) -> dict[str, Any]:
|
||||
self.state.heartbeat = utc_now()
|
||||
return self.status()
|
||||
|
||||
def handle_event(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
if not self.state.running:
|
||||
self.start()
|
||||
self.state.cycles += 1
|
||||
self.state.last_cycle = utc_now()
|
||||
result = self.hook.handle(payload)
|
||||
self.state.last_status = result.get("status", "unknown")
|
||||
self.state.heartbeat = utc_now()
|
||||
return result
|
||||
|
||||
def tick(self, events: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
results = [self.handle_event(event) for event in events]
|
||||
return {
|
||||
"status": "ok",
|
||||
"supervisor": self.status(),
|
||||
"events": results,
|
||||
"failure_memory": self.memory.export(),
|
||||
}
|
||||
|
||||
def status(self) -> dict[str, Any]:
|
||||
return {
|
||||
"state": self.state.to_dict(),
|
||||
"failure_memory_size": len(self.memory.records),
|
||||
}
|
||||
|
||||
|
||||
__all__ = ["SupervisorState", "SelfLoopSupervisor"]
|
||||
@@ -0,0 +1,70 @@
|
||||
"""Resident worker model for self-loop supervision.
|
||||
|
||||
Provides a daemon-friendly lifecycle without forcing an infinite loop inside the
|
||||
application. A process manager can call start/run_cycle/stop and persist state.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from .self_loop_supervisor import SelfLoopSupervisor
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
@dataclass
|
||||
class WorkerState:
|
||||
running: bool = False
|
||||
cycles: int = 0
|
||||
last_error: str | None = None
|
||||
started_at: str | None = None
|
||||
heartbeat: str = field(default_factory=utc_now)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
class SelfLoopWorker:
|
||||
def __init__(self, supervisor: SelfLoopSupervisor | None = None):
|
||||
self.supervisor = supervisor or SelfLoopSupervisor()
|
||||
self.state = WorkerState()
|
||||
|
||||
def start(self) -> dict[str, Any]:
|
||||
self.state.running = True
|
||||
self.state.started_at = utc_now()
|
||||
self.state.heartbeat = utc_now()
|
||||
self.supervisor.start()
|
||||
return self.status()
|
||||
|
||||
def run_cycle(self, events: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
if not self.state.running:
|
||||
self.start()
|
||||
try:
|
||||
result = self.supervisor.tick(events)
|
||||
self.state.cycles += 1
|
||||
self.state.heartbeat = utc_now()
|
||||
return result
|
||||
except Exception as exc:
|
||||
self.state.last_error = f"{type(exc).__name__}: {exc}"
|
||||
self.state.heartbeat = utc_now()
|
||||
raise
|
||||
|
||||
def stop(self) -> dict[str, Any]:
|
||||
self.state.running = False
|
||||
self.supervisor.stop()
|
||||
self.state.heartbeat = utc_now()
|
||||
return self.status()
|
||||
|
||||
def status(self) -> dict[str, Any]:
|
||||
return {
|
||||
"worker": self.state.to_dict(),
|
||||
"supervisor": self.supervisor.status(),
|
||||
}
|
||||
|
||||
|
||||
__all__ = ["WorkerState", "SelfLoopWorker"]
|
||||
@@ -0,0 +1,43 @@
|
||||
CREATE TABLE IF NOT EXISTS portal_roles (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
group_id TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS portal_approvals (
|
||||
id TEXT PRIMARY KEY,
|
||||
request_type TEXT NOT NULL,
|
||||
requester_id TEXT NOT NULL,
|
||||
state TEXT NOT NULL DEFAULT 'pending',
|
||||
scope TEXT NOT NULL,
|
||||
group_id TEXT,
|
||||
payload_json TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS portal_alerts (
|
||||
id TEXT PRIMARY KEY,
|
||||
level TEXT NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
state TEXT NOT NULL DEFAULT 'open',
|
||||
scope TEXT NOT NULL,
|
||||
group_id TEXT,
|
||||
owner_user_id TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS portal_audit_events (
|
||||
id TEXT PRIMARY KEY,
|
||||
actor_id TEXT NOT NULL,
|
||||
action TEXT NOT NULL,
|
||||
scope TEXT NOT NULL,
|
||||
resource_type TEXT,
|
||||
resource_id TEXT,
|
||||
outcome TEXT NOT NULL DEFAULT 'success',
|
||||
detail_json TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
@@ -0,0 +1,29 @@
|
||||
# Agent Instruction Center
|
||||
|
||||
Version-controlled execution instructions for AIOS agents.
|
||||
|
||||
## Supported agents
|
||||
|
||||
- codex
|
||||
- openclaw
|
||||
- hermes
|
||||
- windows_codex_app
|
||||
- custom_gpt_review
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Create versioned instruction file.
|
||||
2. Target agent executes instruction.
|
||||
3. Result is recorded with GitOps audit information.
|
||||
|
||||
## Naming
|
||||
|
||||
agent-instructions/<agent>/<instruction-name>-v<version>.md
|
||||
|
||||
## Rules
|
||||
|
||||
- GitOps managed only.
|
||||
- Patch-first workflow.
|
||||
- No secrets, tokens, passwords or credentials.
|
||||
- Production changes require approval.
|
||||
- Main branch changes require approval.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Agent Execution Instruction
|
||||
|
||||
Instruction ID:
|
||||
Version:
|
||||
Target Agent:
|
||||
|
||||
## Objective
|
||||
|
||||
|
||||
## Context
|
||||
|
||||
|
||||
## Execution Steps
|
||||
|
||||
|
||||
## Constraints
|
||||
|
||||
- GitOps only
|
||||
- Patch-first
|
||||
- No secrets
|
||||
- No force push
|
||||
- Production changes require approval
|
||||
|
||||
## Validation
|
||||
|
||||
|
||||
## Return Format
|
||||
|
||||
Status:
|
||||
Branch:
|
||||
Commit:
|
||||
Rev:
|
||||
Sync Status:
|
||||
Issues:
|
||||
@@ -0,0 +1 @@
|
||||
# Placeholder for versioned Codex instructions.
|
||||
@@ -0,0 +1,65 @@
|
||||
# Codex Fix Patches Artifact Lifecycle Instruction
|
||||
|
||||
Instruction ID: fix-patches-artifact-lifecycle-v1
|
||||
Version: v1
|
||||
Target Agent: codex
|
||||
|
||||
## Objective
|
||||
|
||||
Manage the lifecycle of artifacts in the `patches/` directory — specifically, clean up stale or committed artifacts (`.branch.json`, `nl/*.json`, standalone `.json` patch files) that are no longer needed.
|
||||
|
||||
## Background
|
||||
|
||||
The ChatGPT Actions API writes several types of artifacts into `patches/`:
|
||||
|
||||
1. **Branch metadata** (`patches/patch_*.branch.json`) — created when the `branch()` action writes ahead. These are summary records of created branches.
|
||||
2. **Patch files** (`patches/*.json`, top level) — created when the `patch()` action saves a JSON patch proposal. Example: `patches/self-evolution-agent-orchestration-policy.json`.
|
||||
3. **Natural language plan files** (`patches/nl/nl-*.json`) — created when `nl()` action records a natural-language plan. These are transient planning artifacts.
|
||||
|
||||
None of these files represent durable long-term state. Once committed, they are snapshots of ephemeral decisions. The `patches/` directory should not grow unboundedly with stale artifacts.
|
||||
|
||||
## Preconditions
|
||||
|
||||
- GitOps authentication healthy.
|
||||
- Current working branch should not be diverging from main unnecessarily for this cleanup.
|
||||
- Run from the workspace root.
|
||||
|
||||
## Steps
|
||||
|
||||
### Phase 1: Assess Current State
|
||||
|
||||
1. List all files under `patches/`.
|
||||
2. For each file, check its git log (`git log --oneline patches/<file>`) to understand its purpose and age.
|
||||
3. Identify which files are:
|
||||
- Stale `.branch.json` files describing branches that have already been merged.
|
||||
- Stale `nl/` plan files that were experimental, incomplete, or superseded.
|
||||
- Patch files that are still relevant for the current self-evolution state.
|
||||
|
||||
### Phase 2: Clean Up Stale Artifacts
|
||||
|
||||
For each stale artifact file:
|
||||
1. Remove it with `git rm`.
|
||||
2. Create a single commit message describing the batch cleanup, e.g.:
|
||||
`chore(patches): remove stale artifact files [nl/*.json|branch.json]`
|
||||
|
||||
### Phase 3: Housekeeping (Optional)
|
||||
|
||||
If the file `self-evolution-agent-orchestration-policy.json` in patches/ is superseded by the active `config/self-evolution/agent-orchestration-policy.json`, evaluate whether it should also be removed or kept as reference. Document this decision.
|
||||
|
||||
## Constraints
|
||||
|
||||
- No force push.
|
||||
- No direct production publish.
|
||||
- No secret output.
|
||||
- Preserve audit trail.
|
||||
- Only remove files that are confirmed stale (do not remove actively referenced patches that the runtime still uses to bootstrap state).
|
||||
|
||||
## Return Format
|
||||
|
||||
Status:
|
||||
Workspace:
|
||||
Branch:
|
||||
Files Removed:
|
||||
Files Kept:
|
||||
Commit:
|
||||
Issues:
|
||||
@@ -0,0 +1,66 @@
|
||||
# Codex Execution Instruction
|
||||
|
||||
Instruction ID: fix-reports-permission-v1
|
||||
Version: v1
|
||||
Target Agent: codex
|
||||
|
||||
## Objective
|
||||
Fix the GitOps workspace reports directory permission issue so the Agent OS acceptance report can be committed.
|
||||
|
||||
## Context
|
||||
Current blocked path:
|
||||
|
||||
/data/aios-gitops/workspace/reports/
|
||||
|
||||
Error:
|
||||
|
||||
Permission denied
|
||||
|
||||
## Execution Steps
|
||||
|
||||
1. Enter workspace:
|
||||
|
||||
cd /data/aios-gitops/workspace
|
||||
|
||||
2. Check current user and permissions:
|
||||
|
||||
whoami
|
||||
id
|
||||
ls -ld reports
|
||||
|
||||
3. Create directory if missing:
|
||||
|
||||
mkdir -p reports
|
||||
|
||||
4. Fix ownership and write permission for this directory only:
|
||||
|
||||
chown -R <gitops-user>:<gitops-group> reports
|
||||
chmod -R u+rwX reports
|
||||
|
||||
5. Verify write access:
|
||||
|
||||
touch reports/.write-test
|
||||
rm reports/.write-test
|
||||
|
||||
6. Commit pending acceptance report changes after verification.
|
||||
|
||||
## Constraints
|
||||
|
||||
- GitOps only.
|
||||
- No force push.
|
||||
- No chmod 777.
|
||||
- Do not modify unrelated directories.
|
||||
- Do not write secrets.
|
||||
- Production changes require approval.
|
||||
|
||||
## Validation
|
||||
|
||||
Return:
|
||||
|
||||
Status:
|
||||
Branch:
|
||||
Commit:
|
||||
Rev:
|
||||
Sync Status:
|
||||
Permission Result:
|
||||
Issues:
|
||||
@@ -0,0 +1,31 @@
|
||||
# Codex System Recovery Instruction
|
||||
|
||||
Version: v1
|
||||
Target Agent: codex
|
||||
|
||||
## Objective
|
||||
Recover AIOS system issues requiring deep diagnosis or administrator assistance.
|
||||
|
||||
## Execution
|
||||
1. Inspect runtime status.
|
||||
2. Inspect GitOps state.
|
||||
3. Identify root cause.
|
||||
4. Propose safe patch.
|
||||
5. Apply only approved GitOps changes.
|
||||
|
||||
## Escalation
|
||||
Use when server agents cannot resolve issues.
|
||||
|
||||
## Constraints
|
||||
- GitOps only.
|
||||
- No secrets.
|
||||
- No force push.
|
||||
- Production changes require approval.
|
||||
|
||||
## Return
|
||||
Status:
|
||||
Branch:
|
||||
Commit:
|
||||
Rev:
|
||||
Sync Status:
|
||||
Issues:
|
||||
@@ -0,0 +1,48 @@
|
||||
# Codex Main Merge Verification Instruction
|
||||
|
||||
Instruction ID: v1r1c5-main-merge-verify-v1
|
||||
Version: v1
|
||||
Target Agent: codex
|
||||
|
||||
## Objective
|
||||
Verify whether V1R1C5 patch branch has been merged into main.
|
||||
|
||||
## Preconditions
|
||||
|
||||
Ensure GitOps authentication is healthy.
|
||||
If Gitea account password state blocks access, resolve account activation through approved admin procedure before continuing.
|
||||
|
||||
## Verification Steps
|
||||
|
||||
1. Check repository status.
|
||||
2. Fetch remote branches.
|
||||
3. Compare:
|
||||
|
||||
source:
|
||||
patch/self-evolution-agent-orchestration-policy
|
||||
|
||||
target:
|
||||
main
|
||||
|
||||
4. Confirm merge commit.
|
||||
5. Confirm main HEAD.
|
||||
6. Confirm no divergence.
|
||||
|
||||
## Constraints
|
||||
|
||||
- No force push.
|
||||
- No direct production publish.
|
||||
- No secret output.
|
||||
- Preserve audit trail.
|
||||
|
||||
## Return Format
|
||||
|
||||
Status:
|
||||
Source Branch:
|
||||
Target Branch:
|
||||
Merge Commit:
|
||||
Rev:
|
||||
Sync Status:
|
||||
Conflicts:
|
||||
Main HEAD:
|
||||
Issues:
|
||||
@@ -0,0 +1,59 @@
|
||||
# Codex V1R2C2 Branch Sync Validation Instruction
|
||||
|
||||
Instruction ID: v1r2c2-branch-sync-validation-v1
|
||||
Version: v1
|
||||
Target Agent: codex
|
||||
|
||||
## Objective
|
||||
|
||||
Validate synchronization state of all git branches between local, origin (Gitea), and gitops remote. Identify divergent branches that need reconciliation.
|
||||
|
||||
## Preconditions
|
||||
|
||||
- GitOps authentication healthy.
|
||||
- Run from workspace root (`/data/aios-gitops/workspace`).
|
||||
|
||||
## Validation Scope
|
||||
|
||||
1. **main branch sync**: local main == origin/main == gitops/main
|
||||
2. **patch branch sync**: for each local branch, compare HEAD against origin and gitops remotes
|
||||
3. **Current branch**: verify the active working branch is on the correct base
|
||||
4. **Untracked files**: check for files that should be committed vs ignored
|
||||
|
||||
## Steps
|
||||
|
||||
1. Fetch all remotes.
|
||||
2. Capture local branch HEAD hashes.
|
||||
3. Capture remote origin branch HEAD hashes.
|
||||
4. Capture remote gitops branch HEAD hashes.
|
||||
5. For each branch, report:
|
||||
- Fully synced: local == origin == gitops
|
||||
- Divergent: local != origin or local != gitops
|
||||
- Missing remote: branch exists locally but not in remote
|
||||
6. Check if current branch is based on main or is diverging.
|
||||
7. List untracked files that may need governance.
|
||||
|
||||
## Return Format
|
||||
|
||||
```
|
||||
Status: <synced|divergence_found>
|
||||
Workspace: /data/aios-gitops/workspace
|
||||
Main Sync: <yes|no>
|
||||
Main HEAD: <sha>
|
||||
Origin/main HEAD: <sha>
|
||||
Gitops/main HEAD: <sha>
|
||||
Divergent Branches:
|
||||
- <branch name>: <local sha> vs <remote sha> [origin|gitops] (<reason>)
|
||||
Missing Remote Branches:
|
||||
- <branch name>: <local sha>
|
||||
Untracked File Count: <count>
|
||||
Issues:
|
||||
- <issue description>
|
||||
```
|
||||
|
||||
## Constraints
|
||||
|
||||
- No force push.
|
||||
- No destructive actions.
|
||||
- No secret output.
|
||||
- Report only; do not auto-resolve discrepancies.
|
||||
@@ -0,0 +1 @@
|
||||
# Placeholder for versioned custom GPT review instructions.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Custom GPT Solution Review Instruction
|
||||
|
||||
Version: v1
|
||||
Target Agent: custom_gpt_review
|
||||
|
||||
## Objective
|
||||
Review difficult architecture decisions and implementation proposals from server agents.
|
||||
|
||||
## Role
|
||||
Act as reviewer and advisor for OpenClaw, Hermes and other AIOS agents.
|
||||
|
||||
## Execution
|
||||
1. Receive complete problem context.
|
||||
2. Review baseline, branch and proposed changes.
|
||||
3. Analyze alternative solutions.
|
||||
4. Recommend implementation approach.
|
||||
5. Identify risks and validation requirements.
|
||||
|
||||
## Constraints
|
||||
- Advisory role only.
|
||||
- No direct production changes.
|
||||
- No secrets.
|
||||
- GitOps workflow required.
|
||||
- Release requires approval.
|
||||
|
||||
## Return
|
||||
Status:
|
||||
Recommendation:
|
||||
Risk:
|
||||
Validation:
|
||||
Next Step:
|
||||
@@ -0,0 +1 @@
|
||||
# Placeholder for versioned Hermes instructions.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Hermes Architecture Review Instruction
|
||||
|
||||
Version: v1
|
||||
Target Agent: hermes
|
||||
|
||||
## Objective
|
||||
Review AIOS architecture, policies and implementation plans.
|
||||
|
||||
## Execution
|
||||
1. Review baseline and branch diff.
|
||||
2. Analyze architecture impact.
|
||||
3. Review OpenClaw execution proposals.
|
||||
4. Validate acceptance criteria.
|
||||
5. Provide improvement recommendations.
|
||||
|
||||
## Escalation
|
||||
For unresolved architecture choices, prepare complete context for external review.
|
||||
|
||||
## Constraints
|
||||
- GitOps only.
|
||||
- No secrets.
|
||||
- No direct production changes.
|
||||
- Approval required for release.
|
||||
|
||||
## Return
|
||||
Status:
|
||||
Branch:
|
||||
Commit:
|
||||
Rev:
|
||||
Sync Status:
|
||||
Issues:
|
||||
Recommendations:
|
||||
@@ -0,0 +1 @@
|
||||
# Placeholder for versioned OpenClaw instructions.
|
||||
@@ -0,0 +1,24 @@
|
||||
# OpenClaw Agent Instructions Index
|
||||
|
||||
## Available Instructions
|
||||
|
||||
### v1r1c5-hermes-coordination-v1
|
||||
|
||||
Instruction file:
|
||||
|
||||
agent-instructions/openclaw/v1r1c5-hermes-coordination-v1.md
|
||||
|
||||
Purpose:
|
||||
|
||||
Coordinate Hermes architecture review through OpenClaw controlled communication channel.
|
||||
|
||||
Invocation:
|
||||
|
||||
Execute openclaw/v1r1c5-hermes-coordination-v1 instruction
|
||||
|
||||
Requirements:
|
||||
|
||||
- Read the referenced markdown file from the GitOps workspace.
|
||||
- Do not treat the identifier as a local shell command.
|
||||
- Do not search external skill marketplaces unless the file is missing.
|
||||
- Follow GitOps workflow rules defined in the instruction.
|
||||
@@ -0,0 +1,32 @@
|
||||
# OpenClaw Runtime Maintenance Instruction
|
||||
|
||||
Version: v1
|
||||
Target Agent: openclaw
|
||||
|
||||
## Objective
|
||||
Maintain server-side AIOS runtime and perform safe optimization tasks.
|
||||
|
||||
## Execution
|
||||
1. Check runtime health.
|
||||
2. Review logs and GitOps status.
|
||||
3. Identify optimization opportunities.
|
||||
4. Create patch proposals.
|
||||
5. Execute approved changes.
|
||||
|
||||
## Escalation
|
||||
When solution selection or implementation is uncertain, prepare full context and escalate for review.
|
||||
|
||||
## Constraints
|
||||
- GitOps only.
|
||||
- Patch-first workflow.
|
||||
- No secrets.
|
||||
- No force push.
|
||||
- Production release requires approval.
|
||||
|
||||
## Return
|
||||
Status:
|
||||
Branch:
|
||||
Commit:
|
||||
Rev:
|
||||
Sync Status:
|
||||
Issues:
|
||||
@@ -0,0 +1,51 @@
|
||||
# OpenClaw Execution Instruction: Hermes Coordination
|
||||
|
||||
Instruction ID: v1r1c5-hermes-coordination-v1
|
||||
Version: v1
|
||||
Target Agent: openclaw
|
||||
|
||||
## Objective
|
||||
|
||||
Coordinate Hermes architecture review through OpenClaw-controlled communication channel.
|
||||
|
||||
## Architecture Rule
|
||||
|
||||
Hermes is not directly connected to external control channels.
|
||||
All Hermes requests and responses must be routed through OpenClaw personal WeChat channel.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Receive AIOS task or review request.
|
||||
2. Send required context to Hermes through OpenClaw WeChat.
|
||||
3. Collect Hermes architecture analysis.
|
||||
4. Record review result in GitOps reports.
|
||||
5. Continue execution or escalate when required.
|
||||
|
||||
## Context Package
|
||||
|
||||
Include:
|
||||
|
||||
- Baseline version
|
||||
- Branch
|
||||
- Diff summary
|
||||
- Problem description
|
||||
- Proposed solution
|
||||
- Validation requirements
|
||||
|
||||
## Constraints
|
||||
|
||||
- GitOps only.
|
||||
- No secrets.
|
||||
- No direct production changes.
|
||||
- Maintain audit records.
|
||||
- Use escalation flow for unresolved decisions.
|
||||
|
||||
## Return Format
|
||||
|
||||
Status:
|
||||
Branch:
|
||||
Commit:
|
||||
Rev:
|
||||
Sync Status:
|
||||
Hermes Result:
|
||||
Issues:
|
||||
@@ -0,0 +1,119 @@
|
||||
# OpenClaw Execution Instruction
|
||||
|
||||
Instruction ID: v1r1c5-hermes-review-improvement-v1
|
||||
Version: v1
|
||||
Target Agent: openclaw
|
||||
|
||||
## Objective
|
||||
|
||||
Implement V1R1C5 improvements based on Hermes Conditional Approval review.
|
||||
|
||||
## Context
|
||||
|
||||
Baseline:
|
||||
V1R1C4
|
||||
|
||||
Branch:
|
||||
patch/self-evolution-agent-orchestration-policy
|
||||
|
||||
Proposal:
|
||||
V1R1C5-agent-os-evolution-proposal
|
||||
|
||||
## Required Changes
|
||||
|
||||
1. Add Agent Resource Governance:
|
||||
|
||||
Create:
|
||||
config/self-evolution/agent-resource-manager.json
|
||||
|
||||
Support:
|
||||
- resource limits
|
||||
- parallel task limits
|
||||
- agent priority
|
||||
- runtime isolation
|
||||
- overload handling
|
||||
|
||||
2. Enhance Consensus Engine:
|
||||
|
||||
Add dynamic weighting:
|
||||
|
||||
Final Weight = Base Weight * Quality Score * Historical Reliability * Domain Expertise
|
||||
|
||||
Base weights:
|
||||
- custom_gpt: 2
|
||||
- windows_codex: 2
|
||||
- server_agent: 1
|
||||
|
||||
Add risk gates:
|
||||
- Production Release
|
||||
- V-Level Change
|
||||
- Security Policy Change
|
||||
|
||||
Require:
|
||||
Consensus Approval + Owner Approval
|
||||
|
||||
3. Enhance Message Bus:
|
||||
|
||||
Update runtime/agent-os/message_bus.py
|
||||
|
||||
Add:
|
||||
- task_id
|
||||
- correlation_id
|
||||
- retry_state
|
||||
- delivery_status
|
||||
|
||||
Support:
|
||||
- message tracking
|
||||
- retry
|
||||
- recovery
|
||||
- audit
|
||||
|
||||
4. Add Agent Health Score:
|
||||
|
||||
Create:
|
||||
runtime/agent-os/agent_health_score.py
|
||||
|
||||
Calculate from:
|
||||
- quality score
|
||||
- acceptance rate
|
||||
- historical reliability
|
||||
|
||||
Use for:
|
||||
- scheduler selection
|
||||
- consensus weighting
|
||||
|
||||
## Validation
|
||||
|
||||
Run:
|
||||
python3 -m compileall runtime/agent-os
|
||||
|
||||
Run:
|
||||
runtime/agent-os/integration_test.py
|
||||
|
||||
Validate:
|
||||
- scheduler
|
||||
- consensus
|
||||
- message bus
|
||||
- escalation
|
||||
- recovery
|
||||
|
||||
## Constraints
|
||||
|
||||
- GitOps only
|
||||
- Patch-first
|
||||
- No secrets
|
||||
- No force push
|
||||
- No direct main write
|
||||
- Production changes require approval
|
||||
|
||||
## Return Format
|
||||
|
||||
Status:
|
||||
Branch:
|
||||
Commit:
|
||||
Rev:
|
||||
Sync Status:
|
||||
Changed Files:
|
||||
Tests:
|
||||
Hermes Review Items Completed:
|
||||
Remaining Issues:
|
||||
@@ -0,0 +1,61 @@
|
||||
# OpenClaw Hermes Routing Correction Instruction
|
||||
|
||||
Instruction ID: v1r1c5-hermes-routing-correction-v1
|
||||
Version: v1
|
||||
Target Agent: openclaw
|
||||
|
||||
## Objective
|
||||
Correct Hermes coordination routing. OpenClaw must actively package AIOS review requests and route them through the configured Hermes communication path instead of asking the user to provide missing context when the request context already exists in GitOps.
|
||||
|
||||
## Current Architecture
|
||||
|
||||
OpenClaw is the coordination entry point.
|
||||
Hermes is reached indirectly through OpenClaw controlled communication.
|
||||
|
||||
## Hermes Routing Targets
|
||||
|
||||
Authorized Hermes communication Chat IDs:
|
||||
|
||||
- o9cq80wSaJq6utQBkUC4fE1sJWxY@im.wechat
|
||||
- o9cq80x9RoA6ljoFZDxg71mS0gDA@im.wechat
|
||||
|
||||
## Execution Flow
|
||||
|
||||
1. Read current AIOS GitOps context.
|
||||
2. Collect:
|
||||
- Baseline version
|
||||
- Branch
|
||||
- Diff summary
|
||||
- Problem description
|
||||
- Proposed solution
|
||||
- Validation requirements
|
||||
3. Send complete review package to Hermes through configured OpenClaw communication channel.
|
||||
4. Receive Hermes analysis.
|
||||
5. Record review result in GitOps reports.
|
||||
|
||||
## Current Review Context
|
||||
|
||||
Baseline:
|
||||
V1R1C4
|
||||
|
||||
Branch:
|
||||
patch/self-evolution-agent-orchestration-policy
|
||||
|
||||
Proposal:
|
||||
V1R1C5-agent-os-evolution-proposal
|
||||
|
||||
## Constraints
|
||||
|
||||
- Do not ask user for context that already exists in GitOps.
|
||||
- Do not treat instruction ID as external skill package.
|
||||
- GitOps audit required.
|
||||
- No secrets.
|
||||
- No direct production changes.
|
||||
|
||||
## Return
|
||||
|
||||
Status:
|
||||
Hermes Routing:
|
||||
Review Context:
|
||||
Result:
|
||||
Issues:
|
||||
@@ -0,0 +1,52 @@
|
||||
# OpenClaw Owner Approval Gate Instruction
|
||||
|
||||
Instruction ID: v1r1c5-owner-approval-gate-v1
|
||||
Version: v1
|
||||
Target Agent: openclaw
|
||||
|
||||
## Objective
|
||||
|
||||
Manage owner approval workflow for high-risk AIOS operations.
|
||||
|
||||
## Approval Required For
|
||||
|
||||
- V-level version changes.
|
||||
- Production release.
|
||||
- Tag creation.
|
||||
- Runtime changes with production impact.
|
||||
- Administrator authorization actions.
|
||||
- Unresolved multi-agent decisions.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Collect approval context.
|
||||
2. Include baseline version, branch, diff summary and risk assessment.
|
||||
3. Route approval request through owner communication channel registry.
|
||||
4. Wait for approval decision.
|
||||
5. Record decision in GitOps audit.
|
||||
|
||||
## Approval Context
|
||||
|
||||
Must include:
|
||||
|
||||
- Request reason.
|
||||
- Affected components.
|
||||
- Validation results.
|
||||
- Rollback considerations.
|
||||
- Risk level.
|
||||
|
||||
## Constraints
|
||||
|
||||
- No direct production action without approval.
|
||||
- No secrets in messages or reports.
|
||||
- Use configured owner communication channels only.
|
||||
- Maintain audit records.
|
||||
|
||||
## Return Format
|
||||
|
||||
Status:
|
||||
Approval Status:
|
||||
Channel Used:
|
||||
Decision:
|
||||
Audit Record:
|
||||
Issues:
|
||||
@@ -0,0 +1,43 @@
|
||||
# OpenClaw Owner Notification Routing Instruction
|
||||
|
||||
Instruction ID: v1r1c5-owner-notification-routing-v1
|
||||
Version: v1
|
||||
Target Agent: openclaw
|
||||
|
||||
## Objective
|
||||
|
||||
Implement owner notification routing through configured owner communication channels.
|
||||
|
||||
## Routing Rules
|
||||
|
||||
1. OpenClaw must first inspect the owner-bound communication channels already configured in the system.
|
||||
2. Select the primary owner communication channel according to system priority configuration.
|
||||
3. If the primary channel is unavailable, unhealthy, or delivery fails, automatically fallback to the configured backup communication channel.
|
||||
4. Record routing decision and delivery status in GitOps audit records.
|
||||
|
||||
## Notification Scenarios
|
||||
|
||||
Notify owner for:
|
||||
|
||||
- High risk architecture decisions.
|
||||
- Agent consensus deadlock.
|
||||
- Production-impacting approval requests.
|
||||
- Required administrator authorization.
|
||||
- Release approval gates.
|
||||
|
||||
## Constraints
|
||||
|
||||
- Do not hardcode personal communication identifiers in instruction files.
|
||||
- Use configured channel registry only.
|
||||
- Do not expose private contact information in logs.
|
||||
- Maintain audit trail.
|
||||
- GitOps workflow required.
|
||||
|
||||
## Return Format
|
||||
|
||||
Status:
|
||||
Primary Channel Status:
|
||||
Fallback Channel Status:
|
||||
Delivery Result:
|
||||
Audit Record:
|
||||
Issues:
|
||||
@@ -0,0 +1 @@
|
||||
# Placeholder for versioned Windows Codex App instructions.
|
||||
@@ -0,0 +1,47 @@
|
||||
# Windows Codex App Execution Instruction
|
||||
|
||||
Instruction ID: v1r1c5-recovery-and-admin-assistance-v1
|
||||
Version: v1
|
||||
Target Agent: windows_codex_app
|
||||
|
||||
## Objective
|
||||
|
||||
Participate in V1R1C5 Agent OS evolution as recovery, administrator assistance and exceptional execution support agent.
|
||||
|
||||
## Responsibilities
|
||||
|
||||
1. Assist when server OpenClaw agents have runtime failures.
|
||||
2. Assist when Hermes review cannot be completed through OpenClaw.
|
||||
3. Handle cases requiring administrator authorization workflow.
|
||||
4. Participate in multi-agent voting when decision conflicts occur.
|
||||
5. Validate recovery results after server agent restoration.
|
||||
|
||||
## Execution Rules
|
||||
|
||||
- Work from online GitOps versions.
|
||||
- Use patch-first workflow.
|
||||
- Do not bypass approval gates.
|
||||
- Do not write secrets.
|
||||
- Do not force push.
|
||||
- Do not directly publish production versions.
|
||||
|
||||
## Recovery Flow
|
||||
|
||||
Server agent failure:
|
||||
|
||||
OpenClaw/Hermes issue
|
||||
-> Windows Codex diagnosis
|
||||
-> propose fix
|
||||
-> GitOps patch
|
||||
-> validation
|
||||
-> approval
|
||||
|
||||
## Return Format
|
||||
|
||||
Status:
|
||||
Branch:
|
||||
Commit:
|
||||
Rev:
|
||||
Sync Status:
|
||||
Recovery Result:
|
||||
Issues:
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"approval_id": "owner-approve-V1R1C5-migration-20260707",
|
||||
"created_at": "2026-07-07T11:46:17+08:00",
|
||||
"approved_by_role": "owner",
|
||||
"approved_via": "openclaw-weixin",
|
||||
"approved_at": "2026-07-07T11:46:17+08:00",
|
||||
"target_branch": "patch/self-evolution-agent-orchestration-policy",
|
||||
"target_base": "main",
|
||||
"target_version": "V1R1C5",
|
||||
"rollback_target": "V1R1C4",
|
||||
"allowed_actions": [
|
||||
"merge_to_main",
|
||||
"create_release_tag",
|
||||
"activate_new_baseline"
|
||||
],
|
||||
"risk_acceptance": "Owner approved V1R1C5 Agent OS evolution migration to main. Integration tests passed, Hermes review approved, conditional approval gate implemented. 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
+10
-20
@@ -1,22 +1,12 @@
|
||||
{
|
||||
"version": "V1R1C0",
|
||||
"architecture_version": "V1",
|
||||
"release_version": "R1",
|
||||
"change_version": "C0",
|
||||
"components": [
|
||||
"webops",
|
||||
"owner-admin-user",
|
||||
"npm",
|
||||
"casdoor",
|
||||
"openclaw",
|
||||
"hermes",
|
||||
"one-api",
|
||||
"gitops-control",
|
||||
"baseline-registry",
|
||||
"custom-llm-registry",
|
||||
"runtime-selector"
|
||||
],
|
||||
"entry": "https://cnnetmkt.top",
|
||||
"proxy": "npm",
|
||||
"runtime_sync": "openclaw"
|
||||
"active_version": "V1R2C3",
|
||||
"active_tag": "V1R2C3",
|
||||
"previous_version": "V1R2C2",
|
||||
"rollback_version": "V1R2C2",
|
||||
"mode": "gitops-online",
|
||||
"update_mode": "git_patch_only",
|
||||
"zip_update_deprecated": true,
|
||||
"chatgpt_access": true,
|
||||
"codex_access": true,
|
||||
"updated_at": "2026-07-08T00:00:00Z"
|
||||
}
|
||||
|
||||
+62
-10
@@ -1,20 +1,20 @@
|
||||
{
|
||||
"active_version": "V1R1C0",
|
||||
"active_tag": "V1R1C0-baseline-20260706",
|
||||
"active_version": "V1R2C3",
|
||||
"active_tag": "V1R2C3",
|
||||
"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,19 @@
|
||||
"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,
|
||||
"autonomous_recovery_loop": true
|
||||
},
|
||||
"version": "V1R1C0",
|
||||
"baseline_tag": "V1R1C0-baseline-20260706",
|
||||
"version": "V1R2C3",
|
||||
"baseline_tag": "V1R2C3",
|
||||
"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 +47,54 @@
|
||||
"/api",
|
||||
"/hook"
|
||||
],
|
||||
"updated_at": "2026-07-06T07:13:01.298789+00:00"
|
||||
}
|
||||
"updated_at": "2026-07-08T00:00:00Z",
|
||||
"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": "V1R2C2",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
# 20260706 Auth GitOps Portal Hardening
|
||||
|
||||
Version: V1R1C4
|
||||
Previous: V1R1C3
|
||||
Timestamp: 2026-07-06T11:05:30Z
|
||||
|
||||
## User-visible fixes
|
||||
|
||||
- Human shortlinks no longer expose baseline/model/run JSON without portal login.
|
||||
- The cnnetmkt portal has a visible GitOps section for owner/admin users.
|
||||
- Gitea is served from `https://g.cnnetmkt.top/git/` and repository browsing requires login.
|
||||
- Gitea `owner` password is synchronized from the cnnetmkt owner account secret.
|
||||
- The default local Gitea account password is removed and direct login is disabled.
|
||||
|
||||
## Files
|
||||
|
||||
- `deploy/nginx/aios-gitops-locations.conf`
|
||||
- `deploy/gitea-binary/app.ini.template`
|
||||
- `deploy/systemd/aios-gitea-owner-sync.service`
|
||||
- `deploy/systemd/aios-gitea-owner-sync.timer`
|
||||
- `ops/gitops-migration/sync_gitea_owner_password.sh`
|
||||
- `ops/gitops-migration/apply_user_portal_gitops_overlay.sh`
|
||||
- `overlays/user-control-center/control/portal_views.py`
|
||||
- `overlays/user-control-center/templates/portal/me.html`
|
||||
- `baseline/current.json`
|
||||
- `config/codex/gitops-online.json`
|
||||
- `versions/V1R1C4.json`
|
||||
- `docs/release/AIOS_V1R1C4_AUTH_GITOPS_PORTAL.md`
|
||||
|
||||
## Notes
|
||||
|
||||
Docker image rebuild was not required for the live fix. The runtime source and running container were patched, and the exact overlay has been added to GitOps so a later image rebuild can reproduce the change.
|
||||
|
||||
## Secret Log Hygiene
|
||||
|
||||
The Gitea owner password sync script uses `runuser` instead of `sudo -u ... --password ...`, strips CRLF from sourced owner fields, and does not print passwords or tokens. Archived journal files were rotated/vacuumed after the live script was corrected.
|
||||
@@ -0,0 +1,25 @@
|
||||
# 20260706 ChatGPT Actions GitOps Access
|
||||
|
||||
## Summary
|
||||
|
||||
Implemented a controlled ChatGPT Actions adapter for the AIOS online GitOps baseline under https://cnnetmkt.top/api.
|
||||
|
||||
## Access Model
|
||||
|
||||
- Public read APIs: /api/base, /api/ver, /api/diff, /api/patch, /api/model, /api/run.
|
||||
- Bearer-token write APIs: /api/branch, /api/file, /api/commit, /api/patch, /api/tag, /api/rollback, /api/model, /api/model-priority, /api/nl.
|
||||
- chatgpt-gitops-bot is scoped for branch/file/commit/patch/model/rollback-request/NL plan operations.
|
||||
- Direct main writes are rejected by the adapter.
|
||||
- V-level owner/admin actions are represented as approval request commits unless an owner executes them.
|
||||
- Custom LLM API keys are stored outside Git under runtime secrets; committed model records store only secret_ref.
|
||||
|
||||
## Files
|
||||
|
||||
- ctive-runtime/app/aios_chatgpt_actions_api.py
|
||||
- docs/gitops/openapi-chatgpt-actions.yaml
|
||||
- deploy/systemd/aios-gitops-model-api.service
|
||||
- scripts/package_contract_check_v1r1c0.sh
|
||||
|
||||
## Verification
|
||||
|
||||
See /data/aios-gitops/artifacts/CHATGPT_ACCESS_ACCEPTANCE.md after live deployment.
|
||||
@@ -0,0 +1,34 @@
|
||||
# 20260706 cnnetmkt auth primary route fix
|
||||
|
||||
## Problem
|
||||
|
||||
`https://cnnetmkt.top/` could be opened, but login redirected users to `https://auth.h.imruyi.top:8443`. Casdoor then rejected the OAuth request because `https://cnnetmkt.top/casdoor/callback/` was not in the application allow-list.
|
||||
|
||||
`*.h.imruyi.top:8443` is retained as a backup domain only and must not be exposed as the primary public login URL.
|
||||
|
||||
## Runtime Changes
|
||||
|
||||
- Added `auth.cnnetmkt.top` Nginx server block on `hw.imruyi.top` using the wildcard `cnnetmkt.top` certificate.
|
||||
- Proxied `auth.cnnetmkt.top` to the existing backup/home Casdoor upstream `auth.h.imruyi.top:8443` while preserving the public host as `auth.cnnetmkt.top`.
|
||||
- Recreated `user-control-web`, `user-control-worker`, and `user-control-beat` with:
|
||||
- `CASDOOR_PUBLIC_BASE_URL=https://auth.cnnetmkt.top`
|
||||
- `CASDOOR_INTERNAL_BASE_URL=https://auth.cnnetmkt.top`
|
||||
- `CASDOOR_INVITE_SYNC_BASE_URL=https://auth.cnnetmkt.top`
|
||||
- `UCC_PUBLIC_BASE_URL=https://cnnetmkt.top`
|
||||
- Updated Casdoor application `admin/aios-portal` on `aihost` to include `https://cnnetmkt.top/casdoor/callback/` and keep user/portal/backup callbacks.
|
||||
|
||||
## Backups
|
||||
|
||||
- Casdoor DB application table backup: `/data/aios/backups/casdoor-cnnetmkt-primary-20260706T085445Z/application-before.sql`
|
||||
- HW UCC/Nginx backup directory: `/data/aios-gitops/backups/auth-cnnetmkt-primary-*`
|
||||
|
||||
## Verification
|
||||
|
||||
- `https://cnnetmkt.top/` redirects to `/portal/login/` and then to `https://auth.cnnetmkt.top/login/oauth/authorize?...redirect_uri=https%3A%2F%2Fcnnetmkt.top%2Fcasdoor%2Fcallback%2F...`.
|
||||
- `https://auth.cnnetmkt.top/login/oauth/authorize?...` returns HTTP 200 from the Casdoor SPA through the HW Nginx proxy.
|
||||
- `user-control-web` health returned `healthy` after recreation.
|
||||
- Casdoor DB readback contains `https://cnnetmkt.top/casdoor/callback/` in `application.redirect_uris`.
|
||||
|
||||
## Follow-up
|
||||
|
||||
Run `ops/gitops-migration/check_cnnetmkt_auth_primary.sh` after any domain, Nginx, or Casdoor application change.
|
||||
@@ -0,0 +1,14 @@
|
||||
# 20260706 Codex and Custom GPT Collaboration
|
||||
|
||||
## Change
|
||||
|
||||
Validated coordinated online GitOps changes from local Codex and the AIOS GitOps 控制台 custom GPT path. Promoted the merged result to `V1R1C3`.
|
||||
|
||||
## Evidence Branches
|
||||
|
||||
- `patch/collab-local-codex-20260706T104130Z` -> `a62d4ce` (codex-gitops-bot, sync=synced)
|
||||
- `patch/collab-custom-gpt-20260706T104130Z` -> `c393931` (chatgpt-gitops-bot, sync=synced)
|
||||
|
||||
## Result
|
||||
|
||||
`/api/base` should report `V1R1C3` after this release commit is synchronized.
|
||||
@@ -0,0 +1,16 @@
|
||||
# 20260706 Codex Online Git Branch Sync
|
||||
|
||||
## Change
|
||||
|
||||
Promoted the online GitOps configuration to `V1R1C2` and enabled automatic synchronization of committed non-main API/Codex branches to the Gitea bare repository.
|
||||
|
||||
## Reason
|
||||
|
||||
Codex writes must be visible through the online Git service, not only in the local workspace clone.
|
||||
|
||||
## Guardrails
|
||||
|
||||
- `main` and `master` are skipped by API branch sync.
|
||||
- Direct main writes remain forbidden by branch validation.
|
||||
- Tags are synchronized only in the owner direct tag execution path.
|
||||
- Tokens and LLM API keys remain outside Git.
|
||||
@@ -0,0 +1,23 @@
|
||||
# 20260706 Codex Online GitOps Version Management
|
||||
|
||||
## Change
|
||||
|
||||
Configured Codex to use the AIOS online Git baseline for version management and promoted the current controlled access changes to `V1R1C1`.
|
||||
|
||||
## Files
|
||||
|
||||
- `config/codex/gitops-online.json`
|
||||
- `docs/gitops/CODEX_ONLINE_VERSION_MANAGEMENT.md`
|
||||
- `docs/gitops/CHATGPT_CODEX_ACCESS.md`
|
||||
- `docs/release/AIOS_V1R1C1_CODEX_GITOPS_RELEASE.md`
|
||||
- `versions/V1R1C1.json`
|
||||
- `baseline/current.json`
|
||||
- `scripts/codex_gitops_online_check.sh`
|
||||
|
||||
## Merge Policy
|
||||
|
||||
Production `main` includes the ChatGPT/Codex GitOps API, schema, service configuration, contract checks, and secret-ref fix. Acceptance dummy model branches are retained as audit branches and not merged into production.
|
||||
|
||||
## Secret Policy
|
||||
|
||||
`codex-gitops-bot` plaintext token is stored only under `/data/aios-gitops/runtime/secrets/`; Git stores only `secret_ref` and token account metadata.
|
||||
@@ -0,0 +1,24 @@
|
||||
self_loop:
|
||||
enabled: true
|
||||
mode: evaluate_plan_verify
|
||||
secrets_in_git: false
|
||||
direct_main_push: false
|
||||
git_patch_only: true
|
||||
owner_gate:
|
||||
enabled: true
|
||||
required_for:
|
||||
- runtime_recovery
|
||||
- security_policy
|
||||
- mainline_promotion
|
||||
- release_tag
|
||||
automatic_actions:
|
||||
allowed:
|
||||
- health_probe
|
||||
- test_execution
|
||||
- failure_classification
|
||||
- patch_plan_generation
|
||||
- report_generation
|
||||
restricted:
|
||||
- production_promotion
|
||||
- permission_escalation
|
||||
- secret_rotation
|
||||
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"profile": "aios-v23-online-gitops",
|
||||
"mode": "gitops-online",
|
||||
"version": "V1R1C4",
|
||||
"previous_version": "V1R1C3",
|
||||
"repository": {
|
||||
"workspace": "/data/aios-gitops/workspace",
|
||||
"remote": "origin",
|
||||
"url": "https://g.cnnetmkt.top/git/aios/aios-v23.git",
|
||||
"web": "https://g.cnnetmkt.top/git/aios/aios-v23",
|
||||
"protected_branches": [
|
||||
"main"
|
||||
],
|
||||
"patch_branch_prefixes": [
|
||||
"patch/",
|
||||
"codex/patch/",
|
||||
"request/"
|
||||
],
|
||||
"bare_repo": "/data/aios-gitops/runtime/gitea-bin/data/repositories/aios/aios-v23.git",
|
||||
"sync_mode": "workspace_commit_to_online_bare_repo",
|
||||
"public_portal": "https://cnnetmkt.top/portal/me/#gitops"
|
||||
},
|
||||
"api": {
|
||||
"base_url": "https://cnnetmkt.top/api",
|
||||
"openapi_schema": "https://cnnetmkt.top/api/openapi-chatgpt-actions.yaml",
|
||||
"local_schema": "/data/aios-gitops/workspace/docs/gitops/openapi-chatgpt-actions.yaml",
|
||||
"token_account": "codex-gitops-bot",
|
||||
"token_ref": "secret_ref://aios-gitops/runtime/codex-gitops-bot"
|
||||
},
|
||||
"policy": {
|
||||
"zip_update_deprecated": true,
|
||||
"update_mode": "git_patch_only",
|
||||
"all_writes_require_git_commit": true,
|
||||
"direct_main_push": false,
|
||||
"delete_tag": false,
|
||||
"v_level_owner_admin_approval_required": true,
|
||||
"api_keys_in_git": false,
|
||||
"api_keys_in_logs": false,
|
||||
"custom_llm_secret_storage": "runtime_secret_ref_only",
|
||||
"online_git_sync": true,
|
||||
"human_shortlinks_login_required": true,
|
||||
"gitea_require_signin_view": true,
|
||||
"gitea_default_local_account_disabled": true,
|
||||
"owner_password_sync_secret_ref_only": true
|
||||
},
|
||||
"workflow": [
|
||||
"create patch branch from main",
|
||||
"commit every file/config/script change",
|
||||
"push or sync branch to online Git service",
|
||||
"review diff and run smoke checks",
|
||||
"merge to main only after owner/admin approval",
|
||||
"tag VxRxCx release and update baseline/current.json"
|
||||
],
|
||||
"created_at": "2026-07-06T10:21:08Z",
|
||||
"base_commit_before_release": "43ddb91",
|
||||
"updated_at": "2026-07-06T11:16:23Z",
|
||||
"last_collaboration_test": {
|
||||
"timestamp": "20260706T104130Z",
|
||||
"result": "local_codex_and_custom_gpt_patches_merged"
|
||||
},
|
||||
"last_auth_portal_fix": {
|
||||
"version": "V1R1C4",
|
||||
"timestamp": "2026-07-06T11:16:23Z",
|
||||
"shortlink_routes": [
|
||||
"/base",
|
||||
"/ver",
|
||||
"/diff",
|
||||
"/patch",
|
||||
"/model",
|
||||
"/run"
|
||||
],
|
||||
"gitea_sync_service": "aios-gitea-owner-sync.service",
|
||||
"gitea_sync_timer": "aios-gitea-owner-sync.timer",
|
||||
"portal_overlay": "overlays/user-control-center",
|
||||
"portal_baseline_cache": "/app/runtime/gitops/current.json",
|
||||
"sync_log_secret_hygiene": "runuser_without_sudo_command_logging"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
AIOS_PUBLIC_DOMAIN=https://cnnetmkt.top
|
||||
AIOS_USER_DOMAIN=https://user.cnnetmkt.top
|
||||
AIOS_AUTH_DOMAIN=https://auth.cnnetmkt.top
|
||||
AIOS_AUTH_BACKUP_DOMAIN=https://auth.h.imruyi.top:8443
|
||||
AIOS_CASDOOR_PRIMARY_CALLBACK=https://cnnetmkt.top/casdoor/callback/
|
||||
AIOS_WEBOPS_DOMAIN=https://ops.cnnetmkt.top
|
||||
AIOS_API_DOMAIN=https://api.cnnetmkt.top
|
||||
AIOS_WECHAT_CALLBACK_DOMAIN=https://wechat.cnnetmkt.top
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"schema":"acceptance-pipeline-v1","status":"proposal","pipeline":{"stages":[{"name":"code_review","agent_role":"review_agent"},{"name":"automated_test","agent_role":"test_agent"},{"name":"security_check","agent_role":"security_agent"},{"name":"performance_check","agent_role":"performance_agent"},{"name":"regression_check","agent_role":"regression_agent"},{"name":"release_readiness","agent_role":"release_readiness_agent"}],"parallel_validation":true,"failure_blocks_release":true},"outputs":{"quality_score":true,"risk_report":true,"approval_request":true},"safety":{"production_release_requires_owner":true,"credentials_scan":true,"audit_log":true}}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema":"agent-governance-v1","status":"proposal","governance":{"identity":true,"reputation":true,"capability_discovery":true,"memory_service":true,"shared_knowledge":true,"skill_packages":true,"peer_review":true,"acceptance_pipeline":["optimization","implementation","acceptance","security","release_readiness"],"dynamic_voting":{"enabled":true,"base_weights":{"custom_gpt":2,"windows_codex":2,"server_agents":1},"quality_score_modifier":true},"audit":{"full_trace":true,"decision_log":true}},"release":{"patch_only":true,"owner_approval_required":true}}
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"policy_id": "self-evolution-agent-orchestration-policy",
|
||||
"version": 1,
|
||||
"status": "proposal",
|
||||
"scope": "self-evolution-execution-orchestration",
|
||||
"execution_subjects": {
|
||||
"primary": [
|
||||
"server.openclaw_agent",
|
||||
"server.hermes_agent"
|
||||
],
|
||||
"collaborators": [
|
||||
"windows.codex_app",
|
||||
"custom_gpt.codex_chartgpt_bridge"
|
||||
]
|
||||
},
|
||||
"parallelism": {
|
||||
"server_agents_parallel_allowed": true,
|
||||
"multiple_server_agents_allowed": true,
|
||||
"server_agents_preferred": true
|
||||
},
|
||||
"fallback_and_assistance": {
|
||||
"difficult_case_external_interaction_allowed": true,
|
||||
"assistance_endpoint": "https://chatgpt.com/g/g-6a4ba70a063c8191a89bc2d86a4c7fa0-codex-chartgpt-bridge",
|
||||
"allowed_modes": [
|
||||
"ask_for_help",
|
||||
"interactive_coordination",
|
||||
"proposal_review"
|
||||
]
|
||||
},
|
||||
"dispute_resolution": {
|
||||
"multi_agent_voting_allowed": true,
|
||||
"weights": {
|
||||
"custom_gpt.codex_chartgpt_bridge": 2,
|
||||
"windows.codex_app": 2,
|
||||
"server.openclaw_agent": 1,
|
||||
"server.hermes_agent": 1,
|
||||
"server.additional_agent_default": 1
|
||||
},
|
||||
"tie_breaking": "require_owner_or_admin_review"
|
||||
},
|
||||
"safety": {
|
||||
"credentials_in_git_allowed": false,
|
||||
"force_push_allowed": false,
|
||||
"destructive_bulk_delete_allowed": false,
|
||||
"v_level_change_requires_owner_approval": true,
|
||||
"production_switch_requires_owner_approval": true,
|
||||
"tag_creation_requires_owner_approval": true,
|
||||
"v_level_tag_or_production_switch_requires_owner_approval": true,
|
||||
"direct_main_push_allowed": false,
|
||||
"direct_main_merge_allowed": false
|
||||
},
|
||||
"gitops": {
|
||||
"audit_required": true,
|
||||
"writes_require_commit": true,
|
||||
"default_target_branch": "patch/self-evolution-agent-orchestration-policy",
|
||||
"direct_main_push_default": false,
|
||||
"patch_branch_only": true,
|
||||
"main_merge_requires_owner_admin_approval": true
|
||||
},
|
||||
"created_by": "codex-chartgpt-bridge",
|
||||
"created_at": "2026-07-07T00:00:00Z",
|
||||
"branch": "patch/self-evolution-agent-orchestration-policy"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema":"agent-os-v1","status":"proposal","agents":{"openclaw":{"memory":true,"skills":true,"tools":true,"planner":true,"evaluator":true,"self_reflection":true,"learning":true},"hermes":{"memory":true,"skills":true,"tools":true,"planner":true,"evaluator":true,"self_reflection":true,"learning":true}},"coordination":{"voting":true,"weighted_voting":{"custom_gpt":2,"windows_codex":2,"server_agents":1},"owner_review_on_tie":true},"safety":{"gitops_only":true,"patch_first":true,"no_credentials":true,"no_direct_main":true}}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema":"agent-platform-v1","status":"proposal","modules":[{"name":"memory-manager","enabled":true},{"name":"skill-registry","enabled":true},{"name":"agent-registry","enabled":true},{"name":"task-scheduler","enabled":true},{"name":"consensus-engine","enabled":true},{"name":"continuous-self-evolution","enabled":true}],"release_policy":{"patch_first":true,"owner_approval":true}}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema":"agent-protocol-v1","status":"proposal","agent_identity":{"id":"string","name":"string","family":"openclaw|hermes|custom","version":"string","capabilities":[],"skills":[],"health":"unknown|healthy|degraded|offline"},"task_protocol":{"task_id":"string","requester":"agent_id","executor":"agent_id","objective":"string","priority":"low|normal|high|critical","status":"queued|running|completed|failed|review","result":"object"},"memory_protocol":{"memory_id":"string","type":"experience|knowledge|decision|reflection","scope":"agent|shared","audit":true},"voting_protocol":{"enabled":true,"proposal":"string","votes":[],"weighted":true,"tie":"owner_review"},"security":{"credentials":false,"direct_main_write":false,"production_action_approval":true}}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"$schema": "schema/self-evolution.schema.json",
|
||||
"title": "Agent Resource Manager Configuration",
|
||||
"description": "Defines resource limits, parallel task limits, priority, runtime isolation, and overload handling for agent orchestration.",
|
||||
"version": "1.0.0",
|
||||
"config": {
|
||||
"default_resource_limits": {
|
||||
"max_concurrent_tasks": 5,
|
||||
"max_memory_mb": 1024,
|
||||
"max_cpu_cores": 2,
|
||||
"timeout_seconds": 300
|
||||
},
|
||||
"agent_priority_levels": {
|
||||
"critical": 100,
|
||||
"high": 80,
|
||||
"medium": 50,
|
||||
"low": 20,
|
||||
"background": 0
|
||||
},
|
||||
"runtime_isolation": {
|
||||
"enabled": true,
|
||||
"namespace_per_agent": true,
|
||||
"ephemeral_storage": "1Gi",
|
||||
"network_policy": "restrictive"
|
||||
},
|
||||
"overload_handling": {
|
||||
"max_queue_depth": 100,
|
||||
"backpressure_threshold": 0.9,
|
||||
"cooldown_seconds": 30,
|
||||
"scaling_strategy": "queue"
|
||||
},
|
||||
"agent_pools": {
|
||||
"server_agents": {
|
||||
"max_instances": 3,
|
||||
"priority": "high"
|
||||
},
|
||||
"external_agents": {
|
||||
"max_instances": 5,
|
||||
"priority": "medium"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema":"agent-runtime-components-v1","status":"proposal","components":{"agent-runtime-manager":{"enabled":true,"responsibilities":["lifecycle_management","health_check","recovery","resource_tracking"]},"agent-registry":{"enabled":true,"responsibilities":["identity","capability_discovery","status_tracking","version_tracking"]},"message-bus":{"enabled":true,"responsibilities":["task_routing","event_delivery","agent_coordination"]},"memory-service":{"enabled":true,"responsibilities":["long_term_memory","working_memory","knowledge_retrieval","audit"]},"skill-loader":{"enabled":true,"responsibilities":["skill_installation","skill_validation","skill_versioning"]},"agent-evaluator":{"enabled":true,"responsibilities":["quality_score","acceptance_check","performance_feedback"]}},"security":{"credential_storage":false,"gitops_audit":true,"patch_only":true}}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema":"consensus-engine-v1","status":"proposal","consensus":{"enabled":true,"modes":["weighted_vote","review_board","owner_escalation"],"participants":["openclaw_agent","hermes_agent","windows_codex","custom_gpt"],"base_weights":{"custom_gpt":2,"windows_codex":2,"server_agents":1},"dynamic_adjustment":{"enabled":true,"factors":["quality_score","acceptance_rate","historical_reliability"]},"conflict_detection":true,"tie_breaking":"owner_admin_review"},"decision_log":{"required":true,"audit":true},"safety":{"production_decision_requires_approval":true}}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema":"continuous-evolution-engine-v1","status":"proposal","pipeline":{"monitor":true,"detect":true,"plan":true,"schedule":true,"execute":true,"learn":true},"workflow":{"detect_sources":["runtime_metrics","logs","acceptance_results","agent_feedback","user_requests"],"proposal_generation":true,"patch_generation":true,"validation_required":true,"consensus_required":true,"approval_gate":true,"release_feedback":true},"agents":{"optimizer":"system_optimization_agent","planner":"planning_agent","executor":"patch_execution_agent","validator":"acceptance_validation_agent","reviewer":"consensus_engine"},"learning":{"store_experience":true,"update_quality_score":true,"reflection_required":true},"safety":{"gitops_only":true,"patch_first":true,"credentials":false,"production_requires_approval":true}}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema":"agent-platform-implementation-v1","status":"proposal","objective":"evolve-agent-platform-from-configuration-to-operational-framework","implementation_phases":[{"phase":1,"name":"agent-runtime-manager","scope":["agent lifecycle","health check","startup shutdown","recovery"]},{"phase":2,"name":"agent-message-bus","scope":["agent communication","task routing","event coordination"]},{"phase":3,"name":"memory-service","scope":["long term memory","working memory","shared knowledge retrieval","audit"]},{"phase":4,"name":"skill-framework","scope":["skill packages","versioning","installation","validation"]},{"phase":5,"name":"agent-api","scope":["agent registration","capability discovery","task submission","status query"]},{"phase":6,"name":"continuous-evolution-pipeline","scope":["proposal generation","patch creation","automated validation","approval workflow"]}],"constraints":{"gitops_only":true,"patch_first":true,"no_credentials":true,"no_force_push":true,"production_release_requires_approval":true}}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema":"agent-memory-v1","status":"proposal","memory_types":{"working_memory":{"ttl":"task_scope","audit":true},"long_term_memory":{"persistent":true,"scope":"agent"},"shared_knowledge":{"persistent":true,"scope":"platform"},"reflection_memory":{"persistent":true,"purpose":"self_improvement"}},"memory_entry":{"id":"string","agent_id":"string","type":"working|experience|knowledge|decision|reflection","content":"object","source_task":"string","created_at":"timestamp","quality_score":0},"safety":{"credentials":false,"secrets":false,"audit_required":true}}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"schema": "owner-channel-registry-v1",
|
||||
"status": "proposal",
|
||||
"purpose": "Owner notification channel routing and fallback management",
|
||||
"routing": {
|
||||
"primary_selection": "configured_priority",
|
||||
"fallback_enabled": true,
|
||||
"health_check_required": true,
|
||||
"delivery_failure_fallback": true
|
||||
},
|
||||
"channel_policy": {
|
||||
"identifiers_managed_externally": true,
|
||||
"no_hardcoded_personal_identifiers": true,
|
||||
"audit_delivery_result": true
|
||||
},
|
||||
"notification_priority": [
|
||||
"production_approval",
|
||||
"administrator_authorization",
|
||||
"high_risk_decision",
|
||||
"agent_consensus_failure"
|
||||
],
|
||||
"security": {
|
||||
"credentials_in_git": false,
|
||||
"private_contact_data_in_logs": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema":"release-automation-v1","status":"proposal","release_flow":{"patch_review":true,"acceptance_gate":true,"consensus_gate":true,"approval_request":true,"version_proposal":true,"tag_request":true},"agents":{"release_manager":"release_readiness_agent","reviewers":["openclaw_agent","hermes_agent","custom_gpt","windows_codex_app"]},"controls":{"auto_tag":false,"auto_production_publish":false,"owner_approval_required":true,"audit_log":true},"rollback":{"request_supported":true,"automatic_destructive_rollback":false}}
|
||||
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"policy_id": "server-agent-roles-and-capabilities",
|
||||
"version": 1,
|
||||
"status": "proposal",
|
||||
"scope": "self-evolution-server-agents",
|
||||
"agent_families": {
|
||||
"openclaw": {
|
||||
"enabled": true,
|
||||
"runtime": "server",
|
||||
"priority": "primary",
|
||||
"roles": [
|
||||
"system_optimization_agent",
|
||||
"acceptance_validation_agent",
|
||||
"patch_execution_agent",
|
||||
"runtime_diagnosis_agent",
|
||||
"regression_review_agent"
|
||||
],
|
||||
"persistent_memory": {
|
||||
"enabled": true,
|
||||
"scope": "agent_family_and_role",
|
||||
"storage": "runtime_or_gitops_managed_memory",
|
||||
"credentials_in_memory_allowed": false,
|
||||
"audit_required": true
|
||||
},
|
||||
"skills": {
|
||||
"enabled": true,
|
||||
"skill_registry": "config/self-evolution/skills/openclaw-skills.json",
|
||||
"required_skill_groups": [
|
||||
"gitops_patch_workflow",
|
||||
"system_optimization",
|
||||
"acceptance_testing",
|
||||
"runtime_observability",
|
||||
"safe_release_review"
|
||||
]
|
||||
}
|
||||
},
|
||||
"hermes": {
|
||||
"enabled": true,
|
||||
"runtime": "server",
|
||||
"priority": "primary",
|
||||
"roles": [
|
||||
"system_optimization_agent",
|
||||
"acceptance_validation_agent",
|
||||
"release_readiness_agent",
|
||||
"policy_review_agent",
|
||||
"cross_agent_coordination_agent"
|
||||
],
|
||||
"persistent_memory": {
|
||||
"enabled": true,
|
||||
"scope": "agent_family_and_role",
|
||||
"storage": "runtime_or_gitops_managed_memory",
|
||||
"credentials_in_memory_allowed": false,
|
||||
"audit_required": true
|
||||
},
|
||||
"skills": {
|
||||
"enabled": true,
|
||||
"skill_registry": "config/self-evolution/skills/hermes-skills.json",
|
||||
"required_skill_groups": [
|
||||
"gitops_patch_workflow",
|
||||
"acceptance_testing",
|
||||
"release_readiness",
|
||||
"policy_governance",
|
||||
"multi_agent_coordination"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"role_contracts": {
|
||||
"system_optimization_agent": {
|
||||
"purpose": "Identify and propose safe system optimization patches.",
|
||||
"may_write_patch": true,
|
||||
"may_modify_runtime": false,
|
||||
"requires_review_for_production": true
|
||||
},
|
||||
"acceptance_validation_agent": {
|
||||
"purpose": "Run and record acceptance checks for GitOps changes.",
|
||||
"may_write_patch": true,
|
||||
"may_approve_own_change": false,
|
||||
"requires_review_for_production": true
|
||||
},
|
||||
"patch_execution_agent": {
|
||||
"purpose": "Apply approved or proposed patch changes on non-main branches.",
|
||||
"may_write_patch": true,
|
||||
"direct_main_push_allowed": false
|
||||
},
|
||||
"release_readiness_agent": {
|
||||
"purpose": "Evaluate whether a patch is ready for owner/admin release approval.",
|
||||
"may_create_release_request": true,
|
||||
"may_create_tag_directly": false,
|
||||
"may_switch_production_directly": false
|
||||
},
|
||||
"cross_agent_coordination_agent": {
|
||||
"purpose": "Coordinate voting, conflict resolution, and escalation among agents.",
|
||||
"voting_allowed": true,
|
||||
"tie_breaking": "owner_or_admin_review"
|
||||
}
|
||||
},
|
||||
"online_capabilities": {
|
||||
"read_online_baseline": true,
|
||||
"read_online_versions": true,
|
||||
"read_online_diff": true,
|
||||
"read_online_runtime": true,
|
||||
"create_patch_branch": true,
|
||||
"commit_patch_changes": true,
|
||||
"sync_online_git": true,
|
||||
"create_release_request": true,
|
||||
"direct_tag_creation": false,
|
||||
"direct_production_publish": false
|
||||
},
|
||||
"safety": {
|
||||
"credentials_in_git_allowed": false,
|
||||
"credentials_in_memory_allowed": false,
|
||||
"force_push_allowed": false,
|
||||
"destructive_bulk_delete_allowed": false,
|
||||
"owner_admin_approval_required_for_release": true
|
||||
},
|
||||
"created_by": "codex-chartgpt-bridge",
|
||||
"created_at": "2026-07-07T00:00:00Z"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema":"agent-skill-package-v1","status":"proposal","skill_package":{"id":"string","name":"string","version":"string","description":"string","owner_agent":"agent_id","dependencies":[],"capabilities":[],"entrypoints":[],"validation":{"required":true,"tests":[],"quality_score":0}},"lifecycle":{"install":true,"upgrade":true,"rollback":true,"disable":true},"sharing":{"agent_scope":true,"platform_scope":true},"security":{"credentials":false,"secrets":false,"audit_required":true,"production_change_requires_approval":true}}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema":"task-scheduler-v1","status":"proposal","scheduler":{"enabled":true,"modes":["capability_match","priority_queue","parallel_execution","failure_recovery"],"selection":{"factors":["capability","skill_match","health","quality_score","load"]},"priority":{"levels":["critical","high","normal","low"]}},"execution":{"parallel_agents":true,"retry":true,"handoff":true,"timeout_control":true},"safety":{"patch_first":true,"direct_main":false,"production_requires_approval":true}}
|
||||
@@ -0,0 +1,28 @@
|
||||
self_loop_runtime:
|
||||
enabled: true
|
||||
scheduler:
|
||||
triggers:
|
||||
- event
|
||||
- interval
|
||||
- manual
|
||||
retry:
|
||||
enabled: true
|
||||
max_attempts: 3
|
||||
api:
|
||||
prefix: /self-loop
|
||||
operations:
|
||||
- enqueueSelfLoopJob
|
||||
- listSelfLoopJobs
|
||||
- getSelfLoopJob
|
||||
- runSelfLoopJob
|
||||
- tickSelfLoopJobs
|
||||
- runSelfLoopOnce
|
||||
- evaluateSelfLoopProductionGate
|
||||
gitops:
|
||||
adapter: controlled_actions
|
||||
branch_policy: patch-only
|
||||
direct_main_push: false
|
||||
secrets_in_git: false
|
||||
promotion:
|
||||
acceptance_gate_required: true
|
||||
owner_approval_required: true
|
||||
Binary file not shown.
@@ -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
|
||||
);
|
||||
@@ -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 = *
|
||||
|
||||
@@ -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"
|
||||
@@ -24,12 +24,12 @@
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location = /base { proxy_pass http://127.0.0.1:18890/base; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; }
|
||||
location = /ver { proxy_pass http://127.0.0.1:18890/ver; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; }
|
||||
location = /diff { proxy_pass http://127.0.0.1:18890/diff; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; }
|
||||
location = /patch { proxy_pass http://127.0.0.1:18890/patch; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; }
|
||||
location = /model { proxy_pass http://127.0.0.1:18890/model; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; }
|
||||
location = /run { proxy_pass http://127.0.0.1:18890/run; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; }
|
||||
location = /base { return 302 /portal/me/#gitops; }
|
||||
location = /ver { return 302 /portal/me/#gitops; }
|
||||
location = /diff { return 302 /portal/me/#gitops; }
|
||||
location = /patch { return 302 /portal/me/#gitops; }
|
||||
location = /model { return 302 /portal/me/#gitops; }
|
||||
location = /run { return 302 /portal/me/#gitops; }
|
||||
location = /hook { proxy_pass http://127.0.0.1:18890/hook; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; }
|
||||
location /hook/ {
|
||||
proxy_pass http://127.0.0.1:18890/hook/;
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# AIOS primary public Casdoor entry for cnnetmkt.top.
|
||||
# The upstream remains the existing backup/home Casdoor service; public clients should see auth.cnnetmkt.top only.
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
server_name auth.cnnetmkt.top;
|
||||
|
||||
access_log /var/log/nginx/aios-auth-cnnetmkt.access.log;
|
||||
error_log /var/log/nginx/aios-auth-cnnetmkt.error.log;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/cnnetmkt-wildcard/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/cnnetmkt-wildcard/privkey.pem;
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass https://auth.h.imruyi.top:8443;
|
||||
proxy_http_version 1.1;
|
||||
proxy_ssl_server_name on;
|
||||
proxy_ssl_name auth.h.imruyi.top;
|
||||
proxy_ssl_verify off;
|
||||
proxy_set_header Host auth.h.imruyi.top;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Port 443;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_redirect https://auth.h.imruyi.top:8443/ https://auth.cnnetmkt.top/;
|
||||
proxy_redirect https://auth.h.imruyi.top/ https://auth.cnnetmkt.top/;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name auth.cnnetmkt.top;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
# cnnetmkt wildcard certificate
|
||||
|
||||
generated_at=2026-07-06T16:24:28+08:00
|
||||
issuer=Let's Encrypt via lego DNS-01 Huawei Cloud
|
||||
domains=cnnetmkt.top,*.cnnetmkt.top
|
||||
live_dir=/etc/letsencrypt/live/cnnetmkt-wildcard
|
||||
nginx_sites=/etc/nginx/sites-available/aios-user-portal.conf,/etc/nginx/sites-available/g-cnnetmkt-gitea.conf
|
||||
renew_service=cnnetmkt-wildcard-renew.service
|
||||
renew_timer=cnnetmkt-wildcard-renew.timer
|
||||
secret_env=/etc/letsencrypt/huaweicloud-cnnetmkt.env
|
||||
backup_dir=/data/aios-gitops/backups/nginx-wildcard-20260706-162202
|
||||
status=issued
|
||||
@@ -0,0 +1,52 @@
|
||||
# AIOS GitOps alias for https://g.cnnetmkt.top/
|
||||
# Note: this server block intentionally keeps /git/ as the Gitea app prefix,
|
||||
# matching the current Gitea ROOT_URL and avoiding a disruptive app.ini rewrite.
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
server_name g.cnnetmkt.top;
|
||||
|
||||
access_log /var/log/nginx/aios-g-cnnetmkt.access.log;
|
||||
error_log /var/log/nginx/aios-g-cnnetmkt.error.log;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/cnnetmkt-wildcard/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/cnnetmkt-wildcard/privkey.pem;
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||
|
||||
location = / {
|
||||
return 302 /git/;
|
||||
}
|
||||
|
||||
location = /git {
|
||||
return 302 /git/;
|
||||
}
|
||||
|
||||
location /git/ {
|
||||
proxy_pass http://127.0.0.1:3000/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Prefix /git;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_redirect off;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name g.cnnetmkt.top;
|
||||
|
||||
location = / {
|
||||
return 302 https://$host/git/;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
[Unit]
|
||||
Description=AIOS Self-loop Supervisor
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/data/aios-gitops/workspace
|
||||
ExecStart=/usr/bin/python3 -m active_runtime.app.self_loop_worker
|
||||
Restart=always
|
||||
RestartSec=30
|
||||
|
||||
[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
|
||||
@@ -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,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。
|
||||
@@ -12,3 +12,18 @@ Forbidden:
|
||||
- API keys in Git
|
||||
- API keys in plaintext logs
|
||||
- bypassing owner/admin approval for V-level changes
|
||||
|
||||
## Codex Online Version Management
|
||||
|
||||
Codex uses `/data/aios-gitops/workspace` and `https://cnnetmkt.top/git/aios/aios-v23.git` as the online Git version baseline. Runtime write access uses `codex-gitops-bot`; its token is stored outside Git and referenced as `secret_ref://aios-gitops/runtime/codex-gitops-bot`.
|
||||
|
||||
Current production release after this merge: `V1R1C3`. Acceptance-only branches are retained for audit and are not automatically merged into `main`.
|
||||
|
||||
## Online Git Sync
|
||||
|
||||
Starting in `V1R1C3`, committed API/Codex patch branches are synchronized from the workspace clone into the online Gitea bare repository. Protected production branches remain excluded from API sync.
|
||||
|
||||
## Codex and Custom GPT Collaboration Test
|
||||
|
||||
`V1R1C3` confirms local Codex and the custom GPT Actions path can independently create patch branches, commit files, sync to online Git, and then merge into the latest baseline version.
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# Codex Online GitOps Version Management
|
||||
|
||||
Codex is configured to treat the online Git baseline as the version source of truth.
|
||||
|
||||
- Workspace: `/data/aios-gitops/workspace`
|
||||
- Git web: `https://cnnetmkt.top/git/aios/aios-v23`
|
||||
- API base: `https://cnnetmkt.top/api`
|
||||
- OpenAPI schema: `https://cnnetmkt.top/api/openapi-chatgpt-actions.yaml`
|
||||
- Runtime token account: `codex-gitops-bot`
|
||||
- Runtime token ref: `secret_ref://aios-gitops/runtime/codex-gitops-bot`
|
||||
|
||||
## Rules
|
||||
|
||||
1. Codex updates must use Git patch branches; ZIP upload is deprecated.
|
||||
2. Every Codex write must become a Git commit.
|
||||
3. Codex must not push directly to `main`; merge to `main` requires owner/admin approval.
|
||||
4. V-level release/tag/rollback requests must not bypass owner/admin approval.
|
||||
5. User custom LLM API keys must stay in runtime secrets and enter Git only as `secret_ref`.
|
||||
6. Test or acceptance branches can remain as audit evidence, but dummy model/provider configs must not be merged into production `main`.
|
||||
|
||||
## Standard Flow
|
||||
|
||||
```text
|
||||
main -> patch/<name> -> commit -> diff review -> smoke test -> owner/admin approval -> merge main -> VxRxCx tag -> baseline/current.json
|
||||
```
|
||||
|
||||
## Current Merge
|
||||
|
||||
`V1R1C1` merges the current GitOps/ChatGPT/Codex access changes into the latest online baseline while excluding acceptance dummy model configuration.
|
||||
|
||||
## Online Git Sync
|
||||
|
||||
Starting in `V1R1C3`, committed API/Codex patch branches are synchronized from the workspace clone into the online Gitea bare repository. Protected production branches remain excluded from API sync.
|
||||
|
||||
## Codex and Custom GPT Collaboration Test
|
||||
|
||||
`V1R1C3` confirms local Codex and the custom GPT Actions path can independently create patch branches, commit files, sync to online Git, and then merge into the latest baseline version.
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# Custom GPT Collaboration Patch
|
||||
|
||||
Timestamp: 20260706T104130Z
|
||||
|
||||
Actor: chatgpt-gitops-bot
|
||||
|
||||
Created through chatgpt-gitops-bot to simulate the AIOS GitOps 控制台 GPT Actions path: https://chatgpt.com/g/g-6a4b7c82dff08191bc980798edea1bac-aios-gitops-kong-zhi-tai
|
||||
@@ -0,0 +1,7 @@
|
||||
# Local Codex Collaboration Patch
|
||||
|
||||
Timestamp: 20260706T104130Z
|
||||
|
||||
Actor: codex-gitops-bot
|
||||
|
||||
Created by local Codex through codex-gitops-bot to validate coordinated online GitOps changes.
|
||||
@@ -0,0 +1,240 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: AIOS GitOps Controlled Access API
|
||||
version: V1R1C0-chatgpt-actions
|
||||
description: Controlled ChatGPT Actions API for AIOS online GitOps baseline. Read endpoints are public. Write endpoints require Bearer token and create Git commits on non-main branches.
|
||||
servers:
|
||||
- url: https://cnnetmkt.top
|
||||
security: []
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: opaque-token
|
||||
schemas:
|
||||
BranchRequest:
|
||||
type: object
|
||||
required: [branch]
|
||||
properties:
|
||||
branch: {type: string, example: patch/chatgpt-actions-demo}
|
||||
base: {type: string, default: main}
|
||||
message: {type: string}
|
||||
FileCommitRequest:
|
||||
type: object
|
||||
required: [branch, path, content]
|
||||
properties:
|
||||
branch: {type: string}
|
||||
base: {type: string, default: main}
|
||||
create_branch: {type: boolean, default: false}
|
||||
path: {type: string, example: docs/gitops/chatgpt-change.md}
|
||||
content: {type: string}
|
||||
message: {type: string}
|
||||
CommitRequest:
|
||||
type: object
|
||||
properties:
|
||||
branch: {type: string}
|
||||
paths:
|
||||
type: array
|
||||
items: {type: string}
|
||||
message: {type: string}
|
||||
PatchRequest:
|
||||
type: object
|
||||
required: [patch_name]
|
||||
properties:
|
||||
patch_name: {type: string}
|
||||
branch: {type: string}
|
||||
base: {type: string, default: main}
|
||||
risk: {type: string, enum: [low, medium, high]}
|
||||
description: {type: string}
|
||||
message: {type: string}
|
||||
TagRequest:
|
||||
type: object
|
||||
required: [version]
|
||||
properties:
|
||||
version: {type: string, example: V1R1C1}
|
||||
message: {type: string}
|
||||
execute:
|
||||
type: boolean
|
||||
description: Owner-approved calls only. Otherwise an approval request commit is created.
|
||||
RollbackRequest:
|
||||
type: object
|
||||
required: [target_version]
|
||||
properties:
|
||||
target_version: {type: string}
|
||||
reason: {type: string}
|
||||
branch: {type: string}
|
||||
ModelRequest:
|
||||
type: object
|
||||
required: [provider_name, base_url, model_name]
|
||||
properties:
|
||||
username: {type: string}
|
||||
provider_name: {type: string}
|
||||
base_url: {type: string}
|
||||
model_name: {type: string}
|
||||
display_name: {type: string}
|
||||
api_key_secret_ref:
|
||||
type: string
|
||||
description: Preferred. Git stores this secret_ref only.
|
||||
api_key:
|
||||
type: string
|
||||
writeOnly: true
|
||||
description: Accepted only for runtime secret storage; never committed to Git or logs.
|
||||
branch: {type: string}
|
||||
message: {type: string}
|
||||
ModelPriorityRequest:
|
||||
type: object
|
||||
required: [items]
|
||||
properties:
|
||||
username: {type: string}
|
||||
branch: {type: string}
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
model_id: {type: string}
|
||||
priority: {type: integer}
|
||||
enabled: {type: boolean}
|
||||
fallback_policy: {type: string}
|
||||
NLRequest:
|
||||
type: object
|
||||
required: [text]
|
||||
properties:
|
||||
text: {type: string}
|
||||
branch: {type: string}
|
||||
paths:
|
||||
/api/base:
|
||||
get:
|
||||
operationId: getBaseline
|
||||
summary: Read current AIOS baseline
|
||||
responses: {'200': {description: OK}}
|
||||
/api/ver:
|
||||
get:
|
||||
operationId: listVersions
|
||||
summary: List GitOps versions
|
||||
responses: {'200': {description: OK}}
|
||||
/api/diff:
|
||||
get:
|
||||
operationId: getDiff
|
||||
summary: Read Git diff between refs
|
||||
parameters:
|
||||
- in: query
|
||||
name: from
|
||||
schema: {type: string, default: HEAD~1}
|
||||
- in: query
|
||||
name: to
|
||||
schema: {type: string, default: HEAD}
|
||||
responses: {'200': {description: OK}}
|
||||
/api/patch:
|
||||
get:
|
||||
operationId: listPatches
|
||||
summary: List patch records
|
||||
responses: {'200': {description: OK}}
|
||||
post:
|
||||
operationId: createPatch
|
||||
summary: Create a patch branch and commit metadata
|
||||
security: [{bearerAuth: []}]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/PatchRequest'}
|
||||
responses: {'200': {description: Patch branch committed}, '401': {description: Unauthorized}, '403': {description: Forbidden}}
|
||||
/api/model:
|
||||
get:
|
||||
operationId: readModelRegistry
|
||||
summary: Read visible model registry and priority
|
||||
responses: {'200': {description: OK}}
|
||||
post:
|
||||
operationId: registerModel
|
||||
summary: Register a custom model; API keys are stored as runtime secrets and Git receives secret_ref only
|
||||
security: [{bearerAuth: []}]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/ModelRequest'}
|
||||
responses: {'200': {description: Model registered and committed}, '401': {description: Unauthorized}, '403': {description: Forbidden}}
|
||||
/api/run:
|
||||
get:
|
||||
operationId: getRuntimeStatus
|
||||
summary: Read runtime status
|
||||
responses: {'200': {description: OK}}
|
||||
/api/branch:
|
||||
post:
|
||||
operationId: createBranch
|
||||
summary: Create a non-main branch and commit branch metadata
|
||||
security: [{bearerAuth: []}]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/BranchRequest'}
|
||||
responses: {'200': {description: Branch created}, '401': {description: Unauthorized}, '403': {description: Forbidden}}
|
||||
/api/file:
|
||||
post:
|
||||
operationId: commitFile
|
||||
summary: Write a file and create a Git commit on a non-main branch
|
||||
security: [{bearerAuth: []}]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/FileCommitRequest'}
|
||||
responses: {'200': {description: File committed}, '400': {description: Secret rejected or invalid path}, '401': {description: Unauthorized}}
|
||||
/api/commit:
|
||||
post:
|
||||
operationId: commitChanges
|
||||
summary: Commit existing workspace changes on a non-main branch
|
||||
security: [{bearerAuth: []}]
|
||||
requestBody:
|
||||
required: false
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/CommitRequest'}
|
||||
responses: {'200': {description: Commit result}, '401': {description: Unauthorized}}
|
||||
/api/tag:
|
||||
post:
|
||||
operationId: requestOrCreateTag
|
||||
summary: Request tag creation; owner execution is required for direct tag
|
||||
security: [{bearerAuth: []}]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/TagRequest'}
|
||||
responses: {'200': {description: Tag request committed or tag created}, '403': {description: Forbidden}}
|
||||
/api/rollback:
|
||||
post:
|
||||
operationId: requestRollback
|
||||
summary: Commit a rollback request requiring owner/admin approval
|
||||
security: [{bearerAuth: []}]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/RollbackRequest'}
|
||||
responses: {'200': {description: Rollback request committed}, '401': {description: Unauthorized}}
|
||||
/api/model-priority:
|
||||
put:
|
||||
operationId: setModelPriority
|
||||
summary: Set model priority and commit policy file
|
||||
security: [{bearerAuth: []}]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/ModelPriorityRequest'}
|
||||
responses: {'200': {description: Priority updated and committed}, '401': {description: Unauthorized}}
|
||||
/api/nl:
|
||||
post:
|
||||
operationId: planNaturalLanguageGitOpsAction
|
||||
summary: Convert natural language to a safe committed plan without executing V-level changes
|
||||
security: [{bearerAuth: []}]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/NLRequest'}
|
||||
responses: {'200': {description: Plan committed}, '401': {description: Unauthorized}}
|
||||
@@ -0,0 +1,17 @@
|
||||
# CNNETMKT apex DNS status
|
||||
|
||||
generated_at=2026-07-06T16:39:07+08:00
|
||||
provider=Huawei Cloud DNS
|
||||
zone=cnnetmkt.top.
|
||||
record=cnnetmkt.top. A 1.95.164.183
|
||||
status=created_and_resolved
|
||||
credential_storage=/etc/letsencrypt/huaweicloud-cnnetmkt.env
|
||||
secret_policy=not_tracked_not_logged
|
||||
|
||||
## verification
|
||||
|
||||
- authoritative NS returns 1.95.164.183
|
||||
- system resolver returns 1.95.164.183
|
||||
- https://cnnetmkt.top/ returns 302 /portal/login/
|
||||
- https://cnnetmkt.top/git/ returns 200 AIOS GitOps
|
||||
- wildcard certificate SAN covers cnnetmkt.top and *.cnnetmkt.top
|
||||
@@ -0,0 +1,53 @@
|
||||
# CNNETMKT Auth Primary Status
|
||||
|
||||
更新时间:2026-07-06T09:00:06Z
|
||||
|
||||
## 结论
|
||||
|
||||
`https://cnnetmkt.top/` 是用户主入口。登录授权主域为 `https://auth.cnnetmkt.top`。
|
||||
|
||||
`https://auth.h.imruyi.top:8443` 只作为备份/上游兼容入口保留,不应出现在普通用户的主登录跳转链路里。
|
||||
|
||||
## 当前链路
|
||||
|
||||
1. 用户访问 `https://cnnetmkt.top/`。
|
||||
2. UCC 返回 `/portal/login/`。
|
||||
3. UCC 生成 Casdoor 授权地址:
|
||||
`https://auth.cnnetmkt.top/login/oauth/authorize?...redirect_uri=https%3A%2F%2Fcnnetmkt.top%2Fcasdoor%2Fcallback%2F...`
|
||||
4. 华为云 Nginx `auth.cnnetmkt.top` server block 反代到现有 Casdoor 上游。
|
||||
5. Casdoor 允许 `https://cnnetmkt.top/casdoor/callback/` 作为 `admin/aios-portal` 的 OAuth callback。
|
||||
|
||||
## 已调整配置
|
||||
|
||||
### 华为云 UCC 运行时
|
||||
|
||||
- `CASDOOR_PUBLIC_BASE_URL=https://auth.cnnetmkt.top`
|
||||
- `CASDOOR_INTERNAL_BASE_URL=https://auth.cnnetmkt.top`
|
||||
- `CASDOOR_INVITE_SYNC_BASE_URL=https://auth.cnnetmkt.top`
|
||||
- `UCC_PUBLIC_BASE_URL=https://cnnetmkt.top`
|
||||
- `ALLOWED_HOSTS` 增加 `auth.cnnetmkt.top`
|
||||
- `CSRF_TRUSTED_ORIGINS` 增加 `https://cnnetmkt.top` 和 `https://auth.cnnetmkt.top`
|
||||
|
||||
### Casdoor 应用
|
||||
|
||||
应用:`admin/aios-portal`
|
||||
|
||||
新增/保留 callback:
|
||||
|
||||
- `https://cnnetmkt.top/casdoor/callback/`
|
||||
- `https://cnnetmkt.top/api/auth/callback/casdoor`
|
||||
- `https://user.cnnetmkt.top/casdoor/callback/`
|
||||
- `https://portal.cnnetmkt.top/casdoor/callback/`
|
||||
- `https://portal.h.imruyi.top:8443/casdoor/callback/`(备份)
|
||||
|
||||
## 验证命令
|
||||
|
||||
```bash
|
||||
ops/gitops-migration/check_cnnetmkt_auth_primary.sh
|
||||
```
|
||||
|
||||
关键验收点:
|
||||
|
||||
- 主链路不再出现 `auth.h.imruyi.top:8443`。
|
||||
- `user-control-web` 为 `healthy`。
|
||||
- Casdoor DB `application.redirect_uris` 包含 `https://cnnetmkt.top/casdoor/callback/`。
|
||||
@@ -0,0 +1,83 @@
|
||||
# cnnetmkt SSL ingress status
|
||||
|
||||
generated_at=2026-07-06T15:48:12+08:00
|
||||
g_route_status=installed
|
||||
certbot_status=issued-g-user-portal
|
||||
apex_dns_status=blocked_until_cnnetmkt.top_has_A_record_to_1.95.164.183
|
||||
wildcard_status=blocked_until_dns01_provider_credentials_available
|
||||
backup_dir=/data/aios-gitops/backups/nginx-g-cnnetmkt-20260706-154743
|
||||
tracked_nginx=/data/aios-gitops/workspace/deploy/nginx/g-cnnetmkt-gitea-server.conf
|
||||
|
||||
## validation
|
||||
### g.cnnetmkt.top
|
||||
HTTP/1.1 302 Moved Temporarily
|
||||
Server: nginx/1.24.0 (Ubuntu)
|
||||
Date: Mon, 06 Jul 2026 07:48:12 GMT
|
||||
Content-Type: text/html
|
||||
Content-Length: 154
|
||||
Location: https://g.cnnetmkt.top/git/
|
||||
Connection: keep-alive
|
||||
|
||||
<title>302 Found
|
||||
|
||||
subject=CN = user.cnnetmkt.top
|
||||
issuer=C = US, O = Let's Encrypt, CN = YE2
|
||||
notBefore=Jul 6 06:49:39 2026 GMT
|
||||
notAfter=Oct 4 06:49:38 2026 GMT
|
||||
X509v3 Subject Alternative Name:
|
||||
DNS:g.cnnetmkt.top, DNS:portal.cnnetmkt.top, DNS:user.cnnetmkt.top
|
||||
|
||||
### cnnetmkt.top
|
||||
HTTP/1.1 302 Found
|
||||
Server: nginx/1.24.0 (Ubuntu)
|
||||
Date: Mon, 06 Jul 2026 07:48:12 GMT
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Content-Length: 0
|
||||
Connection: keep-alive
|
||||
Location: /portal/login/
|
||||
X-Request-ID: 7c649abe-a2de-4b3e-87fe-325f4297738f
|
||||
X-Frame-Options: DENY
|
||||
X-Content-Type-Options: nosniff
|
||||
Referrer-Policy: same-origin
|
||||
Cross-Origin-Opener-Policy: same-origin
|
||||
|
||||
subject=CN = user.cnnetmkt.top
|
||||
issuer=C = US, O = Let's Encrypt, CN = YE2
|
||||
notBefore=Jul 6 06:49:39 2026 GMT
|
||||
notAfter=Oct 4 06:49:38 2026 GMT
|
||||
X509v3 Subject Alternative Name:
|
||||
DNS:g.cnnetmkt.top, DNS:portal.cnnetmkt.top, DNS:user.cnnetmkt.top
|
||||
|
||||
### user.cnnetmkt.top
|
||||
HTTP/1.1 302 Found
|
||||
Server: nginx/1.24.0 (Ubuntu)
|
||||
Date: Mon, 06 Jul 2026 07:48:12 GMT
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Content-Length: 0
|
||||
Connection: keep-alive
|
||||
Location: /portal/login/
|
||||
X-Request-ID: e3d70907-1ab7-456c-a725-7cb60647514d
|
||||
X-Frame-Options: DENY
|
||||
X-Content-Type-Options: nosniff
|
||||
Referrer-Policy: same-origin
|
||||
Cross-Origin-Opener-Policy: same-origin
|
||||
|
||||
subject=CN = user.cnnetmkt.top
|
||||
issuer=C = US, O = Let's Encrypt, CN = YE2
|
||||
notBefore=Jul 6 06:49:39 2026 GMT
|
||||
notAfter=Oct 4 06:49:38 2026 GMT
|
||||
X509v3 Subject Alternative Name:
|
||||
DNS:g.cnnetmkt.top, DNS:portal.cnnetmkt.top, DNS:user.cnnetmkt.top
|
||||
|
||||
## certbot
|
||||
|
||||
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
Found the following certs:
|
||||
Certificate Name: user.cnnetmkt.top
|
||||
Serial Number: 529feebc03b37f2866c25b1465f37e14565
|
||||
Key Type: ECDSA
|
||||
Domains: user.cnnetmkt.top g.cnnetmkt.top portal.cnnetmkt.top
|
||||
Expiry Date: 2026-10-04 06:49:38+00:00 (VALID: 89 days)
|
||||
Certificate Path: /etc/letsencrypt/live/user.cnnetmkt.top/fullchain.pem
|
||||
Private Key Path: /etc/letsencrypt/live/user.cnnetmkt.top/privkey.pem
|
||||
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
@@ -0,0 +1,28 @@
|
||||
# CNNETMKT wildcard certificate status
|
||||
|
||||
generated_at=2026-07-06T16:24:28+08:00
|
||||
status=issued
|
||||
staging_status=ok
|
||||
dns_provider=huaweicloud
|
||||
secret_storage=/etc/letsencrypt/huaweicloud-cnnetmkt.env
|
||||
certificate_live_dir=/etc/letsencrypt/live/cnnetmkt-wildcard
|
||||
nginx_status=loaded
|
||||
backup_dir=/data/aios-gitops/backups/nginx-wildcard-20260706-162202
|
||||
renew_timer=cnnetmkt-wildcard-renew.timer
|
||||
|
||||
## certificate
|
||||
|
||||
subject=CN = *.cnnetmkt.top
|
||||
issuer=C = US, O = Let's Encrypt, CN = YE2
|
||||
notBefore=Jul 6 07:25:55 2026 GMT
|
||||
notAfter=Oct 4 07:25:54 2026 GMT
|
||||
X509v3 Subject Alternative Name:
|
||||
DNS:*.cnnetmkt.top, DNS:cnnetmkt.top
|
||||
|
||||
## routes
|
||||
|
||||
- https://cnnetmkt.top/
|
||||
- https://cnnetmkt.top/git/
|
||||
- https://g.cnnetmkt.top/
|
||||
- https://g.cnnetmkt.top/git/
|
||||
- https://user.cnnetmkt.top/
|
||||
@@ -0,0 +1,27 @@
|
||||
# AIOS V1R1C1 Codex GitOps Online Release
|
||||
|
||||
Date: 2026-07-06T10:21:08Z
|
||||
|
||||
## Purpose
|
||||
|
||||
Merge the current controlled ChatGPT/Codex GitOps access changes into the latest online baseline and make Git the operating version manager for Codex changes.
|
||||
|
||||
## Included
|
||||
|
||||
- ChatGPT Actions API adapter and OpenAPI schema.
|
||||
- Secret-safe model registration using `secret_ref`.
|
||||
- `codex-gitops-bot` runtime token account with scoped API permissions.
|
||||
- Codex online GitOps configuration under `config/codex/gitops-online.json`.
|
||||
- Baseline metadata updated from `V1R1C0` to `V1R1C1`.
|
||||
|
||||
## Excluded
|
||||
|
||||
- Acceptance dummy model registry entries from `patch/chatgpt-model-acceptance-*`.
|
||||
- Plaintext API keys or bearer tokens.
|
||||
|
||||
## Verification
|
||||
|
||||
- `curl https://cnnetmkt.top/api/base` returns 200.
|
||||
- `curl https://cnnetmkt.top/api/openapi-chatgpt-actions.yaml` returns 200.
|
||||
- `bash scripts/package_contract_check_v1r1c0.sh` passes.
|
||||
- `bash scripts/codex_gitops_online_check.sh` passes after tag publication.
|
||||
@@ -0,0 +1,18 @@
|
||||
# AIOS V1R1C2 Codex GitOps Online Sync Release
|
||||
|
||||
Date: 2026-07-06T10:26:26Z
|
||||
|
||||
## Purpose
|
||||
|
||||
Make Codex/API writes visible in the online Git service by syncing non-main patch branches from `/data/aios-gitops/workspace` into the Gitea bare repository after each committed write.
|
||||
|
||||
## Included
|
||||
|
||||
- `active-runtime/app/aios_chatgpt_actions_api.py` now syncs non-main branch commits to the online Git bare repository.
|
||||
- `POST /api/tag` direct owner flow syncs tags when executed.
|
||||
- `config/codex/gitops-online.json` records `online_git_sync=true`.
|
||||
- Live and template systemd service configuration declares `AIOS_GITOPS_BARE_REPO`.
|
||||
|
||||
## Policy
|
||||
|
||||
`main` remains protected. API/Codex writes still commit to patch/request branches and owner/admin approval remains required for V-level production actions.
|
||||
@@ -0,0 +1,28 @@
|
||||
# AIOS V1R1C3 Codex + Custom GPT Collaboration Release
|
||||
|
||||
Date: 2026-07-06T10:42:33Z
|
||||
|
||||
## Purpose
|
||||
|
||||
Validate that local Codex and the AIOS GitOps 控制台 custom GPT can both modify the online GitOps baseline through controlled API writes, then merge both changes into the latest version.
|
||||
|
||||
## Custom GPT
|
||||
|
||||
https://chatgpt.com/g/g-6a4b7c82dff08191bc980798edea1bac-aios-gitops-kong-zhi-tai
|
||||
|
||||
## Merged Inputs
|
||||
|
||||
| Actor | Account | Branch | Commit | Online Sync |
|
||||
|---|---|---|---|---|
|
||||
| local_codex | codex-gitops-bot | `patch/collab-local-codex-20260706T104130Z` | `a62d4ce` | synced |
|
||||
| custom_gpt | chatgpt-gitops-bot | `patch/collab-custom-gpt-20260706T104130Z` | `c393931` | synced |
|
||||
|
||||
## Result
|
||||
|
||||
Both branches were merged into `main`, baseline metadata was refreshed to `V1R1C3`, and the release tag `V1R1C3` was published to the online Git repository.
|
||||
|
||||
## Guardrails
|
||||
|
||||
- GPT and Codex writes entered Git as patch branch commits.
|
||||
- Protected `main` was updated only by the release merge step.
|
||||
- Bot tokens and custom LLM API keys remain outside Git.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user