Compare commits

...
7 changed files with 240 additions and 10 deletions
+56 -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:
@@ -215,6 +252,7 @@ class H(BaseHTTPRequestHandler):
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-request','/api/tag-approval-request','/api/stage-tag-request'): return self.tag_request(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)
@@ -242,6 +280,17 @@ class H(BaseHTTPRequestHandler):
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_request(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)
b=checkout(data.get('branch') or f'request/tag-{safe(ver)}', data.get('base','main'), True)
req={'version':ver,'message':data.get('message') or f'request tag {ver}','requested_by':a['account'],'approval_required':True,'execute':False,'tag_created':False,'created_at':now(),'operation':'create_tag_approval_request'}
r=write_json(f'baseline/tag_requests/{safe(ver)}.json', req)
c=commit([r], data.get('commit_message') or f'chore(chatgpt): request tag {ver}')
audit(a['account'],'tag-request',ver,{'branch':b,'request':req,'commit':c})
self.sendj({'status':'tag_request_committed','branch':b,'request':req,'commit':c,'tag_created':False})
def tag(self,data):
a=self.need('tag:create');
if not a: return
@@ -292,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}')
+67 -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]
@@ -57,6 +83,15 @@ components:
execute:
type: boolean
description: Owner-approved calls only. Otherwise an approval request commit is created.
TagApprovalRequest:
type: object
required: [version]
properties:
version: {type: string, example: V1R2C3}
message: {type: string}
branch: {type: string, description: Optional request branch. Defaults to request/tag-<version>.}
commit_message: {type: string}
execute: {type: boolean, default: false, description: Ignored; this action never creates a tag.}
RollbackRequest:
type: object
required: [target_version]
@@ -108,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
@@ -160,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
@@ -194,10 +244,24 @@ paths:
application/json:
schema: {$ref: '#/components/schemas/CommitRequest'}
responses: {'200': {description: Commit result}, '401': {description: Unauthorized}}
/api/tag-request:
post:
operationId: createTagApprovalRequest
summary: Stage a tag approval request without creating a Git tag
description: Low-risk preparation step. It commits an approval request on a non-main branch and never creates a tag.
x-openai-isConsequential: false
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/TagApprovalRequest'}
responses: {'200': {description: Tag approval request committed}, '401': {description: Unauthorized}, '403': {description: Forbidden}}
/api/tag:
post:
operationId: requestOrCreateTag
summary: Request tag creation; owner execution is required for direct tag
x-openai-isConsequential: true
security: [{bearerAuth: []}]
requestBody:
required: true
+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"
}
@@ -0,0 +1,9 @@
{
"id": "patch-1783442954588-5b091d8b",
"patch_name": "v1r2c3-promotion-request-20260708",
"branch": "patch/v1r2c3-promotion-request-20260708",
"risk": "high",
"description": "Promotion request for V1R2C3 after GPT Actions ClientResponseError compatibility recovery. Read/write GitOps Actions passed from codex-chartgpt-bridge. Baseline remains V1R2C2; request owner-reviewed V1R2C3 tag/baseline promotion without direct destructive writes.",
"created_by": "gitops-write-bot",
"created_at": "2026-07-07T16:49:14Z"
}
@@ -0,0 +1,58 @@
# V1R2C3 Tag Approval Action Governance Report
## Status
READY_FOR_OWNER_REIMPORT
## Scope
- Branch: `patch/V1R2C3-tag-approval-action-governance`
- Runtime: `active-runtime/app/aios_chatgpt_actions_api.py`
- Schema: `docs/gitops/openapi-chatgpt-actions.yaml`
- Goal: reduce repeated GPT Actions confirmation by splitting tag staging from tag execution.
## Changes
- Added `POST /api/tag-request` with operationId `createTagApprovalRequest`.
- Marked `createTagApprovalRequest` as `x-openai-isConsequential: false`.
- `createTagApprovalRequest` only commits a tag approval request on a non-main branch.
- `createTagApprovalRequest` never creates a Git tag and returns `tag_created: false`.
- Kept `POST /api/tag` operationId `requestOrCreateTag` for actual tag execution.
- Marked `requestOrCreateTag` as `x-openai-isConsequential: true`.
## Safety Model
- Daily GPT use should call `createTagApprovalRequest` for low-risk staging.
- Actual tag creation remains behind `requestOrCreateTag` and still requires confirmation.
- Direct main writes remain forbidden.
- No force push is allowed.
- V-level, tag, baseline promotion, and production changes still require owner approval.
## Validation
- Python compile for the runtime adapter: PASS
- OpenAPI YAML parse: PASS
- 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
Re-import the schema from:
`https://cnnetmkt.top/api/openapi-chatgpt-actions.yaml`
Then verify:
1. `createTagApprovalRequest` appears as the low-risk tag staging action.
2. `requestOrCreateTag` still appears as the high-risk tag execution action.
3. Use `createTagApprovalRequest` for staging V1R2C3 approval.
4. Use `requestOrCreateTag` only for final owner-approved tag creation.
## Security Notes
- No API key, token, password, bearer value, cookie, or secret value was added.
- No force push was used.
- No tag was created by this deployment.
- No production switch was executed.
@@ -0,0 +1,32 @@
# V1R2C3 GPT Actions Promotion Readiness Report
Status: READY_FOR_OWNER_REVIEW
Verified:
- getRuntimeStatus: PASS
- getBaseline: PASS
- listVersions: PASS
- listPatches: PASS
- getDiff: PASS
- createBranch: PASS
- createPatch: PASS
Current baseline:
- active_version: V1R2C2
- active_tag: V1R2C2
Promotion target:
- V1R2C3
Reason:
GPT Actions ClientResponseError compatibility recovery completed. Changes include route compatibility, OpenAPI response schema compatibility, and Actions request handling improvements.
Risk:
High-risk release actions require owner approval before tag creation and baseline promotion.
No secrets, tokens, credentials, or private keys are included in this report.
+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")"