Compare commits

..
5 changed files with 107 additions and 10 deletions
+44 -7
View File
@@ -110,8 +110,20 @@ def ensure_git():
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)
remote_tip = None
if BARE_REPO.exists():
tip = subprocess.run(
['git', '--git-dir', str(BARE_REPO), 'rev-parse', '--verify', f'refs/heads/{b}'],
text=True,
capture_output=True,
)
if tip.returncode == 0:
remote_tip = tip.stdout.strip()
if remote_tip:
sh(['checkout', '-B', b, remote_tip], True)
else:
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')
@@ -160,10 +172,23 @@ 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 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.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):
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):
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 +203,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','/api/tag','/api/create-tag','/api/tag-request','/api/tag-approval-request','/api/stage-tag-request'}
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:
@@ -304,7 +341,7 @@ 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'],'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-request','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}')
+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]
@@ -117,12 +143,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
@@ -169,7 +205,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
+13
View File
@@ -0,0 +1,13 @@
{
"id": "nl-1783443572161-fcbaed22",
"text": "Owner authorized granting gitops-write-bot the tag:create scope required for V1R2C3 release promotion. Create an auditable GitOps permission change plan only. Do not write or expose tokens, secrets, credentials, or private keys. After scope is actually applied by the permission controller, retry V1R2C3 tag creation and preserve V1R2C2 rollback path.",
"intent": "rollback_request",
"action_plan": [
"commit rollback request",
"wait owner/admin approval"
],
"execute": false,
"owner_approval_required": true,
"created_by": "gitops-write-bot",
"created_at": "2026-07-07T16:59:32Z"
}
@@ -35,6 +35,7 @@ READY_FOR_OWNER_REIMPORT
- Schema flag check:
- `/api/tag-request` -> `createTagApprovalRequest` -> `x-openai-isConsequential: false`
- `/api/tag` -> `requestOrCreateTag` -> `x-openai-isConsequential: true`
- Package contract check includes tag approval route assertions.
## Owner Retest
+5
View File
@@ -23,4 +23,9 @@ python3 -m py_compile \
"$ROOT/adapters/openclaw/openclaw_gitops_commands.py" \
"$ROOT/adapters/hermes/hermes_gitops_nl_router.py"
grep -q "/api/branch" "$ROOT/docs/gitops/openapi-chatgpt-actions.yaml"
grep -q "/api/tag-request" "$ROOT/docs/gitops/openapi-chatgpt-actions.yaml"
grep -q "operationId: createTagApprovalRequest" "$ROOT/docs/gitops/openapi-chatgpt-actions.yaml"
grep -q "x-openai-isConsequential: false" "$ROOT/docs/gitops/openapi-chatgpt-actions.yaml"
grep -q "operationId: requestOrCreateTag" "$ROOT/docs/gitops/openapi-chatgpt-actions.yaml"
grep -q "x-openai-isConsequential: true" "$ROOT/docs/gitops/openapi-chatgpt-actions.yaml"
echo "PACKAGE_CONTRACT_OK $(cat "$ROOT/VERSION")"