Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8476dd0009 | ||
|
|
33935b5f67 | ||
|
|
569d258b47 | ||
|
|
b1fd9db1dd | ||
|
|
2730098095 | ||
|
|
eb48162898 |
@@ -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}')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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")"
|
||||
Reference in New Issue
Block a user