Compare commits

...
Author SHA1 Message Date
aios 1e5d5c1bdd fix(actions): add GPT Actions compatibility responses 2026-07-08 00:38:39 +08:00
aios d4dcc54044 alias test via write bot 2026-07-07 22:43:14 +08:00
aios b1c1e49cc5 fix: add READABLE aliases for all API endpoints (V1R2C3)
Add descriptive, user-friendly aliases for the existing API routes to
improve gitops-agent API discoverability:

  GET  /api/baseline     → /api/base
  GET  /api/versions     → /api/ver
  GET  /api/runtime-status → /api/run
  GET  /api/patches      → /api/patch
  POST /api/create-patch  → /api/patch
  GET  /api/branches     → /api/branch
  POST /api/create-branch → /api/branch
  POST /api/commit-file   → /api/file
  POST /api/create-tag    → /api/tag

All aliases wire through the same handler functions, tested live.
2026-07-07 22:04:47 +08:00
4 changed files with 178 additions and 15 deletions
+52 -12
View File
@@ -160,10 +160,38 @@ def patches():
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 log_request(self, format=''):
import sys
rid = getattr(self, '_request_id', uid('req'))
method = self.command
path = urllib.parse.urlparse(self.path).path
code = self.responses and list(self.responses.keys())[0] if hasattr(self, 'responses') and self.responses else 0
try:
act = self.actor()
a = ('auth_present=true' if act['ok'] else 'auth_present=false')
except:
a = 'auth_present=error'
print(f'[{now()}] {rid} {method} {path} -> {self._resp_code if hasattr(self,"_resp_code") else "?"} {a} pid={os.getpid()}', file=sys.stderr, flush=True)
def compat_headers(self):
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, PUT, HEAD, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Authorization, Content-Type, X-AIOS-Token')
self.send_header('Cache-Control', 'no-store')
def send_empty(self, code=204, ctype='application/json; charset=utf-8'):
self._resp_code = code
self.send_response(code)
self.send_header('Content-Type', ctype)
self.compat_headers()
self.send_header('Content-Length', '0')
self.end_headers()
def sendj(self, data, code=200, head=False):
self._resp_code = code
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.compat_headers(); self.send_header('Content-Length',str(len(b))); self.end_headers()
if not head: self.wfile.write(b)
def sendt(self, text, ctype='text/plain; charset=utf-8', code=200, head=False):
self._resp_code = code
b=text.encode(); self.send_response(code); self.send_header('Content-Type',ctype); self.compat_headers(); self.send_header('Content-Length',str(len(b))); self.end_headers()
if not head: 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 '{}')
@@ -178,6 +206,18 @@ class H(BaseHTTPRequestHandler):
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_OPTIONS(self):
return self.send_empty(204)
def do_HEAD(self):
raw_path = urllib.parse.urlparse(self.path).path
path = raw_path.rstrip('/') if raw_path != '/' else '/'
if path in ('/api/openapi.yaml','/api/openapi-chatgpt-actions.yaml','/api/schema'):
if OPENAPI.exists():
return self.sendt(OPENAPI.read_text(encoding='utf-8'), 'application/yaml; charset=utf-8', 200, head=True)
return self.send_empty(404)
known={'/health','/api/health','/api','/api/status','/api/base','/api/baseline','/base','/api/ver','/api/versions','/ver','/api/diff','/diff','/api/patch','/api/patches','/api/branches','/patch','/api/model','/model','/api/run','/api/runtime-status','/run','/api/action-monitor','/action-monitor'}
if path in known: return self.send_empty(200)
return self.send_empty(404)
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:
@@ -185,15 +225,15 @@ class H(BaseHTTPRequestHandler):
if path in ('/api/openapi.yaml','/api/openapi-chatgpt-actions.yaml','/api/schema'):
return self.sendt(OPENAPI.read_text(encoding='utf-8'), 'application/yaml; charset=utf-8') if OPENAPI.exists() else self.sendj({'error':'schema_missing'},404)
if path in ('/api','/api/status'): return self.sendj(api_spec())
if path in ('/api/base','/base'):
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','/ver'):
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','/patch'): return self.sendj({'patches':patches(),'branches':sh(['branch','--format','%(refname:short)'])['stdout'].splitlines()})
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','/run'):
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'):
@@ -211,11 +251,11 @@ class H(BaseHTTPRequestHandler):
def do_POST(self):
path=urllib.parse.urlparse(self.path).path.rstrip('/'); data=self.body()
try:
if path == '/api/branch': return self.branch(data)
if path == '/api/file': return self.file(data)
if path 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 == '/api/patch': return self.patch(data)
if path == '/api/tag': return self.tag(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)
+44 -3
View File
@@ -13,6 +13,32 @@ components:
scheme: bearer
bearerFormat: opaque-token
schemas:
BaselineResponse:
type: object
additionalProperties: true
properties:
active_version: {type: string}
active_tag: {type: string}
mode: {type: string}
git: {type: string}
VersionsResponse:
type: object
additionalProperties: true
properties:
tags:
type: array
items: {type: string}
versions_dir:
type: array
items: {type: string}
head: {type: string}
RuntimeStatusResponse:
type: object
additionalProperties: true
properties:
service: {type: string}
status: {type: string}
workspace_branch: {type: string}
BranchRequest:
type: object
required: [branch]
@@ -108,12 +134,22 @@ paths:
get:
operationId: getBaseline
summary: Read current AIOS baseline
responses: {'200': {description: OK}}
responses:
'200':
description: OK
content:
application/json:
schema: {$ref: '#/components/schemas/BaselineResponse'}
/api/ver:
get:
operationId: listVersions
summary: List GitOps versions
responses: {'200': {description: OK}}
responses:
'200':
description: OK
content:
application/json:
schema: {$ref: '#/components/schemas/VersionsResponse'}
/api/diff:
get:
operationId: getDiff
@@ -160,7 +196,12 @@ paths:
get:
operationId: getRuntimeStatus
summary: Read runtime status
responses: {'200': {description: OK}}
responses:
'200':
description: OK
content:
application/json:
schema: {$ref: '#/components/schemas/RuntimeStatusResponse'}
/api/branch:
post:
operationId: createBranch
@@ -0,0 +1,9 @@
{
"type": "branch",
"branch": "test/alias-write-verify",
"base": "main",
"created_by": "gitops-write-bot",
"created_at": "2026-07-07T14:43:14Z",
"direct_main_push": false,
"owner_approval_required": true
}
@@ -0,0 +1,73 @@
# V1R2C3 GPT Actions ClientResponseError Compatibility Fix Report
## Status
READY_FOR_OWNER_RETEST
## Scope
- Branch: `patch/V1R2C3-gpt-actions-clientresponse-compat`
- Service: `aios-gitops-model-api.service`
- Runtime entrypoint: `active-runtime/app/aios_chatgpt_actions_api.py`
- Schema: `docs/gitops/openapi-chatgpt-actions.yaml`
- Affected operations: `getBaseline`, `getRuntimeStatus`, `listVersions`
## Finding
The GPT Actions plugin was exposed in the GPT session, but read operations
returned `ClientResponseError`. Public `GET` probes returned `200`, while
`HEAD` and `OPTIONS` returned `501` for the schema and core read routes.
The OpenAPI document also described the core read responses without explicit
`application/json` response schemas.
## Changes
- Added compatibility response headers to the ChatGPT Actions adapter:
`Access-Control-Allow-Origin`, `Access-Control-Allow-Methods`,
`Access-Control-Allow-Headers`, and `Cache-Control`.
- Added `HEAD` handling for schema and read endpoints.
- Added `OPTIONS` handling for schema and read endpoints.
- Added explicit OpenAPI response schemas for:
- `BaselineResponse`
- `VersionsResponse`
- `RuntimeStatusResponse`
- Preserved the existing runtime request logging changes already present in
the live workspace before this fix.
## Verification
- `python3 -m py_compile active-runtime/app/aios_chatgpt_actions_api.py`: PASS
- OpenAPI YAML parse with PyYAML: PASS
- Service restart: PASS
- Public `GET /api/openapi-chatgpt-actions.yaml`: `200`
- Public `HEAD /api/openapi-chatgpt-actions.yaml`: `200`
- Public `OPTIONS /api/openapi-chatgpt-actions.yaml`: `204`
- Public `GET /api/base`: `200`
- Public `HEAD /api/base`: `200`
- Public `OPTIONS /api/base`: `204`
- Public `GET /api/run`: `200`
- Public `HEAD /api/run`: `200`
- Public `OPTIONS /api/run`: `204`
- Public `GET /api/ver`: `200`
- Public `HEAD /api/ver`: `200`
- Public `OPTIONS /api/ver`: `204`
## Owner Retest
Re-import or refresh the GPT Builder Actions schema from:
`https://cnnetmkt.top/api/openapi-chatgpt-actions.yaml`
Then retry in a fresh GPT preview/session:
1. `getRuntimeStatus`
2. `getBaseline`
3. `listVersions`
## Security Notes
- No API key, token, password, bearer value, cookie, or secret value was added.
- No force push was used.
- No tag was created.
- No production switch was executed.
- Direct main writes remain forbidden.