Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65f9e0ac71 | ||
|
|
ff489034ed |
@@ -84,39 +84,14 @@ def token_records():
|
||||
else:
|
||||
out.append({'account':'legacy-token','role':'owner','scopes':['*'],'token_plain':line})
|
||||
return out
|
||||
def normalize_auth_token(raw: str | None) -> tuple[str, str]:
|
||||
value = str(raw or '').strip()
|
||||
state = 'missing'
|
||||
if not value:
|
||||
return '', state
|
||||
state = 'present'
|
||||
for _ in range(4):
|
||||
lowered = value.lower().strip()
|
||||
if lowered.startswith('authorization:'):
|
||||
value = value.split(':', 1)[1].strip()
|
||||
state = 'authorization_prefix_stripped'
|
||||
continue
|
||||
if lowered.startswith('bearer '):
|
||||
value = value[7:].strip()
|
||||
state = 'bearer_prefix_stripped'
|
||||
continue
|
||||
break
|
||||
if (value.startswith('"') and value.endswith('"')) or (value.startswith("'") and value.endswith("'")):
|
||||
value = value[1:-1].strip()
|
||||
return value, state
|
||||
|
||||
def authenticate(token: str):
|
||||
if os.getenv('AIOS_GITOPS_DEV_ALLOW','0') == '1': return {'account':'dev-owner','role':'owner','scopes':['*']}
|
||||
token, _ = normalize_auth_token(token)
|
||||
if not token or token.startswith('secret_ref://'):
|
||||
return None
|
||||
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
|
||||
plain = str(r.get('token_plain') or '')
|
||||
norm_plain, _ = normalize_auth_token(plain)
|
||||
if norm_plain and hmac.compare_digest(norm_plain, token): return r
|
||||
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):
|
||||
@@ -160,16 +135,6 @@ def sync_tag(tag: str) -> dict:
|
||||
if p.returncode != 0: raise RuntimeError(p.stderr or p.stdout or f'git tag push failed for {tag}')
|
||||
return {'status':'synced','target':'origin','tag':tag,'stdout':p.stdout.strip(),'stderr':p.stderr.strip()}
|
||||
|
||||
def commit_summary(c: Any) -> dict:
|
||||
if not isinstance(c, dict):
|
||||
return {'rev': None, 'sync_status': None, 'sync': None}
|
||||
sync = c.get('sync') if isinstance(c.get('sync'), dict) else None
|
||||
return {
|
||||
'rev': c.get('rev'),
|
||||
'sync_status': sync.get('status') if sync else None,
|
||||
'sync': sync,
|
||||
}
|
||||
|
||||
def commit(paths, msg):
|
||||
ensure_git(); paths = [relpath(p) for p in paths] if paths else ['.']; sh(['add'] + paths, True)
|
||||
branch=sh(['rev-parse','--abbrev-ref','HEAD'])['stdout']
|
||||
@@ -194,58 +159,20 @@ class H(BaseHTTPRequestHandler):
|
||||
b=json.dumps(data, ensure_ascii=False, indent=2).encode(); self.send_response(code); self.send_header('Content-Type','application/json; charset=utf-8'); self.send_header('Content-Length',str(len(b))); self.end_headers(); self.wfile.write(b)
|
||||
def sendt(self, text, ctype='text/plain; charset=utf-8', code=200):
|
||||
b=text.encode(); self.send_response(code); self.send_header('Content-Type',ctype); self.send_header('Content-Length',str(len(b))); self.end_headers(); self.wfile.write(b)
|
||||
def send_head_only(self, code=200, ctype='application/json; charset=utf-8', length=0):
|
||||
self.send_response(code); self.send_header('Content-Type',ctype); self.send_header('Content-Length',str(length)); self.end_headers()
|
||||
def body(self):
|
||||
n=int(self.headers.get('Content-Length','0') or '0'); raw=self.rfile.read(n).decode() if n else '{}'
|
||||
try: return json.loads(raw or '{}')
|
||||
except Exception: return {'_raw':raw}
|
||||
def actor(self):
|
||||
auth=self.headers.get('Authorization','')
|
||||
raw = auth if auth else self.headers.get('X-AIOS-Token','')
|
||||
tok, auth_state = normalize_auth_token(raw)
|
||||
if not raw:
|
||||
auth_state = 'missing_bearer'
|
||||
elif tok.startswith('secret_ref://'):
|
||||
auth_state = 'secret_ref_literal_not_token'
|
||||
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 []),'auth_state':'authenticated'}
|
||||
if raw and auth_state not in ('missing_bearer', 'secret_ref_literal_not_token'):
|
||||
auth_state = 'invalid_token'
|
||||
return {'ok':False,'account':'anonymous','role':'anonymous','scopes':set(),'auth_state':auth_state or 'invalid_token'}
|
||||
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']:
|
||||
hint = 'Bearer token required for GitOps write scope'
|
||||
if a.get('auth_state') == 'invalid_token':
|
||||
hint = 'Bearer token was received but does not match the GitOps write token registry'
|
||||
elif a.get('auth_state') == 'secret_ref_literal_not_token':
|
||||
hint = 'secret_ref is a pointer, not a credential; paste the raw runtime token into GPT Builder'
|
||||
self.sendj({
|
||||
'error':'unauthorized',
|
||||
'hint':hint,
|
||||
'auth_state':a.get('auth_state') or 'invalid_token',
|
||||
'required_scope':scope,
|
||||
'token_secret_ref':'secret_ref://aios-gitops/runtime/gitops-write-bot',
|
||||
'auth_header':'Authorization: Bearer <raw-token>',
|
||||
'rebind_required':True,
|
||||
'gpt_builder_note':'When GPT Builder auth type is Bearer, paste only the raw token value. Do not paste secret_ref://... and do not include the word Bearer in the API key field.'
|
||||
},401); return None
|
||||
if '*' not in a['scopes'] and scope not in a['scopes']:
|
||||
self.sendj({'error':'forbidden','required_scope':scope,'account':a['account'],'auth_state':a.get('auth_state'),'token_secret_ref':'secret_ref://aios-gitops/runtime/gitops-write-bot'},403); return None
|
||||
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_HEAD(self):
|
||||
u=urllib.parse.urlparse(self.path); path=u.path.rstrip('/') if u.path != '/' else u.path
|
||||
try:
|
||||
if path in ('/api/openapi.yaml','/api/openapi-chatgpt-actions.yaml','/api/schema'):
|
||||
if not OPENAPI.exists():
|
||||
return self.send_head_only(404)
|
||||
return self.send_head_only(200, 'application/yaml; charset=utf-8', OPENAPI.stat().st_size)
|
||||
if path in ('/health','/api/health','/api','/api/status','/api/base','/base','/api/ver','/ver','/api/diff','/diff','/api/patch','/patch','/api/model','/model','/api/run','/run'):
|
||||
return self.send_head_only(200)
|
||||
return self.send_head_only(404)
|
||||
except Exception:
|
||||
return self.send_head_only(500)
|
||||
def do_GET(self):
|
||||
u=urllib.parse.urlparse(self.path); path=u.path.rstrip('/') if u.path != '/' else u.path; q=urllib.parse.parse_qs(u.query)
|
||||
try:
|
||||
@@ -288,20 +215,20 @@ class H(BaseHTTPRequestHandler):
|
||||
a=self.need('branch:create');
|
||||
if not a: return
|
||||
b=checkout(data.get('branch'), data.get('base','main'), True); meta={'type':'branch','branch':b,'base':data.get('base','main'),'created_by':a['account'],'created_at':now(),'direct_main_push':False,'owner_approval_required':True}
|
||||
r=write_json(f'patches/{safe(b).replace("/","_")}.branch.json', meta); c=commit([r], data.get('message') or f'chore(chatgpt): create {b}'); summary=commit_summary(c); audit(a['account'],'branch',b,meta); self.sendj({'status':'ok','branch':b,'commit':c, **summary})
|
||||
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}'); summary=commit_summary(c); audit(a['account'],'file',r,{'branch':b,'commit':c}); self.sendj({'status':'committed','branch':b,'path':r,'commit':c, **summary})
|
||||
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'); summary=commit_summary(c); audit(a['account'],'commit',b,{'paths':paths,'commit':c}); self.sendj({'status':c['status'],'branch':b,'commit':c, **summary})
|
||||
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}'); summary=commit_summary(c); audit(a['account'],'patch',name,meta); self.sendj({'status':'patch_branch_created','branch':b,'patch':meta,'commit':c, **summary})
|
||||
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
|
||||
@@ -343,23 +270,9 @@ class H(BaseHTTPRequestHandler):
|
||||
b=checkout(data.get('branch') or f'patch/nl-{uid("nl")}', 'main', True); doc={'id':uid('nl'),'text':text,'intent':intent,'action_plan':plan,'execute':False,'owner_approval_required':True,'created_by':a['account'],'created_at':now()}; r=write_json(f'patches/nl/{safe(doc["id"])}.json', doc); c=commit([r], f'chore(chatgpt): record nl plan {intent}'); self.sendj({'status':'planned','branch':b,'request':doc,'commit':c})
|
||||
|
||||
def api_spec():
|
||||
return {
|
||||
'version':'V1R1C0-chatgpt-actions',
|
||||
'openapi':'/api/openapi.yaml',
|
||||
'read':['GET /api/base','GET /api/ver','GET /api/diff','GET /api/patch','GET /api/model','GET /api/run'],
|
||||
'write':['POST /api/branch','POST /api/file','POST /api/commit','POST /api/patch','POST /api/tag','POST /api/rollback','POST /api/model','PUT /api/model-priority','POST /api/nl'],
|
||||
'auth':{
|
||||
'write_bearer_required':True,
|
||||
'write_token_account':'gitops-write-bot',
|
||||
'write_token_secret_ref':'secret_ref://aios-gitops/runtime/gitops-write-bot',
|
||||
'required_header':'Authorization: Bearer <raw-token>',
|
||||
'write_scopes':['branch:create','file:write','commit:create','patch:create','rollback:request','model:write','model:priority','nl:plan','runtime:test'],
|
||||
'bridge_gpts':[{'name':'codex-chartgpt-bridge','gpt_id':'g-6a4ba1b4ea588191bab125d53ce83612','url':'https://chatgpt.com/g/g-6a4ba1b4ea588191bab125d53ce83612-codex-chartgpt-bridge','auth_binding_status':'requires_gpt_builder_bearer_secret_binding','api_key_field':'raw token only, without Bearer prefix'}]
|
||||
},
|
||||
'policy':{'bearer_required_for_write':True,'direct_main_push':False,'delete_tag':False,'owner_approval_required_for_v_level':True,'api_keys_in_git':False,'git_secret_field':'secret_ref','online_git_sync':True}
|
||||
}
|
||||
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()
|
||||
if __name__ == '__main__': main()
|
||||
+1
-51
@@ -46,7 +46,7 @@
|
||||
"/api",
|
||||
"/hook"
|
||||
],
|
||||
"updated_at": "2026-07-06T13:24:28Z",
|
||||
"updated_at": "2026-07-06T11:16:23Z",
|
||||
"auth": {
|
||||
"primary_domain": "https://auth.cnnetmkt.top",
|
||||
"backup_domain": "https://auth.h.imruyi.top:8443",
|
||||
@@ -104,55 +104,5 @@
|
||||
"portal_overlay": "overlays/user-control-center",
|
||||
"portal_baseline_cache": "/app/runtime/gitops/current.json",
|
||||
"sync_log_secret_hygiene": "runuser_without_sudo_command_logging"
|
||||
},
|
||||
"actions_write_auth": {
|
||||
"account": "gitops-write-bot",
|
||||
"token_ref": "secret_ref://aios-gitops/runtime/gitops-write-bot",
|
||||
"header": "Authorization: Bearer <token>",
|
||||
"scopes": [
|
||||
"baseline:read",
|
||||
"version:read",
|
||||
"diff:read",
|
||||
"patch:read",
|
||||
"model:read",
|
||||
"runtime:read",
|
||||
"branch:create",
|
||||
"file:write",
|
||||
"commit:create",
|
||||
"patch:create",
|
||||
"rollback:request",
|
||||
"model:write",
|
||||
"model:priority",
|
||||
"nl:plan",
|
||||
"runtime:test"
|
||||
],
|
||||
"direct_main_push": false,
|
||||
"delete_tag": false,
|
||||
"owner_approval_required": true,
|
||||
"updated_at": "2026-07-06T11:50:41Z",
|
||||
"bridge_gpts": [
|
||||
{
|
||||
"name": "codex-chartgpt-bridge",
|
||||
"gpt_id": "g-6a4ba1b4ea588191bab125d53ce83612",
|
||||
"url": "https://chatgpt.com/g/g-6a4ba1b4ea588191bab125d53ce83612-codex-chartgpt-bridge",
|
||||
"token_ref": "secret_ref://aios-gitops/runtime/gitops-write-bot",
|
||||
"schema_url": "https://cnnetmkt.top/api/openapi-chatgpt-actions.yaml",
|
||||
"auth_binding_status": "requires_gpt_builder_bearer_secret_binding",
|
||||
"server_side_smoke": "pass",
|
||||
"updated_at": "2026-07-06T13:24:28Z"
|
||||
}
|
||||
],
|
||||
"external_actions_binding_note": "ChatGPT GPT Builder must configure Bearer auth with the runtime token behind token_ref; server cannot inject the token into an external GPT by URL alone.",
|
||||
"gpt_builder_api_key_field": "raw token only, without Bearer prefix",
|
||||
"do_not_use_as_api_key": "secret_ref://aios-gitops/runtime/gitops-write-bot",
|
||||
"last_client_response_error_fix": {
|
||||
"updated_at": "2026-07-06T13:41:34Z",
|
||||
"server_changes": [
|
||||
"normalize duplicated Bearer/Authorization prefixes",
|
||||
"return auth_state diagnostics",
|
||||
"flatten branch/rev/sync_status on write success"
|
||||
],
|
||||
"external_builder_requirement": "paste raw token into Bearer API key field"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
{
|
||||
"name": "codex-chartgpt-bridge",
|
||||
"gpt_id": "g-6a4ba1b4ea588191bab125d53ce83612",
|
||||
"gpt_url": "https://chatgpt.com/g/g-6a4ba1b4ea588191bab125d53ce83612-codex-chartgpt-bridge",
|
||||
"api_base": "https://cnnetmkt.top/api",
|
||||
"openapi_schema": "https://cnnetmkt.top/api/openapi-chatgpt-actions.yaml",
|
||||
"auth": {
|
||||
"type": "bearer",
|
||||
"header": "Authorization: Bearer <token>",
|
||||
"token_account": "gitops-write-bot",
|
||||
"token_ref": "secret_ref://aios-gitops/runtime/gitops-write-bot",
|
||||
"runtime_token_file": "/data/aios-gitops/runtime/secrets/gitops-write-bot.token",
|
||||
"plaintext_token_in_git": false
|
||||
},
|
||||
"scopes": {
|
||||
"read": [
|
||||
"baseline:read",
|
||||
"version:read",
|
||||
"diff:read",
|
||||
"patch:read",
|
||||
"model:read",
|
||||
"runtime:read"
|
||||
],
|
||||
"write": [
|
||||
"branch:create",
|
||||
"file:write",
|
||||
"commit:create",
|
||||
"patch:create",
|
||||
"rollback:request",
|
||||
"model:write",
|
||||
"model:priority",
|
||||
"nl:plan",
|
||||
"runtime:test"
|
||||
]
|
||||
},
|
||||
"policy": {
|
||||
"direct_main_push": false,
|
||||
"delete_tag": false,
|
||||
"owner_approval_required": true,
|
||||
"v_level_owner_admin_approval_required": true,
|
||||
"api_keys_in_git": false
|
||||
},
|
||||
"status": {
|
||||
"server_side_smoke": "pass",
|
||||
"external_gpt_builder_auth_binding": "required_in_gpt_builder_ui",
|
||||
"last_updated_at": "2026-07-06T13:24:28Z"
|
||||
},
|
||||
"gpt_builder_api_key_field": "raw token only, without Bearer prefix",
|
||||
"do_not_use_as_api_key": "secret_ref://aios-gitops/runtime/gitops-write-bot",
|
||||
"last_client_response_error_fix": {
|
||||
"updated_at": "2026-07-06T13:41:34Z",
|
||||
"server_changes": [
|
||||
"normalize duplicated Bearer/Authorization prefixes",
|
||||
"return auth_state diagnostics",
|
||||
"flatten branch/rev/sync_status on write success"
|
||||
],
|
||||
"external_builder_requirement": "paste raw token into Bearer API key field"
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@
|
||||
],
|
||||
"created_at": "2026-07-06T10:21:08Z",
|
||||
"base_commit_before_release": "43ddb91",
|
||||
"updated_at": "2026-07-06T13:24:28Z",
|
||||
"updated_at": "2026-07-06T11:16:23Z",
|
||||
"last_collaboration_test": {
|
||||
"timestamp": "20260706T104130Z",
|
||||
"result": "local_codex_and_custom_gpt_patches_merged"
|
||||
@@ -74,55 +74,5 @@
|
||||
"portal_overlay": "overlays/user-control-center",
|
||||
"portal_baseline_cache": "/app/runtime/gitops/current.json",
|
||||
"sync_log_secret_hygiene": "runuser_without_sudo_command_logging"
|
||||
},
|
||||
"actions_write_auth": {
|
||||
"account": "gitops-write-bot",
|
||||
"token_ref": "secret_ref://aios-gitops/runtime/gitops-write-bot",
|
||||
"header": "Authorization: Bearer <token>",
|
||||
"scopes": [
|
||||
"baseline:read",
|
||||
"version:read",
|
||||
"diff:read",
|
||||
"patch:read",
|
||||
"model:read",
|
||||
"runtime:read",
|
||||
"branch:create",
|
||||
"file:write",
|
||||
"commit:create",
|
||||
"patch:create",
|
||||
"rollback:request",
|
||||
"model:write",
|
||||
"model:priority",
|
||||
"nl:plan",
|
||||
"runtime:test"
|
||||
],
|
||||
"direct_main_push": false,
|
||||
"delete_tag": false,
|
||||
"owner_approval_required": true,
|
||||
"updated_at": "2026-07-06T11:50:41Z",
|
||||
"bridge_gpts": [
|
||||
{
|
||||
"name": "codex-chartgpt-bridge",
|
||||
"gpt_id": "g-6a4ba1b4ea588191bab125d53ce83612",
|
||||
"url": "https://chatgpt.com/g/g-6a4ba1b4ea588191bab125d53ce83612-codex-chartgpt-bridge",
|
||||
"token_ref": "secret_ref://aios-gitops/runtime/gitops-write-bot",
|
||||
"schema_url": "https://cnnetmkt.top/api/openapi-chatgpt-actions.yaml",
|
||||
"auth_binding_status": "requires_gpt_builder_bearer_secret_binding",
|
||||
"server_side_smoke": "pass",
|
||||
"updated_at": "2026-07-06T13:24:28Z"
|
||||
}
|
||||
],
|
||||
"external_actions_binding_note": "ChatGPT GPT Builder must configure Bearer auth with the runtime token behind token_ref; server cannot inject the token into an external GPT by URL alone.",
|
||||
"gpt_builder_api_key_field": "raw token only, without Bearer prefix",
|
||||
"do_not_use_as_api_key": "secret_ref://aios-gitops/runtime/gitops-write-bot",
|
||||
"last_client_response_error_fix": {
|
||||
"updated_at": "2026-07-06T13:41:34Z",
|
||||
"server_changes": [
|
||||
"normalize duplicated Bearer/Authorization prefixes",
|
||||
"return auth_state diagnostics",
|
||||
"flatten branch/rev/sync_status on write success"
|
||||
],
|
||||
"external_builder_requirement": "paste raw token into Bearer API key field"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
# Codex ChatGPT Bridge Actions Setup
|
||||
|
||||
Date: 2026-07-06T13:24:28Z
|
||||
GPT: codex-chartgpt-bridge
|
||||
URL: https://chatgpt.com/g/g-6a4ba1b4ea588191bab125d53ce83612-codex-chartgpt-bridge
|
||||
|
||||
## Current Server-Side State
|
||||
|
||||
The AIOS GitOps API write path is ready for this GPT bridge.
|
||||
|
||||
- API base: `https://cnnetmkt.top/api`
|
||||
- OpenAPI schema: `https://cnnetmkt.top/api/openapi-chatgpt-actions.yaml`
|
||||
- Auth type: Bearer token
|
||||
- Required header: `Authorization: Bearer <token>`
|
||||
- Token account: `gitops-write-bot`
|
||||
- Token source: `secret_ref://aios-gitops/runtime/gitops-write-bot`
|
||||
- Runtime token file: `/data/aios-gitops/runtime/secrets/gitops-write-bot.token`
|
||||
|
||||
The token value must not be copied into Git, docs, logs, prompts, or issue comments.
|
||||
|
||||
## Required GPT Builder Action Binding
|
||||
|
||||
The GPT URL alone cannot make the GPT send a Bearer header. In the GPT Builder Actions configuration for `codex-chartgpt-bridge`, bind authentication as:
|
||||
|
||||
```text
|
||||
Authentication type: API Key / Bearer
|
||||
Auth header: Authorization
|
||||
Auth value / API key field: <raw runtime token value only>
|
||||
Schema URL: https://cnnetmkt.top/api/openapi-chatgpt-actions.yaml
|
||||
Secret source: secret_ref://aios-gitops/runtime/gitops-write-bot
|
||||
```
|
||||
|
||||
If the builder asks for the token value, read it only from the runtime secret file on the server and paste it into the GPT Builder secret field. Do not paste it into chat.
|
||||
|
||||
## Server-Side Smoke Already Passed
|
||||
|
||||
- no-token `createPatch`: `401 unauthorized`
|
||||
- `gitops-write-bot` `createPatch`: `200`, branch synced
|
||||
- `gitops-write-bot` `commitFile`: `200`, branch synced
|
||||
- `codex-gitops-bot` `createPatch`: `200`, branch synced
|
||||
- `codex-gitops-bot` `commitFile`: `200`, branch synced
|
||||
|
||||
## Latest ClientResponseError Fix
|
||||
|
||||
Current public API and auth smoke status:
|
||||
|
||||
- `GET /api/base`, `GET /api/run`, `GET /api/status`, and the OpenAPI schema URL return `200`.
|
||||
- no-token writes return `401` with `auth_state=missing_bearer`.
|
||||
- using the public `secret_ref://...` string as the API key returns `401` with `auth_state=secret_ref_literal_not_token`.
|
||||
- duplicated `Bearer ` prefixes are normalized server-side and accepted when the underlying token is valid.
|
||||
- valid raw-token `createPatch` and `commitFile` return `branch`, `rev`, and `sync_status` at top level.
|
||||
- `HEAD /api/base`, `HEAD /api/run`, and `HEAD /api/openapi-chatgpt-actions.yaml` return `200`, preventing endpoint validators from treating public reads as unsupported.
|
||||
|
||||
The GPT Builder API key field must contain only the raw token value.
|
||||
|
||||
If Actions returns `auth_state=invalid_token`, the GPT did send a Bearer value, but it is not the current runtime token. Re-open the GPT Builder Action authentication dialog, replace the hidden API key field with the current raw value from `/data/aios-gitops/runtime/secrets/gitops-write-bot.token`, save the Action, publish/update the GPT, and start a fresh GPT conversation before retesting.
|
||||
|
||||
## External GPT Retest
|
||||
|
||||
After binding the Bearer token in GPT Builder, ask the GPT to run:
|
||||
|
||||
1. `createPatch` with a branch under `patch/`.
|
||||
2. `commitFile` on that same branch.
|
||||
3. `getDiff` for the new branch versus `main`.
|
||||
|
||||
Expected result:
|
||||
|
||||
- `createPatch` returns `200`.
|
||||
- `commitFile` returns `200`.
|
||||
- branch sync status is `synced`.
|
||||
- no direct `main` write occurs.
|
||||
|
||||
## Security Boundary
|
||||
|
||||
- `direct_main_push=false`
|
||||
- `delete_tag=false`
|
||||
- `owner_approval_required=true`
|
||||
- V-level operations still require owner/admin approval.
|
||||
- Custom LLM API keys must use secret refs only.
|
||||
|
||||
Important: because the GPT Builder screenshot has Bearer selected, the API key field must contain only the raw token value. Do not type `Bearer ` in that field. Do not type `secret_ref://aios-gitops/runtime/gitops-write-bot`; that string is a public reference, not a credential.
|
||||
@@ -1,57 +0,0 @@
|
||||
# GitOps Write Auth Fix
|
||||
|
||||
Date: 2026-07-06T11:50:41Z
|
||||
Branch: `patch/gitops-write-auth-fix`
|
||||
|
||||
## Root Cause
|
||||
|
||||
Read operations were working because they are public at the server layer. Write operations returned `401 unauthorized` when ChatGPT Actions did not inject `Authorization: Bearer ...` for write calls.
|
||||
|
||||
Runtime token records already supported write scopes for bot accounts, so the API write path itself was not broken. A bearer-authenticated smoke test proved `createPatch` and `commitFile` succeed when the write token is supplied.
|
||||
|
||||
## Fix
|
||||
|
||||
- Added/normalized runtime bot account: `gitops-write-bot`.
|
||||
- Runtime token file: `/data/aios-gitops/runtime/secrets/gitops-write-bot.token`.
|
||||
- Git-visible credential reference only: `secret_ref://aios-gitops/runtime/gitops-write-bot`.
|
||||
- Added token hash and scopes to `/data/aios-gitops/runtime/secrets/gitops.tokens`.
|
||||
- Updated OpenAPI schema to declare bearer auth at top level and on write operations.
|
||||
- Updated API 401/403 responses to include required scope and `token_secret_ref`, without exposing secrets.
|
||||
- Added smoke test: `scripts/smoke_gitops_write_auth.py`.
|
||||
|
||||
## Required Actions Auth Binding
|
||||
|
||||
Configure ChatGPT Actions / Codex Actions auth with:
|
||||
|
||||
- Auth type: Bearer token
|
||||
- Header: `Authorization: Bearer <token>`
|
||||
- Secret source: `secret_ref://aios-gitops/runtime/gitops-write-bot`
|
||||
|
||||
Do not paste the token into Git, docs, logs, prompts, or issue comments.
|
||||
|
||||
## Smoke
|
||||
|
||||
```bash
|
||||
python3 scripts/smoke_gitops_write_auth.py --api https://cnnetmkt.top/api --token-file /data/aios-gitops/runtime/secrets/gitops-write-bot.token
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- no-token `createPatch` returns `401`
|
||||
- authenticated `createPatch` returns `200`
|
||||
- authenticated `commitFile` returns `200`
|
||||
|
||||
## Rollback
|
||||
|
||||
1. Remove or disable the `gitops-write-bot` JSON record in `/data/aios-gitops/runtime/secrets/gitops.tokens`.
|
||||
2. Remove `/data/aios-gitops/runtime/secrets/gitops-write-bot.token` if rotation is required.
|
||||
3. Revert this patch branch or restore OpenAPI/API files from `main`.
|
||||
4. Restart the API service:
|
||||
|
||||
```bash
|
||||
sudo systemctl restart aios-gitops-model-api.service
|
||||
```
|
||||
|
||||
5. Verify unauthenticated writes still return `401`.
|
||||
|
||||
Important: because the GPT Builder screenshot has Bearer selected, the API key field must contain only the raw token value. Do not type `Bearer ` in that field. Do not type `secret_ref://aios-gitops/runtime/gitops-write-bot`; that string is a public reference, not a credential.
|
||||
@@ -1,135 +0,0 @@
|
||||
# GitOps Write Auth Fix Acceptance
|
||||
|
||||
Time: 2026-07-06T13:26:26Z
|
||||
Branch: `patch/gitops-write-auth-fix`
|
||||
Patch commit: see `git rev-parse --short patch/gitops-write-auth-fix`
|
||||
Bridge GPT: `codex-chartgpt-bridge`
|
||||
Bridge URL: https://chatgpt.com/g/g-6a4ba1b4ea588191bab125d53ce83612-codex-chartgpt-bridge
|
||||
|
||||
## Result
|
||||
|
||||
SERVER-SIDE PASS. The GitOps API write path accepts valid Bearer tokens and rejects no-token writes. The external GPT still must have Bearer authentication configured in GPT Builder Actions; a GPT URL alone cannot inject the header.
|
||||
|
||||
## Root Cause Confirmed
|
||||
|
||||
- Read endpoints were healthy without auth.
|
||||
- Write endpoints returned `401 unauthorized` when Actions did not send `Authorization: Bearer ...`.
|
||||
- Runtime token registry supports write scopes; the broken boundary was Actions auth header injection / write token binding.
|
||||
|
||||
## Fix Applied
|
||||
|
||||
- Added dedicated runtime account `gitops-write-bot`.
|
||||
- Runtime token file: `/data/aios-gitops/runtime/secrets/gitops-write-bot.token`, mode `600`.
|
||||
- Token registry stores hash/scopes only in `/data/aios-gitops/runtime/secrets/gitops.tokens`.
|
||||
- Git-visible token reference only: `secret_ref://aios-gitops/runtime/gitops-write-bot`.
|
||||
- OpenAPI now advertises Bearer auth, write operation scopes, and the bridge GPT URL.
|
||||
- `/api/status` returns `bridge_gpts` with the `codex-chartgpt-bridge` URL and auth-binding status.
|
||||
- Added bridge setup doc: `docs/gitops/CODEX_CHATGPT_BRIDGE_ACTIONS_SETUP.md`.
|
||||
- Added smoke script: `scripts/smoke_gitops_write_auth.py`.
|
||||
|
||||
## Smoke Results
|
||||
|
||||
### gitops-write-bot
|
||||
|
||||
```json
|
||||
{
|
||||
"noauth": {"operation": "createPatch without token", "http": 401, "status": "unauthorized"},
|
||||
"createPatch": {"operation": "createPatch", "http": 200, "status": "patch_branch_created", "branch": "patch/gitops-write-auth-smoke-20260706T115230Z", "rev": "6d3a050", "sync_status": "synced"},
|
||||
"commitFile": {"operation": "commitFile", "http": 200, "status": "committed", "branch": "patch/gitops-write-auth-smoke-20260706T115230Z", "rev": "17344f7", "sync_status": "synced"}
|
||||
}
|
||||
```
|
||||
|
||||
### codex-gitops-bot
|
||||
|
||||
```json
|
||||
{
|
||||
"noauth": {"operation": "createPatch without token", "http": 401, "status": "unauthorized"},
|
||||
"createPatch": {"operation": "createPatch", "http": 200, "status": "patch_branch_created", "branch": "patch/codex-write-auth-smoke-20260706T115256Z", "rev": "d684df2", "sync_status": "synced"},
|
||||
"commitFile": {"operation": "commitFile", "http": 200, "status": "committed", "branch": "patch/codex-write-auth-smoke-20260706T115256Z", "rev": "884a5ec", "sync_status": "synced"}
|
||||
}
|
||||
```
|
||||
|
||||
### codex-chartgpt-bridge binding smoke
|
||||
|
||||
```json
|
||||
{
|
||||
"noauth": {"operation": "createPatch without token", "http": 401, "status": "unauthorized"},
|
||||
"createPatch": {"operation": "createPatch", "http": 200, "status": "patch_branch_created", "branch": "patch/bridge-write-auth-smoke-20260706T132506Z", "rev": "f4ef8e5", "sync_status": "synced"},
|
||||
"commitFile": {"operation": "commitFile", "http": 200, "status": "committed", "branch": "patch/bridge-write-auth-smoke-20260706T132506Z", "rev": "0ed0816", "sync_status": "synced"}
|
||||
}
|
||||
```
|
||||
|
||||
## ClientResponseError Retest (2026-07-06T13:47:06Z)
|
||||
|
||||
The external GPT Actions failure was reproduced as a write-auth boundary issue, not a public endpoint outage. Public reads reached the API and returned typed responses, while writes without a valid Bearer credential returned structured `401` responses.
|
||||
|
||||
```text
|
||||
GET /api/base: 200 application/json
|
||||
GET /api/run: 200 application/json
|
||||
GET /api/status: 200 application/json
|
||||
GET /api/openapi-chatgpt-actions.yaml: 200 application/yaml
|
||||
POST /api/patch without token: 401 auth_state=missing_bearer
|
||||
POST /api/patch with literal secret_ref as token: 401 auth_state=secret_ref_literal_not_token
|
||||
POST /api/patch with duplicated Bearer prefix: 200 branch=patch/actions-double-bearer-20260706T134646Z rev=ddd8711 sync_status=synced
|
||||
POST /api/patch with raw runtime token: 200 branch=patch/actions-raw-token-smoke-20260706T134706Z rev=0e48e1d sync_status=synced
|
||||
POST /api/file with raw runtime token: 200 branch=patch/actions-raw-token-smoke-20260706T134706Z rev=6ab67e9 sync_status=synced
|
||||
```
|
||||
|
||||
Server-side compatibility added in this patch:
|
||||
|
||||
- Normalize accidental `Authorization:` and repeated `Bearer ` prefixes before token verification.
|
||||
- Reject literal `secret_ref://...` as a token and return `auth_state=secret_ref_literal_not_token`.
|
||||
- Return `auth_state`, `required_scope`, and GPT Builder guidance on `401`.
|
||||
- Return top-level `branch`, `rev`, and `sync_status` on write success so Actions can make deterministic pass/fail decisions.
|
||||
- Support `HEAD` probes for public read/schema endpoints so GPT Builder or gateway validators do not see `501` during endpoint discovery.
|
||||
|
||||
GPT Builder still must store the raw runtime token in the hidden API key field. The public `secret_ref://aios-gitops/runtime/gitops-write-bot` value is a pointer only and must not be used as the API key.
|
||||
|
||||
HEAD compatibility retest:
|
||||
|
||||
```text
|
||||
HEAD /api/base: 200 application/json
|
||||
HEAD /api/run: 200 application/json
|
||||
HEAD /api/openapi-chatgpt-actions.yaml: 200 application/yaml
|
||||
```
|
||||
|
||||
## External GPT Retest (2026-07-06T13:59:43Z)
|
||||
|
||||
The custom GPT retest reached the server:
|
||||
|
||||
```text
|
||||
GET /api/run: 200
|
||||
GET /api/base: 200
|
||||
POST /api/branch: 401
|
||||
POST /api/patch: 401
|
||||
```
|
||||
|
||||
This confirms that routing, TLS, schema loading, and read access are healthy. The remaining failure is the external GPT Builder secret binding: the write request does not carry the runtime `gitops-write-bot` raw token value. If a token-like value is present but does not match the token registry, the API now reports `auth_state=invalid_token` and `rebind_required=true`.
|
||||
|
||||
## External GPT Builder Binding Required
|
||||
|
||||
Configure the GPT Actions auth for `codex-chartgpt-bridge` as:
|
||||
|
||||
```text
|
||||
Schema URL: https://cnnetmkt.top/api/openapi-chatgpt-actions.yaml
|
||||
Authentication type: API Key / Bearer
|
||||
Header: Authorization
|
||||
Value / API key field: <raw runtime token value only>
|
||||
Secret source: secret_ref://aios-gitops/runtime/gitops-write-bot
|
||||
```
|
||||
|
||||
The actual token value must be read from `/data/aios-gitops/runtime/secrets/gitops-write-bot.token` and entered only into the GPT Builder secret/auth field. Do not paste it into chat or Git.
|
||||
|
||||
## Exit Criteria
|
||||
|
||||
- ChatGPT Actions write capability: PASS once the GPT Builder auth field is bound to the runtime token.
|
||||
- Server-side authenticated write path: PASS.
|
||||
- Codex API commit capability: PASS.
|
||||
- No-token write requests remain rejected: PASS, `401 unauthorized`.
|
||||
- No plaintext token committed to Git: PASS by workspace secret pattern scan.
|
||||
|
||||
## Security Notes
|
||||
|
||||
The patch does not allow direct `main` writes, does not permit tag deletion, and does not bypass owner/admin approval for V-level actions. Credentials stay in runtime secrets; Git stores only secret refs and token hashes.
|
||||
|
||||
Important: because the GPT Builder screenshot has Bearer selected, the API key field must contain only the raw token value. Do not type `Bearer ` in that field. Do not type `secret_ref://aios-gitops/runtime/gitops-write-bot`; that string is a public reference, not a credential.
|
||||
@@ -3,23 +3,15 @@ info:
|
||||
title: AIOS GitOps Controlled Access API
|
||||
version: V1R1C0-chatgpt-actions
|
||||
description: Controlled ChatGPT Actions API for AIOS online GitOps baseline. Read endpoints are public. Write endpoints require Bearer token and create Git commits on non-main branches.
|
||||
x-aios-bridge-gpts:
|
||||
- name: codex-chartgpt-bridge
|
||||
url: https://chatgpt.com/g/g-6a4ba1b4ea588191bab125d53ce83612-codex-chartgpt-bridge
|
||||
token_secret_ref: secret_ref://aios-gitops/runtime/gitops-write-bot
|
||||
api_key_field: raw token only, without Bearer prefix
|
||||
servers:
|
||||
- url: https://cnnetmkt.top
|
||||
security:
|
||||
- bearerAuth: []
|
||||
security: []
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: opaque-token
|
||||
description: Use Bearer auth for write operations. In GPT Builder Bearer mode, paste only the raw runtime token into the API key field; do not include the word Bearer and do not paste the public secret_ref string. Read endpoints remain public at server level.
|
||||
x-token-secret-ref: secret_ref://aios-gitops/runtime/gitops-write-bot
|
||||
schemas:
|
||||
BranchRequest:
|
||||
type: object
|
||||
@@ -105,26 +97,6 @@ components:
|
||||
priority: {type: integer}
|
||||
enabled: {type: boolean}
|
||||
fallback_policy: {type: string}
|
||||
ActionWriteResult:
|
||||
type: object
|
||||
properties:
|
||||
status: {type: string}
|
||||
branch: {type: string}
|
||||
rev: {type: string}
|
||||
sync_status: {type: string}
|
||||
commit: {type: object}
|
||||
patch: {type: object}
|
||||
path: {type: string}
|
||||
ErrorResponse:
|
||||
type: object
|
||||
properties:
|
||||
error: {type: string}
|
||||
hint: {type: string}
|
||||
auth_state: {type: string}
|
||||
required_scope: {type: string}
|
||||
token_secret_ref: {type: string}
|
||||
auth_header: {type: string}
|
||||
gpt_builder_note: {type: string}
|
||||
NLRequest:
|
||||
type: object
|
||||
required: [text]
|
||||
@@ -161,7 +133,6 @@ paths:
|
||||
responses: {'200': {description: OK}}
|
||||
post:
|
||||
operationId: createPatch
|
||||
x-required-scope: patch:create
|
||||
summary: Create a patch branch and commit metadata
|
||||
security: [{bearerAuth: []}]
|
||||
requestBody:
|
||||
@@ -169,10 +140,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/PatchRequest'}
|
||||
responses:
|
||||
'200': {description: Patch branch committed, content: {application/json: {schema: {$ref: '#/components/schemas/ActionWriteResult'}}}}
|
||||
'401': {description: Unauthorized, content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}}
|
||||
'403': {description: Forbidden, content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}}
|
||||
responses: {'200': {description: Patch branch committed}, '401': {description: Unauthorized}, '403': {description: Forbidden}}
|
||||
/api/model:
|
||||
get:
|
||||
operationId: readModelRegistry
|
||||
@@ -180,7 +148,6 @@ paths:
|
||||
responses: {'200': {description: OK}}
|
||||
post:
|
||||
operationId: registerModel
|
||||
x-required-scope: model:write
|
||||
summary: Register a custom model; API keys are stored as runtime secrets and Git receives secret_ref only
|
||||
security: [{bearerAuth: []}]
|
||||
requestBody:
|
||||
@@ -197,7 +164,6 @@ paths:
|
||||
/api/branch:
|
||||
post:
|
||||
operationId: createBranch
|
||||
x-required-scope: branch:create
|
||||
summary: Create a non-main branch and commit branch metadata
|
||||
security: [{bearerAuth: []}]
|
||||
requestBody:
|
||||
@@ -205,14 +171,10 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/BranchRequest'}
|
||||
responses:
|
||||
'200': {description: Branch created, content: {application/json: {schema: {$ref: '#/components/schemas/ActionWriteResult'}}}}
|
||||
'401': {description: Unauthorized, content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}}
|
||||
'403': {description: Forbidden, content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}}
|
||||
responses: {'200': {description: Branch created}, '401': {description: Unauthorized}, '403': {description: Forbidden}}
|
||||
/api/file:
|
||||
post:
|
||||
operationId: commitFile
|
||||
x-required-scope: file:write
|
||||
summary: Write a file and create a Git commit on a non-main branch
|
||||
security: [{bearerAuth: []}]
|
||||
requestBody:
|
||||
@@ -220,14 +182,10 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/FileCommitRequest'}
|
||||
responses:
|
||||
'200': {description: File committed, content: {application/json: {schema: {$ref: '#/components/schemas/ActionWriteResult'}}}}
|
||||
'400': {description: Secret rejected or invalid path, content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}}
|
||||
'401': {description: Unauthorized, content: {application/json: {schema: {$ref: '#/components/schemas/ErrorResponse'}}}}
|
||||
responses: {'200': {description: File committed}, '400': {description: Secret rejected or invalid path}, '401': {description: Unauthorized}}
|
||||
/api/commit:
|
||||
post:
|
||||
operationId: commitChanges
|
||||
x-required-scope: commit:create
|
||||
summary: Commit existing workspace changes on a non-main branch
|
||||
security: [{bearerAuth: []}]
|
||||
requestBody:
|
||||
@@ -250,7 +208,6 @@ paths:
|
||||
/api/rollback:
|
||||
post:
|
||||
operationId: requestRollback
|
||||
x-required-scope: rollback:request
|
||||
summary: Commit a rollback request requiring owner/admin approval
|
||||
security: [{bearerAuth: []}]
|
||||
requestBody:
|
||||
@@ -262,7 +219,6 @@ paths:
|
||||
/api/model-priority:
|
||||
put:
|
||||
operationId: setModelPriority
|
||||
x-required-scope: model:priority
|
||||
summary: Set model priority and commit policy file
|
||||
security: [{bearerAuth: []}]
|
||||
requestBody:
|
||||
@@ -274,7 +230,6 @@ paths:
|
||||
/api/nl:
|
||||
post:
|
||||
operationId: planNaturalLanguageGitOpsAction
|
||||
x-required-scope: nl:plan
|
||||
summary: Convert natural language to a safe committed plan without executing V-level changes
|
||||
security: [{bearerAuth: []}]
|
||||
requestBody:
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"type": "branch",
|
||||
"branch": "patch/gpt-actions-auth-test",
|
||||
"base": "origin/patch/gpt-actions-auth-test",
|
||||
"created_by": "gitops-write-bot",
|
||||
"created_at": "2026-07-07T17:44:39Z",
|
||||
"direct_main_push": false,
|
||||
"owner_approval_required": true
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
import argparse, json, os, ssl, time, urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def post_json(api: str, path: str, token: str | None, payload: dict) -> tuple[int, dict]:
|
||||
data = json.dumps(payload, ensure_ascii=False).encode('utf-8')
|
||||
req = urllib.request.Request(api.rstrip('/') + path, data=data, method='POST')
|
||||
req.add_header('Content-Type', 'application/json')
|
||||
if token:
|
||||
req.add_header('Authorization', f'Bearer {token}')
|
||||
ctx = ssl._create_unverified_context()
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30, context=ctx) as r:
|
||||
return r.status, json.loads(r.read().decode('utf-8'))
|
||||
except urllib.error.HTTPError as e:
|
||||
body = e.read().decode('utf-8', errors='ignore')
|
||||
try:
|
||||
obj = json.loads(body)
|
||||
except Exception:
|
||||
obj = {'raw': body[:300]}
|
||||
return e.code, obj
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('--api', default=os.environ.get('AIOS_GITOPS_API', 'https://cnnetmkt.top/api'))
|
||||
ap.add_argument('--token-file', default=os.environ.get('AIOS_GITOPS_WRITE_TOKEN_FILE', '/data/aios-gitops/runtime/secrets/gitops-write-bot.token'))
|
||||
ap.add_argument('--token-secret-ref', default=os.environ.get('AIOS_GITOPS_WRITE_TOKEN_REF', 'secret_ref://aios-gitops/runtime/gitops-write-bot'))
|
||||
ap.add_argument('--branch-prefix', default='patch/gitops-write-auth-smoke')
|
||||
args = ap.parse_args()
|
||||
token = Path(args.token_file).read_text(encoding='utf-8').strip()
|
||||
stamp = time.strftime('%Y%m%dT%H%M%SZ', time.gmtime())
|
||||
branch = f'{args.branch_prefix}-{stamp}'
|
||||
patch_name = f'gitops-write-auth-smoke-{stamp}'
|
||||
|
||||
noauth_code, noauth_body = post_json(args.api, '/patch', None, {'patch_name': patch_name + '-noauth'})
|
||||
if noauth_code != 401:
|
||||
raise SystemExit(f'expected noauth 401, got {noauth_code}: {noauth_body}')
|
||||
|
||||
patch_code, patch_body = post_json(args.api, '/patch', token, {
|
||||
'patch_name': patch_name,
|
||||
'branch': branch,
|
||||
'base': 'main',
|
||||
'risk': 'low',
|
||||
'description': 'GitOps write auth smoke using secret_ref token',
|
||||
'message': f'test(gitops): create write auth smoke patch {stamp}',
|
||||
})
|
||||
if patch_code != 200 or patch_body.get('status') != 'patch_branch_created':
|
||||
raise SystemExit(f'createPatch failed: {patch_code}: {patch_body}')
|
||||
|
||||
file_code, file_body = post_json(args.api, '/file', token, {
|
||||
'branch': branch,
|
||||
'base': 'main',
|
||||
'create_branch': False,
|
||||
'path': f'docs/gitops/write-auth-smoke-{stamp}.md',
|
||||
'content': f'# GitOps Write Auth Smoke\n\nTime: {stamp}\nResult: createPatch and commitFile authenticated via secret_ref token.\n',
|
||||
'message': f'test(gitops): commit write auth smoke {stamp}',
|
||||
})
|
||||
if file_code != 200 or file_body.get('status') != 'committed':
|
||||
raise SystemExit(f'commitFile failed: {file_code}: {file_body}')
|
||||
|
||||
def summarize(name: str, code: int, body: dict) -> dict:
|
||||
commit = body.get('commit') or {}
|
||||
return {
|
||||
'operation': name,
|
||||
'http': code,
|
||||
'status': body.get('status'),
|
||||
'branch': body.get('branch') or (body.get('patch') or {}).get('branch'),
|
||||
'rev': commit.get('rev') if isinstance(commit, dict) else None,
|
||||
'sync_status': ((commit.get('sync') or {}).get('status') if isinstance(commit, dict) else None),
|
||||
}
|
||||
|
||||
print(json.dumps({
|
||||
'noauth': {'operation': 'createPatch without token', 'http': noauth_code, 'status': noauth_body.get('error')},
|
||||
'createPatch': summarize('createPatch', patch_code, patch_body),
|
||||
'commitFile': summarize('commitFile', file_code, file_body),
|
||||
'token_secret_ref': args.token_secret_ref,
|
||||
}, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user