Compare commits

...
Author SHA1 Message Date
aios fd445a9448 feat(actions): add GPT Actions consequential governance 2026-07-07 16:06:20 +08:00
9 changed files with 1116 additions and 94 deletions
+59 -3
View File
@@ -20,6 +20,14 @@ BASE_MODELS = [
]
SECRET_RE = re.compile(r'(?i)(api[_-]?key|secret[_-]?access[_-]?key|secret|password|token)\s*[:=]\s*["\']?([A-Za-z0-9_./+=:-]{16,})')
SECRET_VAL_RE = re.compile(r'(sk-[A-Za-z0-9_-]{20,}|HPUA[A-Z0-9]{10,}|SDC[A-Za-z0-9]{20,})')
SECRET_TEXT_RE = re.compile(r'(?i)(Bearer\\s+[A-Za-z0-9._-]{3,}|BEGIN\\s+PRIVATE\\s+KEY|password\\s*=\\s*\\S+|api_key\\s*=\\s*\\S+|access_token\\s*=\\s*\\S+|refresh_token\\s*=\\s*\\S+)')
SECRET_PATH_PREFIXES = ('runtime/secrets/', 'private/', 'prod-secrets/')
LOW_RISK_FILE_SCOPES = {
'/api/gitops/report-file': ('commitReportFile', ('reports/',)),
'/api/gitops/docs-file': ('commitDocsFile', ('docs/',)),
'/api/gitops/proposal-file': ('commitProposalFile', ('proposals/',)),
'/api/gitops/patch-evidence': ('commitPatchEvidence', ('reports/', 'patches/')),
}
def now() -> str: return time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
def uid(p='id') -> str: return f'{p}-{int(time.time()*1000)}-{uuid.uuid4().hex[:8]}'
@@ -35,6 +43,15 @@ def relpath(path: str) -> str:
x = str(path or '').replace('\\','/').strip()
if not x or x.startswith('/') or '..' in Path(x).parts or Path(x).parts[0] == '.git': raise ValueError('invalid path')
return str(Path(x))
def secret_path(path: str) -> bool:
r = relpath(path).replace('\\', '/')
name = Path(r).name
return name.endswith('.token') or r == '.env' or any(r.startswith(prefix) for prefix in SECRET_PATH_PREFIXES)
def path_in_scope(path: str, scopes: tuple[str, ...]) -> bool:
r = relpath(path).replace('\\', '/')
return any(r.startswith(scope) for scope in scopes)
def branch_name(name: str | None, prefix='patch/chatgpt') -> str:
b = safe(name or f'{prefix}-{uid("br")}', 180)
if b in ('main','master') or b.startswith('main/') or b.startswith('master/'): raise ValueError('direct main/master writes are forbidden')
@@ -63,7 +80,7 @@ def content_has_secret(text: str) -> bool:
return _json_has_plain_secret(json.loads(text))
except Exception:
pass
if SECRET_VAL_RE.search(text):
if SECRET_VAL_RE.search(text) or SECRET_TEXT_RE.search(text):
return True
for m in SECRET_RE.finditer(text):
key = str(m.group(1)).lower()
@@ -109,8 +126,10 @@ def checkout(branch, base='main', create=False):
else: sh(['checkout',b], True)
return b
def write_file(path, content):
r=relpath(path)
if secret_path(r): raise ValueError('secret_path_rejected: use runtime secret store or secret_ref')
if content_has_secret(content): raise ValueError('secret_rejected: use secret_ref; plaintext API keys must not enter Git')
r=relpath(path); t=WS/r; t.parent.mkdir(parents=True, exist_ok=True); t.write_text(content, encoding='utf-8'); return r
t=WS/r; t.parent.mkdir(parents=True, exist_ok=True); t.write_text(content, encoding='utf-8'); return r
def write_json(path, data): return write_file(path, json.dumps(data, ensure_ascii=False, indent=2) + '\n')
def sync_branch(branch: str) -> dict:
if not branch or branch in ('main','master') or branch.startswith('main/') or branch.startswith('master/'):
@@ -201,6 +220,9 @@ class H(BaseHTTPRequestHandler):
path=urllib.parse.urlparse(self.path).path.rstrip('/'); data=self.body()
try:
if path == '/api/branch': return self.branch(data)
if path in LOW_RISK_FILE_SCOPES: return self.low_risk_file(path, data)
if path == '/api/gitops/main-file': return self.main_file(data)
if path == '/api/gitops/rollback-request': return self.rollback(data)
if path == '/api/file': return self.file(data)
if path == '/api/commit': return self.commit(data)
if path == '/api/patch': return self.patch(data)
@@ -220,6 +242,40 @@ class H(BaseHTTPRequestHandler):
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}'); audit(a['account'],'file',r,{'branch':b,'commit':c}); self.sendj({'status':'committed','branch':b,'path':r,'commit':c})
def low_risk_file(self,path,data):
a=self.need('file:write');
if not a: return
operation_id, scopes = LOW_RISK_FILE_SCOPES[path]
requested_branch = data.get('branch')
if not requested_branch or branch_name(requested_branch) != requested_branch:
return self.sendj({'error':'low-risk write requires explicit non-main patch/request branch','operationId':operation_id},400)
r=relpath(data.get('path'))
if secret_path(r):
return self.sendj({'error':'Secret-like path rejected. Use runtime secret store or secret_ref.','operationId':operation_id},400)
if not path_in_scope(r, scopes):
return self.sendj({'error':'path_out_of_scope','operationId':operation_id,'allowed_scopes':list(scopes)},400)
if content_has_secret(data.get('content','')):
return self.sendj({'error':'Secret-like content detected. Use runtime secret store or secret_ref.','operationId':operation_id},400)
b=checkout(requested_branch, data.get('base','main'), bool(data.get('create_branch')))
r=write_file(r, data.get('content',''))
c=commit([r], data.get('message') or f'chore(chatgpt): {operation_id} {r}')
audit(a['account'],operation_id,r,{'branch':b,'commit':c})
self.sendj({'status':'committed','operationId':operation_id,'branch':b,'path':r,'commit':c})
def main_file(self,data):
a=self.need('file:main');
if not a: return
if a.get('role') != 'owner':
return self.sendj({'error':'owner approval required for commitMainFile'},403)
r=relpath(data.get('path'))
if secret_path(r):
return self.sendj({'error':'Secret-like path rejected. Use runtime secret store or secret_ref.','operationId':'commitMainFile'},400)
if content_has_secret(data.get('content','')):
return self.sendj({'error':'Secret-like content detected. Use runtime secret store or secret_ref.','operationId':'commitMainFile'},400)
ensure_git(); sh(['checkout','main'], True)
r=write_file(r, data.get('content',''))
c=commit([r], data.get('message') or f'chore(chatgpt): commit main file {r}')
audit(a['account'],'commitMainFile',r,{'branch':'main','commit':c})
self.sendj({'status':'committed','operationId':'commitMainFile','branch':'main','path':r,'commit':c})
def commit(self,data):
a=self.need('commit:create');
if not a: return
@@ -270,7 +326,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/gitops/report-file','POST /api/gitops/docs-file','POST /api/gitops/proposal-file','POST /api/gitops/patch-evidence','POST /api/gitops/main-file','POST /api/commit','POST /api/patch','POST /api/tag','POST /api/rollback','POST /api/gitops/rollback-request','POST /api/model','PUT /api/model-priority','POST /api/nl'],'policy':{'bearer_required_for_write':True,'direct_main_push':False,'low_risk_main_write':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}')
@@ -0,0 +1,70 @@
# GPT Actions Consequential Governance
## Objective
Reduce unnecessary ChatGPT Actions confirmation prompts for low-risk GitOps write operations while preserving confirmation protection for high-risk changes.
## Platform Boundary
Instructions can reduce how often a Custom GPT chooses high-risk actions, but they cannot guarantee that the ChatGPT platform will suppress every external Action confirmation prompt. The platform may still ask the user to confirm writes to external services.
`x-openai-isConsequential` is the OpenAPI-level control used to communicate whether an Action is consequential:
- Low-risk GitOps evidence and documentation writes use `x-openai-isConsequential: false`.
- High-risk runtime, rollback, main-branch, tag, release, baseline, authentication, permission, and production-model changes use `x-openai-isConsequential: true`.
Fully unattended background writes should be executed by the AIOS Orchestrator, not by direct Custom GPT UI Actions.
## Low-Risk Actions
Low-risk Actions are limited to non-main patch branches and controlled evidence paths:
| operationId | Consequential | Path Scope |
|---|---:|---|
| `commitReportFile` | `false` | `reports/**` |
| `commitDocsFile` | `false` | `docs/**` |
| `commitProposalFile` | `false` | `proposals/**` |
| `commitPatchEvidence` | `false` | `reports/**`, `patches/**` |
Low-risk Actions must reject:
- `main` and `master` writes.
- `runtime/secrets/**`.
- `.env`.
- `*.token`.
- `private/**`.
- `prod-secrets/**`.
- Secret-like content such as credential values, passwords, API keys, private keys, or access tokens.
- Merge, rollback, tag, release, runtime, production, baseline, permission, or authentication mutations.
## High-Risk Actions
High-risk Actions intentionally keep confirmation protection:
| operationId | Consequential | Reason |
|---|---:|---|
| `commitFile` | `true` | Legacy generic write API retained for compatibility. |
| `commitMainFile` | `true` | Explicit main-branch write. |
| `createRollbackRequest` | `true` | Rollback-related workflow. |
| `requestRollback` | `true` | Legacy rollback request compatibility. |
| `requestOrCreateTag` | `true` | Release/tag workflow. |
| `setModelPriority` | `true` | Model-routing policy mutation. |
| `registerModel` | `true` | Runtime model registry mutation. |
## Compatibility
The legacy `commitFile` operation is preserved and marked consequential. Custom GPTs should prefer the low-risk specialized operations for docs, reports, proposals, and patch evidence.
## Secret Handling
The schema uses placeholders only. Action authentication remains injected by GPT Builder Actions configuration at runtime. Real tokens, passwords, API keys, private keys, and secret values must never be placed in OpenAPI examples, docs, reports, tests, or scripts.
## Validation
Validation is covered by:
- `tests/test_openapi_consequential_flags.py`.
- `tests/test_gitops_low_risk_commit_policy.py`.
- `scripts/smoke_gpt_actions_consequential_governance.py`.
These checks validate the consequential matrix, path scoping, main-branch rejection for low-risk writes, secret-path rejection, secret-like content rejection, and placeholder-only schema examples.
+470 -91
View File
@@ -2,9 +2,11 @@ openapi: 3.1.0
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.
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.
servers:
- url: https://cnnetmkt.top
- url: https://cnnetmkt.top
security: []
components:
securitySchemes:
@@ -15,226 +17,603 @@ components:
schemas:
BranchRequest:
type: object
required: [branch]
required:
- branch
properties:
branch: {type: string, example: patch/chatgpt-actions-demo}
base: {type: string, default: main}
message: {type: string}
branch:
type: string
example: patch/chatgpt-actions-demo
base:
type: string
default: main
message:
type: string
FileCommitRequest:
type: object
required: [branch, path, content]
required:
- branch
- path
- content
properties:
branch: {type: string}
base: {type: string, default: main}
create_branch: {type: boolean, default: false}
path: {type: string, example: docs/gitops/chatgpt-change.md}
content: {type: string}
message: {type: string}
branch:
type: string
base:
type: string
default: main
create_branch:
type: boolean
default: false
path:
type: string
example: docs/gitops/chatgpt-change.md
content:
type: string
message:
type: string
CommitRequest:
type: object
properties:
branch: {type: string}
branch:
type: string
paths:
type: array
items: {type: string}
message: {type: string}
items:
type: string
message:
type: string
PatchRequest:
type: object
required: [patch_name]
required:
- patch_name
properties:
patch_name: {type: string}
branch: {type: string}
base: {type: string, default: main}
risk: {type: string, enum: [low, medium, high]}
description: {type: string}
message: {type: string}
patch_name:
type: string
branch:
type: string
base:
type: string
default: main
risk:
type: string
enum:
- low
- medium
- high
description:
type: string
message:
type: string
TagRequest:
type: object
required: [version]
required:
- version
properties:
version: {type: string, example: V1R1C1}
message: {type: string}
version:
type: string
example: V1R1C1
message:
type: string
execute:
type: boolean
description: Owner-approved calls only. Otherwise an approval request commit is created.
description: Owner-approved calls only. Otherwise an approval request commit
is created.
RollbackRequest:
type: object
required: [target_version]
required:
- target_version
properties:
target_version: {type: string}
reason: {type: string}
branch: {type: string}
target_version:
type: string
reason:
type: string
branch:
type: string
ModelRequest:
type: object
required: [provider_name, base_url, model_name]
required:
- provider_name
- base_url
- model_name
properties:
username: {type: string}
provider_name: {type: string}
base_url: {type: string}
model_name: {type: string}
display_name: {type: string}
username:
type: string
provider_name:
type: string
base_url:
type: string
model_name:
type: string
display_name:
type: string
api_key_secret_ref:
type: string
description: Preferred. Git stores this secret_ref only.
api_key:
type: string
writeOnly: true
description: Accepted only for runtime secret storage; never committed to Git or logs.
branch: {type: string}
message: {type: string}
description: Accepted only for runtime secret storage; never committed to
Git or logs.
branch:
type: string
message:
type: string
ModelPriorityRequest:
type: object
required: [items]
required:
- items
properties:
username: {type: string}
branch: {type: string}
username:
type: string
branch:
type: string
items:
type: array
items:
type: object
properties:
model_id: {type: string}
priority: {type: integer}
enabled: {type: boolean}
fallback_policy: {type: string}
model_id:
type: string
priority:
type: integer
enabled:
type: boolean
fallback_policy:
type: string
NLRequest:
type: object
required: [text]
required:
- text
properties:
text: {type: string}
branch: {type: string}
text:
type: string
branch:
type: string
LowRiskFileCommitRequest:
type: object
required:
- branch
- path
- content
- message
properties:
branch:
type: string
description: Non-main patch branch. Must start with patch/ or request/.
example: patch/gpt-actions-consequential-governance
base:
type: string
default: main
description: Base branch used when create_branch is true.
create_branch:
type: boolean
default: false
path:
type: string
description: File path constrained by the operation path scope.
example: reports/example-report.md
content:
type: string
description: File content. Must not include secrets, credentials, passwords,
API keys, private keys, or token values.
message:
type: string
description: Commit message. Must not include secrets or credentials.
example: 'docs(actions): add GPT Actions governance evidence'
MainFileCommitRequest:
type: object
required:
- path
- content
- message
properties:
branch:
type: string
default: main
description: Main branch. This high-risk operation requires explicit owner
intent.
path:
type: string
description: File path for explicit main-branch maintenance. Secret paths
are always rejected.
content:
type: string
description: File content. Must not include secrets, credentials, passwords,
API keys, private keys, or token values.
message:
type: string
description: Commit message. Must not include secrets or credentials.
RollbackRequestAction:
type: object
required:
- target_version
- reason
properties:
target_version:
type: string
example: V1R1C4
reason:
type: string
example: Owner-approved rollback request
branch:
type: string
example: request/rollback-V1R1C4
paths:
/api/base:
get:
operationId: getBaseline
summary: Read current AIOS baseline
responses: {'200': {description: OK}}
responses:
'200':
description: OK
/api/ver:
get:
operationId: listVersions
summary: List GitOps versions
responses: {'200': {description: OK}}
responses:
'200':
description: OK
/api/diff:
get:
operationId: getDiff
summary: Read Git diff between refs
parameters:
- in: query
name: from
schema: {type: string, default: HEAD~1}
- in: query
name: to
schema: {type: string, default: HEAD}
responses: {'200': {description: OK}}
- in: query
name: from
schema:
type: string
default: HEAD~1
- in: query
name: to
schema:
type: string
default: HEAD
responses:
'200':
description: OK
/api/patch:
get:
operationId: listPatches
summary: List patch records
responses: {'200': {description: OK}}
responses:
'200':
description: OK
post:
operationId: createPatch
summary: Create a patch branch and commit metadata
security: [{bearerAuth: []}]
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/PatchRequest'}
responses: {'200': {description: Patch branch committed}, '401': {description: Unauthorized}, '403': {description: Forbidden}}
schema:
$ref: '#/components/schemas/PatchRequest'
responses:
'200':
description: Patch branch committed
'401':
description: Unauthorized
'403':
description: Forbidden
x-openai-isConsequential: true
/api/model:
get:
operationId: readModelRegistry
summary: Read visible model registry and priority
responses: {'200': {description: OK}}
responses:
'200':
description: OK
post:
operationId: registerModel
summary: Register a custom model; API keys are stored as runtime secrets and Git receives secret_ref only
security: [{bearerAuth: []}]
summary: Register a custom model; API keys are stored as runtime secrets and
Git receives secret_ref only
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/ModelRequest'}
responses: {'200': {description: Model registered and committed}, '401': {description: Unauthorized}, '403': {description: Forbidden}}
schema:
$ref: '#/components/schemas/ModelRequest'
responses:
'200':
description: Model registered and committed
'401':
description: Unauthorized
'403':
description: Forbidden
x-openai-isConsequential: true
/api/run:
get:
operationId: getRuntimeStatus
summary: Read runtime status
responses: {'200': {description: OK}}
responses:
'200':
description: OK
/api/branch:
post:
operationId: createBranch
summary: Create a non-main branch and commit branch metadata
security: [{bearerAuth: []}]
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/BranchRequest'}
responses: {'200': {description: Branch created}, '401': {description: Unauthorized}, '403': {description: Forbidden}}
schema:
$ref: '#/components/schemas/BranchRequest'
responses:
'200':
description: Branch created
'401':
description: Unauthorized
'403':
description: Forbidden
x-openai-isConsequential: true
/api/file:
post:
operationId: commitFile
summary: Write a file and create a Git commit on a non-main branch
security: [{bearerAuth: []}]
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/FileCommitRequest'}
responses: {'200': {description: File committed}, '400': {description: Secret rejected or invalid path}, '401': {description: Unauthorized}}
schema:
$ref: '#/components/schemas/FileCommitRequest'
responses:
'200':
description: File committed
'400':
description: Secret rejected or invalid path
'401':
description: Unauthorized
x-openai-isConsequential: true
/api/commit:
post:
operationId: commitChanges
summary: Commit existing workspace changes on a non-main branch
security: [{bearerAuth: []}]
security:
- bearerAuth: []
requestBody:
required: false
content:
application/json:
schema: {$ref: '#/components/schemas/CommitRequest'}
responses: {'200': {description: Commit result}, '401': {description: Unauthorized}}
schema:
$ref: '#/components/schemas/CommitRequest'
responses:
'200':
description: Commit result
'401':
description: Unauthorized
x-openai-isConsequential: true
/api/tag:
post:
operationId: requestOrCreateTag
summary: Request tag creation; owner execution is required for direct tag
security: [{bearerAuth: []}]
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/TagRequest'}
responses: {'200': {description: Tag request committed or tag created}, '403': {description: Forbidden}}
schema:
$ref: '#/components/schemas/TagRequest'
responses:
'200':
description: Tag request committed or tag created
'403':
description: Forbidden
x-openai-isConsequential: true
/api/rollback:
post:
operationId: requestRollback
summary: Commit a rollback request requiring owner/admin approval
security: [{bearerAuth: []}]
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/RollbackRequest'}
responses: {'200': {description: Rollback request committed}, '401': {description: Unauthorized}}
schema:
$ref: '#/components/schemas/RollbackRequest'
responses:
'200':
description: Rollback request committed
'401':
description: Unauthorized
x-openai-isConsequential: true
/api/model-priority:
put:
operationId: setModelPriority
summary: Set model priority and commit policy file
security: [{bearerAuth: []}]
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/ModelPriorityRequest'}
responses: {'200': {description: Priority updated and committed}, '401': {description: Unauthorized}}
schema:
$ref: '#/components/schemas/ModelPriorityRequest'
responses:
'200':
description: Priority updated and committed
'401':
description: Unauthorized
x-openai-isConsequential: true
/api/nl:
post:
operationId: planNaturalLanguageGitOpsAction
summary: Convert natural language to a safe committed plan without executing V-level changes
security: [{bearerAuth: []}]
summary: Convert natural language to a safe committed plan without executing
V-level changes
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/NLRequest'}
responses: {'200': {description: Plan committed}, '401': {description: Unauthorized}}
schema:
$ref: '#/components/schemas/NLRequest'
responses:
'200':
description: Plan committed
'401':
description: Unauthorized
x-openai-isConsequential: true
/api/gitops/report-file:
post:
operationId: commitReportFile
x-openai-isConsequential: false
x-aios-path-scope: reports/**
summary: Commit a report file to a controlled GitOps patch branch
description: Low-risk report write. Commits markdown or JSON evidence under
reports/** to a non-main patch branch. This operation must not write secrets,
credentials, token values, runtime secret files, rollback requests, or production
config.
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/LowRiskFileCommitRequest'
responses:
'200':
description: File committed successfully
'400':
description: Invalid branch, path, or secret-like content rejected
'401':
description: Authorization header was not injected
'403':
description: Authorization scope is insufficient
/api/gitops/docs-file:
post:
operationId: commitDocsFile
x-openai-isConsequential: false
x-aios-path-scope: docs/**
summary: Commit a docs file to a controlled GitOps patch branch
description: Low-risk documentation write under docs/** on a non-main patch
branch. It must not include real tokens, passwords, API keys, private keys,
credentials, rollback execution, or production config mutation.
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/LowRiskFileCommitRequest'
responses:
'200':
description: File committed successfully
'400':
description: Invalid branch, path, or secret-like content rejected
'401':
description: Authorization header was not injected
'403':
description: Authorization scope is insufficient
/api/gitops/proposal-file:
post:
operationId: commitProposalFile
x-openai-isConsequential: false
x-aios-path-scope: proposals/**
summary: Commit a proposal file to a controlled GitOps patch branch
description: Low-risk proposal write under proposals/** on a non-main patch
branch. It must not include real tokens, passwords, API keys, private keys,
credentials, runtime secret files, rollback execution, or production config
mutation.
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/LowRiskFileCommitRequest'
responses:
'200':
description: File committed successfully
'400':
description: Invalid branch, path, or secret-like content rejected
'401':
description: Authorization header was not injected
'403':
description: Authorization scope is insufficient
/api/gitops/patch-evidence:
post:
operationId: commitPatchEvidence
x-openai-isConsequential: false
x-aios-path-scope: reports/**, patches/**
summary: Commit patch evidence to a controlled GitOps patch branch
description: Low-risk evidence write under reports/** or patches/** on a non-main
patch branch. It must not include real tokens, passwords, API keys, private
keys, credentials, runtime secret files, rollback execution, or production
config mutation.
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/LowRiskFileCommitRequest'
responses:
'200':
description: File committed successfully
'400':
description: Invalid branch, path, or secret-like content rejected
'401':
description: Authorization header was not injected
'403':
description: Authorization scope is insufficient
/api/gitops/main-file:
post:
operationId: commitMainFile
x-openai-isConsequential: true
summary: Commit a file directly to main branch
description: High-risk explicit main-branch write. This operation exists only
for owner-approved maintenance and must not include real secrets, credentials,
passwords, private keys, API keys, token values, or runtime secret paths.
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/MainFileCommitRequest'
responses:
'200':
description: File committed successfully
'400':
description: Invalid path or secret-like content rejected
'401':
description: Authorization header was not injected
'403':
description: Authorization scope is insufficient or owner approval missing
/api/gitops/rollback-request:
post:
operationId: createRollbackRequest
x-openai-isConsequential: true
summary: Create an owner-review rollback request
description: High-risk rollback request. Creates auditable rollback intent and
must never silently mutate production state or bypass owner/admin review.
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RollbackRequestAction'
responses:
'200':
description: Rollback request committed
'400':
description: Invalid rollback request or secret-like content rejected
'401':
description: Authorization header was not injected
'403':
description: Authorization scope is insufficient or owner approval missing
@@ -0,0 +1,85 @@
# GPT Actions Consequential Governance Report
Status: READY_FOR_REVIEW
Branch: patch/gpt-actions-consequential-governance
Commit: see final OpenClaw response for immutable commit rev
## Objective
Reduce unnecessary ChatGPT Actions confirmation prompts for low-risk GitOps write operations while preserving confirmation protection for high-risk operations.
## Changes
### Low-risk Actions
| operationId | x-openai-isConsequential | Path Scope |
|---|---:|---|
| commitReportFile | false | reports/** |
| commitDocsFile | false | docs/** |
| commitProposalFile | false | proposals/** |
| commitPatchEvidence | false | reports/**, patches/** |
### High-risk Actions
| operationId | x-openai-isConsequential |
|---|---:|
| commitFile | true |
| commitMainFile | true |
| createRollbackRequest | true |
| requestRollback | true |
| requestOrCreateTag | true |
| setModelPriority | true |
| registerModel | true |
## OpenAPI Operations Added
- commitReportFile
- commitDocsFile
- commitProposalFile
- commitPatchEvidence
- commitMainFile
- createRollbackRequest
## OpenAPI Operations Updated
- commitFile marked `x-openai-isConsequential: true`.
- commitChanges marked `x-openai-isConsequential: true`.
- createPatch marked `x-openai-isConsequential: true`.
- createBranch marked `x-openai-isConsequential: true`.
- requestOrCreateTag marked `x-openai-isConsequential: true`.
- requestRollback marked `x-openai-isConsequential: true`.
- registerModel marked `x-openai-isConsequential: true`.
- setModelPriority marked `x-openai-isConsequential: true`.
- planNaturalLanguageGitOpsAction marked `x-openai-isConsequential: true`.
## Security Validation
- No real tokens added.
- No secret values added.
- OpenAPI examples use placeholder values only.
- Action auth remains injected by GPT Builder Actions configuration.
- Low-risk policy rejects `main`, `runtime/secrets/**`, `.env`, `*.token`, `private/**`, `prod-secrets/**`, and secret-like content.
## Tests
- pytest: not run; remote Python environment does not have the `pytest` module installed.
- direct test runner: passed, 14 test functions.
- smoke: passed.
## Smoke Result
`python3 scripts/smoke_gpt_actions_consequential_governance.py` passed.
## Rollback Plan
Discard patch branch before merge, or revert the governance commit after merge.
## Risk
Medium. The change affects GPT Builder Action behavior and write API affordances while preserving high-risk confirmation gates.
## Next Step
Review OpenAPI schema in GPT Builder Actions and retest whether low-risk commit operations show Always allow or reduced confirmation prompts.
+131
View File
@@ -0,0 +1,131 @@
#!/usr/bin/env python3
"""Policy helpers for GPT Actions GitOps write operations.
The helpers intentionally avoid runtime credentials. They are used by tests and
smoke checks to keep the OpenAPI consequential-action split aligned with server
side write policy requirements.
"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import PurePosixPath
import re
LOW_RISK_PATH_SCOPES: dict[str, tuple[str, ...]] = {
"commitReportFile": ("reports/",),
"commitDocsFile": ("docs/",),
"commitProposalFile": ("proposals/",),
"commitPatchEvidence": ("reports/", "patches/"),
}
HIGH_RISK_OPERATIONS: set[str] = {
"commitFile",
"commitMainFile",
"createRollbackRequest",
"requestRollback",
"requestOrCreateTag",
"commitRuntimeFile",
"commitConfigFile",
"mergeMain",
"adjustProductionModelPriority",
"setModelPriority",
"registerModel",
}
SECRET_PATH_PATTERNS: tuple[str, ...] = (
"runtime/secrets/",
".env",
"private/",
"prod-secrets/",
)
SECRET_CONTENT_PATTERNS: tuple[re.Pattern[str], ...] = (
re.compile(r"sk-[A-Za-z0-9_-]{8,}"),
re.compile(r"Bearer\s+\S+", re.IGNORECASE),
re.compile(r"BEGIN\s+PRIVATE\s+KEY", re.IGNORECASE),
re.compile(r"password\s*=\s*\S+", re.IGNORECASE),
re.compile(r"api_key\s*=\s*\S+", re.IGNORECASE),
re.compile(r"access_token\s*=\s*\S+", re.IGNORECASE),
re.compile(r"refresh_token\s*=\s*\S+", re.IGNORECASE),
)
@dataclass(frozen=True)
class PolicyResult:
allowed: bool
reason: str
def normalize_path(path: str) -> str:
raw = (path or "").replace("\\", "/").strip()
if not raw:
raise ValueError("path_required")
posix = PurePosixPath(raw)
if posix.is_absolute():
raise ValueError("absolute_path_forbidden")
if any(part in {"..", ".git"} for part in posix.parts):
raise ValueError("unsafe_path_forbidden")
return str(posix)
def is_main_branch(branch: str | None) -> bool:
value = (branch or "").strip()
return value in {"main", "master"} or value.startswith("main/") or value.startswith("master/")
def is_secret_path(path: str) -> bool:
normalized = normalize_path(path)
name = PurePosixPath(normalized).name
if name.endswith(".token"):
return True
return any(normalized == pattern.rstrip("/") or normalized.startswith(pattern) for pattern in SECRET_PATH_PATTERNS)
def contains_secret_like_content(content: str) -> bool:
text = content or ""
return any(pattern.search(text) for pattern in SECRET_CONTENT_PATTERNS)
def path_in_scope(operation_id: str, path: str) -> bool:
normalized = normalize_path(path)
scopes = LOW_RISK_PATH_SCOPES.get(operation_id, ())
return any(normalized.startswith(scope) for scope in scopes)
def validate_low_risk_commit(operation_id: str, branch: str, path: str, content: str = "") -> PolicyResult:
if operation_id not in LOW_RISK_PATH_SCOPES:
return PolicyResult(False, "unknown_low_risk_operation")
if is_main_branch(branch):
return PolicyResult(False, "low_risk_action_rejects_main")
try:
normalized = normalize_path(path)
except ValueError as exc:
return PolicyResult(False, str(exc))
if is_secret_path(normalized):
return PolicyResult(False, "secret_path_rejected")
if not path_in_scope(operation_id, normalized):
return PolicyResult(False, "path_out_of_scope")
if contains_secret_like_content(content):
return PolicyResult(False, "secret_like_content_detected")
return PolicyResult(True, "allowed")
def validate_write_policy(operation_id: str, branch: str, path: str, content: str = "") -> PolicyResult:
try:
normalized = normalize_path(path)
except ValueError as exc:
return PolicyResult(False, str(exc))
if is_secret_path(normalized):
return PolicyResult(False, "secret_path_rejected")
if contains_secret_like_content(content):
return PolicyResult(False, "secret_like_content_detected")
if operation_id in LOW_RISK_PATH_SCOPES:
return validate_low_risk_commit(operation_id, branch, normalized, content)
if operation_id in HIGH_RISK_OPERATIONS:
return PolicyResult(True, "high_risk_requires_confirmation")
return PolicyResult(False, "unknown_operation")
if __name__ == "__main__":
print("GPT Actions GitOps policy helper loaded")
@@ -0,0 +1,110 @@
#!/usr/bin/env python3
from __future__ import annotations
import json
from pathlib import Path
import re
import sys
import yaml
ROOT = Path(__file__).resolve().parents[1]
SCHEMA = ROOT / "docs" / "gitops" / "openapi-chatgpt-actions.yaml"
LOW_RISK_FALSE = {
"commitReportFile",
"commitDocsFile",
"commitProposalFile",
"commitPatchEvidence",
}
HIGH_RISK_TRUE = {
"commitFile",
"commitMainFile",
"createRollbackRequest",
"requestRollback",
"requestOrCreateTag",
"setModelPriority",
"registerModel",
}
SECRET_LIKE = re.compile(
r"sk-[A-Za-z0-9_-]{8,}|Bearer\s+\S+|BEGIN\s+PRIVATE\s+KEY|password\s*=|api_key\s*=|access_token\s*=|refresh_token\s*=",
re.IGNORECASE,
)
def load_schema() -> dict:
if not SCHEMA.exists():
raise AssertionError(f"schema missing: {SCHEMA}")
return yaml.safe_load(SCHEMA.read_text(encoding="utf-8"))
def operations(schema: dict) -> dict[str, dict]:
found: dict[str, dict] = {}
for path_item in schema.get("paths", {}).values():
if not isinstance(path_item, dict):
continue
for method, operation in path_item.items():
if method.lower() not in {"get", "post", "put", "patch", "delete"}:
continue
if isinstance(operation, dict) and operation.get("operationId"):
found[operation["operationId"]] = operation
return found
def collect_examples(value):
if isinstance(value, dict):
for key, item in value.items():
if key in {"example", "examples"}:
yield item
yield from collect_examples(item)
elif isinstance(value, list):
for item in value:
yield from collect_examples(item)
def main() -> int:
schema = load_schema()
ops = operations(schema)
failures: list[str] = []
for op_id in sorted(LOW_RISK_FALSE):
operation = ops.get(op_id)
if not operation:
failures.append(f"missing low-risk operation: {op_id}")
elif operation.get("x-openai-isConsequential") is not False:
failures.append(f"{op_id} must set x-openai-isConsequential=false")
for op_id in sorted(HIGH_RISK_TRUE):
operation = ops.get(op_id)
if not operation:
failures.append(f"missing high-risk operation: {op_id}")
elif operation.get("x-openai-isConsequential") is not True:
failures.append(f"{op_id} must set x-openai-isConsequential=true")
security_text = json.dumps(schema.get("components", {}).get("securitySchemes", {}), ensure_ascii=False)
if SECRET_LIKE.search(security_text):
failures.append("securitySchemes contain secret-like content")
for example in collect_examples(schema):
if SECRET_LIKE.search(json.dumps(example, ensure_ascii=False)):
failures.append("schema examples contain secret-like content")
break
print("GPT Actions consequential governance smoke")
print("schema:", SCHEMA)
print("low-risk:", ", ".join(sorted(LOW_RISK_FALSE)))
print("high-risk:", ", ".join(sorted(HIGH_RISK_TRUE)))
if failures:
print("status: failed")
for failure in failures:
print("-", failure)
return 1
print("status: passed")
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,45 @@
import ast
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
APP = ROOT / "active-runtime" / "app" / "aios_chatgpt_actions_api.py"
def module_tree():
return ast.parse(APP.read_text(encoding="utf-8"))
def test_runtime_defines_low_risk_route_map():
text = APP.read_text(encoding="utf-8")
for route in [
"/api/gitops/report-file",
"/api/gitops/docs-file",
"/api/gitops/proposal-file",
"/api/gitops/patch-evidence",
"/api/gitops/main-file",
"/api/gitops/rollback-request",
]:
assert route in text
def test_runtime_has_low_risk_and_main_handlers():
names = {node.name for node in ast.walk(module_tree()) if isinstance(node, ast.FunctionDef)}
assert "low_risk_file" in names
assert "main_file" in names
assert "secret_path" in names
assert "path_in_scope" in names
def test_runtime_policy_rejects_main_and_secret_paths_in_low_risk_handler():
text = APP.read_text(encoding="utf-8")
assert "low-risk write requires explicit non-main patch/request branch" in text
assert "Secret-like path rejected. Use runtime secret store or secret_ref." in text
assert "path_out_of_scope" in text
assert "Secret-like content detected. Use runtime secret store or secret_ref." in text
def test_runtime_preserves_legacy_commit_file_route():
text = APP.read_text(encoding="utf-8")
assert "if path == '/api/file': return self.file(data)" in text
assert "def file(self,data):" in text
@@ -0,0 +1,70 @@
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts"))
from gitops_actions_policy import validate_low_risk_commit, validate_write_policy # noqa: E402
def assert_allowed(operation_id, branch, path, content="safe content"):
result = validate_low_risk_commit(operation_id, branch, path, content)
assert result.allowed, result.reason
def assert_rejected(operation_id, branch, path, reason, content="safe content"):
result = validate_low_risk_commit(operation_id, branch, path, content)
assert not result.allowed
assert result.reason == reason
def test_commit_report_file_scope():
assert_allowed("commitReportFile", "patch/gpt-actions-consequential-governance", "reports/example.md")
assert_rejected("commitReportFile", "patch/gpt-actions-consequential-governance", "docs/example.md", "path_out_of_scope")
def test_commit_docs_file_scope():
assert_allowed("commitDocsFile", "patch/gpt-actions-consequential-governance", "docs/architecture/example.md")
assert_rejected("commitDocsFile", "patch/gpt-actions-consequential-governance", "reports/example.md", "path_out_of_scope")
def test_commit_proposal_and_patch_evidence_scopes():
assert_allowed("commitProposalFile", "patch/gpt-actions-consequential-governance", "proposals/change.json")
assert_allowed("commitPatchEvidence", "patch/gpt-actions-consequential-governance", "reports/evidence.md")
assert_allowed("commitPatchEvidence", "patch/gpt-actions-consequential-governance", "patches/evidence.json")
assert_rejected("commitPatchEvidence", "patch/gpt-actions-consequential-governance", "docs/evidence.md", "path_out_of_scope")
def test_low_risk_actions_reject_main_branch():
for operation_id in ["commitReportFile", "commitDocsFile", "commitProposalFile", "commitPatchEvidence"]:
assert_rejected(operation_id, "main", "reports/example.md", "low_risk_action_rejects_main")
def test_all_write_actions_reject_secret_paths():
secret_paths = ["runtime/secrets/key.txt", ".env", "private/key.txt", "prod-secrets/config.json", "reports/value.token"]
for operation_id in ["commitReportFile", "commitDocsFile", "commitPatchEvidence", "commitFile", "commitMainFile", "createRollbackRequest"]:
for path in secret_paths:
result = validate_write_policy(operation_id, "patch/gpt-actions-consequential-governance", path)
assert not result.allowed
assert result.reason == "secret_path_rejected"
def test_secret_like_content_is_rejected():
samples = [
"Bearer abc",
"password=abc",
"api_key=abc",
"BEGIN PRIVATE KEY",
"access_token=abc",
"refresh_token=abc",
]
for sample in samples:
result = validate_low_risk_commit(
"commitReportFile",
"patch/gpt-actions-consequential-governance",
"reports/example.md",
sample,
)
assert not result.allowed
assert result.reason == "secret_like_content_detected"
+76
View File
@@ -0,0 +1,76 @@
import json
from pathlib import Path
import re
import yaml
ROOT = Path(__file__).resolve().parents[1]
SCHEMA = ROOT / "docs" / "gitops" / "openapi-chatgpt-actions.yaml"
def load_schema():
return yaml.safe_load(SCHEMA.read_text(encoding="utf-8"))
def operations_by_id(schema):
operations = {}
for path_item in schema.get("paths", {}).values():
if not isinstance(path_item, dict):
continue
for method, operation in path_item.items():
if method.lower() not in {"get", "post", "put", "patch", "delete"}:
continue
if isinstance(operation, dict) and operation.get("operationId"):
operations[operation["operationId"]] = operation
return operations
def _examples(value):
if isinstance(value, dict):
for key, item in value.items():
if key in {"example", "examples"}:
yield item
yield from _examples(item)
elif isinstance(value, list):
for item in value:
yield from _examples(item)
def test_low_risk_operations_are_non_consequential():
ops = operations_by_id(load_schema())
for op_id in ["commitReportFile", "commitDocsFile", "commitProposalFile", "commitPatchEvidence"]:
assert op_id in ops
assert ops[op_id].get("x-openai-isConsequential") is False
def test_high_risk_operations_remain_consequential():
ops = operations_by_id(load_schema())
for op_id in [
"commitFile",
"commitMainFile",
"createRollbackRequest",
"requestRollback",
"requestOrCreateTag",
"setModelPriority",
"registerModel",
]:
assert op_id in ops
assert ops[op_id].get("x-openai-isConsequential") is True
def test_legacy_commit_file_is_preserved_as_consequential():
ops = operations_by_id(load_schema())
assert ops["commitFile"].get("x-openai-isConsequential") is True
def test_security_schemes_and_examples_do_not_embed_real_credentials():
schema = load_schema()
secret_like = re.compile(
r"sk-[A-Za-z0-9_-]{8,}|BEGIN\s+PRIVATE\s+KEY|password\s*=|api_key\s*=|access_token\s*=|refresh_token\s*=",
re.IGNORECASE,
)
checked = json.dumps(schema.get("components", {}).get("securitySchemes", {}), ensure_ascii=False)
for value in _examples(schema):
checked += "\n" + json.dumps(value, ensure_ascii=False)
assert not secret_like.search(checked)