Compare commits
125
Commits
@@ -1,7 +1,7 @@
|
||||
e4a1f86ef92ec1d3ff724eb75cf9c01758c1cc997e1afadc1c8b561bce5b813a FULL_PACKAGE_MANIFEST.json
|
||||
c59c0288d4d999fbd79a2f275d4fb8c369029cb8bb721f95408308cc51eaa3a2 README.md
|
||||
1095953d5764d9c72f02e84b1d525e265d3a67a5bdb451ab9ef51a8e8b3032b8 VERSION
|
||||
ee6e4fc6bc98a590fbc3585b62707e2562115e5a3f722e69a7c464d966bdc458 active-runtime/app/aios_gitops_model_api.py
|
||||
b090062a92aeb14211bbfc1fcd43b5ee4f315d30054b437f3bcdac657210ee95 VERSION
|
||||
a664714bc1e13160cdd0dc32de460c65a83a3a95aa5cdad1833b1b650ab90e9d active-runtime/app/aios_gitops_model_api.py
|
||||
0ffadf6ee1f297842bc44c6e753010fd2b7b02b1f5fb0d7e0676b7bec898db08 active-runtime/app/aios_v5_webops_app.py
|
||||
0ffadf6ee1f297842bc44c6e753010fd2b7b02b1f5fb0d7e0676b7bec898db08 active-runtime/app/aios_webops_app.py
|
||||
0eb12e0161371464605577707c84df413b555277933f9eb4488ab38032ff699b active-runtime/migrations/001_webops_core_sqlite.sql
|
||||
@@ -13,7 +13,7 @@ ee6e4fc6bc98a590fbc3585b62707e2562115e5a3f722e69a7c464d966bdc458 adapters/gitop
|
||||
013813b58d7844f3860d6f9bb484f84f92487652b573976c474bb35c91df8ae8 adapters/openclaw/openclaw_gitops_commands.py
|
||||
7a34ccdfdcdc132fe5daf01cd1acd9d9b7adbd0429d575b65e0771962ccedc62 adapters/webops/gitops_model_menu_fragment.html
|
||||
86b0cb6ee3192565b080672e5305dabba1283486442e630c71244ebac4910916 config/digital_life.yaml
|
||||
a3872f24e4cd64288758846ecd0b374db4aa41111feec3b17c41438136d6d5d0 config/domains.env
|
||||
5a27d48db343dcb723bcc1d9d8222aa3f72094c57cc43640aebe6558963bd20f config/domains.env
|
||||
7a6f82cbddfc5c74bd37229e4f37fe4e73430ae585b4846abd1a80d852f2fed0 config/gitops_short_links.yaml
|
||||
4139d915bdd733171734d5d5d3411e6fc4f8f202c49af4321d706da176da690b config/model_registry.yaml
|
||||
8d6202d3da60366c96a208f5e5eed1f09fd36d122f4cabdc038c21ce2e72ea16 config/npm_custom_locations.yaml
|
||||
@@ -55,7 +55,7 @@ d8cea35eb6c1300d20804ef480312d26dbdc2ad3a2d26b9430c6ca4808155a95 fixes/patch_ru
|
||||
0eb12e0161371464605577707c84df413b555277933f9eb4488ab38032ff699b migrations/001_webops_core_sqlite.sql
|
||||
6d61da88e7d6b208e7eee7195b521dc47cb68587f91987d7a0dd1c8801ef168d migrations/002_seed_owner_admin_user.sql
|
||||
a2c0c7699e1eb46cfdeabfc905cb79b71ed14fb6bcf81ecc99ec47975cf05767 migrations/004_gitops_custom_llm_runtime.sql
|
||||
b77bce1ca78cede27de6594e25f6ce5bb0c4990c0953eeb79f7e2608d2515c32 package_verify_full.sh
|
||||
ca49f4165b50171968485ffa5535bc33d2158237ea55af10a3888a5fa0f7a535 package_verify_full.sh
|
||||
f65b7fba5d6bdac3070ff4309830e286cd6319a80ab2e55263e46022fed2bf7f scripts/apply_migrations.py
|
||||
bc0452d261def2366cdefd845e62d7177b42806f93ba78294ece29a8a369157e scripts/backup_before_fix.sh
|
||||
bdb439086c86b3b23f8240cb2bef0f757a6be242d21d13a79eb1e44434efbe2e scripts/create_patch.sh
|
||||
@@ -63,11 +63,11 @@ bdb439086c86b3b23f8240cb2bef0f757a6be242d21d13a79eb1e44434efbe2e scripts/create
|
||||
1fdeecf0315f3ab4ec461c3376128054df9ac48e2fe5a3f64a138cd7d710d44d scripts/generate_release_manifest.py
|
||||
80c6c10f259245681c42932f3f5f9b9a0fbd9cadce6a3f495d82cbdbc8618c73 scripts/healthcheck.sh
|
||||
3795cadfa6429a434c955913dbdcbf552181c88301a196b4b458f4f5dd206cd8 scripts/init_gitops_baseline.sh
|
||||
4d54a4ed6d35bbb6e9fe2924f3cfceeea9c3eed898017788f6c5ab8041394a47 scripts/install_v1r1c0_gitops_model_overlay.sh
|
||||
e76cd986d2387b76439e4a7fe503e48a2de8fc70fb52c45a13641c9d943f108f scripts/install_v1r1c0_gitops_model_overlay.sh
|
||||
0064539ac1e1bd32dc8062ecc307ad2bc8520895009583a72d46868d1cf44e3c scripts/install_webops_overlay.sh
|
||||
9fadafc4b4d098c006d159cb3b401eb19cf9ab9c5fa4b5cb109e946501c0b8e1 scripts/owner_wechat_notify_worker.py
|
||||
e0abd8ad44d4f088108f2046fd9d7bbd4465af50f79bd3b9fcbb72a8c61bee65 scripts/package_contract_check_v1r1c0.sh
|
||||
abef8df642c6d233af4a03c512876b02b326e19c924020b8dd8cc74236d18817 scripts/release_version.sh
|
||||
f537c58fc08685601f47fac5c0a3a2856e3ce9806c1918848c87a194a49c28a0 scripts/package_contract_check_v1r1c0.sh
|
||||
14ab40a7c342e21e8315def14d552dcff4b214d23f1f4be493e9d3fc2cee1a2f scripts/release_version.sh
|
||||
bbdb3f9992b203ddc2a6a83241052663de4f021d0705724bd9df05acaa99b022 scripts/rollback_last_delta.sh
|
||||
0814c26ecf6732cf2c24f5f414ad0128a159768cfbbeb21d557274064feb1188 scripts/rollback_version.sh
|
||||
da84a5cc61c1d54a7d1e96e4b0a7186e192edf3ec12d694fc9d7608c3b1086ef scripts/run_acceptance_with_autofix.sh
|
||||
|
||||
@@ -0,0 +1,379 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Task-scoped continuous authorization controller for the AIOS Candidate writer."""
|
||||
from __future__ import annotations
|
||||
import base64, fcntl, hashlib, json, os, pwd, grp, re, socket, sqlite3, stat, subprocess, sys, tempfile, time, urllib.error, urllib.parse, urllib.request
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path, PurePosixPath, PureWindowsPath
|
||||
from typing import Any
|
||||
|
||||
TASK='AIOS-AUTONOMOUS-RELEASE-FULL-IMPLEMENTATION-20260726'
|
||||
AUTH_ID='authz-aios-autonomous-release-20260726'
|
||||
BRANCH='patch/chatgpt-autonomous-completion-contract-20260726'
|
||||
BASE='875d41c3101093149414292067fd700dc214d4d3'
|
||||
REPO='aios/aios'
|
||||
WRITER_AUDIENCE='chatgpt-autonomous-release-writer'
|
||||
ACTOR='chatgpt-gitops-bot'
|
||||
WRITER_INSTANCE='writer-6e32f321-1240-46f8-9693-0b0631d9c77b'
|
||||
CHILD_OPERATION_MAX_USES=20
|
||||
ROOT=Path('/data/aios-gitops')
|
||||
WORKTREE=ROOT/'worktrees/chatgpt-autonomous-completion-contract-20260726'
|
||||
BARE=ROOT/'repo.git'
|
||||
TASK_ROOT=ROOT/'runtime/tasks'/TASK
|
||||
AUTH_DIR=Path('/etc/aios-gitops/task-authorizations')
|
||||
PARENT=AUTH_DIR/'authz-aios-autonomous-release-20260726.json'
|
||||
STATE_DIR=Path('/var/lib/aios-autonomous-release-authorization')
|
||||
STATE=STATE_DIR/'state.json'
|
||||
LOCK=STATE_DIR/'controller.lock'
|
||||
AUDIT=TASK_ROOT/'continuous-authorization.audit.jsonl'
|
||||
SOCKET_DIR=Path('/run/aios-autonomous-release-authorization')
|
||||
SOCKET_PATH=SOCKET_DIR/'controller.sock'
|
||||
PROJECTION=Path('/etc/aios-gitops/credentials/chatgpt-autonomous-release-writer')
|
||||
REGISTRY=Path('/var/lib/aios-chatgpt-autonomous-release-writer/token-registry.sqlite3')
|
||||
WRITER_LEDGER=Path('/var/lib/aios-chatgpt-autonomous-release-writer/operation-ledger.sqlite3')
|
||||
ASKPASS=Path('/usr/libexec/aios-autonomous-release-authorization/gitea_askpass.py')
|
||||
PATH_POLICY=Path('/etc/aios-gitops/policies-chatgpt-autonomous-release/autonomous-release-path-policy.json')
|
||||
ROUTE_BINDING=Path('/etc/aios-gitops/action-route-bindings/chatgpt-autonomous-release-20260726.json')
|
||||
OID_RE=re.compile(r'^[0-9a-f]{40}$')
|
||||
OP_RE=re.compile(r'^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$')
|
||||
TERMINAL={'COMPLETED','CANCELLED','FAILED_FINAL','REVOKED','REVOKED_IDLE_TIMEOUT','REVOKED_WALL_CLOCK','RECONCILIATION_REQUIRED'}
|
||||
|
||||
sys.path.insert(0,'/usr/libexec/aios-chatgpt-autonomous-release-writer-fullprofile-8ac2f90f')
|
||||
from isolated_writer_token_registry import TokenRegistry
|
||||
|
||||
def now() -> datetime: return datetime.now(timezone.utc)
|
||||
def stamp() -> str: return now().replace(microsecond=0).isoformat()
|
||||
def canonical(value: Any) -> bytes: return json.dumps(value,sort_keys=True,separators=(',',':'),ensure_ascii=False).encode('utf-8')
|
||||
def fail(code: str, status: int=503, **details: Any) -> None: raise ControlError(code,status,details)
|
||||
class ControlError(RuntimeError):
|
||||
def __init__(self,code:str,status:int=503,details:dict[str,Any]|None=None): self.code,self.status,self.details=code,status,details or {}; super().__init__(code)
|
||||
|
||||
def owned_regular(path:Path, mode:int) -> os.stat_result:
|
||||
try: info=os.lstat(path)
|
||||
except FileNotFoundError: fail('authorization_config_missing')
|
||||
if not stat.S_ISREG(info.st_mode) or stat.S_ISLNK(info.st_mode) or info.st_uid!=0 or stat.S_IMODE(info.st_mode)&~mode: fail('authorization_config_permissions_rejected')
|
||||
return info
|
||||
|
||||
def load_json(path:Path,mode:int=0o600) -> dict[str,Any]:
|
||||
owned_regular(path,mode)
|
||||
try: value=json.loads(path.read_text(encoding='utf-8'))
|
||||
except Exception: fail('authorization_config_json_rejected')
|
||||
if not isinstance(value,dict): fail('authorization_config_schema_rejected')
|
||||
return value
|
||||
|
||||
def atomic_json(path:Path,value:dict[str,Any],mode:int=0o600) -> None:
|
||||
path.parent.mkdir(parents=True,exist_ok=True)
|
||||
raw=canonical(value)+b'\n'
|
||||
fd,tmp=tempfile.mkstemp(prefix='.new-',dir=path.parent)
|
||||
try:
|
||||
os.fchmod(fd,mode); os.write(fd,raw); os.fsync(fd); os.close(fd); os.chown(tmp,0,0); os.replace(tmp,path)
|
||||
os.chmod(path,mode)
|
||||
finally:
|
||||
try: os.close(fd)
|
||||
except OSError: pass
|
||||
if os.path.exists(tmp): os.unlink(tmp)
|
||||
|
||||
def append_audit(event:dict[str,Any]) -> None:
|
||||
TASK_ROOT.mkdir(parents=True,exist_ok=True)
|
||||
flags=os.O_WRONLY|os.O_CREAT|os.O_APPEND|getattr(os,'O_NOFOLLOW',0)
|
||||
fd=os.open(AUDIT,flags,0o600)
|
||||
try: os.write(fd,canonical({'at':stamp(),'task_id':TASK,'secrets_included':False,**event})+b'\n'); os.fsync(fd)
|
||||
finally: os.close(fd)
|
||||
|
||||
def parent() -> dict[str,Any]:
|
||||
p=load_json(PARENT)
|
||||
required={'schema_version','development_authorization_id','authorization_type','task_id','actor_identity','audience','repository','exact_branch','approved_base_oid','allowed_path_roots','allowed_exact_paths','allowed_operations','created_at','valid_until','maximum_wall_clock_seconds','idle_timeout_seconds','manual_reapproval_per_commit','maximum_concurrent_writes','path_policy_sha256','writer_instance_id'}
|
||||
if set(p)!=required or p.get('schema_version')!=1: fail('parent_authorization_schema_rejected')
|
||||
fixed={'development_authorization_id':AUTH_ID,'authorization_type':'TASK_SCOPED_CONTINUOUS_DEVELOPMENT','task_id':TASK,'actor_identity':ACTOR,'audience':WRITER_AUDIENCE,'repository':REPO,'exact_branch':BRANCH,'approved_base_oid':BASE,'manual_reapproval_per_commit':False,'maximum_concurrent_writes':1,'writer_instance_id':WRITER_INSTANCE}
|
||||
if any(p.get(k)!=v for k,v in fixed.items()): fail('parent_authorization_binding_rejected')
|
||||
if p.get('maximum_wall_clock_seconds')!=86400 or p.get('idle_timeout_seconds')!=2700: fail('parent_authorization_lifetime_rejected')
|
||||
if not isinstance(p.get('allowed_path_roots'),list) or not isinstance(p.get('allowed_exact_paths'),list) or not isinstance(p.get('allowed_operations'),list): fail('parent_authorization_scope_rejected')
|
||||
raw=PATH_POLICY.read_bytes()
|
||||
if hashlib.sha256(raw).hexdigest()!=p.get('path_policy_sha256'): fail('parent_authorization_path_policy_changed')
|
||||
return p
|
||||
|
||||
def parse_time(v:Any) -> datetime:
|
||||
try:
|
||||
d=datetime.fromisoformat(str(v).replace('Z','+00:00'))
|
||||
return d.astimezone(timezone.utc)
|
||||
except Exception: fail('authorization_time_rejected')
|
||||
|
||||
def components(path:str)->tuple[str,...]:
|
||||
if not isinstance(path,str) or not path or path!=path.strip() or '\\' in path or '\x00' in path: fail('authorization_path_rejected',403)
|
||||
pp=PurePosixPath(path); wp=PureWindowsPath(path)
|
||||
parts=tuple(path.split('/'))
|
||||
if pp.is_absolute() or wp.is_absolute() or bool(wp.drive) or any(x in {'','.','..'} for x in parts): fail('authorization_path_rejected',403)
|
||||
return parts
|
||||
|
||||
def path_allowed(p:dict[str,Any],path:str)->bool:
|
||||
item=components(path)
|
||||
for root in p['allowed_path_roots']:
|
||||
r=components(str(root).rstrip('/'))
|
||||
if item[:len(r)]==r: return True
|
||||
return any(item==components(x) for x in p['allowed_exact_paths'])
|
||||
|
||||
def git(*args:str,cwd:Path|None=None,env:dict[str,str]|None=None)->str:
|
||||
e=os.environ.copy(); e.update(env or {}); e['GIT_CONFIG_COUNT']='1'; e['GIT_CONFIG_KEY_0']='safe.directory'; e['GIT_CONFIG_VALUE_0']=str(WORKTREE)
|
||||
r=subprocess.run(['git',*args],cwd=cwd,text=True,capture_output=True,env=e)
|
||||
if r.returncode: fail('authorization_git_probe_failed',503)
|
||||
return r.stdout.strip()
|
||||
|
||||
def credential_lines()->list[str]:
|
||||
cp=Path(os.environ.get('CREDENTIALS_DIRECTORY',''))/'gitea_git_http'
|
||||
try:
|
||||
info=os.lstat(cp); lines=cp.read_text(encoding='utf-8').splitlines()
|
||||
except OSError: fail('authorization_gitea_credential_unavailable')
|
||||
if not stat.S_ISREG(info.st_mode) or stat.S_ISLNK(info.st_mode) or len(lines)!=2 or lines[0]!='gitops-bot' or not lines[1]: fail('authorization_gitea_credential_rejected')
|
||||
return lines
|
||||
|
||||
def remote_oid()->str:
|
||||
credential_lines()
|
||||
e={'GIT_ASKPASS':str(ASKPASS),'GIT_TERMINAL_PROMPT':'0'}
|
||||
out=git('ls-remote','--heads','http://127.0.0.1:3000/aios/aios.git',f'refs/heads/{BRANCH}',cwd=WORKTREE,env=e)
|
||||
fields=out.split()
|
||||
if len(fields)!=2 or not OID_RE.fullmatch(fields[0]): fail('authorization_git_ls_remote_rejected')
|
||||
return fields[0]
|
||||
|
||||
def api_oid()->str:
|
||||
user,password=credential_lines(); branch=urllib.parse.quote(BRANCH,safe='')
|
||||
encoded=base64.b64encode((user+':'+password).encode()).decode()
|
||||
request=urllib.request.Request('http://127.0.0.1:3000/api/v1/repos/aios/aios/branches/'+branch,headers={'Authorization':'Basic '+encoded,'Host':'g.cnnetmkt.top'})
|
||||
try:
|
||||
with urllib.request.urlopen(request,timeout=10) as response: value=json.loads(response.read())
|
||||
except Exception: fail('authorization_gitea_api_unavailable')
|
||||
oid=str(value.get('commit',{}).get('id',''))
|
||||
if not OID_RE.fullmatch(oid): fail('authorization_gitea_api_oid_rejected')
|
||||
return oid
|
||||
|
||||
def four_way(expected:str)->None:
|
||||
local=git('rev-parse','HEAD',cwd=WORKTREE)
|
||||
bare=git('--git-dir',str(BARE),'rev-parse',f'refs/heads/{BRANCH}')
|
||||
remote=remote_oid(); api=api_oid()
|
||||
if any(x!=expected for x in (local,bare,remote,api)): fail('RECONCILIATION_REQUIRED',409,current_head_oid=local)
|
||||
if git('status','--porcelain',cwd=WORKTREE)!='': fail('authorization_worktree_dirty',409)
|
||||
|
||||
def state()->dict[str,Any]:
|
||||
s=load_json(STATE)
|
||||
required={'schema_version','development_authorization_id','task_id','status','current_head_oid','last_activity_at','writer_child','test_child','inflight','updated_at'}
|
||||
if set(s)!=required or s.get('schema_version')!=1 or s.get('development_authorization_id')!=AUTH_ID or s.get('task_id')!=TASK or not OID_RE.fullmatch(str(s.get('current_head_oid',''))): fail('authorization_state_schema_rejected')
|
||||
return s
|
||||
|
||||
def save_state(s:dict[str,Any])->None: s['updated_at']=stamp(); atomic_json(STATE,s)
|
||||
|
||||
def lock():
|
||||
STATE_DIR.mkdir(parents=True,exist_ok=True); os.chmod(STATE_DIR,0o700)
|
||||
f=open(LOCK,'a+b'); os.chmod(LOCK,0o600); fcntl.flock(f.fileno(),fcntl.LOCK_EX); return f
|
||||
|
||||
def route_binding_valid(p:dict[str,Any])->None:
|
||||
b=load_json(ROUTE_BINDING,0o640)
|
||||
identities=b.get('action_identities')
|
||||
if b.get('task_id')!=TASK or b.get('writer_instance_id')!=WRITER_INSTANCE or b.get('repository')!=REPO or b.get('branch')!=BRANCH or b.get('approved_base_oid')!=BASE or b.get('path_policy_sha256')!=p.get('path_policy_sha256') or not isinstance(identities,list) or len(identities)!=1 or identities[0].get('account')!=ACTOR:
|
||||
fail('authorization_route_binding_changed',409)
|
||||
|
||||
def task_live(p:dict[str,Any],s:dict[str,Any],*,touch:bool)->None:
|
||||
route_binding_valid(p)
|
||||
if s['status'] in TERMINAL: fail('authorization_task_terminal',409)
|
||||
if now()>=parse_time(p['valid_until']):
|
||||
s['status']='REVOKED_WALL_CLOCK'; save_state(s); cleanup(s,'wall_clock'); fail('authorization_task_terminal',409)
|
||||
if (now()-parse_time(s['last_activity_at'])).total_seconds()>p['idle_timeout_seconds']:
|
||||
s['status']='REVOKED_IDLE_TIMEOUT'; save_state(s); cleanup(s,'idle_timeout'); fail('authorization_task_terminal',409)
|
||||
if touch: s['last_activity_at']=stamp(); save_state(s)
|
||||
|
||||
def projection_token()->str|None:
|
||||
if not PROJECTION.exists(): return None
|
||||
try:
|
||||
info=os.lstat(PROJECTION)
|
||||
if not stat.S_ISREG(info.st_mode) or stat.S_ISLNK(info.st_mode) or info.st_uid!=0 or stat.S_IMODE(info.st_mode)!=0o600: fail('authorization_projection_rejected')
|
||||
token=PROJECTION.read_text(encoding='utf-8').strip()
|
||||
return token if len(token.encode())>=32 else None
|
||||
except OSError: fail('authorization_projection_rejected')
|
||||
|
||||
def write_projection(token:str)->None:
|
||||
PROJECTION.parent.mkdir(parents=True,exist_ok=True)
|
||||
fd,tmp=tempfile.mkstemp(prefix='.writer-',dir=PROJECTION.parent)
|
||||
try:
|
||||
os.fchmod(fd,0o600); os.write(fd,(token+'\n').encode()); os.fsync(fd); os.close(fd); os.chown(tmp,0,0); os.replace(tmp,PROJECTION); os.chmod(PROJECTION,0o600)
|
||||
finally:
|
||||
try: os.close(fd)
|
||||
except OSError: pass
|
||||
if os.path.exists(tmp): os.unlink(tmp)
|
||||
|
||||
def post_revoke_401(token:str)->None:
|
||||
req=urllib.request.Request('http://127.0.0.1:19893/api/runtime-status',headers={'Authorization':'Bearer '+token})
|
||||
try: urllib.request.urlopen(req,timeout=5)
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code==401:return
|
||||
except Exception: pass
|
||||
fail('authorization_old_credential_revocation_unverified')
|
||||
|
||||
def rotate_child(p:dict[str,Any],s:dict[str,Any],reason:str)->str:
|
||||
old=projection_token(); registry=TokenRegistry(REGISTRY)
|
||||
old_record=registry.find(old) if old else None
|
||||
if old_record and (old_record.get('task_id')!=TASK or old_record.get('development_authorization_id')!=AUTH_ID):
|
||||
if parse_time(old_record.get('expires_at'))>=now(): fail('authorization_existing_projection_binding_rejected')
|
||||
if not registry.revoke(str(old_record['token_id'])): fail('authorization_historical_child_revoke_failed')
|
||||
post_revoke_401(old)
|
||||
append_audit({'event':'historical_expired_child_revoked','old_credential_http_status':401})
|
||||
old_record=None
|
||||
issued=now().replace(microsecond=0); expires=issued+timedelta(minutes=20)
|
||||
prefixes=[str(x).rstrip('/')+'/' for x in p['allowed_path_roots']]
|
||||
grant={'token_id':'continuous-child-'+issued.strftime('%Y%m%dT%H%M%SZ')+'-'+os.urandom(3).hex(),'credential_id':'credential-'+issued.strftime('%Y%m%dT%H%M%SZ')+'-'+os.urandom(3).hex(),'development_authorization_id':AUTH_ID,'audience':WRITER_AUDIENCE,'task_id':TASK,'actor_identity':ACTOR,'writer_instance_id':WRITER_INSTANCE,'repository_identity':REPO,'source_cidrs':['127.0.0.1/32'],'allowed_branch':BRANCH,'current_expected_head_oid':s['current_head_oid'],'approved_base_oid':BASE,'allowed_operations':p['allowed_operations'],'allowed_paths':p['allowed_exact_paths'],'allowed_path_prefixes':prefixes,'not_before':issued.isoformat(),'expires_at':expires.isoformat(),'renewable':False,'max_uses':{operation:CHILD_OPERATION_MAX_USES for operation in p['allowed_operations']},'issued_by':'task_authorization_controller'}
|
||||
token,record=registry.issue(grant)
|
||||
try:
|
||||
write_projection(token)
|
||||
if old_record:
|
||||
if not registry.revoke(str(old_record['token_id'])): fail('authorization_old_child_revoke_failed')
|
||||
post_revoke_401(old)
|
||||
except Exception:
|
||||
registry.revoke(str(record['token_id']))
|
||||
try: PROJECTION.unlink()
|
||||
except FileNotFoundError: pass
|
||||
raise
|
||||
s['writer_child']={'credential_id':record['credential_id'],'issued_at':record['not_before'],'expires_at':record['expires_at'],'current_expected_head_oid':s['current_head_oid'],'rotation_reason':reason}
|
||||
save_state(s); append_audit({'event':'child_grant_rotated','reason':reason,'current_head_oid':s['current_head_oid'],'old_grant_revoked':bool(old_record),'old_credential_http_status':401 if old_record else None})
|
||||
return token
|
||||
|
||||
def child_operation_quota_exhausted(rec:dict[str,Any],registry:TokenRegistry)->bool:
|
||||
token_id=str(rec.get('token_id',''))
|
||||
allowed=rec.get('allowed_operations')
|
||||
limits=rec.get('max_uses')
|
||||
if not token_id or not isinstance(allowed,list) or not isinstance(limits,dict): fail('authorization_child_quota_schema_rejected')
|
||||
for operation in allowed:
|
||||
maximum=limits.get(operation)
|
||||
if not isinstance(operation,str) or not isinstance(maximum,int) or maximum<1: fail('authorization_child_quota_schema_rejected')
|
||||
if registry.usage(token_id,operation)>=maximum: return True
|
||||
return False
|
||||
|
||||
def ensure_child(p:dict[str,Any],s:dict[str,Any],reason:str)->str:
|
||||
token=projection_token(); meta=s.get('writer_child') or {}
|
||||
expired=True
|
||||
try: expired=(parse_time(meta.get('expires_at'))-now()).total_seconds()<=300
|
||||
except Exception: pass
|
||||
if not token or expired or meta.get('current_expected_head_oid')!=s['current_head_oid']:
|
||||
return rotate_child(p,s,reason)
|
||||
registry=TokenRegistry(REGISTRY)
|
||||
rec=registry.find(token)
|
||||
if not rec or rec.get('revoked') or rec.get('development_authorization_id')!=AUTH_ID or rec.get('current_expected_head_oid')!=s['current_head_oid']:
|
||||
return rotate_child(p,s,reason)
|
||||
if child_operation_quota_exhausted(rec,registry):
|
||||
return rotate_child(p,s,'operation_quota_exhausted')
|
||||
return token
|
||||
|
||||
def ledger_verified(operation_id:str,commit_oid:str)->None:
|
||||
with sqlite3.connect(WRITER_LEDGER) as db:
|
||||
row=db.execute('select operation_id,branch,commit_oid,state,verification_status from writer_operations where operation_id=?',(operation_id,)).fetchone()
|
||||
if not row or row[1]!=BRANCH or row[2]!=commit_oid or row[3]!='COMPLETED' or row[4]!='bare+gitea': fail('authorization_writer_ledger_rejected',409)
|
||||
|
||||
def reconcile(p:dict[str,Any],s:dict[str,Any],previous:str,new:str,operation_id:str)->dict[str,Any]:
|
||||
inflight=s.get('inflight')
|
||||
if not isinstance(inflight,dict) or inflight.get('previous_head_oid')!=previous or inflight.get('operation_id')!=operation_id: fail('authorization_inflight_rejected',409)
|
||||
if previous!=s['current_head_oid'] or not OID_RE.fullmatch(new): fail('authorization_head_transition_rejected',409)
|
||||
ledger_verified(operation_id,new); four_way(new)
|
||||
s['current_head_oid']=new; s['inflight']=None; s['last_activity_at']=stamp(); save_state(s)
|
||||
ensure_child(p,s,'head_advance')
|
||||
append_audit({'event':'authorization_head_advanced','previous_head_oid':previous,'new_commit_oid':new,'four_way_ref_status':True,'worktree_clean':True})
|
||||
return {'authorization_current_head_oid':new,'automatic_head_advance':True,'four_way_ref_status':True}
|
||||
|
||||
def prepare(p:dict[str,Any],s:dict[str,Any],data:dict[str,Any])->dict[str,Any]:
|
||||
required={'expected_current_head_oid','operation_id','idempotency_key','exact_path','content_digest','commit_message_digest','branch','path','message'}
|
||||
if set(data)!=required: fail('authorization_request_schema_rejected',403)
|
||||
if data['branch']!=BRANCH or data['path']!=data['exact_path'] or not path_allowed(p,str(data['path'])): fail('authorization_path_rejected',403)
|
||||
if data['expected_current_head_oid']!=s['current_head_oid'] or not OID_RE.fullmatch(str(data['expected_current_head_oid'])): fail('BRANCH_HEAD_CHANGED',409,current_head_oid=s['current_head_oid'])
|
||||
if not OP_RE.fullmatch(str(data['operation_id'])) or not OP_RE.fullmatch(str(data['idempotency_key'])): fail('authorization_operation_id_rejected',403)
|
||||
if not all(isinstance(data[k],str) and re.fullmatch(r'[0-9a-f]{64}',data[k]) for k in ('content_digest','commit_message_digest')): fail('authorization_digest_rejected',403)
|
||||
if hashlib.sha256(data['message'].encode()).hexdigest()!=data['commit_message_digest']: fail('authorization_commit_message_digest_rejected',403)
|
||||
inflight=s.get('inflight')
|
||||
if inflight:
|
||||
if inflight.get('operation_id')==data['operation_id'] and inflight.get('idempotency_key')==data['idempotency_key']: return {'expected_head_oid':s['current_head_oid'],'idempotency_key':data['idempotency_key']}
|
||||
fail('BRANCH_HEAD_CHANGED',409,current_head_oid=s['current_head_oid'])
|
||||
four_way(s['current_head_oid'])
|
||||
s['inflight']={'operation_id':data['operation_id'],'idempotency_key':data['idempotency_key'],'previous_head_oid':s['current_head_oid'],'path':data['path'],'created_at':stamp()}; s['last_activity_at']=stamp(); save_state(s)
|
||||
append_audit({'event':'write_prepared','operation_id':data['operation_id'],'path':data['path'],'expected_head_oid':s['current_head_oid']})
|
||||
return {'expected_head_oid':s['current_head_oid'],'idempotency_key':data['idempotency_key']}
|
||||
|
||||
def test_authorize(p:dict[str,Any],s:dict[str,Any],operation:str)->dict[str,Any]:
|
||||
allowed={'pytest_dormant','pytest_autonomous_release','compileall_autonomous_release','git_diff_check'}
|
||||
if operation not in allowed: fail('candidate_test_executor_operation_rejected',403)
|
||||
four_way(s['current_head_oid'])
|
||||
child=s.get('test_child') or {}; refresh=True
|
||||
try: refresh=(parse_time(child.get('expires_at'))-now()).total_seconds()<=300
|
||||
except Exception: pass
|
||||
if refresh:
|
||||
issued=now().replace(microsecond=0); child={'test_authorization_id':'test-authz-aios-autonomous-release-20260726','grant_id':'test-child-'+issued.strftime('%Y%m%dT%H%M%SZ'),'issued_at':issued.isoformat(),'expires_at':(issued+timedelta(minutes=20)).isoformat()}; s['test_child']=child
|
||||
s['last_activity_at']=stamp(); save_state(s); append_audit({'event':'test_authorized','operation':operation})
|
||||
return {'test_authorization_id':child['test_authorization_id'],'operation':operation,'expires_at':child['expires_at']}
|
||||
|
||||
def cleanup(s:dict[str,Any],reason:str)->None:
|
||||
old=projection_token()
|
||||
if old:
|
||||
reg=TokenRegistry(REGISTRY); rec=reg.find(old)
|
||||
if rec: reg.revoke(str(rec['token_id']))
|
||||
try: PROJECTION.unlink()
|
||||
except FileNotFoundError: pass
|
||||
try: post_revoke_401(old)
|
||||
except ControlError: pass
|
||||
for unit in ('aios-chatgpt-autonomous-release-route.service','aios-chatgpt-autonomous-release-writer.service'):
|
||||
subprocess.run(['/usr/bin/systemctl','stop',unit],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,timeout=20)
|
||||
subprocess.run(['/usr/bin/systemctl','stop','aios-autonomous-release-dormant-test-executor@*.service'],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,timeout=20)
|
||||
try: ROUTE_BINDING.unlink()
|
||||
except FileNotFoundError: pass
|
||||
try: SOCKET_PATH.unlink()
|
||||
except FileNotFoundError: pass
|
||||
append_audit({'event':'authorization_cleanup','reason':reason,'projection_removed':not PROJECTION.exists(),'route_overlay_removed':not ROUTE_BINDING.exists(),'task_services_stop_requested':True})
|
||||
|
||||
def request(data:dict[str,Any])->dict[str,Any]:
|
||||
op=data.get('op'); f=lock()
|
||||
try:
|
||||
p=parent(); s=state(); task_live(p,s,touch=False)
|
||||
if op=='describe_authorization':
|
||||
four_way(s['current_head_oid'])
|
||||
return {'development_authorization_id':AUTH_ID,'task_id':TASK,'actor_identity':ACTOR,'audience':WRITER_AUDIENCE,'repository':REPO,'branch':BRANCH,'approved_base_oid':BASE,'authorization_status':s['status'],'current_head_oid':s['current_head_oid']}
|
||||
if op=='get_writer_token':
|
||||
four_way(s['current_head_oid']); token=ensure_child(p,s,'scheduled_or_requested_rotation'); return {'token':token,'current_head_oid':s['current_head_oid']}
|
||||
if op=='prepare_write':
|
||||
ensure_child(p,s,'pre_write_quota_check')
|
||||
return prepare(p,s,data.get('request') if isinstance(data.get('request'),dict) else {})
|
||||
if op=='reconcile_commit':
|
||||
r=data.get('result') if isinstance(data.get('result'),dict) else {}; return reconcile(p,s,str(r.get('previous_head_oid','')),str(r.get('new_commit_oid','')),str(r.get('operation_id','')))
|
||||
if op=='fail_write':
|
||||
r=data.get('result') if isinstance(data.get('result'),dict) else {}; inflight=s.get('inflight')
|
||||
if isinstance(inflight,dict) and inflight.get('operation_id')==r.get('operation_id'): s['inflight']=None; save_state(s)
|
||||
return {'cleared':True}
|
||||
if op=='authorize_test': return test_authorize(p,s,str(data.get('operation','')))
|
||||
fail('authorization_operation_rejected',403)
|
||||
finally:
|
||||
fcntl.flock(f.fileno(),fcntl.LOCK_UN); f.close()
|
||||
|
||||
def peer_allowed(conn:socket.socket)->bool:
|
||||
try:
|
||||
import struct
|
||||
_pid,uid,gid=struct.unpack('3i',conn.getsockopt(socket.SOL_SOCKET,socket.SO_PEERCRED,12))
|
||||
return uid==pwd.getpwnam('aiman').pw_uid and gid==grp.getgrnam('gitops').gr_gid
|
||||
except Exception: return False
|
||||
|
||||
def serve()->None:
|
||||
SOCKET_DIR.mkdir(parents=True,exist_ok=True); os.chown(SOCKET_DIR,0,grp.getgrnam('gitops').gr_gid); os.chmod(SOCKET_DIR,0o750)
|
||||
try: SOCKET_PATH.unlink()
|
||||
except FileNotFoundError: pass
|
||||
with socket.socket(socket.AF_UNIX,socket.SOCK_STREAM) as server:
|
||||
server.bind(str(SOCKET_PATH)); os.chown(SOCKET_PATH,0,grp.getgrnam('gitops').gr_gid); os.chmod(SOCKET_PATH,0o660); server.listen(32); server.settimeout(1.0)
|
||||
last=0.0
|
||||
while True:
|
||||
if time.monotonic()-last>30:
|
||||
last=time.monotonic()
|
||||
try:
|
||||
f=lock(); p=parent(); s=state(); task_live(p,s,touch=False); four_way(s['current_head_oid']); ensure_child(p,s,'scheduled_rotation'); fcntl.flock(f.fileno(),fcntl.LOCK_UN); f.close()
|
||||
except ControlError as e:
|
||||
append_audit({'event':'scheduled_check_blocked','error':e.code})
|
||||
if e.code=='authorization_task_terminal': return
|
||||
except Exception: append_audit({'event':'scheduled_check_blocked','error':'authorization_controller_internal_error'})
|
||||
try: conn,_=server.accept()
|
||||
except socket.timeout: continue
|
||||
with conn:
|
||||
if not peer_allowed(conn): conn.sendall(canonical({'ok':False,'status':403,'error':'authorization_peer_rejected'})+b'\n'); continue
|
||||
try:
|
||||
raw=conn.recv(131072); data=json.loads(raw.decode()); result=request(data); conn.sendall(canonical({'ok':True,'result':result})+b'\n')
|
||||
except ControlError as e: conn.sendall(canonical({'ok':False,'status':e.status,'error':e.code,**e.details})+b'\n')
|
||||
except Exception as exc:
|
||||
append_audit({'event':'controller_internal_error','error_type':type(exc).__name__})
|
||||
conn.sendall(canonical({'ok':False,'status':503,'error':'authorization_controller_internal_error'})+b'\n')
|
||||
if __name__=='__main__': serve()
|
||||
@@ -0,0 +1,567 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Task-bound bridge from the legacy ChatGPT Action to an isolated Writer V2."""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sqlite3
|
||||
import socket
|
||||
import stat
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from datetime import datetime, timezone
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path, PurePosixPath, PureWindowsPath
|
||||
from typing import Any
|
||||
|
||||
|
||||
CONFIG_PATH = Path(os.environ["AIOS_ACTION_ROUTE_BINDING"])
|
||||
POLICY_PATH = Path(os.environ["AIOS_ACTION_ROUTE_PATH_POLICY"])
|
||||
KNOWN_IDENTITIES_PATH = Path(os.environ["AIOS_ACTION_ROUTE_KNOWN_IDENTITIES"])
|
||||
STATE_PATH = Path(os.environ["AIOS_ACTION_ROUTE_STATE"])
|
||||
AUDIT_PATH = Path(os.environ["AIOS_ACTION_ROUTE_AUDIT"])
|
||||
HOST = os.environ.get("AIOS_ACTION_ROUTE_HOST", "127.0.0.1")
|
||||
PORT = int(os.environ.get("AIOS_ACTION_ROUTE_PORT", "19894"))
|
||||
EXPECTED_TASK_ID = os.environ["AIOS_ACTION_ROUTE_TASK_ID"]
|
||||
EXPECTED_ROUTE_HEADER = os.environ["AIOS_ACTION_ROUTE_HEADER"]
|
||||
AUTHORIZATION_SOCKET = Path(os.environ["AIOS_CONTINUOUS_AUTHORIZATION_SOCKET"])
|
||||
OID_RE = re.compile(r"[0-9a-f]{40}")
|
||||
SECRET_RE = re.compile(
|
||||
r"(?i)(api[_-]?key|secret[_-]?access[_-]?key|secret|password|token)"
|
||||
r"\s*[:=]\s*[\"']?([A-Za-z0-9_./+=:-]{16,})"
|
||||
)
|
||||
SECRET_VALUE_RE = re.compile(
|
||||
r"(sk-[A-Za-z0-9_-]{20,}|HPUA[A-Z0-9]{10,}|SDC[A-Za-z0-9]{20,}|"
|
||||
r"-----BEGIN (?:OPENSSH |RSA |EC )?PRIVATE KEY-----)"
|
||||
)
|
||||
ALLOWED_REQUEST_FIELDS = frozenset({"base","branch","content","create_branch","message","path","task_id","development_authorization_id","actor_identity","audience","approved_base_oid","expected_current_head_oid","operation_id","idempotency_key","exact_path","content_digest","commit_message_digest"})
|
||||
|
||||
|
||||
class RouteError(RuntimeError):
|
||||
def __init__(self, code: str, status: int = 400, **details: Any):
|
||||
super().__init__(code)
|
||||
self.code = code
|
||||
self.status = status
|
||||
self.details = details
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
|
||||
def canonical_json(value: Any) -> bytes:
|
||||
return json.dumps(
|
||||
value, ensure_ascii=False, sort_keys=True, separators=(",", ":")
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def load_root_managed_json(
|
||||
path: Path, *, expected_mode: int = 0o640
|
||||
) -> tuple[dict[str, Any], bytes]:
|
||||
info = os.lstat(path)
|
||||
if stat.S_ISLNK(info.st_mode) or not stat.S_ISREG(info.st_mode):
|
||||
raise RouteError("writer_route_config_type_rejected", 503)
|
||||
if info.st_uid != 0 or stat.S_IMODE(info.st_mode) & ~expected_mode:
|
||||
raise RouteError("writer_route_config_permissions_rejected", 503)
|
||||
raw = path.read_bytes()
|
||||
try:
|
||||
value = json.loads(raw)
|
||||
except (UnicodeDecodeError, json.JSONDecodeError):
|
||||
raise RouteError("writer_route_config_json_rejected", 503) from None
|
||||
if not isinstance(value, dict):
|
||||
raise RouteError("writer_route_config_object_required", 503)
|
||||
return value, raw
|
||||
|
||||
|
||||
def load_binding() -> dict[str, Any]:
|
||||
binding, _ = load_root_managed_json(CONFIG_PATH)
|
||||
required = {
|
||||
"schema_version",
|
||||
"task_id",
|
||||
"writer_instance_id",
|
||||
"repository",
|
||||
"branch",
|
||||
"approved_base_oid",
|
||||
"writer_url",
|
||||
"path_policy_sha256",
|
||||
"action_identities",
|
||||
}
|
||||
if set(binding) != required or binding.get("schema_version") != 1:
|
||||
raise RouteError("writer_route_binding_schema_rejected", 503)
|
||||
if binding["task_id"] != EXPECTED_TASK_ID:
|
||||
raise RouteError("writer_route_task_binding_rejected", 503)
|
||||
if binding["repository"] != "aios/aios":
|
||||
raise RouteError("writer_route_repository_rejected", 503)
|
||||
if not OID_RE.fullmatch(str(binding["approved_base_oid"])):
|
||||
raise RouteError("writer_route_base_oid_rejected", 503)
|
||||
if binding["writer_url"] != "http://127.0.0.1:19893":
|
||||
raise RouteError("writer_route_upstream_rejected", 503)
|
||||
identities = binding.get("action_identities")
|
||||
if not isinstance(identities, list) or len(identities) != 1:
|
||||
raise RouteError("writer_route_action_identity_rejected", 503)
|
||||
identity = identities[0]
|
||||
if (
|
||||
not isinstance(identity, dict)
|
||||
or set(identity) != {"account", "token_sha256"}
|
||||
or identity.get("account") != "chatgpt-gitops-bot"
|
||||
or not re.fullmatch(
|
||||
r"[0-9a-f]{64}", str(identity.get("token_sha256", ""))
|
||||
)
|
||||
):
|
||||
raise RouteError("writer_route_action_identity_rejected", 503)
|
||||
return binding
|
||||
|
||||
|
||||
def allowed_path_scope(binding: dict[str, Any]) -> tuple[set[str], tuple[str, ...]]:
|
||||
policy, raw = load_root_managed_json(POLICY_PATH)
|
||||
actual = hashlib.sha256(raw).hexdigest()
|
||||
if actual != binding["path_policy_sha256"]:
|
||||
raise RouteError("writer_route_path_policy_digest_mismatch", 503)
|
||||
if policy.get("allowed_branch") != binding["branch"]:
|
||||
raise RouteError("writer_route_path_policy_branch_mismatch", 503)
|
||||
operation = (policy.get("allowed_operations") or {}).get("commit_file.atomic")
|
||||
paths = operation.get("exact_paths") if isinstance(operation, dict) else None
|
||||
prefixes = operation.get("prefix_paths") if isinstance(operation, dict) else None
|
||||
if not isinstance(paths, list) or not paths or not all(
|
||||
isinstance(x, str) for x in paths
|
||||
):
|
||||
raise RouteError("writer_route_path_policy_rejected", 503)
|
||||
if not isinstance(prefixes, list) or not prefixes or not all(
|
||||
isinstance(x, str) and x.endswith("/") for x in prefixes
|
||||
):
|
||||
raise RouteError("writer_route_path_policy_rejected", 503)
|
||||
if any(not valid_relative_path(x[:-1]) for x in prefixes):
|
||||
raise RouteError("writer_route_path_policy_rejected", 503)
|
||||
if any(not valid_relative_path(x) for x in paths):
|
||||
raise RouteError("writer_route_path_policy_rejected", 503)
|
||||
return set(paths), tuple(prefixes)
|
||||
|
||||
|
||||
def valid_relative_path(path: str) -> bool:
|
||||
if not isinstance(path, str) or not path or path != path.strip():
|
||||
return False
|
||||
if "\x00" in path or "\\" in path:
|
||||
return False
|
||||
try:
|
||||
decoded = urllib.parse.unquote(path, errors="strict")
|
||||
except (UnicodeDecodeError, ValueError):
|
||||
return False
|
||||
if decoded != path:
|
||||
return False
|
||||
posix = PurePosixPath(path)
|
||||
windows = PureWindowsPath(path)
|
||||
return not (
|
||||
posix.is_absolute()
|
||||
or windows.is_absolute()
|
||||
or bool(windows.drive)
|
||||
or any(part in {"", ".", ".."} for part in path.split("/"))
|
||||
)
|
||||
|
||||
|
||||
def path_allowed(binding: dict[str, Any], path: str) -> bool:
|
||||
if not valid_relative_path(path):
|
||||
return False
|
||||
exact_paths, prefixes = allowed_path_scope(binding)
|
||||
return path in exact_paths or any(path.startswith(prefix) for prefix in prefixes)
|
||||
|
||||
|
||||
def bearer(headers: Any) -> str:
|
||||
value = headers.get("Authorization", "")
|
||||
if not value.startswith("Bearer "):
|
||||
raise RouteError("unauthorized", 401)
|
||||
token = value[7:].strip()
|
||||
if not token:
|
||||
raise RouteError("unauthorized", 401)
|
||||
return token
|
||||
|
||||
|
||||
def authorize_action(headers: Any, binding: dict[str, Any]) -> str:
|
||||
digest = hashlib.sha256(bearer(headers).encode("utf-8")).hexdigest()
|
||||
identity = binding["action_identities"][0]
|
||||
if not hmac.compare_digest(identity["token_sha256"], digest):
|
||||
known, _ = load_root_managed_json(KNOWN_IDENTITIES_PATH)
|
||||
identities = known.get("identities")
|
||||
if known.get("schema_version") != 1 or not isinstance(identities, list):
|
||||
raise RouteError("writer_route_identity_catalog_rejected", 503)
|
||||
observed = "unregistered"
|
||||
allowed_accounts = {
|
||||
"chatgpt-gitops-bot",
|
||||
"gitops-write-bot",
|
||||
"codex-gitops-bot",
|
||||
}
|
||||
for candidate in identities:
|
||||
if not isinstance(candidate, dict) or set(candidate) != {
|
||||
"account",
|
||||
"token_sha256",
|
||||
}:
|
||||
raise RouteError("writer_route_identity_catalog_rejected", 503)
|
||||
account = str(candidate.get("account", ""))
|
||||
token_sha256 = str(candidate.get("token_sha256", ""))
|
||||
if account not in allowed_accounts or not re.fullmatch(
|
||||
r"[0-9a-f]{64}", token_sha256
|
||||
):
|
||||
raise RouteError("writer_route_identity_catalog_rejected", 503)
|
||||
if hmac.compare_digest(token_sha256, digest):
|
||||
observed = account
|
||||
raise RouteError(
|
||||
"writer_route_action_identity_rejected",
|
||||
403,
|
||||
accepted_action_identity=str(identity["account"]),
|
||||
observed_action_identity=observed,
|
||||
identity_alias_accepted=False,
|
||||
)
|
||||
return str(identity["account"])
|
||||
|
||||
|
||||
def authorization_call(operation: str, **payload: Any) -> dict[str, Any]:
|
||||
request = {"op": operation, **payload}
|
||||
try:
|
||||
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client:
|
||||
client.settimeout(15)
|
||||
client.connect(str(AUTHORIZATION_SOCKET))
|
||||
client.sendall(canonical_json(request))
|
||||
raw = client.recv(131072)
|
||||
response = json.loads(raw.decode("utf-8"))
|
||||
except (OSError, TimeoutError, UnicodeDecodeError, json.JSONDecodeError):
|
||||
raise RouteError("authorization_controller_unavailable", 503) from None
|
||||
if not isinstance(response, dict) or not response.get("ok"):
|
||||
if not isinstance(response, dict):
|
||||
raise RouteError("authorization_controller_response_rejected", 503)
|
||||
details = {k: v for k, v in response.items() if k not in {"ok", "status", "error"}}
|
||||
raise RouteError(str(response.get("error") or "authorization_controller_rejected"), int(response.get("status") or 503), **details)
|
||||
result = response.get("result")
|
||||
if not isinstance(result, dict):
|
||||
raise RouteError("authorization_controller_response_rejected", 503)
|
||||
return result
|
||||
|
||||
|
||||
def writer_token() -> str:
|
||||
value = authorization_call("get_writer_token").get("token")
|
||||
if not isinstance(value, str) or len(value.encode("utf-8")) < 32:
|
||||
raise RouteError("writer_route_credential_rejected", 503)
|
||||
return value
|
||||
|
||||
|
||||
def writer_call(
|
||||
binding: dict[str, Any],
|
||||
method: str,
|
||||
path: str,
|
||||
payload: dict[str, Any] | None = None,
|
||||
) -> tuple[int, dict[str, Any]]:
|
||||
data = canonical_json(payload) if payload is not None else None
|
||||
headers = {"Authorization": "Bearer " + writer_token()}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
request = urllib.request.Request(
|
||||
binding["writer_url"] + path, data=data, method=method, headers=headers
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=90) as response:
|
||||
body = json.loads(response.read())
|
||||
return response.status, body
|
||||
except urllib.error.HTTPError as exc:
|
||||
try:
|
||||
body = json.loads(exc.read())
|
||||
except Exception:
|
||||
body = {"error": "writer_route_upstream_non_json_error"}
|
||||
return exc.code, body
|
||||
except (OSError, TimeoutError):
|
||||
raise RouteError("writer_route_upstream_unavailable", 503) from None
|
||||
|
||||
|
||||
def validate_request(data: Any, binding: dict[str, Any]) -> dict[str, Any]:
|
||||
if not isinstance(data, dict):
|
||||
raise RouteError("writer_route_request_object_required")
|
||||
if not {"branch", "path", "content"}.issubset(data):
|
||||
raise RouteError("writer_route_request_incomplete")
|
||||
if not set(data).issubset(ALLOWED_REQUEST_FIELDS):
|
||||
raise RouteError("writer_route_request_fields_rejected")
|
||||
if data["branch"] != binding["branch"]:
|
||||
raise RouteError("writer_route_request_binding_failed", 403)
|
||||
if data.get("create_branch") not in (None, False):
|
||||
raise RouteError("writer_route_request_binding_failed", 403)
|
||||
if data.get("base", "main") not in {"main", binding["approved_base_oid"]}:
|
||||
raise RouteError("writer_route_request_binding_failed", 403)
|
||||
path = data["path"]
|
||||
if not isinstance(path, str) or not path_allowed(binding, path):
|
||||
raise RouteError("writer_route_request_binding_failed", 403)
|
||||
content = data["content"]
|
||||
if not isinstance(content, str) or "\x00" in content or "\r" in content:
|
||||
raise RouteError("writer_route_content_rejected")
|
||||
if len(content.encode("utf-8")) > 1_048_576:
|
||||
raise RouteError("writer_route_content_too_large", 413)
|
||||
if SECRET_RE.search(content) or SECRET_VALUE_RE.search(content):
|
||||
raise RouteError("writer_route_secret_content_rejected", 403)
|
||||
message = data.get("message") or f"chore(chatgpt): update {path}"
|
||||
if not isinstance(message, str) or not 1 <= len(message) <= 200 or "\x00" in message or "\n" in message or "\r" in message:
|
||||
raise RouteError("writer_route_message_rejected")
|
||||
return {"branch": binding["branch"], "path": path, "content": content, "message": message}
|
||||
|
||||
|
||||
def complete_request(data: dict[str, Any], normalized: dict[str, Any], binding: dict[str, Any], account: str) -> dict[str, Any]:
|
||||
authorization = authorization_call("describe_authorization")
|
||||
expected = {
|
||||
"task_id": binding["task_id"],
|
||||
"development_authorization_id": authorization.get("development_authorization_id"),
|
||||
"actor_identity": account,
|
||||
"audience": authorization.get("audience"),
|
||||
"branch": binding["branch"],
|
||||
"approved_base_oid": binding["approved_base_oid"],
|
||||
}
|
||||
required = {
|
||||
"task_id": EXPECTED_TASK_ID,
|
||||
"development_authorization_id": "authz-aios-autonomous-release-20260726",
|
||||
"actor_identity": "chatgpt-gitops-bot",
|
||||
"audience": "chatgpt-autonomous-release-writer",
|
||||
"branch": binding["branch"],
|
||||
"approved_base_oid": binding["approved_base_oid"],
|
||||
}
|
||||
if expected != required or authorization.get("authorization_status") != "ACTIVE" or authorization.get("current_head_oid") is None:
|
||||
raise RouteError("writer_route_request_binding_failed", 403)
|
||||
for key, value in required.items():
|
||||
if key in data and data[key] != value:
|
||||
raise RouteError("writer_route_request_binding_failed", 403)
|
||||
if "exact_path" in data and data["exact_path"] != normalized["path"]:
|
||||
raise RouteError("writer_route_request_binding_failed", 403)
|
||||
current_head = str(authorization["current_head_oid"])
|
||||
if not OID_RE.fullmatch(current_head):
|
||||
raise RouteError("authorization_current_head_rejected", 503)
|
||||
if "expected_current_head_oid" in data and data["expected_current_head_oid"] != current_head:
|
||||
raise RouteError("BRANCH_HEAD_CHANGED", 409, current_head_oid=current_head)
|
||||
content_digest = hashlib.sha256(normalized["content"].encode("utf-8")).hexdigest()
|
||||
message_digest = hashlib.sha256(normalized["message"].encode("utf-8")).hexdigest()
|
||||
if "content_digest" in data and data["content_digest"] != content_digest:
|
||||
raise RouteError("writer_route_request_binding_failed", 403)
|
||||
if "commit_message_digest" in data and data["commit_message_digest"] != message_digest:
|
||||
raise RouteError("writer_route_request_binding_failed", 403)
|
||||
seed = {**required, "expected_current_head_oid": current_head, "path": normalized["path"], "content_digest": content_digest, "commit_message_digest": message_digest}
|
||||
request_digest = hashlib.sha256(canonical_json(seed)).hexdigest()
|
||||
idempotency_key = "action-" + request_digest
|
||||
operation_id = hashlib.sha256((EXPECTED_TASK_ID + "\0" + idempotency_key).encode("utf-8")).hexdigest()
|
||||
if "idempotency_key" in data and data["idempotency_key"] != idempotency_key:
|
||||
raise RouteError("writer_route_request_binding_failed", 403)
|
||||
if "operation_id" in data and data["operation_id"] != operation_id:
|
||||
raise RouteError("writer_route_request_binding_failed", 403)
|
||||
return {**normalized, **required, "expected_current_head_oid": current_head, "operation_id": operation_id, "idempotency_key": idempotency_key, "exact_path": normalized["path"], "content_digest": content_digest, "commit_message_digest": message_digest, "request_digest": request_digest}
|
||||
|
||||
|
||||
def current_writer_head(binding: dict[str, Any]) -> str:
|
||||
status, body = writer_call(binding, "GET", "/api/runtime-status")
|
||||
if status != 200:
|
||||
raise RouteError(
|
||||
"writer_route_runtime_status_failed", 503, upstream_status=status
|
||||
)
|
||||
required = {
|
||||
"service": "chatgpt-autonomous-release-writer",
|
||||
"writes_enabled": True,
|
||||
"allowed_branch": binding["branch"],
|
||||
"approved_base_oid": binding["approved_base_oid"],
|
||||
"path_policy_sha256": binding["path_policy_sha256"],
|
||||
}
|
||||
if any(body.get(key) != value for key, value in required.items()):
|
||||
raise RouteError("writer_route_runtime_binding_mismatch", 503)
|
||||
head = str(body.get("workspace_head_oid", ""))
|
||||
if not OID_RE.fullmatch(head):
|
||||
raise RouteError("writer_route_runtime_head_rejected", 503)
|
||||
return head
|
||||
|
||||
|
||||
def operation_binding(
|
||||
request_digest: str, binding: dict[str, Any]
|
||||
) -> tuple[str, str]:
|
||||
STATE_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
with sqlite3.connect(STATE_PATH, timeout=30, isolation_level=None) as db:
|
||||
db.execute("PRAGMA journal_mode=WAL")
|
||||
db.execute("PRAGMA synchronous=FULL")
|
||||
db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS action_operations (
|
||||
request_digest TEXT PRIMARY KEY,
|
||||
expected_head_oid TEXT NOT NULL,
|
||||
idempotency_key TEXT NOT NULL UNIQUE,
|
||||
created_at TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
row = db.execute(
|
||||
"SELECT expected_head_oid,idempotency_key FROM action_operations "
|
||||
"WHERE request_digest=?",
|
||||
(request_digest,),
|
||||
).fetchone()
|
||||
if row:
|
||||
return str(row[0]), str(row[1])
|
||||
head = current_writer_head(binding)
|
||||
key = "action-" + request_digest
|
||||
with sqlite3.connect(STATE_PATH, timeout=30, isolation_level=None) as db:
|
||||
db.execute("PRAGMA journal_mode=WAL")
|
||||
db.execute("PRAGMA synchronous=FULL")
|
||||
db.execute("BEGIN IMMEDIATE")
|
||||
db.execute(
|
||||
"INSERT OR IGNORE INTO action_operations"
|
||||
"(request_digest,expected_head_oid,idempotency_key,created_at) "
|
||||
"VALUES(?,?,?,?)",
|
||||
(request_digest, head, key, utc_now()),
|
||||
)
|
||||
row = db.execute(
|
||||
"SELECT expected_head_oid,idempotency_key FROM action_operations "
|
||||
"WHERE request_digest=?",
|
||||
(request_digest,),
|
||||
).fetchone()
|
||||
db.execute("COMMIT")
|
||||
return str(row[0]), str(row[1])
|
||||
|
||||
|
||||
def append_audit(event: dict[str, Any]) -> None:
|
||||
AUDIT_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND | getattr(os, "O_NOFOLLOW", 0)
|
||||
fd = os.open(AUDIT_PATH, flags, 0o600)
|
||||
try:
|
||||
os.write(fd, canonical_json(event) + b"\n")
|
||||
os.fsync(fd)
|
||||
finally:
|
||||
os.close(fd)
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
server_version = "AIOSActionTaskRouter/1"
|
||||
|
||||
def log_message(self, _format: str, *_args: Any) -> None:
|
||||
return
|
||||
|
||||
def send_json(self, status: int, value: dict[str, Any]) -> None:
|
||||
body = json.dumps(value, ensure_ascii=False, sort_keys=True).encode("utf-8")
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def do_GET(self) -> None:
|
||||
path = urllib.parse.urlparse(self.path).path.rstrip("/") or "/"
|
||||
if path != "/health":
|
||||
return self.send_json(404, {"error": "not_found"})
|
||||
try:
|
||||
binding = load_binding()
|
||||
self.send_json(
|
||||
200,
|
||||
{
|
||||
"status": "ok",
|
||||
"service": "aios-action-task-router",
|
||||
"task_id": binding["task_id"],
|
||||
"writer_instance_id": binding["writer_instance_id"],
|
||||
"branch": binding["branch"],
|
||||
},
|
||||
)
|
||||
except RouteError as exc:
|
||||
self.send_json(exc.status, {"error": exc.code, **exc.details})
|
||||
|
||||
def do_POST(self) -> None:
|
||||
path = urllib.parse.urlparse(self.path).path.rstrip("/") or "/"
|
||||
if path != "/api/file":
|
||||
return self.send_json(404, {"error": "not_found"})
|
||||
if self.client_address[0] not in {"127.0.0.1", "::1"}:
|
||||
return self.send_json(403, {"error": "writer_route_source_rejected"})
|
||||
if self.headers.get("X-AIOS-Route-Task") != EXPECTED_ROUTE_HEADER:
|
||||
return self.send_json(403, {"error": "writer_route_header_rejected"})
|
||||
try:
|
||||
length = int(self.headers.get("Content-Length", "0"))
|
||||
if not 0 < length <= 1_100_000:
|
||||
raise RouteError("writer_route_content_length_rejected", 413)
|
||||
try:
|
||||
data = json.loads(self.rfile.read(length).decode("utf-8"))
|
||||
except (UnicodeDecodeError, json.JSONDecodeError):
|
||||
raise RouteError("writer_route_request_json_rejected") from None
|
||||
binding = load_binding()
|
||||
account = authorize_action(self.headers, binding)
|
||||
normalized = validate_request(data, binding)
|
||||
normalized = complete_request(data, normalized, binding, account)
|
||||
prepared = authorization_call("prepare_write", request={key: normalized[key] for key in ("expected_current_head_oid", "operation_id", "idempotency_key", "exact_path", "content_digest", "commit_message_digest", "branch", "path", "message")})
|
||||
expected_head = str(prepared["expected_head_oid"])
|
||||
writer_payload = {"branch": normalized["branch"], "path": normalized["path"], "content": normalized["content"], "message": normalized["message"], "expected_head_oid": expected_head, "idempotency_key": str(prepared["idempotency_key"])}
|
||||
request_digest = hashlib.sha256(canonical_json({"task_id": binding["task_id"], "operation_id": normalized["operation_id"], "idempotency_key": normalized["idempotency_key"], "content_digest": normalized["content_digest"], "commit_message_digest": normalized["commit_message_digest"]})).hexdigest()
|
||||
status, result = writer_call(
|
||||
binding, "POST", "/api/commit-file", writer_payload
|
||||
)
|
||||
audit = {
|
||||
"at": utc_now(),
|
||||
"task_id": binding["task_id"],
|
||||
"writer_instance_id": binding["writer_instance_id"],
|
||||
"account": account,
|
||||
"branch": normalized["branch"],
|
||||
"path": normalized["path"],
|
||||
"request_digest": request_digest,
|
||||
"expected_head_oid": expected_head,
|
||||
"status": status,
|
||||
"operation_id": result.get("operation_id"),
|
||||
"commit_oid": result.get("commit_oid"),
|
||||
"error": result.get("error"),
|
||||
"secrets_included": False,
|
||||
}
|
||||
append_audit(audit)
|
||||
if status != 200:
|
||||
try:
|
||||
authorization_call("fail_write", result={"operation_id": normalized["operation_id"]})
|
||||
except RouteError:
|
||||
pass
|
||||
return self.send_json(status, result)
|
||||
advanced = authorization_call("reconcile_commit", result={"previous_head_oid": expected_head, "new_commit_oid": result.get("commit_oid"), "operation_id": result.get("operation_id")})
|
||||
return self.send_json(
|
||||
200,
|
||||
{
|
||||
"status": "committed",
|
||||
"branch": normalized["branch"],
|
||||
"path": normalized["path"],
|
||||
"commit": {
|
||||
"status": "committed",
|
||||
"branch": normalized["branch"],
|
||||
"rev": result.get("commit_oid"),
|
||||
"previous_head_oid": expected_head,
|
||||
"new_tree_oid": result.get("new_tree_oid"),
|
||||
"push_status": result.get("push_status"),
|
||||
"four_way_ref_status": advanced.get("four_way_ref_status") is True,
|
||||
"sync": {"status": "verified", "target": "bare+git-ls-remote+gitea-api"},
|
||||
},
|
||||
"writer_instance_id": binding["writer_instance_id"],
|
||||
"operation_id": result.get("operation_id"),
|
||||
"idempotent": result.get("idempotent"),
|
||||
"secrets_included": False,
|
||||
},
|
||||
)
|
||||
except RouteError as exc:
|
||||
if exc.code == "writer_route_action_identity_rejected":
|
||||
try:
|
||||
append_audit(
|
||||
{
|
||||
"at": utc_now(),
|
||||
"task_id": EXPECTED_TASK_ID,
|
||||
"event": "action_identity_rejected",
|
||||
"accepted_action_identity": exc.details.get(
|
||||
"accepted_action_identity"
|
||||
),
|
||||
"observed_action_identity": exc.details.get(
|
||||
"observed_action_identity"
|
||||
),
|
||||
"identity_alias_accepted": False,
|
||||
"secrets_included": False,
|
||||
}
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
self.send_json(exc.status, {"error": exc.code, **exc.details})
|
||||
except Exception:
|
||||
self.send_json(500, {"error": "writer_route_internal_error"})
|
||||
|
||||
|
||||
def main() -> None:
|
||||
load_binding()
|
||||
server = ThreadingHTTPServer((HOST, PORT), Handler)
|
||||
server.serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,23 @@
|
||||
"""Task-bound Integration Refresh control-plane primitives."""
|
||||
|
||||
from .integration_merge_writer import IntegrationMergeWriter, MergeError, MergeRequest
|
||||
from .integration_refresh import (
|
||||
AppendOnlyAudit,
|
||||
Binding,
|
||||
GiteaBranchReadProjection,
|
||||
GrantStore,
|
||||
IntegrationRefreshProvisioner,
|
||||
ProvisioningError,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"AppendOnlyAudit",
|
||||
"Binding",
|
||||
"GiteaBranchReadProjection",
|
||||
"GrantStore",
|
||||
"IntegrationMergeWriter",
|
||||
"IntegrationRefreshProvisioner",
|
||||
"MergeError",
|
||||
"MergeRequest",
|
||||
"ProvisioningError",
|
||||
]
|
||||
@@ -0,0 +1,90 @@
|
||||
"""Constrained integration-only merge writer with rollback and reconciliation states."""
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
|
||||
class MergeError(RuntimeError):
|
||||
def __init__(self, code: str, status: int = 409):
|
||||
super().__init__(code); self.code, self.status = code, status
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MergeRequest:
|
||||
integration_branch: str
|
||||
source_branch: str
|
||||
approved_source_oid: str
|
||||
|
||||
|
||||
class IntegrationMergeWriter:
|
||||
def __init__(self, workspace: Path | str, remote: str, projection):
|
||||
self.workspace, self.remote, self.projection = Path(workspace), remote, projection
|
||||
|
||||
def _git(self, *args: str, check=True) -> str:
|
||||
p = subprocess.run(["git", *args], cwd=self.workspace, text=True, capture_output=True)
|
||||
if check and p.returncode:
|
||||
raise MergeError("git_operation_failed", 500)
|
||||
return p.stdout.strip()
|
||||
|
||||
def merge(self, request: MergeRequest) -> dict:
|
||||
def bad(value: str) -> bool:
|
||||
return (
|
||||
value == "main"
|
||||
or value.startswith("main/")
|
||||
or value == "governance"
|
||||
or value.startswith("governance/")
|
||||
or value == "approvals"
|
||||
or value.startswith("refs/")
|
||||
or value.startswith("tags/")
|
||||
)
|
||||
if (bad(request.integration_branch) or bad(request.source_branch) or
|
||||
not request.integration_branch.startswith("integration/") or
|
||||
re.fullmatch(r"[0-9a-fA-F]{40}", request.approved_source_oid) is None):
|
||||
raise MergeError("integration_merge_scope_rejected", 403)
|
||||
for branch in (request.integration_branch, request.source_branch):
|
||||
if subprocess.run(["git", "check-ref-format", "--branch", branch], cwd=self.workspace,
|
||||
text=True, capture_output=True).returncode:
|
||||
raise MergeError("integration_merge_scope_rejected", 403)
|
||||
if self._git("rev-parse", "--abbrev-ref", "HEAD") != request.integration_branch:
|
||||
raise MergeError("integration_branch_checkout_required")
|
||||
before = self._git("rev-parse", "HEAD")
|
||||
remote_before = self.projection(request.integration_branch)
|
||||
if remote_before != before:
|
||||
raise MergeError("integration_remote_advanced")
|
||||
source = self._git("rev-parse", "--verify", f"refs/heads/{request.source_branch}^{{commit}}")
|
||||
if source != request.approved_source_oid:
|
||||
raise MergeError("integration_source_not_approved", 403)
|
||||
attempt = subprocess.run(["git", "merge", "--no-ff", "--no-commit", request.source_branch], cwd=self.workspace, text=True, capture_output=True)
|
||||
if attempt.returncode:
|
||||
subprocess.run(["git", "merge", "--abort"], cwd=self.workspace, text=True, capture_output=True)
|
||||
raise MergeError("integration_merge_conflict")
|
||||
try:
|
||||
self._git("commit", "-m", "merge(integration): approved source")
|
||||
merged = self._git("rev-parse", "HEAD")
|
||||
parents = self._git("show", "-s", "--format=%P", "HEAD").split()
|
||||
if parents != [before, source]:
|
||||
raise MergeError("integration_parent_invariant_failed", 500)
|
||||
pushed = subprocess.run(["git", "push", "--porcelain", self.remote, f"HEAD:refs/heads/{request.integration_branch}"], cwd=self.workspace, text=True, capture_output=True)
|
||||
if pushed.returncode:
|
||||
try:
|
||||
observed = self.projection(request.integration_branch)
|
||||
except Exception:
|
||||
return {"state": "RECONCILIATION_REQUIRED", "pre_merge_oid": before, "merge_oid": merged}
|
||||
if observed == before:
|
||||
self._git("reset", "--hard", before)
|
||||
return {"state": "PUSH_FAILED_RESTORED", "pre_merge_oid": before}
|
||||
return {"state": "RECONCILIATION_REQUIRED", "pre_merge_oid": before, "merge_oid": merged}
|
||||
try:
|
||||
observed = self.projection(request.integration_branch)
|
||||
except Exception:
|
||||
return {"state": "RECONCILIATION_REQUIRED", "pre_merge_oid": before, "merge_oid": merged}
|
||||
if observed != merged:
|
||||
return {"state": "RECONCILIATION_REQUIRED", "pre_merge_oid": before, "merge_oid": merged}
|
||||
return {"state": "COMPLETED", "pre_merge_oid": before, "merge_oid": merged}
|
||||
except MergeError:
|
||||
if self._git("rev-parse", "HEAD") != before:
|
||||
self._git("reset", "--hard", before)
|
||||
raise
|
||||
@@ -0,0 +1,257 @@
|
||||
"""Fail-closed, task-bound integration refresh provisioning primitives.
|
||||
|
||||
This module deliberately has no HTTP or credential-provider dependency. The
|
||||
caller supplies a branch-read projection and an apply callback, making the
|
||||
authorization and recovery boundary independently testable.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import argparse
|
||||
import secrets
|
||||
import sqlite3
|
||||
import threading
|
||||
import re
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
|
||||
class ProvisioningError(RuntimeError):
|
||||
def __init__(self, code: str, status: int = 403):
|
||||
super().__init__(code)
|
||||
self.code, self.status = code, status
|
||||
|
||||
|
||||
def _now() -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
def _digest(secret: str) -> str:
|
||||
return hashlib.sha256(secret.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def _canonical(value: dict) -> str:
|
||||
return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
|
||||
|
||||
|
||||
def _is_oid(value: object) -> bool:
|
||||
return isinstance(value, str) and re.fullmatch(r"[0-9a-fA-F]{40}", value) is not None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Binding:
|
||||
task_id: str
|
||||
repository: str
|
||||
branch: str
|
||||
actor_identity: str
|
||||
audience: str
|
||||
paths: tuple[str, ...]
|
||||
quota: int
|
||||
|
||||
|
||||
class AppendOnlyAudit:
|
||||
"""Append only redacted JSONL audit, durable before a result is returned."""
|
||||
def __init__(self, path: Path | str):
|
||||
self.path = Path(path)
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def append(self, event: dict) -> None:
|
||||
with self._lock:
|
||||
safe = {key: value for key, value in event.items() if key not in {"token", "credential", "authorization"}}
|
||||
previous = "0" * 64
|
||||
if self.path.exists():
|
||||
lines = self.path.read_text(encoding="utf-8").splitlines()
|
||||
if lines:
|
||||
previous = json.loads(lines[-1])["event_hash"]
|
||||
safe["previous_event_hash"] = previous
|
||||
safe["event_hash"] = _canonical(safe)
|
||||
encoded = (json.dumps(safe, sort_keys=True, separators=(",", ":")) + "\n").encode()
|
||||
fd = os.open(self.path, os.O_APPEND | os.O_CREAT | os.O_WRONLY, 0o600)
|
||||
try:
|
||||
os.write(fd, encoded)
|
||||
os.fsync(fd)
|
||||
finally:
|
||||
os.close(fd)
|
||||
|
||||
|
||||
class GrantStore:
|
||||
"""Digest-only short grant registry; SQLite transactions serialize quota use."""
|
||||
def __init__(self, path: Path | str):
|
||||
self.path = Path(path)
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with self._db() as db:
|
||||
db.execute("""CREATE TABLE IF NOT EXISTS grants(
|
||||
token_digest TEXT PRIMARY KEY, binding_json TEXT NOT NULL,
|
||||
expires_at TEXT NOT NULL, quota INTEGER NOT NULL, uses INTEGER NOT NULL,
|
||||
revoked INTEGER NOT NULL DEFAULT 0)""")
|
||||
|
||||
@contextmanager
|
||||
def _db(self):
|
||||
db = sqlite3.connect(self.path, timeout=15, isolation_level=None)
|
||||
db.row_factory = sqlite3.Row
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def issue(self, binding: Binding, *, ttl_seconds: int = 300) -> str:
|
||||
if ttl_seconds < 1 or ttl_seconds > 1800 or binding.quota < 1 or not binding.paths:
|
||||
raise ValueError("invalid_short_grant")
|
||||
token = secrets.token_urlsafe(32)
|
||||
payload = json.dumps(binding.__dict__ | {"paths": list(binding.paths)}, sort_keys=True)
|
||||
with self._db() as db:
|
||||
db.execute("INSERT INTO grants VALUES(?,?,?,?,?,0)", (_digest(token), payload,
|
||||
(_now() + timedelta(seconds=ttl_seconds)).isoformat(), binding.quota, 0))
|
||||
return token
|
||||
|
||||
def rotate(self, old_token: str, binding: Binding, *, revocation_probe, ttl_seconds: int = 300) -> str:
|
||||
self.revoke(old_token)
|
||||
try: status = revocation_probe(old_token)
|
||||
except TimeoutError as exc: raise ProvisioningError("revocation_probe_timeout", 503) from exc
|
||||
if status != 401: raise ProvisioningError("revocation_probe_rejected", 503)
|
||||
return self.issue(binding, ttl_seconds=ttl_seconds)
|
||||
|
||||
def revoke(self, token: str) -> None:
|
||||
with self._db() as db:
|
||||
db.execute("BEGIN IMMEDIATE")
|
||||
db.execute("UPDATE grants SET revoked=1 WHERE token_digest=?", (_digest(token),))
|
||||
db.execute("COMMIT")
|
||||
|
||||
def consume(self, token: str, binding: Binding, path: str) -> None:
|
||||
with self._db() as db:
|
||||
db.execute("BEGIN IMMEDIATE")
|
||||
row = db.execute("SELECT * FROM grants WHERE token_digest=?", (_digest(token),)).fetchone()
|
||||
if not row or row["revoked"]:
|
||||
db.execute("ROLLBACK"); raise ProvisioningError("credential_rejected", 401)
|
||||
if _now() >= datetime.fromisoformat(row["expires_at"]):
|
||||
db.execute("ROLLBACK"); raise ProvisioningError("credential_expired", 401)
|
||||
stored = json.loads(row["binding_json"])
|
||||
requested = binding.__dict__ | {"paths": list(binding.paths)}
|
||||
if stored != requested or path not in stored["paths"]:
|
||||
db.execute("ROLLBACK"); raise ProvisioningError("grant_binding_rejected")
|
||||
if row["uses"] >= row["quota"]:
|
||||
db.execute("ROLLBACK"); raise ProvisioningError("grant_quota_exhausted")
|
||||
db.execute("UPDATE grants SET uses=uses+1 WHERE token_digest=?", (_digest(token),))
|
||||
db.execute("COMMIT")
|
||||
|
||||
|
||||
class GiteaBranchReadProjection:
|
||||
"""Normalizes a trusted task-scoped Gitea reader; every ambiguity fails closed."""
|
||||
def __init__(self, reader: Callable[[str, str], dict]):
|
||||
self.reader = reader
|
||||
|
||||
def head(self, repository: str, branch: str) -> str:
|
||||
try:
|
||||
reply = self.reader(repository, branch)
|
||||
except TimeoutError as exc:
|
||||
raise ProvisioningError("gitea_projection_timeout", 503) from exc
|
||||
except Exception as exc:
|
||||
raise ProvisioningError("gitea_projection_unavailable", 503) from exc
|
||||
if not isinstance(reply, dict) or reply.get("status") in {401, 403}:
|
||||
raise ProvisioningError("gitea_projection_forbidden", 503)
|
||||
oid = reply.get("oid")
|
||||
if reply.get("status") != 200 or not _is_oid(oid):
|
||||
raise ProvisioningError("gitea_projection_invalid", 503)
|
||||
return oid.lower()
|
||||
|
||||
|
||||
class IntegrationRefreshProvisioner:
|
||||
_locks: dict[tuple[str, str], threading.Lock] = {}
|
||||
_locks_guard = threading.Lock()
|
||||
|
||||
def __init__(self, *, grants: GrantStore, projection: GiteaBranchReadProjection,
|
||||
ledger_path: Path | str, audit: AppendOnlyAudit, approved_binding: Binding):
|
||||
self.grants, self.projection, self.audit, self.approved_binding = grants, projection, audit, approved_binding
|
||||
self.ledger_path = Path(ledger_path); self.ledger_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with self._ledger() as db:
|
||||
db.execute("CREATE TABLE IF NOT EXISTS refreshes(operation_id TEXT PRIMARY KEY, digest TEXT NOT NULL, state TEXT NOT NULL, result_oid TEXT)")
|
||||
|
||||
@contextmanager
|
||||
def _ledger(self):
|
||||
db = sqlite3.connect(self.ledger_path)
|
||||
try:
|
||||
yield db
|
||||
db.commit()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def refresh(self, *, token: str, binding: Binding, path: str, operation_id: str,
|
||||
target_head_oid: str, apply: Callable[[], str]) -> dict:
|
||||
if not operation_id or not _is_oid(target_head_oid):
|
||||
raise ProvisioningError("refresh_request_rejected")
|
||||
if binding != self.approved_binding:
|
||||
raise ProvisioningError("grant_binding_rejected")
|
||||
request = {"binding": binding.__dict__ | {"paths": list(binding.paths)}, "path": path, "target": target_head_oid}
|
||||
digest = _canonical(request)
|
||||
with self._locks_guard:
|
||||
lock = self._locks.setdefault((binding.repository, binding.branch), threading.Lock())
|
||||
with lock, self._ledger() as db:
|
||||
db.execute("BEGIN IMMEDIATE")
|
||||
row = db.execute("SELECT * FROM refreshes WHERE operation_id=?", (operation_id,)).fetchone()
|
||||
new_operation = row is None
|
||||
if row:
|
||||
if row[1] != digest:
|
||||
db.execute("ROLLBACK"); raise ProvisioningError("operation_id_reuse_rejected", 409)
|
||||
if row[2] == "COMPLETED":
|
||||
db.execute("COMMIT")
|
||||
return {"state": row[2], "operation_id": operation_id, "oid": row[3], "idempotent": True}
|
||||
if row[2] not in {"PENDING", "RECOVERABLE"}:
|
||||
db.execute("COMMIT")
|
||||
return {"state": row[2], "operation_id": operation_id, "oid": row[3], "idempotent": True}
|
||||
else:
|
||||
db.execute("INSERT INTO refreshes VALUES(?,?,?,NULL)", (operation_id, digest, "PENDING"))
|
||||
db.execute("COMMIT")
|
||||
if new_operation:
|
||||
self.grants.consume(token, binding, path)
|
||||
current = self.projection.head(binding.repository, binding.branch)
|
||||
if current != target_head_oid.lower():
|
||||
self._state(operation_id, "STALE_TARGET")
|
||||
raise ProvisioningError("stale_target_head", 409)
|
||||
try:
|
||||
oid = apply()
|
||||
if not _is_oid(oid):
|
||||
raise ProvisioningError("apply_result_invalid", 500)
|
||||
# Detect a competing remote advancement before claiming completion.
|
||||
observed = self.projection.head(binding.repository, binding.branch)
|
||||
if observed != oid.lower():
|
||||
self._state(operation_id, "RECONCILIATION_REQUIRED")
|
||||
raise ProvisioningError("remote_concurrent_advance", 409)
|
||||
self._state(operation_id, "COMPLETED", oid)
|
||||
self.audit.append({"operation_id": operation_id, "task_id": binding.task_id, "repository": binding.repository,
|
||||
"branch": binding.branch, "actor_identity": binding.actor_identity, "audience": binding.audience, "path": path, "state": "COMPLETED"})
|
||||
return {"state": "COMPLETED", "operation_id": operation_id, "oid": oid, "idempotent": False}
|
||||
except ProvisioningError:
|
||||
raise
|
||||
except Exception as exc:
|
||||
self._state(operation_id, "RECOVERABLE")
|
||||
self.audit.append({"operation_id": operation_id, "task_id": binding.task_id, "repository": binding.repository,
|
||||
"branch": binding.branch, "actor_identity": binding.actor_identity, "audience": binding.audience, "path": path, "state": "RECOVERABLE"})
|
||||
raise ProvisioningError("refresh_apply_failed", 503) from exc
|
||||
|
||||
def _state(self, operation_id: str, state: str, oid: str | None = None) -> None:
|
||||
with self._ledger() as db:
|
||||
db.execute("UPDATE refreshes SET state=?, result_oid=COALESCE(?,result_oid) WHERE operation_id=?", (state, oid, operation_id))
|
||||
|
||||
|
||||
def _sandbox_preflight(state_dir: Path) -> int:
|
||||
if os.environ.get("AIOS_GITOPS_WRITES_ENABLED") != "false":
|
||||
raise ProvisioningError("global_gitops_writes_must_remain_disabled", 500)
|
||||
state_dir.mkdir(parents=True, exist_ok=True)
|
||||
audit = AppendOnlyAudit(state_dir / "preflight-audit.jsonl")
|
||||
audit.append({"event": "SANDBOX_PREFLIGHT", "writes_enabled": False})
|
||||
print(json.dumps({"status": "READY", "mode": "sandbox", "writes_enabled": False}, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--sandbox-preflight", action="store_true", required=True)
|
||||
parser.add_argument("--state-dir", type=Path, required=True)
|
||||
options = parser.parse_args()
|
||||
raise SystemExit(_sandbox_preflight(options.state_dir))
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"approval_plan_id": "v2r1c4-security-v3-candidate-plan",
|
||||
"expected_base_oid": "473b86a2f1f5a5fe3b8ad42a6da26c29c78c4170",
|
||||
"host": "127.0.0.1",
|
||||
"port": 8080,
|
||||
"release_branch": "patch/v2r1c4-security-invariants-v3-20260729",
|
||||
"repository": "aios/aios",
|
||||
"root": "/var/lib/aios-v2r1c4-candidate",
|
||||
"tag_oid": "1111111111111111111111111111111111111111",
|
||||
"target_oid": "473b86a2f1f5a5fe3b8ad42a6da26c29c78c4170",
|
||||
"target_tree_oid": "c6d1ceb28b0519fe460a26a6a2a7e3c0086b3a1f",
|
||||
"version": "V2R1C4"
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"actor_identity": "codex-chartgpt-bridge",
|
||||
"allowed_branch_patterns": [
|
||||
"patch/**",
|
||||
"feature/**",
|
||||
"fix/**",
|
||||
"release-candidate/**"
|
||||
],
|
||||
"allowed_operations": [
|
||||
"commit_file.atomic"
|
||||
],
|
||||
"allowed_path_patterns": [
|
||||
"active-runtime/**",
|
||||
"services/**",
|
||||
"deploy/**",
|
||||
"tests/**",
|
||||
"docs/**",
|
||||
"versions/**",
|
||||
"release/**"
|
||||
],
|
||||
"denied_branch_patterns": [
|
||||
"main",
|
||||
"master",
|
||||
"governance/approvals",
|
||||
"integration/**",
|
||||
"production/**"
|
||||
],
|
||||
"denied_operations": [
|
||||
"force_push",
|
||||
"history_rewrite",
|
||||
"branch_delete",
|
||||
"tag.write",
|
||||
"registry.write",
|
||||
"baseline.write",
|
||||
"production.deploy",
|
||||
"approval.submit",
|
||||
"secret.read"
|
||||
],
|
||||
"expires_at": "2027-07-29T00:00:00Z",
|
||||
"policy_id": "STANDING-GOVERNED-DEVELOPMENT-WRITER-V1",
|
||||
"repository": "aios/aios",
|
||||
"writer_audience": "aios-governed-development-writer"
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"actor_identity": "codex-chartgpt-bridge",
|
||||
"allowed_operations": ["commit_file.atomic"],
|
||||
"allowed_paths": ["services/**", "deploy/**", "tests/**", "docs/**", "release/**", "evidence/**"],
|
||||
"branch": "patch/v2r1c4-security-invariants-v3-20260729",
|
||||
"credential_name": "gitea_http",
|
||||
"credential_token_id": 1,
|
||||
"credential_username": "aios-governed-development-writer",
|
||||
"expected_base_oid": "473b86a2f1f5a5fe3b8ad42a6da26c29c78c4170",
|
||||
"expected_index_sha256": "e8571df23aba77fd393b4d98beb0b94676dc547fdc25d6492a43b43ed90aee94",
|
||||
"expected_policy_sha256": "462c850d0476266a33bf1227f4989d13e03d807fd8ebb684cbe389f4d49e92fe",
|
||||
"expected_tree_oid": "c6d1ceb28b0519fe460a26a6a2a7e3c0086b3a1f",
|
||||
"expires_at": "2026-08-05T00:00:00Z",
|
||||
"git_common_dir": "/data/aios-gitops/repo.git",
|
||||
"git_user_email": "chatgpt-final-release-writer@cnnetmkt.top",
|
||||
"git_user_name": "chatgpt-final-release-writer",
|
||||
"grant_id": "grant-v2r1c4-security-v3-candidate",
|
||||
"max_operations": 200,
|
||||
"max_total_bytes": 67108864,
|
||||
"push_ref": "refs/heads/patch/v2r1c4-security-invariants-v3-20260729",
|
||||
"remote_name": "gitea-local-candidate",
|
||||
"remote_url": "http://127.0.0.1:3000/aios/aios.git",
|
||||
"repository": "aios/aios",
|
||||
"task_id": "V2R1C4-SECURITY-INVARIANTS-V3",
|
||||
"worktree_path": "/data/aios-gitops/worktrees/v2r1c4-security-invariants-v3-20260729",
|
||||
"writer_audience": "aios-governed-development-writer"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"authoritative_git_dir": "/data/aios-gitops/repo.git",
|
||||
"poll_seconds": 5,
|
||||
"receipts_dir": "/var/lib/aios-gitops/autonomous-control-plane/mirror/receipts",
|
||||
"requests_dir": "/var/lib/aios-gitops/autonomous-control-plane/mirror/requests",
|
||||
"source_git_dir": "/data/aios-gitops/runtime/gitea-bin/data/gitea-repositories/aios/aios.git"
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"allowed_git_dir_roots": ["/data/aios-gitops/repo.git"],
|
||||
"allowed_peer_uids": [0],
|
||||
"audit_path": "/var/lib/aios-governance-publisher-evidence/audit.jsonl",
|
||||
"base_branch": "governance/approvals",
|
||||
"credential_name": "gitea_http",
|
||||
"gitea_base_url": "http://127.0.0.1:3000",
|
||||
"head_prefix": "governance/request/",
|
||||
"operation_ledger": "/var/lib/aios-governance-publisher-state/operations.sqlite",
|
||||
"owner_reviewer": "owner",
|
||||
"remote_url": "http://127.0.0.1:3000/aios/aios.git",
|
||||
"repository": "aios/aios",
|
||||
"socket_path": "/run/aios-governance-publisher-runtime/publisher.sock"
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"authoritative_git_dir": "/data/aios-gitops/repo.git",
|
||||
"input_bindings": {
|
||||
"expected_input_owner_uid": 0,
|
||||
"release_root_path": "/var/lib/aios-gitops/release-inputs/V2R1C4-release-root.json",
|
||||
"approval_plan_path": "/var/lib/aios-gitops/release-inputs/V2R1C4-approval-plan.json",
|
||||
"artifact_path": "/var/lib/aios-gitops/artifacts/V2R1C4.tar.zst",
|
||||
"rollback_artifact_path": "/var/lib/aios-gitops/artifacts/V2R1C4-rollback.tar.zst",
|
||||
"sbom_path": "/var/lib/aios-gitops/release-inputs/V2R1C4-sbom.json",
|
||||
"test_evidence_path": "/var/lib/aios-gitops/release-inputs/V2R1C4-test-evidence.json",
|
||||
"tag_plan_path": "/var/lib/aios-gitops/release-inputs/V2R1C4-tag-plan.json",
|
||||
"deployment_plan_path": "/var/lib/aios-gitops/release-inputs/V2R1C4-deployment-plan.json",
|
||||
"rollback_plan_path": "/var/lib/aios-gitops/release-inputs/V2R1C4-rollback-plan.json",
|
||||
"registry_plan_path": "/var/lib/aios-gitops/autonomous-control-plane/release/registry-plan.json",
|
||||
"baseline_plan_path": "/var/lib/aios-gitops/autonomous-control-plane/release/baseline-plan.txt",
|
||||
"evidence_closure_path": "evidence/releases/V2R1C4/evidence-closure.json"
|
||||
},
|
||||
"approval": {
|
||||
"gitea_db_path": "/data/aios-gitops/runtime/gitea-bin/data/gitea.db",
|
||||
"repo_id": 1,
|
||||
"owner_user_id": 2,
|
||||
"governance_ref": "refs/heads/governance/approvals",
|
||||
"required_machine_contexts": [
|
||||
"aios/branch-protection",
|
||||
"aios/evidence-closure",
|
||||
"aios/secret-scan",
|
||||
"aios/state-machine",
|
||||
"aios/validator"
|
||||
]
|
||||
},
|
||||
"merge_transport": {
|
||||
"transport": "unix_json",
|
||||
"socket_path": "/run/aios-gitops/integration-merge-writer.sock",
|
||||
"timeout_seconds": 120
|
||||
},
|
||||
"tag_transport": {
|
||||
"transport": "http_json",
|
||||
"url": "http://127.0.0.1:8080/api/create-tag",
|
||||
"credential_name": "tag_api",
|
||||
"timeout_seconds": 120,
|
||||
"expected_statuses": [
|
||||
200
|
||||
]
|
||||
},
|
||||
"remote_tag": {
|
||||
"remote_url": "https://g.cnnetmkt.top/git/aios/aios.git",
|
||||
"gitea_base_url": "https://g.cnnetmkt.top/git",
|
||||
"repo_owner": "aios",
|
||||
"repo_name": "aios",
|
||||
"credential_name": "gitea_http"
|
||||
},
|
||||
"deployment": {
|
||||
"artifact_path": "/var/lib/aios-gitops/artifacts/V2R1C4.tar.zst",
|
||||
"artifact_manifest_member": "artifact-manifest.json",
|
||||
"release_root": "/opt/aios/releases",
|
||||
"current_symlink": "/opt/aios/current",
|
||||
"previous_symlink": "/opt/aios/previous",
|
||||
"state_path": "/var/lib/aios-gitops/autonomous-control-plane/release/deployment-state.json",
|
||||
"service_units": [
|
||||
"aios-gitops-model-api.service"
|
||||
],
|
||||
"health_checks": [
|
||||
{
|
||||
"url": "http://127.0.0.1:8080/health",
|
||||
"expected_status": 200,
|
||||
"timeout_seconds": 10
|
||||
}
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"registry_path": "/var/lib/aios-gitops/release/versions/V2R1C4.json",
|
||||
"baseline_path": "/var/lib/aios-gitops/release/PRODUCTION_BASELINE",
|
||||
"registry_plan_path": "/var/lib/aios-gitops/autonomous-control-plane/release/registry-plan.json",
|
||||
"baseline_plan_path": "/var/lib/aios-gitops/autonomous-control-plane/release/baseline-plan.txt",
|
||||
"metadata_snapshot_root": "/var/lib/aios-gitops/autonomous-control-plane/release/metadata-snapshots"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"approval_expires_at": "2026-08-05T00:00:00Z",
|
||||
"approval_head_oid": "<40_HEX_OID>",
|
||||
"approval_manifest_path": "approvals/release/V2R1C4-PRODUCTION-RELEASE-ENVELOPE.json",
|
||||
"approval_manifest_sha256": "<64_HEX_SHA256>",
|
||||
"approval_merge_oid": "<40_HEX_OID>",
|
||||
"approval_plan_id": "<OWNER_APPROVED_PLAN_ID>",
|
||||
"approval_plan_sha256": "<64_HEX_SHA256>",
|
||||
"approval_pr_number": 1,
|
||||
"artifact_sha256": "<64_HEX_SHA256>",
|
||||
"baseline_plan_sha256": "<64_HEX_SHA256>",
|
||||
"deployment_plan_sha256": "<64_HEX_SHA256>",
|
||||
"evidence_closure_oid": "<40_HEX_OID>",
|
||||
"evidence_closure_sha256": "<64_HEX_SHA256>",
|
||||
"expected_base_oid": "<40_HEX_BASE_OID>",
|
||||
"owner_review_oid": 1,
|
||||
"publisher_identity": "aios-governance-request-publisher",
|
||||
"release_branch": "patch/v2r1c4-security-invariants-v3-20260729",
|
||||
"release_root_sha256": "<64_HEX_SHA256>",
|
||||
"repository": "aios/aios",
|
||||
"registry_plan_sha256": "<64_HEX_SHA256>",
|
||||
"rollback_artifact_sha256": "<64_HEX_SHA256>",
|
||||
"rollback_plan_sha256": "<64_HEX_SHA256>",
|
||||
"sbom_sha256": "<64_HEX_SHA256>",
|
||||
"tag_plan_sha256": "<64_HEX_SHA256>",
|
||||
"target_oid": "<40_HEX_TARGET_OID>",
|
||||
"target_tree_oid": "<40_HEX_TREE_OID>",
|
||||
"target_environment": "production",
|
||||
"test_evidence_sha256": "<64_HEX_SHA256>",
|
||||
"version": "V2R1C4",
|
||||
"writer_identity": "aios-governed-development-writer"
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"state_path": "/var/lib/aios-gitops/autonomous-control-plane/release/state.json",
|
||||
"audit_path": "/var/lib/aios-gitops/autonomous-control-plane/release/audit.jsonl",
|
||||
"hooks": {
|
||||
"verify_owner_approval": [
|
||||
"/usr/bin/aios-release-hook",
|
||||
"verify_owner_approval",
|
||||
"--config",
|
||||
"/etc/aios-gitops/autonomous-control-plane/release-actions.json"
|
||||
],
|
||||
"merge_release": [
|
||||
"/usr/bin/aios-release-hook",
|
||||
"merge_release",
|
||||
"--config",
|
||||
"/etc/aios-gitops/autonomous-control-plane/release-actions.json"
|
||||
],
|
||||
"create_tag": [
|
||||
"/usr/bin/aios-release-hook",
|
||||
"create_tag",
|
||||
"--config",
|
||||
"/etc/aios-gitops/autonomous-control-plane/release-actions.json"
|
||||
],
|
||||
"verify_remote_tag": [
|
||||
"/usr/bin/aios-release-hook",
|
||||
"verify_remote_tag",
|
||||
"--config",
|
||||
"/etc/aios-gitops/autonomous-control-plane/release-actions.json"
|
||||
],
|
||||
"deploy": [
|
||||
"/usr/bin/aios-release-hook",
|
||||
"deploy",
|
||||
"--config",
|
||||
"/etc/aios-gitops/autonomous-control-plane/release-actions.json"
|
||||
],
|
||||
"health_check": [
|
||||
"/usr/bin/aios-release-hook",
|
||||
"health_check",
|
||||
"--config",
|
||||
"/etc/aios-gitops/autonomous-control-plane/release-actions.json"
|
||||
],
|
||||
"rollback": [
|
||||
"/usr/bin/aios-release-hook",
|
||||
"rollback",
|
||||
"--config",
|
||||
"/etc/aios-gitops/autonomous-control-plane/release-actions.json"
|
||||
],
|
||||
"verify_previous": [
|
||||
"/usr/bin/aios-release-hook",
|
||||
"verify_previous",
|
||||
"--config",
|
||||
"/etc/aios-gitops/autonomous-control-plane/release-actions.json"
|
||||
],
|
||||
"restore": [
|
||||
"/usr/bin/aios-release-hook",
|
||||
"restore",
|
||||
"--config",
|
||||
"/etc/aios-gitops/autonomous-control-plane/release-actions.json"
|
||||
],
|
||||
"verify_restored": [
|
||||
"/usr/bin/aios-release-hook",
|
||||
"verify_restored",
|
||||
"--config",
|
||||
"/etc/aios-gitops/autonomous-control-plane/release-actions.json"
|
||||
],
|
||||
"update_registry": [
|
||||
"/usr/bin/aios-release-hook",
|
||||
"update_registry",
|
||||
"--config",
|
||||
"/etc/aios-gitops/autonomous-control-plane/release-actions.json"
|
||||
],
|
||||
"update_baseline": [
|
||||
"/usr/bin/aios-release-hook",
|
||||
"update_baseline",
|
||||
"--config",
|
||||
"/etc/aios-gitops/autonomous-control-plane/release-actions.json"
|
||||
],
|
||||
"verify_consistency": [
|
||||
"/usr/bin/aios-release-hook",
|
||||
"verify_consistency",
|
||||
"--config",
|
||||
"/etc/aios-gitops/autonomous-control-plane/release-actions.json"
|
||||
],
|
||||
"snapshot_metadata": [
|
||||
"/usr/bin/aios-release-hook",
|
||||
"snapshot_metadata",
|
||||
"--config",
|
||||
"/etc/aios-gitops/autonomous-control-plane/release-actions.json"
|
||||
],
|
||||
"restore_metadata": [
|
||||
"/usr/bin/aios-release-hook",
|
||||
"restore_metadata",
|
||||
"--config",
|
||||
"/etc/aios-gitops/autonomous-control-plane/release-actions.json"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"allowed_output_roots": [
|
||||
"/var/lib/aios-gitops/autonomous-control-plane/projection/releases"
|
||||
],
|
||||
"poll_seconds": 5,
|
||||
"receipts_dir": "/var/lib/aios-gitops/autonomous-control-plane/projection/receipts",
|
||||
"requests_dir": "/var/lib/aios-gitops/autonomous-control-plane/projection/requests",
|
||||
"source_git_dir": "/data/aios-gitops/runtime/gitea-bin/data/gitea-repositories/aios/aios.git"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"poll_seconds": 10,
|
||||
"receipts_dir": "/var/lib/aios-gitops/autonomous-control-plane/renewal/receipts",
|
||||
"requests_dir": "/var/lib/aios-gitops/autonomous-control-plane/renewal/requests"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"admin_credential_name": "admin_gitea_http",
|
||||
"expected_writer_username": "aios-governed-development-writer",
|
||||
"gitea_base_url": "http://127.0.0.1:3000",
|
||||
"grant_path": "/var/lib/aios-development-writer-state/grant.json",
|
||||
"journal_path": "/var/lib/aios-development-writer-state/revocation-journal.json",
|
||||
"receipt_path": "/var/lib/aios-development-writer-evidence/revocation-receipt.json",
|
||||
"revoked_grant_path": "/var/lib/aios-development-writer-state/grant.revoked.json",
|
||||
"terminal_trigger_path": "/var/lib/aios-development-writer-state/terminal-revocation.json",
|
||||
"writer_credential_name": "writer_gitea_http",
|
||||
"writer_unit": "aios-governed-development-writer.service"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"exact_paths": [
|
||||
"/usr/libexec/aios-autonomous-release-authorization/controller.py",
|
||||
"/usr/libexec/aios-chatgpt-action-task-router-continuous-20260726/action_task_router.py"
|
||||
],
|
||||
"output_path": "/var/lib/aios-gitops/autonomous-control-plane/projection/runtime-source-evidence.json",
|
||||
"task_id": "V2R1C4-RELEASE-COMPLETION"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"gitea_db_path": "/data/aios-gitops/runtime/gitea-bin/data/gitea.db",
|
||||
"output_dir": "/var/lib/aios-gitops/autonomous-control-plane/watcher/status",
|
||||
"owner_user_id": 2,
|
||||
"poll_seconds": 5,
|
||||
"repo_id": 1,
|
||||
"requests_dir": "/var/lib/aios-gitops/autonomous-control-plane/watcher/requests"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"allowed_peer_uids": [0],
|
||||
"audit_path": "/var/lib/aios-development-writer-evidence/audit.jsonl",
|
||||
"grant_path": "/var/lib/aios-development-writer-state/grant.json",
|
||||
"grant_quota_ledger": "/var/lib/aios-development-writer-state/grant-quota.sqlite",
|
||||
"max_request_bytes": 4194304,
|
||||
"operation_ledger": "/var/lib/aios-development-writer-state/operations.sqlite",
|
||||
"policy_path": "/etc/aios-gitops/autonomous-control-plane/development-policy.json",
|
||||
"socket_path": "/run/aios-development-writer-runtime/writer.sock",
|
||||
"worktree_lock_path": "/var/lib/aios-development-writer-locks/worktree.lock"
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
[Unit]
|
||||
Description=AIOS Autonomous Release Candidate (dormant shadow only)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=aiman
|
||||
Group=gitops
|
||||
WorkingDirectory=/data/aios-gitops/worktrees/chatgpt-autonomous-completion-contract-20260726/services/aios-autonomous-release-candidate
|
||||
Environment=AIOS_GITOPS_WRITES_ENABLED=false
|
||||
Environment=AIOS_TASK_PLANE_EXECUTION_ENABLED=false
|
||||
Environment=AIOS_SILENT_RELEASE_EXECUTION_ENABLED=false
|
||||
Environment=AIOS_PRODUCTION_DEPLOYMENT_ENABLED=false
|
||||
Environment=AIOS_CANDIDATE_LOOPBACK_HOST=127.0.0.1
|
||||
Environment=PYTHONDONTWRITEBYTECODE=1
|
||||
ExecStart=/usr/bin/python3 -m aios_autonomous_release_candidate.service --host 127.0.0.1 --port 19951 --state /var/lib/aios-autonomous-release-candidate/state.sqlite3
|
||||
Restart=on-failure
|
||||
RestartSec=2
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
LockPersonality=true
|
||||
RestrictSUIDSGID=true
|
||||
RestrictRealtime=true
|
||||
MemoryDenyWriteExecute=true
|
||||
RuntimeDirectory=aios-autonomous-release-candidate
|
||||
StateDirectory=aios-autonomous-release-candidate
|
||||
CacheDirectory=aios-autonomous-release-candidate
|
||||
ReadOnlyPaths=/data/aios-gitops/worktrees/chatgpt-autonomous-completion-contract-20260726/services/aios-autonomous-release-candidate
|
||||
ReadWritePaths=/run/aios-autonomous-release-candidate
|
||||
ReadWritePaths=/var/lib/aios-autonomous-release-candidate
|
||||
ReadWritePaths=/var/cache/aios-autonomous-release-candidate
|
||||
UMask=0077
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,51 @@
|
||||
[Unit]
|
||||
Description=AIOS Autonomous Release Candidate Worker (Dormant/Shadow Only)
|
||||
After=network.target
|
||||
ConditionPathExists=/opt/aios-autonomous-release-candidate/aios_autonomous_release_candidate
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=aiman
|
||||
Group=gitops
|
||||
UMask=0077
|
||||
WorkingDirectory=/opt/aios-autonomous-release-candidate
|
||||
Environment=PYTHONDONTWRITEBYTECODE=1
|
||||
Environment=PYTHONPATH=/opt/aios-autonomous-release-candidate
|
||||
Environment=AIOS_TASK_PLANE_EXECUTION_ENABLED=false
|
||||
Environment=AIOS_GITOPS_WRITES_ENABLED=false
|
||||
Environment=AIOS_PRODUCTION_DEPLOYMENT_ENABLED=false
|
||||
Environment=AIOS_CANDIDATE_CONTROL_ENDPOINT=127.0.0.1:19951
|
||||
Environment=AIOS_OPENCLAW_CREDENTIAL_REFERENCE=systemd-credential:openclaw-gateway-candidate
|
||||
Environment=AIOS_CODEX_CREDENTIAL_REFERENCE=systemd-credential:codex-app-server-candidate
|
||||
Environment=AIOS_NATIVE_CONTROL_CREDENTIAL_REFERENCE=systemd-credential:session-native-adapter-control-candidate
|
||||
Environment=AIOS_GITEA_CREDENTIAL_REFERENCE=systemd-credential:gitea-admin-api-candidate
|
||||
ImportCredential=openclaw-gateway-candidate
|
||||
ImportCredential=codex-app-server-candidate
|
||||
ImportCredential=session-native-adapter-control-candidate
|
||||
ImportCredential=gitea-admin-api-candidate
|
||||
ExecStart=/usr/bin/python3 -m aios_autonomous_release_candidate.worker_service --state-directory /var/lib/aios-autonomous-release-worker-candidate
|
||||
StateDirectory=aios-autonomous-release-worker-candidate
|
||||
RuntimeDirectory=aios-autonomous-release-worker-candidate
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectKernelLogs=true
|
||||
ProtectControlGroups=true
|
||||
ProtectClock=true
|
||||
ProtectHostname=true
|
||||
LockPersonality=true
|
||||
RestrictSUIDSGID=true
|
||||
RestrictRealtime=true
|
||||
MemoryDenyWriteExecute=true
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
IPAddressDeny=any
|
||||
IPAddressAllow=localhost
|
||||
ReadOnlyPaths=/opt/aios-autonomous-release-candidate
|
||||
ReadWritePaths=/var/lib/aios-autonomous-release-worker-candidate
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,31 @@
|
||||
[Unit]
|
||||
Description=AIOS autonomous execution renewal controller
|
||||
After=network-online.target
|
||||
ConditionPathExists=/etc/aios-gitops/autonomous-control-plane/renewal.json
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Group=gitops
|
||||
Environment=AIOS_GITOPS_WRITES_ENABLED=false
|
||||
ExecStart=/usr/bin/aios-autonomous-renewal-controller --config /etc/aios-gitops/autonomous-control-plane/renewal.json
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
ProtectHome=true
|
||||
ProtectSystem=strict
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
ProtectKernelLogs=true
|
||||
RestrictSUIDSGID=true
|
||||
RestrictRealtime=true
|
||||
LockPersonality=true
|
||||
MemoryDenyWriteExecute=true
|
||||
UMask=0027
|
||||
ReadWritePaths=/var/lib/aios-gitops/autonomous-control-plane/renewal /run/aios-gitops/autonomous-control-plane
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,24 @@
|
||||
[Unit]
|
||||
Description=AIOS governance approval watcher
|
||||
After=network-online.target
|
||||
ConditionPathExists=/etc/aios-gitops/autonomous-control-plane/watcher.json
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Group=gitops
|
||||
Environment=AIOS_GITOPS_WRITES_ENABLED=false
|
||||
ExecStart=/usr/bin/aios-governance-approval-watcher --config /etc/aios-gitops/autonomous-control-plane/watcher.json
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
ProtectHome=true
|
||||
ProtectSystem=strict
|
||||
ReadOnlyPaths=/data/aios-gitops/runtime/gitea-bin/data/gitea.db
|
||||
ReadWritePaths=/var/lib/aios-gitops/autonomous-control-plane /run/aios-gitops/autonomous-control-plane
|
||||
RestrictAddressFamilies=AF_UNIX
|
||||
IPAddressDeny=any
|
||||
UMask=0027
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,26 @@
|
||||
[Unit]
|
||||
Description=AIOS dormant single-owner machine-status prerequisite
|
||||
RefuseManualStop=no
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=aiman
|
||||
Group=gitops
|
||||
Environment=AIOS_SINGLE_OWNER_GOVERNANCE_ENABLED=false
|
||||
Environment=AIOS_SINGLE_OWNER_ACTIVATION_ACTIVE=false
|
||||
Environment=AIOS_GITOPS_WRITES_ENABLED=false
|
||||
ExecStart=/usr/bin/python3 -m aios_single_owner_governance.service --component machine-status-producer --self-check --checkpoint-dir /var/lib/aios-gitops/runtime/single-owner-governance
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
RestrictSUIDSGID=true
|
||||
LockPersonality=true
|
||||
MemoryDenyWriteExecute=true
|
||||
RestrictAddressFamilies=AF_UNIX
|
||||
ReadOnlyPaths=/data/aios-gitops/repo.git
|
||||
ReadWritePaths=/var/lib/aios-gitops/runtime/single-owner-governance
|
||||
@@ -0,0 +1,31 @@
|
||||
[Unit]
|
||||
Description=AIOS governance approval mirror worker
|
||||
After=network-online.target
|
||||
ConditionPathExists=/etc/aios-gitops/autonomous-control-plane/mirror.json
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Group=gitops
|
||||
Environment=AIOS_GITOPS_WRITES_ENABLED=false
|
||||
ExecStart=/usr/bin/aios-governance-mirror-worker --config /etc/aios-gitops/autonomous-control-plane/mirror.json
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
ProtectHome=true
|
||||
ProtectSystem=strict
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
ProtectKernelLogs=true
|
||||
RestrictSUIDSGID=true
|
||||
RestrictRealtime=true
|
||||
LockPersonality=true
|
||||
MemoryDenyWriteExecute=true
|
||||
UMask=0027
|
||||
ReadWritePaths=/data/aios-gitops/repo.git /var/lib/aios-gitops/autonomous-control-plane/mirror /run/aios-gitops/autonomous-control-plane
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,26 @@
|
||||
[Unit]
|
||||
Description=AIOS dormant single-owner review adapter prerequisite
|
||||
RefuseManualStop=no
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=aiman
|
||||
Group=gitops
|
||||
Environment=AIOS_SINGLE_OWNER_GOVERNANCE_ENABLED=false
|
||||
Environment=AIOS_SINGLE_OWNER_ACTIVATION_ACTIVE=false
|
||||
Environment=AIOS_GITOPS_WRITES_ENABLED=false
|
||||
ExecStart=/usr/bin/python3 -m aios_single_owner_governance.service --component owner-review-adapter --self-check --checkpoint-dir /var/lib/aios-gitops/runtime/single-owner-governance
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
RestrictSUIDSGID=true
|
||||
LockPersonality=true
|
||||
MemoryDenyWriteExecute=true
|
||||
RestrictAddressFamilies=AF_UNIX
|
||||
ReadOnlyPaths=/data/aios-gitops/repo.git
|
||||
ReadWritePaths=/var/lib/aios-gitops/runtime/single-owner-governance
|
||||
@@ -0,0 +1,30 @@
|
||||
[Unit]
|
||||
Description=AIOS governance request publisher
|
||||
After=network-online.target
|
||||
ConditionPathExists=/etc/aios-gitops/autonomous-control-plane/publisher.json
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=aios-governance-request-publisher
|
||||
Group=gitops
|
||||
StateDirectory=aios-governance-publisher-state aios-governance-publisher-evidence aios-governance-publisher-locks
|
||||
StateDirectoryMode=0750
|
||||
RuntimeDirectory=aios-governance-publisher-runtime
|
||||
RuntimeDirectoryMode=0750
|
||||
Environment=AIOS_GITOPS_WRITES_ENABLED=false
|
||||
ExecStart=/usr/bin/aios-governance-request-publisher --config /etc/aios-gitops/autonomous-control-plane/publisher.json
|
||||
LoadCredentialEncrypted=gitea_http:/etc/credstore.encrypted/aios-governance-request-publisher.gitea_http
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
ProtectHome=true
|
||||
ProtectSystem=strict
|
||||
RestrictSUIDSGID=true
|
||||
MemoryDenyWriteExecute=true
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
IPAddressAllow=127.0.0.1
|
||||
IPAddressDeny=any
|
||||
UMask=0027
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,20 @@
|
||||
[Unit]
|
||||
Description=AIOS single-owner governance activation verifier
|
||||
ConditionPathExists=/etc/aios-gitops/single-owner-activation-bindings.json
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=root
|
||||
Group=gitops
|
||||
Environment=AIOS_GITOPS_WRITES_ENABLED=false
|
||||
ExecStart=/usr/bin/aios-single-owner-governance --verify --bindings /etc/aios-gitops/single-owner-activation-bindings.json --state-root /var/lib/aios-gitops/governance-activation --lock-path /run/aios-gitops/governance-activation.lock
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectHome=true
|
||||
ProtectSystem=strict
|
||||
ReadWritePaths=/var/lib/aios-gitops/governance-activation /run/aios-gitops
|
||||
RestrictSUIDSGID=true
|
||||
UMask=0027
|
||||
|
||||
# Deliberately not installable. Deployment remains disabled/inactive;
|
||||
# activation is an explicit approved CLI transaction, never an always-on unit.
|
||||
@@ -0,0 +1,28 @@
|
||||
[Unit]
|
||||
Description=AIOS governed development writer credential revoker
|
||||
After=network-online.target
|
||||
ConditionPathExists=/etc/aios-gitops/autonomous-control-plane/development-policy.json
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=root
|
||||
Group=gitops
|
||||
Environment=AIOS_GITOPS_WRITES_ENABLED=false
|
||||
ExecStart=/usr/bin/aios-development-writer-revoker --config /etc/aios-gitops/autonomous-control-plane/revoker.json
|
||||
LoadCredentialEncrypted=writer_gitea_http:/etc/credstore.encrypted/aios-governed-development-writer.gitea_http
|
||||
LoadCredentialEncrypted=admin_gitea_http:/etc/credstore.encrypted/aios-governance-admin.gitea_http
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
ProtectHome=true
|
||||
ProtectSystem=strict
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
ProtectKernelLogs=true
|
||||
RestrictSUIDSGID=true
|
||||
RestrictRealtime=true
|
||||
LockPersonality=true
|
||||
MemoryDenyWriteExecute=true
|
||||
UMask=0027
|
||||
ReadWritePaths=/var/lib/aios-development-writer-state /var/lib/aios-development-writer-evidence /var/lib/aios-development-writer-locks /run/aios-development-writer-runtime
|
||||
@@ -0,0 +1,11 @@
|
||||
[Unit]
|
||||
Description=AIOS governed development writer revocation timer
|
||||
|
||||
[Timer]
|
||||
OnBootSec=5min
|
||||
OnUnitActiveSec=5min
|
||||
AccuracySec=30s
|
||||
Unit=aios-governed-development-writer-revoker.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -0,0 +1,37 @@
|
||||
[Unit]
|
||||
Description=AIOS governed development writer
|
||||
After=network-online.target
|
||||
ConditionPathExists=/etc/aios-gitops/autonomous-control-plane/development-policy.json
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=aios-governed-development-writer
|
||||
Group=gitops
|
||||
StateDirectory=aios-development-writer-state aios-development-writer-evidence aios-development-writer-locks
|
||||
StateDirectoryMode=0750
|
||||
RuntimeDirectory=aios-development-writer-runtime
|
||||
RuntimeDirectoryMode=0750
|
||||
Environment=AIOS_GITOPS_WRITES_ENABLED=false
|
||||
ExecStart=/usr/bin/aios-development-writer-server --config /etc/aios-gitops/autonomous-control-plane/writer-server.json
|
||||
LoadCredentialEncrypted=gitea_http:/etc/credstore.encrypted/aios-governed-development-writer.gitea_http
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
ProtectHome=true
|
||||
ProtectSystem=strict
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
ProtectKernelLogs=true
|
||||
RestrictSUIDSGID=true
|
||||
RestrictRealtime=true
|
||||
LockPersonality=true
|
||||
MemoryDenyWriteExecute=true
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET
|
||||
IPAddressAllow=127.0.0.1
|
||||
IPAddressDeny=any
|
||||
ReadWritePaths=/data/aios-gitops/worktrees/v2r1c4-security-invariants-v3-20260729
|
||||
UMask=0027
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,23 @@
|
||||
[Unit]
|
||||
Description=AIOS Integration Refresh Provisioner (sandbox control plane)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
User=aios
|
||||
Group=aios
|
||||
Environment=AIOS_GITOPS_WRITES_ENABLED=false
|
||||
Environment=AIOS_INTEGRATION_REFRESH_MODE=sandbox
|
||||
Environment=AIOS_CREDENTIAL_PROVIDER=disabled
|
||||
NoNewPrivileges=yes
|
||||
PrivateTmp=yes
|
||||
ProtectSystem=strict
|
||||
ProtectHome=yes
|
||||
ReadWritePaths=/var/lib/aios/integration-refresh
|
||||
RestrictAddressFamilies=AF_UNIX
|
||||
IPAddressDeny=any
|
||||
ExecStart=/usr/bin/python3 /opt/aios/active-runtime/app/aios_isolated_writer_api/integration_refresh.py --sandbox-preflight --state-dir /var/lib/aios/integration-refresh
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,31 @@
|
||||
[Unit]
|
||||
Description=AIOS exact release object projection
|
||||
After=network-online.target
|
||||
ConditionPathExists=/etc/aios-gitops/autonomous-control-plane/release-projection.json
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Group=gitops
|
||||
Environment=AIOS_GITOPS_WRITES_ENABLED=false
|
||||
ExecStart=/usr/bin/aios-release-object-projector --config /etc/aios-gitops/autonomous-control-plane/release-projection.json
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
ProtectHome=true
|
||||
ProtectSystem=strict
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
ProtectKernelLogs=true
|
||||
RestrictSUIDSGID=true
|
||||
RestrictRealtime=true
|
||||
LockPersonality=true
|
||||
MemoryDenyWriteExecute=true
|
||||
UMask=0027
|
||||
ReadWritePaths=/var/lib/aios-gitops/autonomous-control-plane/projection /run/aios-gitops/autonomous-control-plane
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,28 @@
|
||||
[Unit]
|
||||
Description=AIOS exact runtime source read projection
|
||||
After=network-online.target
|
||||
ConditionPathExists=/etc/aios-gitops/autonomous-control-plane/runtime-projection.json
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=root
|
||||
Group=gitops
|
||||
Environment=AIOS_GITOPS_WRITES_ENABLED=false
|
||||
ExecStart=/usr/bin/aios-runtime-source-projector --config /etc/aios-gitops/autonomous-control-plane/runtime-projection.json
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
ProtectHome=true
|
||||
ProtectSystem=strict
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
ProtectKernelLogs=true
|
||||
RestrictSUIDSGID=true
|
||||
RestrictRealtime=true
|
||||
LockPersonality=true
|
||||
MemoryDenyWriteExecute=true
|
||||
UMask=0027
|
||||
ReadWritePaths=/var/lib/aios-gitops/autonomous-control-plane/projection /run/aios-gitops/autonomous-control-plane
|
||||
|
||||
PrivateNetwork=true
|
||||
@@ -0,0 +1,29 @@
|
||||
[Unit]
|
||||
Description=AIOS V2R1C4 isolated candidate tag and health fixture
|
||||
After=network.target
|
||||
ConditionPathExists=/etc/aios-gitops/autonomous-control-plane/candidate-fixture.json
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=aios-v2r1c4-candidate
|
||||
Group=gitops
|
||||
StateDirectory=aios-v2r1c4-candidate
|
||||
StateDirectoryMode=0750
|
||||
RuntimeDirectory=aios-v2r1c4-candidate
|
||||
RuntimeDirectoryMode=0750
|
||||
ExecStart=/usr/bin/aios-v2r1c4-candidate-fixture --config /etc/aios-gitops/autonomous-control-plane/candidate-fixture.json
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
ProtectHome=true
|
||||
ProtectSystem=strict
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
RestrictSUIDSGID=true
|
||||
LockPersonality=true
|
||||
MemoryDenyWriteExecute=true
|
||||
RestrictAddressFamilies=AF_INET AF_UNIX
|
||||
IPAddressAllow=127.0.0.1
|
||||
IPAddressDeny=any
|
||||
UMask=0027
|
||||
@@ -0,0 +1,12 @@
|
||||
[Unit]
|
||||
Description=AIOS V2R1C4 isolated candidate integration merge socket
|
||||
|
||||
[Socket]
|
||||
ListenStream=/run/aios-v2r1c4-candidate/integration-merge.sock
|
||||
SocketMode=0600
|
||||
DirectoryMode=0750
|
||||
Accept=yes
|
||||
RemoveOnStop=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=sockets.target
|
||||
@@ -0,0 +1,28 @@
|
||||
[Unit]
|
||||
Description=AIOS V2R1C4 isolated candidate merge request
|
||||
ConditionPathExists=/etc/aios-gitops/autonomous-control-plane/candidate-fixture.json
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=aios-v2r1c4-candidate
|
||||
Group=gitops
|
||||
StateDirectory=aios-v2r1c4-candidate
|
||||
StateDirectoryMode=0750
|
||||
StandardInput=socket
|
||||
StandardOutput=socket
|
||||
StandardError=journal
|
||||
ExecStart=/usr/bin/aios-v2r1c4-candidate-fixture --merge-stdio --config /etc/aios-gitops/autonomous-control-plane/candidate-fixture.json
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
PrivateNetwork=true
|
||||
ProtectHome=true
|
||||
ProtectSystem=strict
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
RestrictSUIDSGID=true
|
||||
LockPersonality=true
|
||||
MemoryDenyWriteExecute=true
|
||||
RestrictAddressFamilies=AF_UNIX
|
||||
UMask=0027
|
||||
@@ -0,0 +1,29 @@
|
||||
[Unit]
|
||||
Description=AIOS V2R1C4 unified governed release envelope executor
|
||||
After=network-online.target
|
||||
ConditionPathExists=/etc/aios-gitops/autonomous-control-plane/release-hooks.json
|
||||
ConditionPathExists=/var/lib/aios-gitops/autonomous-control-plane/release/envelope.json
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=root
|
||||
Group=gitops
|
||||
Environment=AIOS_GITOPS_WRITES_ENABLED=false
|
||||
ExecStart=/usr/bin/aios-v2r1c4-release-executor --config /etc/aios-gitops/autonomous-control-plane/release-hooks.json --envelope /var/lib/aios-gitops/autonomous-control-plane/release/envelope.json --operation-id v2r1c4-production-release
|
||||
LoadCredentialEncrypted=gitea_http:/etc/credstore.encrypted/aios-release-executor.gitea_http
|
||||
LoadCredentialEncrypted=tag_api:/etc/credstore.encrypted/aios-release-executor.tag_api
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
ProtectHome=true
|
||||
ProtectSystem=strict
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
ProtectKernelLogs=true
|
||||
RestrictSUIDSGID=true
|
||||
RestrictRealtime=true
|
||||
LockPersonality=true
|
||||
MemoryDenyWriteExecute=true
|
||||
UMask=0027
|
||||
ReadWritePaths=/var/lib/aios-gitops/autonomous-control-plane/release /var/lib/aios-gitops/release /opt/aios /run/aios-gitops/autonomous-control-plane
|
||||
@@ -0,0 +1,16 @@
|
||||
d /var/lib/aios-gitops/autonomous-control-plane 0750 root gitops -
|
||||
d /var/lib/aios-gitops/autonomous-control-plane/mirror 0750 root gitops -
|
||||
d /var/lib/aios-gitops/autonomous-control-plane/projection 0750 root gitops -
|
||||
d /var/lib/aios-gitops/autonomous-control-plane/renewal 0750 root gitops -
|
||||
d /var/lib/aios-gitops/autonomous-control-plane/release 0750 root gitops -
|
||||
d /var/lib/aios-gitops/autonomous-control-plane/watcher 0750 root gitops -
|
||||
d /var/lib/aios-gitops/autonomous-control-plane/watcher/requests 0750 root gitops -
|
||||
d /var/lib/aios-gitops/autonomous-control-plane/watcher/status 0750 root gitops -
|
||||
d /var/lib/aios-gitops/autonomous-control-plane/mirror/requests 0750 root gitops -
|
||||
d /var/lib/aios-gitops/autonomous-control-plane/mirror/receipts 0750 root gitops -
|
||||
d /var/lib/aios-gitops/autonomous-control-plane/projection/requests 0750 root gitops -
|
||||
d /var/lib/aios-gitops/autonomous-control-plane/projection/receipts 0750 root gitops -
|
||||
d /var/lib/aios-gitops/autonomous-control-plane/projection/releases 0750 root gitops -
|
||||
d /var/lib/aios-gitops/autonomous-control-plane/renewal/requests 0750 root gitops -
|
||||
d /var/lib/aios-gitops/autonomous-control-plane/renewal/receipts 0750 root gitops -
|
||||
d /var/lib/aios-gitops/autonomous-control-plane/release/metadata-snapshots 0750 root gitops -
|
||||
@@ -0,0 +1,4 @@
|
||||
d /var/lib/aios-gitops/governance-activation 0750 root gitops -
|
||||
d /var/lib/aios-gitops/governance-activation/wal 0750 root gitops -
|
||||
d /var/lib/aios-gitops/governance-activation/receipts 0750 root gitops -
|
||||
d /run/aios-gitops 0750 root gitops -
|
||||
@@ -0,0 +1,71 @@
|
||||
# AIOS Autonomous Release Agent Deployment Instructions
|
||||
|
||||
## Objective
|
||||
|
||||
Deploy only candidate/shadow environments first. Production activation requires a separate owner release approval.
|
||||
|
||||
## Agent execution order
|
||||
|
||||
1. Verify git references:
|
||||
- local
|
||||
- authoritative bare
|
||||
- remote
|
||||
- Gitea API
|
||||
|
||||
2. Build candidate artifact.
|
||||
|
||||
3. Run:
|
||||
- unit tests
|
||||
- security scans
|
||||
- secret scans
|
||||
- artifact verification
|
||||
|
||||
4. Start candidate workers:
|
||||
- OpenClaw native adapter
|
||||
- Codex native adapter
|
||||
- dedicated writer
|
||||
|
||||
5. Execute shadow task:
|
||||
|
||||
```
|
||||
TaskPlan
|
||||
-> Planner
|
||||
-> Worker Router
|
||||
-> Session lifecycle
|
||||
-> Evidence chain
|
||||
-> Artifact verification
|
||||
-> ReleasePlan
|
||||
```
|
||||
|
||||
## Hard restrictions
|
||||
|
||||
Never:
|
||||
|
||||
- write main directly
|
||||
- force push
|
||||
- rewrite tags
|
||||
- export secrets
|
||||
- bypass owner approval
|
||||
- modify production flags during candidate validation
|
||||
|
||||
## Production release gate
|
||||
|
||||
Require:
|
||||
|
||||
```
|
||||
owner_approval=true
|
||||
artifact_verified=true
|
||||
evidence_closure=true
|
||||
rollback_ready=true
|
||||
remote_tag_verified=true
|
||||
baseline_verified=true
|
||||
registry_verified=true
|
||||
```
|
||||
|
||||
Until then keep:
|
||||
|
||||
```
|
||||
AIOS_GITOPS_WRITES_ENABLED=false
|
||||
AIOS_TASK_PLANE_EXECUTION_ENABLED=false
|
||||
AIOS_SILENT_RELEASE_EXECUTION_ENABLED=false
|
||||
```
|
||||
@@ -0,0 +1,34 @@
|
||||
# AIOS Autonomous Release Candidate 架构
|
||||
|
||||
本 Candidate 把规划、工作流、凭据引用、原生会话、证据和发布控制拆成独立边界。它只负责验证约束,不拥有生产授权。
|
||||
|
||||
## 信任边界
|
||||
|
||||
- Owner Approval 的签名和撤销状态来自外部 HSM/KMS 或可信 Approval Plane。Candidate 只有 verifier,没有 signer。
|
||||
- Secret Manager V2 只返回 `systemd-credential:*` 或受控的 Candidate reference,不返回认证正文。
|
||||
- OpenClaw、Codex 和 Gitea provider 身份由管理面供应;模拟 provider 永远不能把原生 canary 标成已验证。
|
||||
- Writer 只写任务绑定分支和允许路径。生产 main、tag、baseline、registry 和部署不属于 Candidate 权限。
|
||||
|
||||
## 核心模块
|
||||
|
||||
- `models.py`:TaskPlan、Approval、Credential、Result 和 ReleasePlan 强类型约束。
|
||||
- `store.py`:私有 SQLite、WAL、全同步、schema 检查、事务和原子 step transition。
|
||||
- `workflow.py`:依赖、claim、lease、retry、timeout recovery 和终态。
|
||||
- `credential.py` / `secret_manager.py`:reference-only 生命周期、补偿撤销、跨任务拒绝和 post-revocation 验证。
|
||||
- `native_adapters.py`:OpenClaw/Codex 原生 provider ID、parent/child tree、status、interrupt、close 和纯文本回执。
|
||||
- `gitea_canary.py`:仅通过 deploy-key provider 的 list/create/delete 操作完成 canary 与撤销验证。
|
||||
- `evidence.py`:落库的哈希链、重启回放、篡改检测和 required-event closure。
|
||||
- `artifact.py`:确定性 tar、逐文件哈希、package closure、链接/特殊文件/敏感标记拒绝。
|
||||
- `release.py` / `release_gate.py`:部署审批与回滚审批分离,tag object 与 peeled target 分离验证。
|
||||
|
||||
## 会话真实性
|
||||
|
||||
以下状态必须分别记录,不能互相替代:
|
||||
|
||||
```text
|
||||
broker_session_verified
|
||||
openclaw_native_child_session_verified
|
||||
codex_child_session_verified
|
||||
```
|
||||
|
||||
Shadow provider 只能证明 Broker 合同和清理路径,不能证明 OpenClaw 或 Codex 原生会话。
|
||||
@@ -0,0 +1,133 @@
|
||||
# AIOS V2R1C4 Dormant Shadow-Only Release Execution
|
||||
|
||||
## Purpose
|
||||
|
||||
Deploy verified V2R1C4 code while keeping autonomous execution capabilities disabled.
|
||||
|
||||
This release separates code deployment from capability activation. Capability activation requires a later, independently approved release.
|
||||
|
||||
## Deployment mode
|
||||
|
||||
```text
|
||||
deployment_mode=dormant_shadow_only
|
||||
```
|
||||
|
||||
Required production flags:
|
||||
|
||||
```text
|
||||
AIOS_GITOPS_WRITES_ENABLED=false
|
||||
AIOS_TASK_PLANE_EXECUTION_ENABLED=false
|
||||
AIOS_SILENT_RELEASE_EXECUTION_ENABLED=false
|
||||
```
|
||||
|
||||
Required dispatch transport:
|
||||
|
||||
```text
|
||||
protectedDispatchTransport=sandbox
|
||||
```
|
||||
|
||||
Disabled capabilities:
|
||||
|
||||
```text
|
||||
native_openclaw_enabled=false
|
||||
native_codex_enabled=false
|
||||
automatic_release_enabled=false
|
||||
production_side_effects=false
|
||||
activation_deferred=true
|
||||
```
|
||||
|
||||
## Release flow
|
||||
|
||||
```text
|
||||
Integration verification
|
||||
|
|
||||
v
|
||||
Owner dormant release approval
|
||||
|
|
||||
v
|
||||
Merge PR
|
||||
|
|
||||
v
|
||||
Read final main merge OID
|
||||
|
|
||||
v
|
||||
Rebuild production artifact
|
||||
|
|
||||
v
|
||||
Deploy dormant runtime
|
||||
|
|
||||
v
|
||||
Create V2R1C4 tag
|
||||
|
|
||||
v
|
||||
Verify Registry and Baseline consistency
|
||||
```
|
||||
|
||||
## Two-stage approval model
|
||||
|
||||
### Pre-merge approval
|
||||
|
||||
The pre-merge approval binds:
|
||||
|
||||
- version
|
||||
- PR number
|
||||
- integration OID and tree OID
|
||||
- candidate artifact and evidence hashes
|
||||
- deployment mode
|
||||
- all production execution flags remaining false
|
||||
|
||||
It authorizes the governed merge and production artifact rebuild. It does not predeclare the final production target OID.
|
||||
|
||||
### Final release approval
|
||||
|
||||
After the governed merge, the final approval binds:
|
||||
|
||||
- final main merge OID
|
||||
- final main tree OID
|
||||
- production artifact, manifest, package closure, and evidence closure
|
||||
- rollback artifact and production configuration hashes
|
||||
- V2R1C4 tag target
|
||||
|
||||
The integration OID must never be used as the final production target OID.
|
||||
|
||||
## Deferred activation
|
||||
|
||||
The following are separate activation gates and must not block dormant code deployment:
|
||||
|
||||
- OpenClaw native session provider
|
||||
- Codex app-server provider
|
||||
- Gitea Admin API automation
|
||||
- Secret Manager V2
|
||||
- autonomous production execution
|
||||
|
||||
They require a later activation release with separate evidence and approval.
|
||||
|
||||
## Hard restrictions
|
||||
|
||||
Never:
|
||||
|
||||
- write main directly
|
||||
- force push
|
||||
- rewrite tags
|
||||
- export secrets
|
||||
- enable autonomous production execution without approval
|
||||
- treat integration OID as production target OID
|
||||
- update Registry or Baseline before remote and Gitea tag verification
|
||||
|
||||
## Dormant production acceptance
|
||||
|
||||
A dormant deployment is acceptable only when:
|
||||
|
||||
```text
|
||||
deployment_mode=dormant_shadow_only
|
||||
production_side_effects=false
|
||||
protectedDispatchTransport=sandbox
|
||||
native_openclaw_enabled=false
|
||||
native_codex_enabled=false
|
||||
automatic_release_enabled=false
|
||||
AIOS_GITOPS_WRITES_ENABLED=false
|
||||
AIOS_TASK_PLANE_EXECUTION_ENABLED=false
|
||||
AIOS_SILENT_RELEASE_EXECUTION_ENABLED=false
|
||||
```
|
||||
|
||||
The release report must preserve deferred activation blockers rather than falsely marking native-agent or automatic-release capabilities as enabled.
|
||||
@@ -0,0 +1,82 @@
|
||||
# AIOS Autonomous Release Execution Package
|
||||
|
||||
## Release Strategy
|
||||
|
||||
V2R1C4 uses a two-stage model:
|
||||
|
||||
1. Dormant code deployment.
|
||||
2. Capability activation through a separate approved release.
|
||||
|
||||
## Dormant Deployment
|
||||
|
||||
The first production release must keep autonomous execution disabled:
|
||||
|
||||
```text
|
||||
AIOS_GITOPS_WRITES_ENABLED=false
|
||||
AIOS_TASK_PLANE_EXECUTION_ENABLED=false
|
||||
AIOS_SILENT_RELEASE_EXECUTION_ENABLED=false
|
||||
```
|
||||
|
||||
Required mode:
|
||||
|
||||
```text
|
||||
deployment_mode=dormant_shadow_only
|
||||
```
|
||||
|
||||
Required properties:
|
||||
|
||||
```text
|
||||
production_side_effects=false
|
||||
native_openclaw_enabled=false
|
||||
native_codex_enabled=false
|
||||
automatic_release_enabled=false
|
||||
```
|
||||
|
||||
## Deployment Sequence
|
||||
|
||||
```text
|
||||
Verify Integration
|
||||
|
|
||||
v
|
||||
Owner Dormant Release Approval
|
||||
|
|
||||
v
|
||||
Merge Pull Request
|
||||
|
|
||||
v
|
||||
Read final main merge OID
|
||||
|
|
||||
v
|
||||
Build Production Artifact
|
||||
|
|
||||
v
|
||||
Deploy Dormant Runtime
|
||||
|
|
||||
v
|
||||
Create Version Tag
|
||||
|
|
||||
v
|
||||
Verify Registry and Baseline
|
||||
```
|
||||
|
||||
## Capability Activation (Separate)
|
||||
|
||||
The following require separate approval and evidence:
|
||||
|
||||
- OpenClaw native provider
|
||||
- Codex app-server provider
|
||||
- Gitea Admin API lifecycle
|
||||
- Secret Manager V2
|
||||
- Autonomous production execution
|
||||
|
||||
## Hard Governance Rules
|
||||
|
||||
Never:
|
||||
|
||||
- direct write main
|
||||
- force push
|
||||
- rewrite tags
|
||||
- export credentials
|
||||
- enable autonomous production execution without approval
|
||||
- use integration OID as production target OID
|
||||
- update Baseline before remote tag verification
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"execution_renewal_manifest_blob_oid": "417f78abb3d795f7b0c04a0894069bd166bb15df",
|
||||
"execution_renewal_manifest_sha256": "069091eff9c7c46bceccde365da6d07a8538e76bee9e288a57211f3114b94ab3",
|
||||
"execution_renewal_merge_oid": "3a22c3c7d86930efd4bd93ac05d80ad322050cb9",
|
||||
"observed_at": "2026-07-28T09:09:27.841968Z",
|
||||
"runtime_source_evidence_sha256": "23f03d9c6abe6f11e1acda5210b8708696e3b77caa0083b6bfaef30a44b2a35f",
|
||||
"runtime_source_observed_at": "2026-07-28T05:53:01.253909Z",
|
||||
"schema": "aios.integration-refresh-source-map/v1",
|
||||
"source_mapping_approval_merge_oid": "9514062cf33e45941abe4a8c98377e0713b0b3f6",
|
||||
"source_mapping_manifest_blob_oid": "00c5e90e529cb98c7750a5761b0a0d6ccf507174",
|
||||
"source_mapping_manifest_sha256": "17af8ebc43673c0e46bb9ab1bf44497e8c147f1b4a9d7a8a404edd5c0abd6f21",
|
||||
"sources": [
|
||||
{
|
||||
"byte_for_byte_equal": true,
|
||||
"canonical_blob_oid": "9c68be36e0b6a18260e3099e3d7b0dcab6d814aa",
|
||||
"canonical_commit_oid": "85426f4f570e4d40b212318d47e52e4aed5cb06a",
|
||||
"canonical_git_path": "active-runtime/app/aios_autonomous_release_authorization/controller.py",
|
||||
"canonical_tree_oid": "cdbd9bb845b18d4b5224d91aace2656a51eb7c14",
|
||||
"runtime_blob_oid": "9c68be36e0b6a18260e3099e3d7b0dcab6d814aa",
|
||||
"runtime_path": "/usr/libexec/aios-autonomous-release-authorization/controller.py",
|
||||
"runtime_sha256": "39b532c3058c4ae49ed691d7c66c3b7745a25111fa61039f388c8335fd262c50"
|
||||
},
|
||||
{
|
||||
"byte_for_byte_equal": true,
|
||||
"canonical_blob_oid": "ee0595ca61a0598f8cb3dc10865a255e21679b2a",
|
||||
"canonical_commit_oid": "85426f4f570e4d40b212318d47e52e4aed5cb06a",
|
||||
"canonical_git_path": "active-runtime/app/aios_chatgpt_action_task_router/action_task_router.py",
|
||||
"canonical_tree_oid": "cdbd9bb845b18d4b5224d91aace2656a51eb7c14",
|
||||
"runtime_blob_oid": "ee0595ca61a0598f8cb3dc10865a255e21679b2a",
|
||||
"runtime_path": "/usr/libexec/aios-chatgpt-action-task-router-continuous-20260726/action_task_router.py",
|
||||
"runtime_sha256": "738fb7035a6b792115d6467b4d1e4ffd47a8c5d5646f7032ebc68ea587cbd554"
|
||||
}
|
||||
],
|
||||
"task_id": "AIOS-INTEGRATION-REFRESH-PROVISIONER-IMPLEMENTATION-20260726"
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
# Candidate 运维与验收
|
||||
|
||||
## 安全默认值
|
||||
|
||||
两个 systemd 模板均为 Dormant/Shadow-only,绑定 loopback,默认关闭 Task Plane 执行、GitOps 写入和生产部署。模板进入仓库不等于安装或启动服务。
|
||||
|
||||
外部信任根缺失时必须保留以下阻断:
|
||||
|
||||
```text
|
||||
BLOCKED_OWNER_APPROVAL_VERIFIER_UNAVAILABLE
|
||||
BLOCKED_GITEA_ADMIN_CREDENTIAL_REQUIRED
|
||||
BLOCKED_EXTERNAL_OWNER_SIGNATURE
|
||||
```
|
||||
|
||||
不得用模拟身份、个人账户或生产 provider 身份替代这些根。
|
||||
|
||||
## 独立验收
|
||||
|
||||
从仓库根目录运行:
|
||||
|
||||
```text
|
||||
PYTHONPATH=services/aios-autonomous-release-candidate \
|
||||
python3 -m pytest -p no:cacheprovider -q tests/autonomous_release
|
||||
|
||||
PYTHONPATH=services/aios-autonomous-release-candidate \
|
||||
python3 scripts/autonomous-release/run_acceptance.py \
|
||||
--repo-root . \
|
||||
--output /task-evidence/candidate-acceptance.json
|
||||
```
|
||||
|
||||
Acceptance report 必须满足:
|
||||
|
||||
```text
|
||||
all_passed=true
|
||||
skipped=0
|
||||
direct_gitea_database_access=false
|
||||
production_path_hits=[]
|
||||
secret_hits=[]
|
||||
production_side_effects=false
|
||||
```
|
||||
|
||||
## Artifact
|
||||
|
||||
Artifact builder 拒绝覆盖现有文件、链接、硬链接、特殊文件、仓库元数据和敏感内容标记。输出 tar 和 manifest 后必须再次执行 `ArtifactBuilder.verify`。
|
||||
|
||||
## 外部 canary
|
||||
|
||||
真实 native session 与 Gitea canary 只能在任务绑定的 runtime credential 投影存在时运行。每次 canary 完成后必须关闭 provider session、撤销上游对象、删除 projection,并保留非敏感回执。
|
||||
@@ -0,0 +1,26 @@
|
||||
# Governed ReleasePlan 关闭条件
|
||||
|
||||
ReleasePlan 必须绑定 task、repository、integration branch、完整 target/tree OID、artifact/manifest/evidence/rollback digest、生产配置 digest,以及互不相同的部署和回滚 ApprovalPlan ID。
|
||||
|
||||
## Candidate 与 Integration
|
||||
|
||||
1. Candidate 分支先完成测试、扫描、Shadow、Artifact 和外部门禁盘点。
|
||||
2. 重新读取 authoritative `main` 完整 OID。
|
||||
3. 从该 OID 创建 integration 分支。
|
||||
4. 使用普通 `merge --no-ff` 合入 Candidate;禁止 squash、rebase、amend 和 force push。
|
||||
5. 在 integration 上重新执行全部测试、扫描和 artifact closure。
|
||||
|
||||
## 生产停止线
|
||||
|
||||
下列项目任一为 false,流程必须停止在 `READY_FOR_OWNER_RELEASE_APPROVAL`,不得部署或创建生产 tag:
|
||||
|
||||
```text
|
||||
openclaw_native_child_session_verified
|
||||
codex_child_session_verified
|
||||
gitea_admin_api_lifecycle_canary
|
||||
owner_signer_separation_verified
|
||||
credential_incident_closed
|
||||
external_owner_release_approval_verified
|
||||
```
|
||||
|
||||
聊天文本、Candidate 自签记录或模拟 canary 不能改变这条停止线。
|
||||
@@ -0,0 +1,414 @@
|
||||
# AIOS V2R1C4 Dormant Release — Final Agent Execution Contract
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
This document is the machine/operator execution contract for completing the governed V2R1C4 dormant release after Candidate development is frozen.
|
||||
|
||||
It does not grant production authority. Every consequential action remains subject to the exact approval, Git reference, artifact, tag, Registry and Baseline gates defined below.
|
||||
|
||||
## 2. Release mode
|
||||
|
||||
The only permitted deployment mode for V2R1C4 is:
|
||||
|
||||
```text
|
||||
deployment_mode=dormant_shadow_only
|
||||
```
|
||||
|
||||
The following values are mandatory before, during and after deployment:
|
||||
|
||||
```text
|
||||
AIOS_GITOPS_WRITES_ENABLED=false
|
||||
AIOS_TASK_PLANE_EXECUTION_ENABLED=false
|
||||
AIOS_SILENT_RELEASE_EXECUTION_ENABLED=false
|
||||
AIOS_PRODUCTION_DEPLOYMENT_ENABLED=false
|
||||
production_side_effects=false
|
||||
native_openclaw_enabled=false
|
||||
native_codex_enabled=false
|
||||
automatic_release_enabled=false
|
||||
activation_deferred=true
|
||||
protectedDispatchTransport=sandbox
|
||||
```
|
||||
|
||||
Any deviation is a hard stop.
|
||||
|
||||
## 3. Authoritative branches
|
||||
|
||||
```text
|
||||
candidate_branch=patch/chatgpt-autonomous-completion-contract-20260726
|
||||
integration_branch=integration/chatgpt-autonomous-completion-20260726
|
||||
production_branch=main
|
||||
version=V2R1C4
|
||||
pull_request=20
|
||||
```
|
||||
|
||||
Never infer a target OID from workspace HEAD. Always read and record full 40-character OIDs.
|
||||
|
||||
## 4. Phase A — Candidate freeze and verification
|
||||
|
||||
1. Read the current Candidate OID and tree OID.
|
||||
2. Verify the Candidate worktree is clean.
|
||||
3. Verify four-way Candidate reference closure:
|
||||
- local branch ref;
|
||||
- authoritative bare ref;
|
||||
- `git ls-remote` ref;
|
||||
- Gitea API branch ref.
|
||||
4. Run the complete test and static validation suite defined in `V2R1C4_TEST_AND_RELEASE_CHECKLIST.md`.
|
||||
5. Build the deterministic Candidate artifact and manifest from the exact Candidate OID.
|
||||
6. Record:
|
||||
- artifact SHA-256;
|
||||
- manifest SHA-256;
|
||||
- package closure SHA-256;
|
||||
- test evidence SHA-256;
|
||||
- rollback artifact SHA-256;
|
||||
- production configuration SHA-256.
|
||||
7. Do not reuse any digest generated from an older Candidate OID.
|
||||
|
||||
Failure state:
|
||||
|
||||
```text
|
||||
CANDIDATE_ACCEPTANCE_BLOCKED
|
||||
```
|
||||
|
||||
## 5. Phase B — Integration refresh
|
||||
|
||||
1. Refresh the Integration branch from the latest Candidate using a governed non-fast-forward merge.
|
||||
2. Use:
|
||||
|
||||
```text
|
||||
merge --no-ff
|
||||
```
|
||||
|
||||
3. Do not squash, rebase, force push or rewrite Candidate history.
|
||||
4. Verify the Candidate OID is reachable from the new Integration OID.
|
||||
5. Verify four-way Integration reference closure.
|
||||
6. Run the full Candidate test suite again from the Integration worktree.
|
||||
7. Rebuild Integration evidence and Candidate package metadata if any tracked file changed during the merge.
|
||||
|
||||
Failure state:
|
||||
|
||||
```text
|
||||
INTEGRATION_ACCEPTANCE_BLOCKED
|
||||
```
|
||||
|
||||
## 6. Phase C — Candidate acceptance
|
||||
|
||||
Create Candidate acceptance evidence and execute:
|
||||
|
||||
```text
|
||||
python scripts/autonomous-release/run_acceptance.py \
|
||||
--phase candidate \
|
||||
--input <candidate-evidence.json> \
|
||||
--output <candidate-acceptance.json>
|
||||
```
|
||||
|
||||
The result must contain:
|
||||
|
||||
```text
|
||||
phase=candidate
|
||||
status=READY
|
||||
production_action_authorized=false
|
||||
```
|
||||
|
||||
The Candidate gate must verify:
|
||||
|
||||
```text
|
||||
candidate_four_way_ref_closure=true
|
||||
integration_four_way_ref_closure=true
|
||||
candidate_reachable_from_integration=true
|
||||
worktree_clean=true
|
||||
tests_failed=0
|
||||
tests_passed>0
|
||||
```
|
||||
|
||||
It must also verify all dormant runtime flags remain disabled.
|
||||
|
||||
`READY` authorizes review only. It does not authorize merge, deployment, tagging, Registry writes or Baseline writes.
|
||||
|
||||
## 7. Phase D — Pre-merge owner approval
|
||||
|
||||
Obtain a governed Owner approval plan for the exact Integration package.
|
||||
|
||||
The approval subject must bind at least:
|
||||
|
||||
```text
|
||||
version
|
||||
pull_request
|
||||
candidate_oid
|
||||
candidate_tree_oid
|
||||
integration_oid
|
||||
integration_tree_oid
|
||||
candidate_acceptance_digest
|
||||
candidate_release_digest
|
||||
artifact_sha256
|
||||
manifest_sha256
|
||||
package_closure_sha256
|
||||
evidence_closure_oid
|
||||
rollback_artifact_sha256
|
||||
production_config_sha256
|
||||
deployment_mode=dormant_shadow_only
|
||||
all execution flags=false
|
||||
```
|
||||
|
||||
The approval must not predeclare the final production target OID.
|
||||
|
||||
Without an Owner-approved plan, stop with:
|
||||
|
||||
```text
|
||||
BLOCKED_EXTERNAL_OWNER_SIGNATURE
|
||||
```
|
||||
|
||||
## 8. Phase E — Governed merge
|
||||
|
||||
1. Merge PR #20 only through the governed merge path.
|
||||
2. Do not push directly to `main`.
|
||||
3. After merge, read the exact resulting `main` merge OID and tree OID.
|
||||
4. Verify four-way `main` reference closure.
|
||||
5. Verify the merged Integration OID is reachable from the final `main` merge OID.
|
||||
6. Never use the Integration OID as the production target OID.
|
||||
|
||||
Failure state:
|
||||
|
||||
```text
|
||||
MAIN_RECONCILIATION_REQUIRED
|
||||
```
|
||||
|
||||
## 9. Phase F — Production artifact rebuild
|
||||
|
||||
Rebuild all production artifacts from the exact final `main` merge OID.
|
||||
|
||||
Record new values for:
|
||||
|
||||
```text
|
||||
artifact_sha256
|
||||
manifest_sha256
|
||||
package_closure_sha256
|
||||
evidence_closure_oid
|
||||
rollback_artifact_sha256
|
||||
production_config_sha256
|
||||
```
|
||||
|
||||
Candidate or Integration artifacts may not be promoted by renaming or metadata substitution. The final artifact must be demonstrably rebuilt from the final production target.
|
||||
|
||||
## 10. Phase G — Final release digest and Owner approval
|
||||
|
||||
1. Construct the `FinalDormantRelease` object using the final `main` target and rebuilt artifact evidence.
|
||||
2. Compute its exact `release_digest`.
|
||||
3. Obtain an Owner approval receipt bound to that exact digest.
|
||||
4. Record:
|
||||
|
||||
```text
|
||||
approval_plan_id
|
||||
approval_receipt_sha256
|
||||
approved_release_digest
|
||||
owner_approval_verified=true
|
||||
```
|
||||
|
||||
Required equality:
|
||||
|
||||
```text
|
||||
approved_release_digest == FinalDormantRelease.digest()
|
||||
```
|
||||
|
||||
A generic approval, previous approval, Candidate approval or Integration approval is insufficient.
|
||||
|
||||
## 11. Phase H — Final acceptance
|
||||
|
||||
Execute:
|
||||
|
||||
```text
|
||||
python scripts/autonomous-release/run_acceptance.py \
|
||||
--phase final \
|
||||
--input <final-evidence.json> \
|
||||
--output <final-acceptance.json>
|
||||
```
|
||||
|
||||
The result must contain:
|
||||
|
||||
```text
|
||||
phase=final
|
||||
status=READY
|
||||
production_action_authorized=false
|
||||
```
|
||||
|
||||
Required final evidence:
|
||||
|
||||
```text
|
||||
main_four_way_ref_closure=true
|
||||
target_is_main_head=true
|
||||
worktree_clean=true
|
||||
artifact_rebuilt_from_target=true
|
||||
rollback_verified=true
|
||||
runtime_boundary_verified=true
|
||||
owner_approval_verified=true
|
||||
approved_release_digest=<exact final release digest>
|
||||
```
|
||||
|
||||
The acceptance report is evidence, not an execution credential.
|
||||
|
||||
## 12. Phase I — Dormant deployment
|
||||
|
||||
Deployment requires a separate production execution approval referencing:
|
||||
|
||||
```text
|
||||
version=V2R1C4
|
||||
target_oid=<final main merge OID>
|
||||
evidence_closure_oid=<final evidence closure OID>
|
||||
approval_plan_id=<approved plan>
|
||||
final_acceptance_digest=<final acceptance digest>
|
||||
```
|
||||
|
||||
Deploy only the dormant runtime. Before starting services, verify all disabled flags and the sandbox transport in the rendered production configuration.
|
||||
|
||||
After start, verify:
|
||||
|
||||
```text
|
||||
services_active=true
|
||||
loopback_bindings_only=true
|
||||
production_side_effects=false
|
||||
write_endpoints_disabled=true
|
||||
task_plane_execution_disabled=true
|
||||
silent_release_execution_disabled=true
|
||||
native_agent_execution_disabled=true
|
||||
```
|
||||
|
||||
If any side-effect-capable path is active, stop services and mark:
|
||||
|
||||
```text
|
||||
DORMANT_RUNTIME_BOUNDARY_VIOLATION
|
||||
```
|
||||
|
||||
## 13. Phase J — Tag governance
|
||||
|
||||
Tagging is permitted only after dormant deployment verification and exact Owner approval.
|
||||
|
||||
Every tag request must carry:
|
||||
|
||||
```text
|
||||
version=V2R1C4
|
||||
target_oid=<full final main merge OID>
|
||||
evidence_closure_oid=<full final evidence closure OID>
|
||||
approval_plan_id=<approved plan ID>
|
||||
```
|
||||
|
||||
When an existing same-name tag is found:
|
||||
|
||||
- identical tag OID and peeled target: treat as idempotent and reverify;
|
||||
- different tag OID or peeled target: stop with `MANUAL_OWNER_REVIEW_REQUIRED`.
|
||||
|
||||
Never force push, delete, move or rewrite a formal tag.
|
||||
|
||||
## 14. Phase K — Remote and Gitea tag acceptance
|
||||
|
||||
Before any Registry or Baseline update, verify:
|
||||
|
||||
```text
|
||||
local_tag_exists=true
|
||||
remote_tag_exists=true
|
||||
gitea_api_tag_exists=true
|
||||
local_tag_oid == remote_tag_oid
|
||||
remote_peeled_target_oid == target_oid
|
||||
gitea_peeled_target_oid == target_oid
|
||||
repository_identity=aios/aios
|
||||
```
|
||||
|
||||
For annotated tags, verify both the tag object and peeled commit.
|
||||
|
||||
Failure state:
|
||||
|
||||
```text
|
||||
REMOTE_PUSH_PENDING
|
||||
```
|
||||
|
||||
or:
|
||||
|
||||
```text
|
||||
RECONCILIATION_REQUIRED
|
||||
```
|
||||
|
||||
## 15. Phase L — Registry and Baseline
|
||||
|
||||
Only after remote and Gitea tag acceptance:
|
||||
|
||||
1. register or update the V2R1C4 Registry entry;
|
||||
2. reread the Registry and verify the exact target OID;
|
||||
3. update the Production Baseline to V2R1C4;
|
||||
4. reread the Baseline;
|
||||
5. run final release consistency acceptance.
|
||||
|
||||
Required equality:
|
||||
|
||||
```text
|
||||
Registry version
|
||||
= Production Baseline
|
||||
= local refs/tags/V2R1C4
|
||||
= remote refs/tags/V2R1C4
|
||||
= Gitea API tag
|
||||
= expected final main target
|
||||
```
|
||||
|
||||
Do not write `PRODUCTION_DEPLOYED` before this equality is proven.
|
||||
|
||||
## 16. Rollback
|
||||
|
||||
If deployment verification fails:
|
||||
|
||||
1. disable the V2R1C4 Candidate services;
|
||||
2. preserve evidence and logs;
|
||||
3. submit a governed rollback request with the target version and reason;
|
||||
4. distinguish `requested`, `approved`, `executed` and `verified` states.
|
||||
|
||||
Do not delete tags, rewrite history or independently perform a production rollback.
|
||||
|
||||
## 17. Mandatory stop conditions
|
||||
|
||||
Stop without consequential execution when any of the following is missing or false:
|
||||
|
||||
```text
|
||||
four_way_ref_closure
|
||||
clean_worktree
|
||||
full_test_closure
|
||||
artifact_digest_verification
|
||||
rollback_evidence
|
||||
runtime_boundary_verification
|
||||
exact_owner_approval
|
||||
exact_target_oid
|
||||
exact_evidence_closure_oid
|
||||
exact_approval_plan_id
|
||||
remote_tag_verification
|
||||
gitea_tag_verification
|
||||
```
|
||||
|
||||
## 18. Final report schema
|
||||
|
||||
Return:
|
||||
|
||||
```text
|
||||
execution_status=COMPLETED|PARTIALLY_COMPLETED|BLOCKED|FAILED
|
||||
candidate_oid=<full OID>
|
||||
integration_oid=<full OID>
|
||||
main_target_oid=<full OID>
|
||||
artifact_sha256=<digest>
|
||||
manifest_sha256=<digest>
|
||||
package_closure_sha256=<digest>
|
||||
evidence_closure_oid=<full OID>
|
||||
candidate_acceptance_status=<status>
|
||||
final_acceptance_status=<status>
|
||||
owner_approval_verified=<bool>
|
||||
dormant_deployment_verified=<bool>
|
||||
local_tag_verified=<bool>
|
||||
remote_tag_verified=<bool>
|
||||
gitea_tag_verified=<bool>
|
||||
registry_verified=<bool>
|
||||
baseline_verified=<bool>
|
||||
release_consistency=<status>
|
||||
```
|
||||
|
||||
Always include:
|
||||
|
||||
```text
|
||||
force_push_used: false
|
||||
tag_rewritten: false
|
||||
direct_main_push: false
|
||||
secrets_committed: false
|
||||
owner_approval_respected: true
|
||||
```
|
||||
@@ -0,0 +1,30 @@
|
||||
# AIOS V2R1C4 Dormant Release Agent Execution
|
||||
|
||||
## Scope
|
||||
|
||||
This package is a dormant, shadow-only Candidate release. It does not authorize a production deployment, tag, baseline update, registry update, or a write to main.
|
||||
|
||||
## Required runtime boundary
|
||||
|
||||
deployment_mode=dormant_shadow_only
|
||||
AIOS_GITOPS_WRITES_ENABLED=false
|
||||
AIOS_TASK_PLANE_EXECUTION_ENABLED=false
|
||||
AIOS_SILENT_RELEASE_EXECUTION_ENABLED=false
|
||||
production_side_effects=false
|
||||
|
||||
## Governed sequence
|
||||
|
||||
1. Verify the Integration branch with the Candidate test suite and four-way Git reference closure.
|
||||
2. Obtain Owner approval for dormant review only.
|
||||
3. Merge only after a separate production release approval; this document does not grant that approval.
|
||||
4. Read the resulting main merge OID before building an artifact.
|
||||
5. Deploy only the dormant Candidate runtime after explicit production approval.
|
||||
6. Create a tag and update Registry or Baseline only under their separate governed approvals.
|
||||
|
||||
## Evidence requirements
|
||||
|
||||
The execution agent must retain task ID, commit OID, artifact and manifest digests, test evidence, rollback package reference, and approval receipt metadata. It must not include plaintext credentials or private keys.
|
||||
|
||||
## Stop conditions
|
||||
|
||||
Stop with NOT_EXECUTED if any approval, four-way reference closure, artifact verification, or deployment gate is absent. Do not infer production authorization from Candidate authorization.
|
||||
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"evidence_type": "FULL_PACKAGE_FILE_MANIFEST_DRIFT_CLOSURE",
|
||||
"release": "V2R1C4",
|
||||
"entries": [
|
||||
{
|
||||
"path": "VERSION",
|
||||
"old_sha256": "1095953d5764d9c72f02e84b1d525e265d3a67a5bdb451ab9ef51a8e8b3032b8",
|
||||
"new_sha256": "b090062a92aeb14211bbfc1fcd43b5ee4f315d30054b437f3bcdac657210ee95",
|
||||
"source_commit": "9917145a643c7938a1a4f4f72690674f13f4c3dd",
|
||||
"reason": "V2R1C4 release completion froze the candidate package version",
|
||||
"resolution": "accept the V2R1C4 candidate content and update only this manifest entry"
|
||||
},
|
||||
{
|
||||
"path": "active-runtime/app/aios_gitops_model_api.py",
|
||||
"old_sha256": "ee6e4fc6bc98a590fbc3585b62707e2562115e5a3f722e69a7c464d966bdc458",
|
||||
"new_sha256": "a664714bc1e13160cdd0dc32de460c65a83a3a95aa5cdad1833b1b650ab90e9d",
|
||||
"source_commit": "45dd78ec796faf9fdfba397e5bab73176aeef3f6",
|
||||
"reason": "tracked GitOps API gained the governed short-link and webhook endpoint",
|
||||
"resolution": "accept current tracked content and update only this manifest entry"
|
||||
},
|
||||
{
|
||||
"path": "config/domains.env",
|
||||
"old_sha256": "a3872f24e4cd64288758846ecd0b374db4aa41111feec3b17c41438136d6d5d0",
|
||||
"new_sha256": "5a27d48db343dcb723bcc1d9d8222aa3f72094c57cc43640aebe6558963bd20f",
|
||||
"source_commit": "e7d36ad706473c95a4d4b9ec791120271ed856cd",
|
||||
"reason": "tracked authentication routing made cnnetmkt Casdoor the primary endpoint",
|
||||
"resolution": "accept current tracked content and update only this manifest entry"
|
||||
},
|
||||
{
|
||||
"path": "scripts/install_v1r1c0_gitops_model_overlay.sh",
|
||||
"old_sha256": "4d54a4ed6d35bbb6e9fe2924f3cfceeea9c3eed898017788f6c5ab8041394a47",
|
||||
"new_sha256": "e76cd986d2387b76439e4a7fe503e48a2de8fc70fb52c45a13641c9d943f108f",
|
||||
"source_commit": "a2481421589e167680e7d801433758c6a01db832",
|
||||
"reason": "tracked installer added online Codex patch-branch synchronization",
|
||||
"resolution": "accept current tracked content and update only this manifest entry"
|
||||
},
|
||||
{
|
||||
"path": "scripts/package_contract_check_v1r1c0.sh",
|
||||
"old_sha256": "e0abd8ad44d4f088108f2046fd9d7bbd4465af50f79bd3b9fcbb72a8c61bee65",
|
||||
"new_sha256": "f537c58fc08685601f47fac5c0a3a2856e3ce9806c1918848c87a194a49c28a0",
|
||||
"source_commit": "9917145a643c7938a1a4f4f72690674f13f4c3dd",
|
||||
"reason": "release completion generalized the legacy version check without weakening required files or compilation checks",
|
||||
"resolution": "accept the generalized semantic version check and update only this manifest entry"
|
||||
},
|
||||
{
|
||||
"path": "scripts/release_version.sh",
|
||||
"old_sha256": "abef8df642c6d233af4a03c512876b02b326e19c924020b8dd8cc74236d18817",
|
||||
"new_sha256": "14ab40a7c342e21e8315def14d552dcff4b214d23f1f4be493e9d3fc2cee1a2f",
|
||||
"source_commit": "27a0ae126b96d63c40544174e7a3859e30eba056",
|
||||
"reason": "tracked release helper closed tag-publication governance gaps",
|
||||
"resolution": "accept current tracked content and update only this manifest entry"
|
||||
}
|
||||
],
|
||||
"verifier_change": {
|
||||
"path": "package_verify_full.sh",
|
||||
"old_sha256": "b77bce1ca78cede27de6594e25f6ce5bb0c4990c0953eeb79f7e2608d2515c32",
|
||||
"new_sha256": "ca49f4165b50171968485ffa5535bc33d2158237ea55af10a3888a5fa0f7a535",
|
||||
"source_commit": "V2R1C4 release completion candidate",
|
||||
"reason": "remove the checksum failure suppression and fail closed when the manifest is absent or mismatched",
|
||||
"resolution": "update only the verifier self-entry after the six historical drifts were closed"
|
||||
},
|
||||
"entry_count": 6,
|
||||
"whole_manifest_regenerated": false,
|
||||
"verifier_fail_closed": true
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
# Agent Integration Plan
|
||||
|
||||
1. Verify package SHA-256 and FILE_SHA256.json.
|
||||
2. Apply added/modified source files to patch/v2r1c4-release-completion-20260729.
|
||||
3. Do not copy __pycache__, .pytest_cache, TEST_RECEIPT.json, or local package metadata into runtime paths.
|
||||
4. Preserve existing approved WeChat and single-owner activation Git objects.
|
||||
5. Install the Python package and verify every pyproject console entrypoint.
|
||||
6. Render environment-specific JSON configs from config/examples.
|
||||
7. Use systemd encrypted credentials only; never persist plaintext tokens.
|
||||
8. Run compileall, all repository tests, systemd unit verification, secret scan, and online copied-Gitea acceptance.
|
||||
9. Build one autonomous-control-plane Artifact and rollback Artifact.
|
||||
10. Create one Owner approval for AIOS_AUTONOMOUS_RELEASE_CONTROL_PLANE_BOOTSTRAP.
|
||||
11. After deployment, complete V2R1C4 code freeze and create one V2R1C4_PRODUCTION_RELEASE_ENVELOPE.
|
||||
12. Do not split unchanged code/artifact/permission scope into additional approvals.
|
||||
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"capabilities": {
|
||||
"approval_mirror_worker": "DELIVERED",
|
||||
"approval_watcher": "DELIVERED",
|
||||
"artifact_atomic_deployment": "DELIVERED",
|
||||
"automatic_renewal_controller": "DELIVERED",
|
||||
"credential_revoker_with_old_token_401": "DELIVERED",
|
||||
"governance_request_publisher": "DELIVERED",
|
||||
"governed_development_writer_server": "DELIVERED",
|
||||
"governed_merge_transport": "DELIVERED",
|
||||
"governed_tag_transport": "DELIVERED",
|
||||
"grant_quota_idempotency": "DELIVERED",
|
||||
"grant_revocation_evidence": "DELIVERED",
|
||||
"health_rollback_restore": "DELIVERED",
|
||||
"owner_exact_head_release_approval_verification": "DELIVERED",
|
||||
"registry_baseline_snapshot_compensation": "DELIVERED",
|
||||
"release_object_projection": "DELIVERED",
|
||||
"remote_tag_and_final_consistency": "DELIVERED",
|
||||
"runtime_source_projection": "DELIVERED",
|
||||
"task_bound_writer_grant": "DELIVERED",
|
||||
"unified_release_envelope_executor": "DELIVERED"
|
||||
},
|
||||
"delivery_status": "CODE_COMPLETE_OFFLINE_VALIDATED",
|
||||
"direct_main_push": false,
|
||||
"entrypoints": {
|
||||
"aios-autonomous-control-plane": "aios_autonomous_control_plane.cli:main",
|
||||
"aios-autonomous-renewal-controller": "aios_autonomous_control_plane.renewal_service:main",
|
||||
"aios-development-writer-revoker": "aios_autonomous_control_plane.revoker:main",
|
||||
"aios-development-writer-server": "aios_autonomous_control_plane.writer_server:main",
|
||||
"aios-governance-approval-watcher": "aios_autonomous_control_plane.watcher_service:main",
|
||||
"aios-governance-mirror-worker": "aios_autonomous_control_plane.mirror_service:main",
|
||||
"aios-governance-request-publisher": "aios_autonomous_control_plane.publisher:main",
|
||||
"aios-release-hook": "aios_autonomous_control_plane.release_action:main",
|
||||
"aios-release-object-projector": "aios_autonomous_control_plane.projection_service:release_main",
|
||||
"aios-runtime-source-projector": "aios_autonomous_control_plane.projection_service:runtime_main",
|
||||
"aios-v2r1c4-release-executor": "aios_autonomous_control_plane.release_service:main"
|
||||
},
|
||||
"force_push": false,
|
||||
"global_writes_must_remain_false": true,
|
||||
"package_version": "0.2.0",
|
||||
"schema": "aios.code-handoff/v2",
|
||||
"source": "codex-chartgpt-bridge",
|
||||
"systemd_units": [
|
||||
"deploy/systemd/aios-autonomous-renewal-controller.service",
|
||||
"deploy/systemd/aios-governance-approval-watcher.service",
|
||||
"deploy/systemd/aios-governance-mirror-worker.service",
|
||||
"deploy/systemd/aios-governance-request-publisher.service",
|
||||
"deploy/systemd/aios-governed-development-writer-revoker.service",
|
||||
"deploy/systemd/aios-governed-development-writer-revoker.timer",
|
||||
"deploy/systemd/aios-governed-development-writer.service",
|
||||
"deploy/systemd/aios-release-object-projection.service",
|
||||
"deploy/systemd/aios-runtime-source-read-projection.service",
|
||||
"deploy/systemd/aios-v2r1c4-release-envelope.service"
|
||||
],
|
||||
"tag_rewrite": false,
|
||||
"target_branch": "patch/v2r1c4-release-completion-20260729",
|
||||
"tests": {
|
||||
"collection_errors": 0,
|
||||
"compileall": "PASS",
|
||||
"pytest_failed": 0,
|
||||
"pytest_passed": 21,
|
||||
"secret_findings": [
|
||||
{
|
||||
"path": "tests/autonomous_control_plane/test_policy_and_writer.py",
|
||||
"rule": "literal-credential-assignment"
|
||||
}
|
||||
],
|
||||
"secret_findings_classification": {
|
||||
"tests/autonomous_control_plane/test_policy_and_writer.py": "Deliberate fake api_key assignment used by the negative secret-rejection test."
|
||||
},
|
||||
"secret_scan": "PASS_TEST_FIXTURE_ONLY"
|
||||
},
|
||||
"wechat_backend": {
|
||||
"new_code_generated": false,
|
||||
"reason": "The controlled PR #20 full diff remains too large for this interface; existing exact Git objects must be retained and validated, not re-invented.",
|
||||
"required_agent_action": "Preserve already integrated approved WeChat code, run its online acceptance suite, and include its exact object IDs in the final release envelope."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"added_files": [
|
||||
"AGENT_INTEGRATION_PLAN.md",
|
||||
"config/examples/grant.json",
|
||||
"config/examples/mirror.json",
|
||||
"config/examples/publisher.json",
|
||||
"config/examples/release-actions.json",
|
||||
"config/examples/release-envelope.json",
|
||||
"config/examples/release-hooks.json",
|
||||
"config/examples/release-projection.json",
|
||||
"config/examples/renewal.json",
|
||||
"config/examples/revoker.json",
|
||||
"config/examples/runtime-projection.json",
|
||||
"config/examples/watcher.json",
|
||||
"config/examples/writer-server.json",
|
||||
"deploy/systemd/aios-v2r1c4-release-envelope.service",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/credentials.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/gitea_api.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/grant.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/mirror_service.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/projection_service.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/publisher.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/release_action.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/release_deploy.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/release_hooks.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/release_service.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/release_transport.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/renewal_service.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/revoker.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/socket_service.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/watcher_service.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/writer_server.py",
|
||||
"tests/autonomous_control_plane/test_deployment_and_contracts.py",
|
||||
"tests/autonomous_control_plane/test_services_and_hooks.py"
|
||||
],
|
||||
"base_package_sha256": "795feb406541af86f30c1fb31f85e6d7485ee72a9d3ac93351f9a63fe4ad3e8f",
|
||||
"deleted_files": [
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/__pycache__/__init__.cpython-313.pyc",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/__pycache__/cli.cpython-313.pyc",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/__pycache__/common.cpython-313.pyc",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/__pycache__/ledger.cpython-313.pyc",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/__pycache__/mirror.cpython-313.pyc",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/__pycache__/policy.cpython-313.pyc",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/__pycache__/projection.cpython-313.pyc",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/__pycache__/release.cpython-313.pyc",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/__pycache__/renewal.cpython-313.pyc",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/__pycache__/watcher.cpython-313.pyc",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/__pycache__/writer.cpython-313.pyc",
|
||||
"tests/.pytest_cache/.gitignore",
|
||||
"tests/.pytest_cache/CACHEDIR.TAG",
|
||||
"tests/.pytest_cache/README.md",
|
||||
"tests/.pytest_cache/v/cache/lastfailed",
|
||||
"tests/.pytest_cache/v/cache/nodeids",
|
||||
"tests/__pycache__/conftest.cpython-313-pytest-9.0.2.pyc",
|
||||
"tests/autonomous_control_plane/__pycache__/test_mirror_projection.cpython-313-pytest-9.0.2.pyc",
|
||||
"tests/autonomous_control_plane/__pycache__/test_policy_and_writer.cpython-313-pytest-9.0.2.pyc",
|
||||
"tests/autonomous_control_plane/__pycache__/test_renewal_release.cpython-313-pytest-9.0.2.pyc",
|
||||
"tests/autonomous_control_plane/__pycache__/test_watcher.cpython-313-pytest-9.0.2.pyc"
|
||||
],
|
||||
"deletion_policy": "Only generated caches listed here may be removed. Do not delete repository source files.",
|
||||
"integration_strategy": "Apply additions and replacements on patch/v2r1c4-release-completion-20260729; preserve existing approved Git objects and user code.",
|
||||
"modified_files": [
|
||||
"CODE_HANDOFF_MANIFEST.json",
|
||||
"INTEGRATION_NOTES.md",
|
||||
"TEST_RECEIPT.json",
|
||||
"deploy/systemd/aios-autonomous-renewal-controller.service",
|
||||
"deploy/systemd/aios-governance-approval-watcher.service",
|
||||
"deploy/systemd/aios-governance-mirror-worker.service",
|
||||
"deploy/systemd/aios-governance-request-publisher.service",
|
||||
"deploy/systemd/aios-governed-development-writer-revoker.service",
|
||||
"deploy/systemd/aios-governed-development-writer.service",
|
||||
"deploy/systemd/aios-release-object-projection.service",
|
||||
"deploy/systemd/aios-runtime-source-read-projection.service",
|
||||
"deploy/tmpfiles.d/aios-autonomous-control-plane.conf",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/projection.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/release.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/writer.py",
|
||||
"services/aios-autonomous-control-plane/pyproject.toml",
|
||||
"tests/autonomous_control_plane/test_mirror_projection.py",
|
||||
"tests/autonomous_control_plane/test_renewal_release.py"
|
||||
],
|
||||
"schema": "aios.code-handoff-delta/v1",
|
||||
"target_package_name": "aios_v2r1c4_autonomy_patch_v2_20260729.zip"
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"AGENT_INTEGRATION_PLAN.md": "0af0bb6c3ed22d30c48e1e9af74540a0f4571dabe5dcf97bc67077d5cca6ea1d",
|
||||
"CODE_HANDOFF_MANIFEST.json": "b1e3fde19a864671b28945ff21fd6c4f1fe2fd4bb7d78048ba1430d3de286cf9",
|
||||
"DELTA_FROM_V1.json": "296571cac8fe3117443ad04c045a02203f1bb9eef5284f1704dda9c04e75e9ae",
|
||||
"INTEGRATION_NOTES.md": "e70df9ca86ed136d193b1773596f7187d6c1f8751f40c133d3e93f017233d05c",
|
||||
"TEST_RECEIPT.json": "e1e3cd8934d8b79a88bf740575e37c1407ab2a6dbad099a2730476e3b073564a",
|
||||
"config/examples/development-policy.json": "51ab926a733c9a5173df43a3bf23d25ee3bd2b4743f2005cc458dcef232f62e4",
|
||||
"config/examples/grant.json": "2dbece7418377880c63108e07155a3e97859f8b24d087ad353749cddf2572ffd",
|
||||
"config/examples/mirror.json": "41b3948ad34a0a58b5af389d93f9a51241544449b08b90daa93b74ab5d22d292",
|
||||
"config/examples/publisher.json": "d5198ba71dc87e9863b1fa1fcce0fabcc5d347e508fa2736041772fa68c083ea",
|
||||
"config/examples/release-actions.json": "08d08fe97f3fdd336cd8f0dbc1c358bb5c9a9b2c7b1f66d5ab0682ea6aa0572e",
|
||||
"config/examples/release-envelope.json": "6b3f31e75a9ec0d820e304932487c8a7961488d65b98c59a45320c52b3ca7a0f",
|
||||
"config/examples/release-hooks.json": "6bb755673fee080e968395478cbf3d4697b8afc47d1f791890b528693cfcdbf2",
|
||||
"config/examples/release-projection.json": "fa898f3fdb877f1c449999a827a9afb8797e026fd574ca7375b386069cf60e3d",
|
||||
"config/examples/renewal.json": "59c27809e4f066e7b12cd0b57fecf46dedf28640be1c92c6906c1b1c19e7b25a",
|
||||
"config/examples/revoker.json": "e4eaf3255e444c50ed29e9ee78b5d3284d26d74b55e17d0060b7e285b88b5845",
|
||||
"config/examples/runtime-projection.json": "4f11d1d15d8daf26b2c6dc7e01d5626315fa7f2ca3c32923f029728a67b86534",
|
||||
"config/examples/watcher.json": "95d83d7eedc3cbaf43901e9ec313f3cf08d9f5832c02910837187a2e32b9a465",
|
||||
"config/examples/writer-server.json": "f0b5648f3f8e4e37ad934aaff8c55480a25139b731f676e49adf9ae1a0ebe0e1",
|
||||
"deploy/systemd/aios-autonomous-renewal-controller.service": "38cf3269cac8e73cc004de50e051f26ea2e1f1a1427682e129b47960210e3c31",
|
||||
"deploy/systemd/aios-governance-approval-watcher.service": "e48174384113644771eaf893c68e49432fc7b5e8ab4b4fa301d191f079b4e30f",
|
||||
"deploy/systemd/aios-governance-mirror-worker.service": "1ef26557563b93d411b7a116800ca960dcde69051fd222c64555bf274ec15ca8",
|
||||
"deploy/systemd/aios-governance-request-publisher.service": "c4a69bea0fc23849aa3307d8939e08c3925c9df445d0e43ef7fb689e285e5f89",
|
||||
"deploy/systemd/aios-governed-development-writer-revoker.service": "f9128b9a2a4f77bde98f7e8a735babc67e14186860e8cb34eb94f6126dd6f7b5",
|
||||
"deploy/systemd/aios-governed-development-writer-revoker.timer": "0bf2bc416012b5009ccb9c7b8f76d546fa6485295a926829fd3b26e0c1f071ce",
|
||||
"deploy/systemd/aios-governed-development-writer.service": "b5d052b9f3cf81bc5587dd0545ac25ae55d29e37c32c760fb9725dced351598e",
|
||||
"deploy/systemd/aios-release-object-projection.service": "91acf73dcab189aebe7900237f25a3accaa804624c1b4f52a2b477128142ed46",
|
||||
"deploy/systemd/aios-runtime-source-read-projection.service": "45cf8a5c17fcd6960970ed8f26b02a1d3fdc53874d39c14b4a1d1d89d543679e",
|
||||
"deploy/systemd/aios-v2r1c4-release-envelope.service": "b9b4ffb3a41cf90256cb970fb300a8e633e91527f19c8a8b8ee1e3cabdda4492",
|
||||
"deploy/tmpfiles.d/aios-autonomous-control-plane.conf": "d5f3aefe63515b5699fbb81421480015bfa3bc1794ec02b8214a3c21f9cea8f2",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/__init__.py": "52a7864b6a4ac94be8f67100c825813b19ccf889283bbb7dbecbd342b9ec29dc",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/cli.py": "cd42b70c35b2d810433db0df013a439dc7993c45c68ea930a9efd5b5faced84c",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/common.py": "395c669df606ed366774663d4ac229225469c77ab3ff72f6a9efcbcef8031f9e",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/credentials.py": "61322d9f494a63764f1148550853d7ca7bcd9be74451bd3c3cf3cbe6388d2136",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/gitea_api.py": "9b0a7d9db9a4497078d8f4b0bf7622b292b40282204a1e1855e11f1edc93b6de",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/grant.py": "5b1cb302a1365f4a6f03df73f8c5f8a1595304d65e9d7ac129178d51b38dde59",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/ledger.py": "a51eea543d4e44c2faab6b59c7850600908f36090f0a1ddf448c1ca7c21acdbd",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/mirror.py": "20093cf68c0b4db4940d1092b0f09d7638ed210e2739973b1a558a4016208e01",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/mirror_service.py": "a46549c4c8b29006ed2b1aec201e470e88e2f9b347ef2fa81839ac7d8093320b",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/policy.py": "5fc5b50e781cfa47e99164635155a9559d168f1f556d8ca2f1694b57b1b57195",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/projection.py": "6ee91b9b6482d4f10c98aa7e78bd23dc5f9f759c875db2e0a3207cfe27714c4c",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/projection_service.py": "2019ca8e51affcebe6c776c2c3b51a54e1d6134874e3a9b12fc774393023172a",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/publisher.py": "fd65490c1b384b2725315a066715c9822255bbef3c213ab343495c6c96a7e90f",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/release.py": "2579fc38834d77c6370dcdc4269134fe42cf85540efcebf9e4f769ab6cbde200",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/release_action.py": "26254215e032f361737584e2b8eb8367fe9f6930b946511e16f57d8e7f50cee9",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/release_deploy.py": "bfb46b6dfacd88c69cee14c9f482a99a95a7b12f1de4b8778fdcdf1b66cfdbd3",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/release_hooks.py": "10e485d39a4fcfba4323297101f4281e6a0a94a60f6ee64a8e22a5d171d79965",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/release_service.py": "8d67a4b1562879b500120f65fb8f030183b0fb70acee646656dd235f00d0b618",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/release_transport.py": "822257f2c48c1fe861d8766aad2d61302e8b4e223a15b26f3c75c993c30140e2",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/renewal.py": "2272c428999a769218142b27b0a80e938c96b7cf1e553fdfd94873123dd9ad86",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/renewal_service.py": "c7fc5e307bd6eaef35844868cecc530b463088b4f320e531800d8683f3a3733a",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/revoker.py": "6953ec6442635a495ec602fbf6b2815aee40e43ad3790bf972a887bbb2de537d",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/socket_service.py": "a42c80002e8fcea7000ee0f4fd2a437b44b18773edbdbb9dfc088d385d2782e7",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/watcher.py": "eed557d5f6acfe44881bee44dd92fea6e04ac19f33e2dea874c2a4662b04c824",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/watcher_service.py": "db502f08bafad752935180477894bb78d5b36f6261f8af13d8db2d9ecce184bb",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/writer.py": "59b8f8d5c96a455e280de5a12280aca05070f86deb443e8f6fd842d7872dc60f",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/writer_server.py": "1680ca5a053da5b9a7430a616904f474cf27833496f50ead2803227598fd901f",
|
||||
"services/aios-autonomous-control-plane/pyproject.toml": "4dae4bf9a88d033511a8de6d2871583d36cff868c3e0c6287c78d584933816df",
|
||||
"tests/autonomous_control_plane/test_deployment_and_contracts.py": "2606242519f4cdf5783dd36708357c6ec9520d66f8f8ec18ef28529e3bd240d4",
|
||||
"tests/autonomous_control_plane/test_mirror_projection.py": "5dc56cc0b5910d4b83afa4354f0375a2668ed5a328823357dc3ef2819235f26e",
|
||||
"tests/autonomous_control_plane/test_policy_and_writer.py": "723a6fafcc542de7da028d2a2f76a6eb2b7975d4e6e9d1a23c17973019f74b7c",
|
||||
"tests/autonomous_control_plane/test_renewal_release.py": "3199b9e87995d43ee97e8e62ebe85ef4f242afb16912bd1551d7570cb7d7474c",
|
||||
"tests/autonomous_control_plane/test_services_and_hooks.py": "e58bde487ed38dfbb123a37ae43733c28a519d294a7729d01823adf7d2e48dd5",
|
||||
"tests/autonomous_control_plane/test_watcher.py": "025ab4432522478ab7094751edf8da702b0e7be25d93876115cf9f416b7188b1",
|
||||
"tests/conftest.py": "32a3df0734d253b070594fce0327b929c8ee0bbb136ea6fa83a2abecb826415b"
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
# AIOS V2R1C4 Autonomy Patch v2
|
||||
|
||||
This package completes the capabilities that were missing from the first handoff.
|
||||
|
||||
## Newly delivered
|
||||
|
||||
- Idempotent, credential-isolated governance request publisher.
|
||||
- Authenticated Unix-socket development writer server with SO_PEERCRED checks.
|
||||
- Root-owned task/branch/repository/path-bound Writer Grant and idempotent quota ledger.
|
||||
- Writer credential revocation, old-token HTTP 401 proof, encrypted credential removal,
|
||||
and read-only revoked Grant evidence.
|
||||
- Persistent watcher, mirror, projection, and renewal service entrypoints.
|
||||
- Complete release hook implementation:
|
||||
- Owner exact-head and machine-status validation.
|
||||
- Approved governance Manifest verification.
|
||||
- Governed merge and tag transports.
|
||||
- Remote/Gitea tag verification.
|
||||
- Safe Artifact extraction and atomic release symlink deployment.
|
||||
- systemd health checks.
|
||||
- rollback and restore.
|
||||
- Registry/Baseline snapshots, compensation, and final consistency.
|
||||
- Unified V2R1C4 release executor with automatic failure rollback.
|
||||
- 11 installed console entrypoints and 10 systemd/timer contracts.
|
||||
|
||||
## Offline verification
|
||||
|
||||
- compileall: PASS
|
||||
- pytest: 21 passed
|
||||
- Secret scan: REVIEW
|
||||
- Path traversal archive test: PASS
|
||||
- Metadata compensation test: PASS
|
||||
- Publisher idempotency test: PASS
|
||||
- Grant replay/quota test: PASS
|
||||
- Old credential 401 test: PASS
|
||||
|
||||
## Integration rule
|
||||
|
||||
Apply this package to `patch/v2r1c4-release-completion-20260729`.
|
||||
Agent should only supply environment configuration, existing governed endpoint
|
||||
addresses, encrypted credentials, exact Git object IDs, and online acceptance.
|
||||
Do not redesign or replace the delivered control-plane logic.
|
||||
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"compatibility_adaptations": [
|
||||
{
|
||||
"binding": "resolved active Python executable; release hook rejects symlink executables",
|
||||
"new_sha256": "bd5c90b12f5f128cb3151f83e27748e8c382d1f07672dc67e3a97f5cb7ef5b03",
|
||||
"old_sha256": "e58bde487ed38dfbb123a37ae43733c28a519d294a7729d01823adf7d2e48dd5",
|
||||
"path": "tests/autonomous_control_plane/test_services_and_hooks.py",
|
||||
"product_runtime_modified": false,
|
||||
"reason": "host Python is 3.12 and package declares requires-python >=3.11; remove fixture-only /usr/bin/python3.13 binding"
|
||||
}
|
||||
],
|
||||
"compiled_cache_files_integrated": false,
|
||||
"delta_added_count": 32,
|
||||
"delta_deleted_cache_count": 21,
|
||||
"delta_modified_count": 18,
|
||||
"direct_main_push": false,
|
||||
"force_push_used": false,
|
||||
"gitea_api_bootstrap_token_status": 401,
|
||||
"gitea_api_preapply_oid": "e5dc9185dd153efdfb007f9b674197f613c10796",
|
||||
"gitea_api_reader_token_status": 401,
|
||||
"global_gitops_writes_enabled": false,
|
||||
"handoff_metadata_location": "evidence/releases/V2R1C4-code-handoff-v2",
|
||||
"integrated_runtime_and_test_paths": [
|
||||
"config/examples/grant.json",
|
||||
"config/examples/mirror.json",
|
||||
"config/examples/publisher.json",
|
||||
"config/examples/release-actions.json",
|
||||
"config/examples/release-envelope.json",
|
||||
"config/examples/release-hooks.json",
|
||||
"config/examples/release-projection.json",
|
||||
"config/examples/renewal.json",
|
||||
"config/examples/revoker.json",
|
||||
"config/examples/runtime-projection.json",
|
||||
"config/examples/watcher.json",
|
||||
"config/examples/writer-server.json",
|
||||
"deploy/systemd/aios-v2r1c4-release-envelope.service",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/credentials.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/gitea_api.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/grant.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/mirror_service.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/projection_service.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/publisher.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/release_action.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/release_deploy.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/release_hooks.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/release_service.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/release_transport.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/renewal_service.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/revoker.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/socket_service.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/watcher_service.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/writer_server.py",
|
||||
"tests/autonomous_control_plane/test_deployment_and_contracts.py",
|
||||
"tests/autonomous_control_plane/test_services_and_hooks.py",
|
||||
"deploy/systemd/aios-autonomous-renewal-controller.service",
|
||||
"deploy/systemd/aios-governance-approval-watcher.service",
|
||||
"deploy/systemd/aios-governance-mirror-worker.service",
|
||||
"deploy/systemd/aios-governance-request-publisher.service",
|
||||
"deploy/systemd/aios-governed-development-writer-revoker.service",
|
||||
"deploy/systemd/aios-governed-development-writer.service",
|
||||
"deploy/systemd/aios-release-object-projection.service",
|
||||
"deploy/systemd/aios-runtime-source-read-projection.service",
|
||||
"deploy/tmpfiles.d/aios-autonomous-control-plane.conf",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/projection.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/release.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/writer.py",
|
||||
"services/aios-autonomous-control-plane/pyproject.toml",
|
||||
"tests/autonomous_control_plane/test_mirror_projection.py",
|
||||
"tests/autonomous_control_plane/test_renewal_release.py"
|
||||
],
|
||||
"schema": "aios.code-handoff-integration/v2",
|
||||
"source_file_hash_entries": 64,
|
||||
"source_file_hashes_verified": true,
|
||||
"source_zip_sha256": "7afbcfae0ff692d2b859cd1e7e96536b8e80188667554803da438dcddc292d69",
|
||||
"tag_rewritten": false,
|
||||
"v1_modified_base_files_verified": 15
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"collection_errors": 0,
|
||||
"compileall_returncode": 0,
|
||||
"delta_added_files": 32,
|
||||
"delta_deleted_files": 21,
|
||||
"delta_modified_files": 18,
|
||||
"entrypoint_count": 11,
|
||||
"generated_at": "2026-07-29T08:11:08.888275Z",
|
||||
"pytest_failed": 0,
|
||||
"pytest_passed": 21,
|
||||
"pytest_returncode": 0,
|
||||
"secret_findings": [
|
||||
{
|
||||
"path": "tests/autonomous_control_plane/test_policy_and_writer.py",
|
||||
"rule": "literal-credential-assignment"
|
||||
}
|
||||
],
|
||||
"secret_scan": "PASS_TEST_FIXTURE_ONLY",
|
||||
"systemd_contract_count": 10
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"actions": {
|
||||
"artifact_built": false,
|
||||
"baseline_modified": false,
|
||||
"control_plane_deployed": false,
|
||||
"core_redesigned_by_agent": false,
|
||||
"main_modified": false,
|
||||
"online_write_acceptance_executed": false,
|
||||
"owner_approval_pr_created": false,
|
||||
"production_modified": false,
|
||||
"registry_modified": false,
|
||||
"tag_created": false
|
||||
},
|
||||
"blocking_layer": "CODE_HANDOFF_SECURITY_INVARIANTS",
|
||||
"candidate_base_oid": "e5dc9185dd153efdfb007f9b674197f613c10796",
|
||||
"confirmed_security_findings": [
|
||||
{
|
||||
"evidence": "release_action.py:53-98",
|
||||
"id": "P0-OWNER-ENVELOPE-BINDING",
|
||||
"summary": "approved manifest digest is checked but manifest fields are not parsed and made equal to the release envelope"
|
||||
},
|
||||
{
|
||||
"evidence": "release.py:20-30,52-59",
|
||||
"id": "P0-UNCONSUMED-RELEASE-DIGESTS",
|
||||
"summary": "rollback, SBOM, test, evidence and tag/deployment/rollback plan digests are format checked but not verified against objects before side effects"
|
||||
},
|
||||
{
|
||||
"evidence": "release_deploy.py:173-188; release.py:168-170,210-214",
|
||||
"id": "P0-PARTIAL-DEPLOY-NO-COMPENSATION",
|
||||
"summary": "deployment side effects occur while phase is TAG_VERIFIED, which is excluded from automatic rollback"
|
||||
},
|
||||
{
|
||||
"evidence": "release_deploy.py:191-212",
|
||||
"id": "P0-ROLLBACK-TARGET-UNBOUND",
|
||||
"summary": "previous and restored directories are not constrained to release_root and are not bound to the approved rollback digest"
|
||||
},
|
||||
{
|
||||
"evidence": "policy.py:72-76; grant.py:10-25,62-71; writer.py:94-107; writer_server.py:28-38,64-68",
|
||||
"id": "P0-WRITER-GRANT-BOUNDARY",
|
||||
"summary": "expected base ancestry, exact task worktree/common-dir/remote and task credential identity are not closed"
|
||||
},
|
||||
{
|
||||
"evidence": "revoker.py:36-64; aios-governed-development-writer-revoker.timer:4-8",
|
||||
"id": "P0-REVOKER-EXPIRY-RECOVERY",
|
||||
"summary": "fixed five-minute timer ignores grant expiry and credential deletion precedes recoverable grant and receipt closure"
|
||||
},
|
||||
{
|
||||
"evidence": "deploy/tmpfiles.d/aios-autonomous-control-plane.conf:1-19",
|
||||
"id": "P1-SERVICE-DIRECTORY-PERMISSIONS",
|
||||
"summary": "0750 root:gitops directories are not writable by non-root writer and publisher users"
|
||||
},
|
||||
{
|
||||
"evidence": "release_transport.py:65-76; release_deploy.py:221-228",
|
||||
"id": "P1-LOOPBACK-URL-PREFIX",
|
||||
"summary": "string prefix checks accept non-loopback hosts and can forward credentials"
|
||||
}
|
||||
],
|
||||
"deployment_decision": "BLOCKED_PRODUCTION_DEPLOYMENT",
|
||||
"execution_status": "PARTIALLY_COMPLETED",
|
||||
"governance": {
|
||||
"AIOS_GITOPS_WRITES_ENABLED": false,
|
||||
"direct_main_push": false,
|
||||
"force_push_used": false,
|
||||
"owner_approval_respected": true,
|
||||
"secrets_committed": false,
|
||||
"tag_rewritten": false
|
||||
},
|
||||
"integrity": {
|
||||
"file_sha256": "64/64 PASS",
|
||||
"hardcoded_credentials": 0,
|
||||
"path_traversal_entries": 0,
|
||||
"secret_scan": "PASS",
|
||||
"v1_modified_base_files": "15/15 PASS",
|
||||
"zip_sha256": "7afbcfae0ff692d2b859cd1e7e96536b8e80188667554803da438dcddc292d69"
|
||||
},
|
||||
"live_binding_inventory": {
|
||||
"authoritative_bare": "/data/aios-gitops/repo.git",
|
||||
"candidate_health_endpoint_127_0_0_1_18890": "PRESENT_HTTP_200_NOT_APPROVED_AS_RELEASE_BINDING",
|
||||
"configured_health_endpoint_127_0_0_1_8080": "ABSENT",
|
||||
"configured_tag_endpoint_127_0_0_1_8080": "ABSENT",
|
||||
"gitea_bare": "/data/aios-gitops/runtime/gitea-bin/data/repositories/aios/aios.git",
|
||||
"gitea_db": "/data/aios-gitops/runtime/gitea-bin/data/gitea.db",
|
||||
"gitea_repo_id": 1,
|
||||
"integration_merge_writer_socket": "ABSENT",
|
||||
"owner_user_id": 2,
|
||||
"production_baseline_path": "ABSENT",
|
||||
"release_root_current_previous": "ABSENT",
|
||||
"v2r1c4_artifact": "ABSENT",
|
||||
"v2r1c4_registry": "ABSENT"
|
||||
},
|
||||
"observed_at": "2026-07-29T09:11:19.725943+00:00",
|
||||
"schema": "aios.code-handoff-validation/v2",
|
||||
"verification": {
|
||||
"collection_errors": 0,
|
||||
"compileall": "PASS",
|
||||
"console_entrypoints": "11/11 load PASS",
|
||||
"package_tests_after_fixture_adapter": "21 passed",
|
||||
"package_tests_original_host": "20 passed, 1 failed due fixture-only /usr/bin/python3.13 binding",
|
||||
"package_version": "0.2.0",
|
||||
"repository_tests": "660 passed, 1 skipped, 180 subtests passed",
|
||||
"runtime_dependencies": "0",
|
||||
"systemd_units": "10/10 staged-root syntax PASS"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"raw_evidence": "wheel-repro.json",
|
||||
"sha256": "a0415df8e4d131a32a3ce39a6e997b0304657224642bf47d73adddb684a7d4ca",
|
||||
"status": "PASS"
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"files": [
|
||||
"config/examples/candidate-fixture.json",
|
||||
"config/examples/grant.json",
|
||||
"config/examples/publisher.json",
|
||||
"config/examples/release-actions.json",
|
||||
"config/examples/release-envelope.json",
|
||||
"config/examples/revoker.json",
|
||||
"config/examples/writer-server.json",
|
||||
"deploy/systemd/aios-governance-request-publisher.service",
|
||||
"deploy/systemd/aios-governed-development-writer-revoker.service",
|
||||
"deploy/systemd/aios-governed-development-writer.service",
|
||||
"deploy/systemd/aios-v2r1c4-candidate-api.service",
|
||||
"deploy/systemd/aios-v2r1c4-candidate-merge.socket",
|
||||
"deploy/systemd/aios-v2r1c4-candidate-merge@.service",
|
||||
"deploy/tmpfiles.d/aios-autonomous-control-plane.conf",
|
||||
"scripts/generate_v2r1c4_v3_evidence.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/approval_binding.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/candidate_fixture.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/gitea_api.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/grant.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/release.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/release_action.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/release_deploy.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/release_hooks.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/release_inputs.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/release_transport.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/revoker.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/writer.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/writer_server.py",
|
||||
"services/aios-autonomous-control-plane/pyproject.toml",
|
||||
"tests/autonomous_control_plane/test_candidate_integration_v3.py",
|
||||
"tests/autonomous_control_plane/test_evidence_generator_v3.py",
|
||||
"tests/autonomous_control_plane/test_non_root_execution_v3.py",
|
||||
"tests/autonomous_control_plane/test_policy_and_writer.py",
|
||||
"tests/autonomous_control_plane/test_release_security_v3.py",
|
||||
"tests/autonomous_control_plane/test_renewal_release.py",
|
||||
"tests/autonomous_control_plane/test_services_and_hooks.py",
|
||||
"tests/autonomous_control_plane/test_systemd_security_v3.py",
|
||||
"tests/autonomous_control_plane/test_writer_revoker_security_v3.py"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"hardcoded_credentials": 0,
|
||||
"raw_evidence": "scans.json",
|
||||
"status": "PASS"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"declared_runtime_dependencies": 0,
|
||||
"raw_evidence": "scans.json",
|
||||
"status": "PASS"
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"changed_file_count": 38,
|
||||
"numstat": [
|
||||
"13\t0\tconfig/examples/candidate-fixture.json",
|
||||
"19\t15\tconfig/examples/grant.json",
|
||||
"8\t13\tconfig/examples/publisher.json",
|
||||
"15\t0\tconfig/examples/release-actions.json",
|
||||
"9\t1\tconfig/examples/release-envelope.json",
|
||||
"5\t4\tconfig/examples/revoker.json",
|
||||
"8\t14\tconfig/examples/writer-server.json",
|
||||
"4\t1\tdeploy/systemd/aios-governance-request-publisher.service",
|
||||
"1\t1\tdeploy/systemd/aios-governed-development-writer-revoker.service",
|
||||
"5\t1\tdeploy/systemd/aios-governed-development-writer.service",
|
||||
"29\t0\tdeploy/systemd/aios-v2r1c4-candidate-api.service",
|
||||
"12\t0\tdeploy/systemd/aios-v2r1c4-candidate-merge.socket",
|
||||
"28\t0\tdeploy/systemd/aios-v2r1c4-candidate-merge@.service",
|
||||
"0\t3\tdeploy/tmpfiles.d/aios-autonomous-control-plane.conf",
|
||||
"356\t0\tscripts/generate_v2r1c4_v3_evidence.py",
|
||||
"83\t0\tservices/aios-autonomous-control-plane/aios_autonomous_control_plane/approval_binding.py",
|
||||
"253\t0\tservices/aios-autonomous-control-plane/aios_autonomous_control_plane/candidate_fixture.py",
|
||||
"29\t3\tservices/aios-autonomous-control-plane/aios_autonomous_control_plane/gitea_api.py",
|
||||
"45\t2\tservices/aios-autonomous-control-plane/aios_autonomous_control_plane/grant.py",
|
||||
"162\t69\tservices/aios-autonomous-control-plane/aios_autonomous_control_plane/release.py",
|
||||
"59\t13\tservices/aios-autonomous-control-plane/aios_autonomous_control_plane/release_action.py",
|
||||
"211\t28\tservices/aios-autonomous-control-plane/aios_autonomous_control_plane/release_deploy.py",
|
||||
"29\t24\tservices/aios-autonomous-control-plane/aios_autonomous_control_plane/release_hooks.py",
|
||||
"95\t0\tservices/aios-autonomous-control-plane/aios_autonomous_control_plane/release_inputs.py",
|
||||
"50\t5\tservices/aios-autonomous-control-plane/aios_autonomous_control_plane/release_transport.py",
|
||||
"229\t38\tservices/aios-autonomous-control-plane/aios_autonomous_control_plane/revoker.py",
|
||||
"90\t18\tservices/aios-autonomous-control-plane/aios_autonomous_control_plane/writer.py",
|
||||
"13\t8\tservices/aios-autonomous-control-plane/aios_autonomous_control_plane/writer_server.py",
|
||||
"2\t1\tservices/aios-autonomous-control-plane/pyproject.toml",
|
||||
"161\t0\ttests/autonomous_control_plane/test_candidate_integration_v3.py",
|
||||
"205\t0\ttests/autonomous_control_plane/test_evidence_generator_v3.py",
|
||||
"9\t0\ttests/autonomous_control_plane/test_non_root_execution_v3.py",
|
||||
"140\t5\ttests/autonomous_control_plane/test_policy_and_writer.py",
|
||||
"384\t0\ttests/autonomous_control_plane/test_release_security_v3.py",
|
||||
"8\t0\ttests/autonomous_control_plane/test_renewal_release.py",
|
||||
"8\t0\ttests/autonomous_control_plane/test_services_and_hooks.py",
|
||||
"82\t0\ttests/autonomous_control_plane/test_systemd_security_v3.py",
|
||||
"155\t0\ttests/autonomous_control_plane/test_writer_revoker_security_v3.py"
|
||||
],
|
||||
"range": "473b86a2f1f5a5fe3b8ad42a6da26c29c78c4170..56bb00cd134f4d545935416256bdb018d5c8d58b"
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"executed_at": "2026-07-29T12:31:16Z",
|
||||
"files": {
|
||||
"ARTIFACT_REPRODUCIBILITY.json": "4ee6f688a0e5b43c3de0b757d768f99ea7603b186c0605e263ca7bf598432d24",
|
||||
"CHANGED_FILES.json": "8344ba734fdb2663e3386d40ab2c6f7093843feb72a2db35695361ee8597f343",
|
||||
"CREDENTIAL_LEAK_SCAN.json": "b300a5dac4ad08444f979cbc7e6e48fd10dd51f774fe31b6d1bcf1c35e30a2fc",
|
||||
"DEPENDENCY_SCAN.json": "32d7568a291719983158ade57116bfeaa92a34977d64d6a2261b1305d7b0b55d",
|
||||
"DIFF_SUMMARY.json": "568b44f156494f9c9dd92bc449f312538056d9b54d226e213358b5bb8991e358",
|
||||
"FAILURE_INJECTION_RESULTS.json": "98ff60bdf055be342865ff101f9b6655bd39b93f0ef10af984dce3b16e76682e",
|
||||
"FILE_SHA256.json": "26c1b6829312864ad799d7db184e9344bb951d07ea5d2f28e70f80b0ad44ff60",
|
||||
"NON_ROOT_PERMISSION_RESULTS.json": "922c629666f9e965b7570ab84b430403597473cc6df01414c1bf1c1541347575",
|
||||
"PATH_TRAVERSAL_SCAN.json": "b0d4f4754de0acff8bd0251a3a55cc89fa0ea24953aaa9c9b0fd23dbc30f99a8",
|
||||
"RELEASE_IDENTIFIERS.json": "48bef9575e55429d39ac6540a8b68b7678d481fbdced187f8d83095f88c6d10f",
|
||||
"SECRET_SCAN.json": "cb569592255594eafc4c97adcfd181d37cc47240a5ad9f03433d091c13d0c305",
|
||||
"SECURITY_INVARIANT_MATRIX.json": "97909294c3925af144222bac9ddb861e7f751672d43811a1e61969c10bb9ea25",
|
||||
"SOURCE_MANIFEST.json": "fb0ed7cfb94f08aa6f6c601e680b4e11c0304b7d9b2ece10230b869391695977",
|
||||
"SYSTEMD_VALIDATION.json": "a9831840ee384886ce8d5141db991487308a44d8ca2b39cd2c4e98c8df7f548c",
|
||||
"TEST_COMMANDS.json": "b62f32ad541c1b6277b3085dc6731a6aa9e5085eeb3624962f9bd43fbc6bc1d1",
|
||||
"TEST_RESULTS.json": "ca4fff6020d1a67e044b2afd47256964753ffac8df801f3ac0cd9f8f88c35ecf",
|
||||
"THREAT_MODEL.md": "cbfec5ee4b781ad5214a852b74dd9ab977c9e7c4332f01cc76d01cfdb0b65d95",
|
||||
"UNRESOLVED_ISSUES.json": "572e1bbe502a080d86f416d60b9eb204470806b3f2867095dbf0b2f04b20a37f",
|
||||
"VALIDATOR_REPORT.json": "d49cc1e6b8a64eec02df1149e674a9a0a39be72c0c9e67503d3bd153be4faab1",
|
||||
"candidate-live.json": "883b22ded27242311a30c3c24885910cfbdbd78df523b0be3aa849e9a5d1325e",
|
||||
"compileall.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"gitleaks.json": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570",
|
||||
"nonroot-focused.xml": "c1df7f399b07c3e58bed510b66f0e2eb16884e9bc0e67397f8f8567e0ceb2218",
|
||||
"pytest-legacy.xml": "200bbd033d29dc5037eeccda2fd00fcee55ff8f55619d04fe931dc72e52fdc35",
|
||||
"pytest-main.xml": "bb0b715c612fdf6c02480f6f74a0a4aa9a9b2d2acd0b6328798b4423b793fa78",
|
||||
"scans.json": "8ac15b04eb58e59d91653eb484b025e28ac771254e91052cb7a8ba826a2ecb0f",
|
||||
"systemd-verify.log": "5a6c2f127505660e2fd40b4d0cf79752ab3871a9e6df0ce1ff59f5d801256da0",
|
||||
"validator.json": "4caa6f58e27afad059bb3ca8ff0a6c6ac8e9549d9b61f82a644bc2fa7f6eaaba",
|
||||
"wheel-repro.json": "b235ad7318886f26752021444933d5acc71a9f1ab5f05a79a9e79d509244ef63"
|
||||
},
|
||||
"generator": "scripts/generate_v2r1c4_v3_evidence.py",
|
||||
"generator_schema": "aios.v2r1c4-security-evidence/v3",
|
||||
"git_version": "git version 2.43.0",
|
||||
"platform": "Linux-6.8.0-124-generic-x86_64-with-glibc2.39",
|
||||
"python_executable": "/usr/bin/python3.12",
|
||||
"python_version": "3.12.3",
|
||||
"source_commit_oid": "56bb00cd134f4d545935416256bdb018d5c8d58b",
|
||||
"source_tree_oid": "20adf8d65858131ce29bc979b8d1c7c0444d52d3",
|
||||
"status": "CANDIDATE_READY_FOR_INDEPENDENT_VALIDATION"
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"failed": 0,
|
||||
"passed": 6,
|
||||
"raw_evidence": [
|
||||
"pytest-main.xml"
|
||||
],
|
||||
"scenarios": [
|
||||
"deploy_mid_restart_resume",
|
||||
"deploy_compensation",
|
||||
"rollback_binding_mismatch",
|
||||
"revoker_receipt_checkpoint_crash",
|
||||
"loopback_parser_bypass",
|
||||
"writer_binding_mismatch"
|
||||
],
|
||||
"status": "PASS"
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"files": {
|
||||
"ARTIFACT_REPRODUCIBILITY.json": "4ee6f688a0e5b43c3de0b757d768f99ea7603b186c0605e263ca7bf598432d24",
|
||||
"CHANGED_FILES.json": "8344ba734fdb2663e3386d40ab2c6f7093843feb72a2db35695361ee8597f343",
|
||||
"CREDENTIAL_LEAK_SCAN.json": "b300a5dac4ad08444f979cbc7e6e48fd10dd51f774fe31b6d1bcf1c35e30a2fc",
|
||||
"DEPENDENCY_SCAN.json": "32d7568a291719983158ade57116bfeaa92a34977d64d6a2261b1305d7b0b55d",
|
||||
"DIFF_SUMMARY.json": "568b44f156494f9c9dd92bc449f312538056d9b54d226e213358b5bb8991e358",
|
||||
"FAILURE_INJECTION_RESULTS.json": "98ff60bdf055be342865ff101f9b6655bd39b93f0ef10af984dce3b16e76682e",
|
||||
"NON_ROOT_PERMISSION_RESULTS.json": "922c629666f9e965b7570ab84b430403597473cc6df01414c1bf1c1541347575",
|
||||
"PATH_TRAVERSAL_SCAN.json": "b0d4f4754de0acff8bd0251a3a55cc89fa0ea24953aaa9c9b0fd23dbc30f99a8",
|
||||
"RELEASE_IDENTIFIERS.json": "48bef9575e55429d39ac6540a8b68b7678d481fbdced187f8d83095f88c6d10f",
|
||||
"SECRET_SCAN.json": "cb569592255594eafc4c97adcfd181d37cc47240a5ad9f03433d091c13d0c305",
|
||||
"SECURITY_INVARIANT_MATRIX.json": "97909294c3925af144222bac9ddb861e7f751672d43811a1e61969c10bb9ea25",
|
||||
"SOURCE_MANIFEST.json": "fb0ed7cfb94f08aa6f6c601e680b4e11c0304b7d9b2ece10230b869391695977",
|
||||
"SYSTEMD_VALIDATION.json": "a9831840ee384886ce8d5141db991487308a44d8ca2b39cd2c4e98c8df7f548c",
|
||||
"TEST_COMMANDS.json": "b62f32ad541c1b6277b3085dc6731a6aa9e5085eeb3624962f9bd43fbc6bc1d1",
|
||||
"TEST_RESULTS.json": "ca4fff6020d1a67e044b2afd47256964753ffac8df801f3ac0cd9f8f88c35ecf",
|
||||
"THREAT_MODEL.md": "cbfec5ee4b781ad5214a852b74dd9ab977c9e7c4332f01cc76d01cfdb0b65d95",
|
||||
"UNRESOLVED_ISSUES.json": "572e1bbe502a080d86f416d60b9eb204470806b3f2867095dbf0b2f04b20a37f",
|
||||
"VALIDATOR_REPORT.json": "d49cc1e6b8a64eec02df1149e674a9a0a39be72c0c9e67503d3bd153be4faab1",
|
||||
"candidate-live.json": "883b22ded27242311a30c3c24885910cfbdbd78df523b0be3aa849e9a5d1325e",
|
||||
"compileall.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"gitleaks.json": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570",
|
||||
"nonroot-focused.xml": "c1df7f399b07c3e58bed510b66f0e2eb16884e9bc0e67397f8f8567e0ceb2218",
|
||||
"pytest-legacy.xml": "200bbd033d29dc5037eeccda2fd00fcee55ff8f55619d04fe931dc72e52fdc35",
|
||||
"pytest-main.xml": "bb0b715c612fdf6c02480f6f74a0a4aa9a9b2d2acd0b6328798b4423b793fa78",
|
||||
"scans.json": "8ac15b04eb58e59d91653eb484b025e28ac771254e91052cb7a8ba826a2ecb0f",
|
||||
"systemd-verify.log": "5a6c2f127505660e2fd40b4d0cf79752ab3871a9e6df0ce1ff59f5d801256da0",
|
||||
"validator.json": "4caa6f58e27afad059bb3ca8ff0a6c6ac8e9549d9b61f82a644bc2fa7f6eaaba",
|
||||
"wheel-repro.json": "b235ad7318886f26752021444933d5acc71a9f1ab5f05a79a9e79d509244ef63"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"raw_evidence": "nonroot-focused.xml",
|
||||
"status": "PASS",
|
||||
"tests_passed": 20,
|
||||
"uid": 1000
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entries": 0,
|
||||
"raw_evidence": "scans.json",
|
||||
"status": "PASS",
|
||||
"symlinks": 0
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"artifact_sha256": "NOT_BUILT",
|
||||
"expected_base_oid": "473b86a2f1f5a5fe3b8ad42a6da26c29c78c4170",
|
||||
"production_baseline": "V2R1C3_UNCHANGED",
|
||||
"registry_version": "V2R1C3_UNCHANGED",
|
||||
"release_commit_oid": "56bb00cd134f4d545935416256bdb018d5c8d58b",
|
||||
"release_tag": "NOT_CREATED",
|
||||
"release_tree_oid": "20adf8d65858131ce29bc979b8d1c7c0444d52d3",
|
||||
"rollback_sha256": "NOT_BUILT"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"leaks": 0,
|
||||
"raw_evidence": "gitleaks.json",
|
||||
"status": "PASS"
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
{
|
||||
"fixed": 9,
|
||||
"invariants": [
|
||||
{
|
||||
"id": 1,
|
||||
"locations": [
|
||||
"approval_binding.py",
|
||||
"release.py",
|
||||
"release_action.py"
|
||||
],
|
||||
"name": "owner_manifest_release_envelope_full_binding",
|
||||
"raw_evidence": [
|
||||
"pytest-main.xml"
|
||||
],
|
||||
"status": "PASS"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"locations": [
|
||||
"release_inputs.py",
|
||||
"release_action.py"
|
||||
],
|
||||
"name": "all_digests_recomputed_before_side_effects",
|
||||
"raw_evidence": [
|
||||
"pytest-main.xml"
|
||||
],
|
||||
"status": "PASS"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"locations": [
|
||||
"release.py",
|
||||
"release_deploy.py"
|
||||
],
|
||||
"name": "transactional_deploy_compensation_and_resume",
|
||||
"raw_evidence": [
|
||||
"pytest-main.xml"
|
||||
],
|
||||
"status": "PASS"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"locations": [
|
||||
"release_deploy.py"
|
||||
],
|
||||
"name": "rollback_restore_approved_release_root_binding",
|
||||
"raw_evidence": [
|
||||
"pytest-main.xml"
|
||||
],
|
||||
"status": "PASS"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"locations": [
|
||||
"grant.py",
|
||||
"writer.py",
|
||||
"writer_server.py"
|
||||
],
|
||||
"name": "writer_repo_base_worktree_remote_credential_closure",
|
||||
"raw_evidence": [
|
||||
"nonroot-focused.xml",
|
||||
"pytest-main.xml"
|
||||
],
|
||||
"status": "PASS"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"locations": [
|
||||
"revoker.py"
|
||||
],
|
||||
"name": "expiry_aware_crash_recoverable_revoker",
|
||||
"raw_evidence": [
|
||||
"nonroot-focused.xml",
|
||||
"pytest-main.xml"
|
||||
],
|
||||
"status": "PASS"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"locations": [
|
||||
"deploy/systemd",
|
||||
"deploy/tmpfiles.d"
|
||||
],
|
||||
"name": "non_root_writer_publisher_directory_ownership",
|
||||
"raw_evidence": [
|
||||
"nonroot-focused.xml",
|
||||
"pytest-main.xml",
|
||||
"systemd-verify.log"
|
||||
],
|
||||
"status": "PASS"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"locations": [
|
||||
"release_transport.py",
|
||||
"gitea_api.py"
|
||||
],
|
||||
"name": "structured_loopback_and_redirect_denial",
|
||||
"raw_evidence": [
|
||||
"pytest-main.xml"
|
||||
],
|
||||
"status": "PASS"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"locations": [
|
||||
"candidate_fixture.py",
|
||||
"candidate systemd units"
|
||||
],
|
||||
"name": "candidate_only_live_integration_fixtures",
|
||||
"raw_evidence": [
|
||||
"candidate-live.json",
|
||||
"nonroot-focused.xml",
|
||||
"pytest-main.xml"
|
||||
],
|
||||
"status": "PASS"
|
||||
}
|
||||
],
|
||||
"total": 9
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"authority_main_oid": "875d41c3101093149414292067fd700dc214d4d3",
|
||||
"executed_at": "2026-07-29T12:31:16Z",
|
||||
"expected_base_oid": "473b86a2f1f5a5fe3b8ad42a6da26c29c78c4170",
|
||||
"generator": "scripts/generate_v2r1c4_v3_evidence.py",
|
||||
"generator_schema": "aios.v2r1c4-security-evidence/v3",
|
||||
"git_version": "git version 2.43.0",
|
||||
"platform": "Linux-6.8.0-124-generic-x86_64-with-glibc2.39",
|
||||
"python_executable": "/usr/bin/python3.12",
|
||||
"python_version": "3.12.3",
|
||||
"release_branch": "patch/v2r1c4-security-invariants-v3-20260729",
|
||||
"source_commit_oid": "56bb00cd134f4d545935416256bdb018d5c8d58b",
|
||||
"source_tree_oid": "20adf8d65858131ce29bc979b8d1c7c0444d52d3"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"raw_evidence": "systemd-verify.log",
|
||||
"status": "PASS",
|
||||
"units_verified": 6,
|
||||
"warnings": [
|
||||
"Accepting user/group name 'aios-governed-development-writer', which does not match strict user/group name rules.",
|
||||
"Accepting user/group name 'aios-governance-request-publisher', which does not match strict user/group name rules."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"commands": [
|
||||
{
|
||||
"command": "python3 -m compileall -q active-runtime services scripts tests",
|
||||
"id": "compileall",
|
||||
"raw_evidence": [
|
||||
"compileall.log"
|
||||
],
|
||||
"returncode": 0
|
||||
},
|
||||
{
|
||||
"command": "python3 -m pytest -q tests --ignore=tests/autonomous-release --junitxml=pytest-main.xml",
|
||||
"id": "pytest_main",
|
||||
"raw_evidence": [
|
||||
"pytest-main.xml"
|
||||
],
|
||||
"returncode": 0
|
||||
},
|
||||
{
|
||||
"command": "python3 -m pytest -q tests/autonomous-release --junitxml=pytest-legacy.xml",
|
||||
"id": "pytest_legacy",
|
||||
"raw_evidence": [
|
||||
"pytest-legacy.xml"
|
||||
],
|
||||
"returncode": 0
|
||||
},
|
||||
{
|
||||
"command": "python3 -m pytest -q test_non_root_execution_v3.py test_writer_revoker_security_v3.py test_systemd_security_v3.py test_candidate_integration_v3.py --junitxml=nonroot-focused.xml",
|
||||
"id": "non_root_focused",
|
||||
"raw_evidence": [
|
||||
"nonroot-focused.xml"
|
||||
],
|
||||
"returncode": 0
|
||||
},
|
||||
{
|
||||
"command": "systemd-analyze --root=STAGED_ROOT verify SIX_CANDIDATE_UNITS",
|
||||
"id": "systemd_verify",
|
||||
"raw_evidence": [
|
||||
"systemd-verify.log"
|
||||
],
|
||||
"returncode": 0
|
||||
},
|
||||
{
|
||||
"command": "gitleaks detect --redact --log-opts=EXPECTED_BASE..SOURCE_COMMIT --report-format=json",
|
||||
"id": "secret_scan",
|
||||
"raw_evidence": [
|
||||
"gitleaks.json"
|
||||
],
|
||||
"returncode": 0
|
||||
},
|
||||
{
|
||||
"command": "scan tracked paths and changed source for traversal symlinks credentials and dependencies",
|
||||
"id": "structural_scans",
|
||||
"raw_evidence": [
|
||||
"scans.json"
|
||||
],
|
||||
"returncode": 0
|
||||
},
|
||||
{
|
||||
"command": "SOURCE_DATE_EPOCH=SOURCE_COMMIT python3 -m build --wheel --no-isolation twice",
|
||||
"id": "artifact_reproducibility",
|
||||
"raw_evidence": [
|
||||
"wheel-repro.json"
|
||||
],
|
||||
"returncode": 0
|
||||
},
|
||||
{
|
||||
"command": "run UID1000 candidate health tag-plan and systemd-socket-activate merge probes",
|
||||
"id": "candidate_live",
|
||||
"raw_evidence": [
|
||||
"candidate-live.json"
|
||||
],
|
||||
"returncode": 0
|
||||
},
|
||||
{
|
||||
"command": "independent read-only validator audit SOURCE_COMMIT",
|
||||
"id": "independent_validator",
|
||||
"raw_evidence": [
|
||||
"validator.json"
|
||||
],
|
||||
"returncode": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"collection_errors": 0,
|
||||
"junit_tests": 899,
|
||||
"tests_failed": 0,
|
||||
"tests_passed": 898,
|
||||
"tests_skipped": 1
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# V2R1C4 v3 threat model
|
||||
|
||||
Assets: Owner approval provenance, release objects, rollback root, Writer credentials, candidate-only state.
|
||||
|
||||
Trust boundaries: Gitea/Git authority, non-root Writer and Publisher, local loopback HTTP, Unix socket activation, systemd state directories.
|
||||
|
||||
Closed threats: manifest substitution, digest-only declaration, partial activation, rollback target substitution, worktree/remote/credential confusion, early or partial revocation, root-owned directory denial, loopback parser bypass/redirect leak, and candidate state escaping into production.
|
||||
|
||||
Residual boundary: the real live-tag test remains intentionally disabled because this task forbids formal tag creation.
|
||||
@@ -0,0 +1,12 @@
|
||||
[
|
||||
{
|
||||
"description": "Formal live tag validation is disabled because this task forbids tag creation.",
|
||||
"id": "formal-live-tag",
|
||||
"status": "ACCEPTED_BOUNDARY"
|
||||
},
|
||||
{
|
||||
"description": "Raw local validation logs are digest-bound but not externally signed.",
|
||||
"id": "unsigned-local-raw-evidence",
|
||||
"status": "ACCEPTED_BOUNDARY"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"blocking_layer": "NONE",
|
||||
"product_invariants_passed": 9,
|
||||
"raw_evidence": "validator.json",
|
||||
"source_commit_oid": "56bb00cd134f4d545935416256bdb018d5c8d58b",
|
||||
"status": "PASS"
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"candidate_only": true,
|
||||
"health": "PASS",
|
||||
"merge_socket": "PASS",
|
||||
"process_uid": 1000,
|
||||
"production_modified": false,
|
||||
"source_commit_oid": "56bb00cd134f4d545935416256bdb018d5c8d58b",
|
||||
"status": "PASS",
|
||||
"tag_execute": false
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><testsuites name="pytest tests"><testsuite name="pytest" errors="0" failures="0" skipped="0" tests="20" time="0.798" timestamp="2026-07-29T20:28:16.382718+08:00" hostname="cn-network-mkt"><properties><property name="process_uid" value="1000" /></properties><testcase classname="tests.autonomous_control_plane.test_non_root_execution_v3" name="test_non_root_execution_identity" time="0.004" /><testcase classname="tests.autonomous_control_plane.test_writer_revoker_security_v3" name="test_revoker_does_not_revoke_before_expiry" time="0.007" /><testcase classname="tests.autonomous_control_plane.test_writer_revoker_security_v3" name="test_revoker_revokes_at_clock_boundary_and_is_idempotent" time="0.021" /><testcase classname="tests.autonomous_control_plane.test_writer_revoker_security_v3" name="test_revoker_resumes_after_partial_api_failure" time="0.021" /><testcase classname="tests.autonomous_control_plane.test_writer_revoker_security_v3" name="test_revoker_preserves_token_revoked_checkpoint_on_401_failure" time="0.019" /><testcase classname="tests.autonomous_control_plane.test_writer_revoker_security_v3" name="test_revoker_recovers_across_receipt_checkpoint_crash" time="0.023" /><testcase classname="tests.autonomous_control_plane.test_writer_revoker_security_v3" name="test_terminal_trigger_allows_early_idempotent_revocation" time="0.022" /><testcase classname="tests.autonomous_control_plane.test_systemd_security_v3" name="test_non_root_writer_has_service_owned_private_directories" time="0.000" /><testcase classname="tests.autonomous_control_plane.test_systemd_security_v3" name="test_non_root_publisher_has_no_shared_root_owned_write_directory" time="0.000" /><testcase classname="tests.autonomous_control_plane.test_systemd_security_v3" name="test_revoker_recovery_entrypoint_keeps_revoked_credential_source_loadable" time="0.000" /><testcase classname="tests.autonomous_control_plane.test_systemd_security_v3" name="test_candidate_systemd_contract_is_socket_and_loopback_only" time="0.000" /><testcase classname="tests.autonomous_control_plane.test_systemd_security_v3" name="test_candidate_entrypoint_and_configuration_are_explicit" time="0.001" /><testcase classname="tests.autonomous_control_plane.test_candidate_integration_v3" name="test_candidate_initialization_isolated_and_bound" time="0.008" /><testcase classname="tests.autonomous_control_plane.test_candidate_integration_v3" name="test_candidate_rejects_production_roots[/var/lib/aios-gitops/release/v2r1c4]" time="0.000" /><testcase classname="tests.autonomous_control_plane.test_candidate_integration_v3" name="test_candidate_rejects_production_roots[/var/lib/aios-gitops/artifacts/v2r1c4]" time="0.000" /><testcase classname="tests.autonomous_control_plane.test_candidate_integration_v3" name="test_candidate_rejects_production_roots[/opt/aios/v2r1c4]" time="0.000" /><testcase classname="tests.autonomous_control_plane.test_candidate_integration_v3" name="test_candidate_rejects_production_roots[/data/aios-gitops/runtime/releases/v2r1c4]" time="0.000" /><testcase classname="tests.autonomous_control_plane.test_candidate_integration_v3" name="test_candidate_merge_and_tag_fail_closed" time="0.001" /><testcase classname="tests.autonomous_control_plane.test_candidate_integration_v3" name="test_merge_stdio_uses_resolved_absolute_interpreter" time="0.082" /><testcase classname="tests.autonomous_control_plane.test_candidate_integration_v3" name="test_candidate_live_port_8080_health_and_tag_no_write" time="0.529" /></testsuite></testsuites>
|
||||
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><testsuites name="pytest tests"><testsuite name="pytest" errors="0" failures="0" skipped="0" tests="2" time="0.045" timestamp="2026-07-29T20:28:16.185613+08:00" hostname="cn-network-mkt"><testcase classname="tests.autonomous-release.test_core_contracts" name="test_plan_policy_accepts_candidate_plan" time="0.000" /><testcase classname="tests.autonomous-release.test_core_contracts" name="test_evidence_chain_detects_valid_closure" time="0.000" /></testsuite></testsuites>
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"credential_pattern_hits": {
|
||||
"authorization_header": [],
|
||||
"credential_url": [],
|
||||
"hardcoded_token": [],
|
||||
"private_key": []
|
||||
},
|
||||
"dependency_scan": {
|
||||
"declared_runtime_dependencies": 0,
|
||||
"status": "PASS"
|
||||
},
|
||||
"hardcoded_credentials": 0,
|
||||
"path_traversal_entries": [],
|
||||
"source_commit_oid": "56bb00cd134f4d545935416256bdb018d5c8d58b",
|
||||
"symlink_entries": [],
|
||||
"tracked_path_count": 765
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"blocking_layer": "NONE",
|
||||
"product_invariants_passed": 9,
|
||||
"source_commit_oid": "56bb00cd134f4d545935416256bdb018d5c8d58b",
|
||||
"status": "PASS"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"first_sha256": "a0415df8e4d131a32a3ce39a6e997b0304657224642bf47d73adddb684a7d4ca",
|
||||
"second_sha256": "a0415df8e4d131a32a3ce39a6e997b0304657224642bf47d73adddb684a7d4ca",
|
||||
"source_commit_oid": "56bb00cd134f4d545935416256bdb018d5c8d58b",
|
||||
"status": "PASS"
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"components": [
|
||||
"governed-development-writer",
|
||||
"approval-mirror-worker",
|
||||
"approval-watcher",
|
||||
"release-object-projection",
|
||||
"runtime-source-projection",
|
||||
"automatic-renewal-controller",
|
||||
"v2r1c4-release-envelope"
|
||||
],
|
||||
"direct_main_push": false,
|
||||
"force_push": false,
|
||||
"global_writes_must_remain_false": true,
|
||||
"schema": "aios.code-handoff/v1",
|
||||
"source": "codex-chartgpt-bridge",
|
||||
"tag_rewrite": false,
|
||||
"target_branch": "patch/v2r1c4-release-completion-20260729",
|
||||
"wechat_backend": {
|
||||
"code_generated": false,
|
||||
"reason": "PR #20 source diff exceeded the available controlled diff interface; do not invent API contracts.",
|
||||
"required_action": "Import the exact approved PR #20 objects, then apply the release-envelope tests and hardening checks."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"CODE_HANDOFF_MANIFEST.json": "5723d17bee218d5fac627b439b898fe57506f3a153feab4561013b1f37937c26",
|
||||
"INTEGRATION_NOTES.md": "e058b51d6c0fabb25abcdff44058a56f5d6adcaf9733b29b9cc4fc10de1603b3",
|
||||
"TEST_RECEIPT.json": "e8699510eca3b805dac879ba3d6955954ebe17de7cd2142e60c205902627b8cf",
|
||||
"config/examples/development-policy.json": "51ab926a733c9a5173df43a3bf23d25ee3bd2b4743f2005cc458dcef232f62e4",
|
||||
"deploy/systemd/aios-autonomous-renewal-controller.service": "1e8825341884c428535ed0e56259c5c5919387d7dba130d36812bbb3312d4e29",
|
||||
"deploy/systemd/aios-governance-approval-watcher.service": "19d5a1e1b5e89405dc8fbe3332daaa4fd652cc903d12e1a81a3e5fb8cf5ad538",
|
||||
"deploy/systemd/aios-governance-mirror-worker.service": "8d7ed60961ec19fe72ef183d28c5ce8185decff5000f5f884acfb83a4bf21bbd",
|
||||
"deploy/systemd/aios-governance-request-publisher.service": "037c709d737677257ac3911285768db5549d557cada7f31f3913131873f93c70",
|
||||
"deploy/systemd/aios-governed-development-writer-revoker.service": "ac566dd3a01d4b83e44a32fcd78d34355a2cf439c91d3e53f554e65c8e480246",
|
||||
"deploy/systemd/aios-governed-development-writer-revoker.timer": "0bf2bc416012b5009ccb9c7b8f76d546fa6485295a926829fd3b26e0c1f071ce",
|
||||
"deploy/systemd/aios-governed-development-writer.service": "2f3061c2fe114243b3bb66031bb50175d65ead7448d8c83082faa2135af7b6cc",
|
||||
"deploy/systemd/aios-release-object-projection.service": "e5341f89ceb4c362b2ed87a6eba7c8bf150aec293788c1aa997d5dfc689dff44",
|
||||
"deploy/systemd/aios-runtime-source-read-projection.service": "1a83e23203f7edee365d74d8a825f4a3e1dfea6177b84c3faa6c5dda6c1ee78b",
|
||||
"deploy/tmpfiles.d/aios-autonomous-control-plane.conf": "9e36e522d87f85a13fd03c68f006e63068cf94bec06525eb420344dc734fa162",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/__init__.py": "52a7864b6a4ac94be8f67100c825813b19ccf889283bbb7dbecbd342b9ec29dc",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/__pycache__/__init__.cpython-313.pyc": "6704553f751bce9e6a544178580eedf7c3a11f58b6f9eab554e1dc53a027020e",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/__pycache__/cli.cpython-313.pyc": "9d827fb60110c05593916050fcd647708886c12c6e9e784533a56f3525dd8c9b",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/__pycache__/common.cpython-313.pyc": "9e6368b69c75abf46cf708267c7dde591b0a6cdd535f87b6022ea9393ac7c95b",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/__pycache__/ledger.cpython-313.pyc": "abf49a51060c8637eb3aae378a86ea3938177b5d442bd93f1f581434a2c45604",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/__pycache__/mirror.cpython-313.pyc": "0dbdfc445d19227346c75815597476e8dc5bbcba6e08a748624bc5813f49dcb9",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/__pycache__/policy.cpython-313.pyc": "75724693bc0810d8e58fb44389afba62cb6a828e8447e76b61751de83d7c5d60",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/__pycache__/projection.cpython-313.pyc": "a09ae9226e4398fe1b469772b06383832d2266134493fa79f19c62fcd3101da2",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/__pycache__/release.cpython-313.pyc": "dafaafbb1076b5e590b0e25eb103cceb5c6ae336fc2db31e014e8cc3fffc7432",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/__pycache__/renewal.cpython-313.pyc": "9438c81cda1987100cf5f6448186688a58fc01ac59333497afefaeb4c080026a",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/__pycache__/watcher.cpython-313.pyc": "4a729399caf5421c688fd7ab37c98fe796a9b9b5361929185fcb2a1ea3ed3456",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/__pycache__/writer.cpython-313.pyc": "75e593baf6864353004edb76abe72f1f566ca77a820c23dcee5703e092c5c621",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/cli.py": "cd42b70c35b2d810433db0df013a439dc7993c45c68ea930a9efd5b5faced84c",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/common.py": "395c669df606ed366774663d4ac229225469c77ab3ff72f6a9efcbcef8031f9e",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/ledger.py": "a51eea543d4e44c2faab6b59c7850600908f36090f0a1ddf448c1ca7c21acdbd",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/mirror.py": "20093cf68c0b4db4940d1092b0f09d7638ed210e2739973b1a558a4016208e01",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/policy.py": "5fc5b50e781cfa47e99164635155a9559d168f1f556d8ca2f1694b57b1b57195",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/projection.py": "3498affdf02fcb51d8a702ff57046ac6d528480ced808b70de1ad56250f61522",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/release.py": "21f4a93a7121c893aaa52bbb31909215a4f64db32673fb10abc61abfdd94897c",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/renewal.py": "2272c428999a769218142b27b0a80e938c96b7cf1e553fdfd94873123dd9ad86",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/watcher.py": "eed557d5f6acfe44881bee44dd92fea6e04ac19f33e2dea874c2a4662b04c824",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/writer.py": "79503f3819469051ffb5f222156891a22981fa62ed37b72ce42da6247ae5792d",
|
||||
"services/aios-autonomous-control-plane/pyproject.toml": "52200725816a1cdbb5e22b6c477fd80539a11013bcf0d6545d99807b29a0c697",
|
||||
"tests/.pytest_cache/.gitignore": "3ed731b65d06150c138e2dadb0be0697550888a6b47eb8c45ecc9adba8b8e9bd",
|
||||
"tests/.pytest_cache/CACHEDIR.TAG": "37dc88ef9a0abeddbe81053a6dd8fdfb13afb613045ea1eb4a5c815a74a3bde4",
|
||||
"tests/.pytest_cache/README.md": "73fd6fccdd802c419a6b2d983d6c3173b7da97558ac4b589edec2dfe443db9ad",
|
||||
"tests/.pytest_cache/v/cache/lastfailed": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
|
||||
"tests/.pytest_cache/v/cache/nodeids": "bcee695f0c2c1125930b78eae1badd0ffce72495c63bdbb0a44cd50e4d54be8d",
|
||||
"tests/__pycache__/conftest.cpython-313-pytest-9.0.2.pyc": "10fb2a851650254dd15bbfb89011cd8373d6c7a052d3181d8e258110d2a3f961",
|
||||
"tests/autonomous_control_plane/__pycache__/test_mirror_projection.cpython-313-pytest-9.0.2.pyc": "3cbb8295357bee399ab2d2c33b08d55cccb8e2cd5568accb9eedcb756b3be4d1",
|
||||
"tests/autonomous_control_plane/__pycache__/test_policy_and_writer.cpython-313-pytest-9.0.2.pyc": "41573389ecbc16ae8c1b9990db12dd2f5994469584388ebc82cd0aeb345ca36f",
|
||||
"tests/autonomous_control_plane/__pycache__/test_renewal_release.cpython-313-pytest-9.0.2.pyc": "e7b05c53ba735723d27cedb7275b24450310109b1e752178c54122a534b2775b",
|
||||
"tests/autonomous_control_plane/__pycache__/test_watcher.cpython-313-pytest-9.0.2.pyc": "5e13e650b803412db0105d3e7f3e84cada3187a170173360c6da0304a193cb1e",
|
||||
"tests/autonomous_control_plane/test_mirror_projection.py": "818410d5571d36cb23df0db0be444a9c08c705325ecfeeebe88fc0d82dbffcd3",
|
||||
"tests/autonomous_control_plane/test_policy_and_writer.py": "723a6fafcc542de7da028d2a2f76a6eb2b7975d4e6e9d1a23c17973019f74b7c",
|
||||
"tests/autonomous_control_plane/test_renewal_release.py": "29dd1031e59da0d3b5a96a4f613f1a110e0feb77f983ef2072b53795212e7ea1",
|
||||
"tests/autonomous_control_plane/test_watcher.py": "025ab4432522478ab7094751edf8da702b0e7be25d93876115cf9f416b7188b1",
|
||||
"tests/conftest.py": "32a3df0734d253b070594fce0327b929c8ee0bbb136ea6fa83a2abecb826415b"
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
# AIOS V2R1C4 Autonomy Patch
|
||||
|
||||
This package contains code developed for integration into
|
||||
`patch/v2r1c4-release-completion-20260729`.
|
||||
|
||||
It deliberately does not invent WeChat Backend endpoints or schemas. The
|
||||
business implementation from PR #20 must be imported from its real Git objects,
|
||||
then tested against the release envelope. The control-plane code in this package
|
||||
is complete and independently testable without production credentials.
|
||||
|
||||
## Components
|
||||
|
||||
- Governed Development Writer with branch/path/operation policy, idempotent
|
||||
ledger, secret scan, expected-head CAS, push verification and reconciliation.
|
||||
- Approval Mirror Worker using file quarantine, add-only approvals validation,
|
||||
fast-forward-only authoritative update and connectivity fsck.
|
||||
- Gitea SQLite Approval Watcher with exact-head Owner review validation.
|
||||
- Release Object Projection and root-only Runtime Source Projection.
|
||||
- Automatic Renewal Controller with immutable-field comparison and bounded
|
||||
renewal windows.
|
||||
- Unified V2R1C4 Release Envelope executor covering merge, tag, remote
|
||||
verification, deploy, health, rollback, restore, Registry, Baseline and final
|
||||
consistency.
|
||||
- Hardened systemd units and tmpfiles definitions.
|
||||
|
||||
## Agent integration constraints
|
||||
|
||||
- Keep `AIOS_GITOPS_WRITES_ENABLED=false`.
|
||||
- Apply on a non-main branch only.
|
||||
- Do not copy sample credentials or secrets.
|
||||
- Resolve actual Gitea repository and database paths dynamically.
|
||||
- Replace server wrapper placeholders under `/usr/libexec` with thin adapters
|
||||
calling the supplied Python package.
|
||||
- Run the supplied tests plus existing repository tests before requesting Owner
|
||||
approval.
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"compiled_cache_files_integrated": false,
|
||||
"credential_scan": "PASS_TEST_REJECTION_FIXTURE_ONLY",
|
||||
"direct_main_push": false,
|
||||
"file_sha256_entries": 52,
|
||||
"file_sha256_verified": true,
|
||||
"fixture_value_sha256": "653bb1245e828fcda4fa53fcd5a3def5bd7654e651f54b4132b73d74e64435c4",
|
||||
"force_push_used": false,
|
||||
"global_gitops_writes_enabled": false,
|
||||
"path_traversal_entries": 0,
|
||||
"schema": "aios.code-handoff-integration/v1",
|
||||
"source_files_integrated": [
|
||||
"config/examples/development-policy.json",
|
||||
"deploy/tmpfiles.d/aios-autonomous-control-plane.conf",
|
||||
"services/aios-autonomous-control-plane/pyproject.toml",
|
||||
"tests/conftest.py",
|
||||
"deploy/systemd/aios-autonomous-renewal-controller.service",
|
||||
"deploy/systemd/aios-governance-approval-watcher.service",
|
||||
"deploy/systemd/aios-governance-mirror-worker.service",
|
||||
"deploy/systemd/aios-governance-request-publisher.service",
|
||||
"deploy/systemd/aios-governed-development-writer-revoker.service",
|
||||
"deploy/systemd/aios-governed-development-writer-revoker.timer",
|
||||
"deploy/systemd/aios-governed-development-writer.service",
|
||||
"deploy/systemd/aios-release-object-projection.service",
|
||||
"deploy/systemd/aios-runtime-source-read-projection.service",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/__init__.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/cli.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/common.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/ledger.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/mirror.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/policy.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/projection.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/release.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/renewal.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/watcher.py",
|
||||
"services/aios-autonomous-control-plane/aios_autonomous_control_plane/writer.py",
|
||||
"tests/autonomous_control_plane/test_mirror_projection.py",
|
||||
"tests/autonomous_control_plane/test_policy_and_writer.py",
|
||||
"tests/autonomous_control_plane/test_renewal_release.py",
|
||||
"tests/autonomous_control_plane/test_watcher.py"
|
||||
],
|
||||
"tag_rewritten": false,
|
||||
"zip_sha256": "795feb406541af86f30c1fb31f85e6d7485ee72a9d3ac93351f9a63fe4ad3e8f"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"compileall_returncode": 0,
|
||||
"pytest_returncode": 0,
|
||||
"pytest_stderr": "Spreadsheet runtime warmup failed during python startup\nTraceback (most recent call last):\n File \"/tmp/tmp.yTcnQsZYiA/artifact_tool_v2-2.8.4/artifact_tool/patches/warm_spreadsheet_runtime_on_startup.py\", line 26, in warm_spreadsheet_runtime_on_startup\n File \"/tmp/tmp.yTcnQsZYiA/artifact_tool_v2-2.8.4/artifact_tool/spreadsheet_warmup.py\", line 785, in warm_spreadsheet_runtime\n File \"/tmp/tmp.yTcnQsZYiA/artifact_tool_v2-2.8.4/artifact_tool/spreadsheet_warmup.py\", line 720, in _warm_feature_flows\n File \"/tmp/tmp.yTcnQsZYiA/artifact_tool_v2-2.8.4/artifact_tool/spreadsheet_warmup.py\", line 704, in _warm_collaboration_flows\n File \"/tmp/tmp.yTcnQsZYiA/artifact_tool_v2-2.8.4/artifact_tool/generated/interface/models.py\", line 30820, in hydrate_crdt_from_proto\n File \"/tmp/tmp.yTcnQsZYiA/artifact_tool_v2-2.8.4/artifact_tool/rpc/remote.py\", line 749, in __call__\n File \"/tmp/tmp.yTcnQsZYiA/artifact_tool_v2-2.8.4/artifact_tool/rpc/client.py\", line 150, in call\nartifact_tool.rpc.client.RemoteError: hydrateCrdtFromProto requires an empty collaborative document.\n",
|
||||
"pytest_stdout": "\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m [100%]\u001b[0m\n\u001b[32m\u001b[32m\u001b[1m10 passed\u001b[0m\u001b[32m in 0.45s\u001b[0m\u001b[0m\n"
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"artifact_path": "/var/lib/aios-gitops/runtime/tasks/AIOS-SINGLE-OWNER-GOVERNANCE-ACTIVATION-20260729/artifacts/hardening-71def340e7cc34f4f19918dcf8d3c847abbe4fbf/live-activation-71def340e7cc34f4f19918dcf8d3c847abbe4fbf.tar.gz",
|
||||
"artifact_sha256": "32bc49c207ec454b6fd81b0484f5b257bb789481b37175995029a2ec78cc62fa",
|
||||
"copied_live_db_acceptance": "PASS",
|
||||
"implementation_commit_oid": "71def340e7cc34f4f19918dcf8d3c847abbe4fbf",
|
||||
"implementation_tree_oid": "3cc61bfbf69cbe0a24d54a0e53a6c50bcc130066",
|
||||
"independent_validator": "ALLOW_CANDIDATE",
|
||||
"production_mutation": false,
|
||||
"result": "PASS",
|
||||
"rollback_artifact_path": "/var/lib/aios-gitops/runtime/tasks/AIOS-SINGLE-OWNER-GOVERNANCE-ACTIVATION-20260729/artifacts/hardening-71def340e7cc34f4f19918dcf8d3c847abbe4fbf/live-activation-rollback-71def340e7cc34f4f19918dcf8d3c847abbe4fbf.tar.gz",
|
||||
"rollback_artifact_sha256": "4efe1ea73d4fd72e5364fc91a16ad5e9d1e6ebe78d8aa466f7b11c4915b08403",
|
||||
"runtime_source_sha256": {
|
||||
"services/aios-single-owner-governance/aios_single_owner_governance/activation.py": "48846a58fa3afb2b7c8ffe6e88504a9333863932df867f33ba0321ad3bac6003",
|
||||
"services/aios-single-owner-governance/aios_single_owner_governance/gitea_protected_branch.py": "00775efe3f3d631d88379eff4ed2dcfaf386a2fa43111abe961e75109e80d768",
|
||||
"services/aios-single-owner-governance/aios_single_owner_governance/journal.py": "a8f121245e04487d99c99fb03ada83f0c907723f8eb18552872ff46dcf5a7873",
|
||||
"services/aios-single-owner-governance/aios_single_owner_governance/state_machine.py": "0b08a2c378d143cb9031a25132afc7b7d825bdc03d11efe10610341d4b70167e",
|
||||
"services/aios-single-owner-governance/aios_single_owner_governance/validator.py": "754639846cb15cb997d612a1fe0ceb0224ddef5bbc3ccac9802a3747f122dac1"
|
||||
},
|
||||
"subtests_passed": 17,
|
||||
"task_id": "AIOS-SINGLE-OWNER-GOVERNANCE-ACTIVATION-20260729",
|
||||
"test_evidence_path": "/var/lib/aios-gitops/runtime/tasks/AIOS-SINGLE-OWNER-GOVERNANCE-ACTIVATION-20260729/artifacts/hardening-71def340e7cc34f4f19918dcf8d3c847abbe4fbf/test-evidence-71def340e7cc34f4f19918dcf8d3c847abbe4fbf.json",
|
||||
"test_evidence_sha256": "c1633732767f3f051ffb01404d12d05a6168a4b01b91efdb34a64988d6dee684",
|
||||
"tests_failed": 0,
|
||||
"tests_passed": 90
|
||||
}
|
||||
+10
-1
@@ -2,6 +2,15 @@
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||
cd "$ROOT"
|
||||
manifest_log="${TMPDIR:-/tmp}/aios_manifest_check.$$.log"
|
||||
cleanup() {
|
||||
rm -f -- "$manifest_log"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
bash scripts/package_contract_check_v1r1c0.sh
|
||||
if [ -f FULL_PACKAGE_FILE_MANIFEST.sha256 ]; then sha256sum -c FULL_PACKAGE_FILE_MANIFEST.sha256 --ignore-missing >/tmp/aios_manifest_check.log || true; fi
|
||||
if [ ! -f FULL_PACKAGE_FILE_MANIFEST.sha256 ]; then
|
||||
echo "FULL_PACKAGE_FILE_MANIFEST.sha256 missing" >&2
|
||||
exit 1
|
||||
fi
|
||||
sha256sum -c FULL_PACKAGE_FILE_MANIFEST.sha256 --ignore-missing >"$manifest_log"
|
||||
echo "AIOS_V1R1C0_PACKAGE_VERIFY_OK"
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build deterministic dormant-release candidate artifacts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import tarfile
|
||||
|
||||
|
||||
def sha256(value: bytes) -> str:
|
||||
return hashlib.sha256(value).hexdigest()
|
||||
|
||||
|
||||
def build(source: Path, output: Path, source_oid: str, tree_oid: str) -> dict[str, str]:
|
||||
if not source.exists() or not source.is_dir():
|
||||
raise SystemExit("source directory missing")
|
||||
files = []
|
||||
for item in sorted(source.rglob("*")):
|
||||
if item.is_file() and ".git" not in item.parts:
|
||||
files.append(item)
|
||||
artifact = output / "candidate.tar"
|
||||
manifest = output / "manifest.json"
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
with tarfile.open(artifact, "w", format=tarfile.PAX_FORMAT) as archive:
|
||||
for item in files:
|
||||
info = archive.gettarinfo(str(item), arcname=str(item.relative_to(source)))
|
||||
info.mtime = 0
|
||||
info.uid = 0
|
||||
info.gid = 0
|
||||
with item.open("rb") as handle:
|
||||
archive.addfile(info, handle)
|
||||
payload = {
|
||||
"source_oid": source_oid,
|
||||
"tree_oid": tree_oid,
|
||||
"artifact_sha256": sha256(artifact.read_bytes()),
|
||||
"files": {
|
||||
str(item.relative_to(source)): sha256(item.read_bytes())
|
||||
for item in files
|
||||
},
|
||||
}
|
||||
manifest.write_text(json.dumps(payload, sort_keys=True, indent=2), encoding="utf-8")
|
||||
return {
|
||||
"artifact_sha256": payload["artifact_sha256"],
|
||||
"manifest_sha256": sha256(manifest.read_bytes()),
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("source", type=Path)
|
||||
parser.add_argument("output", type=Path)
|
||||
parser.add_argument("source_oid")
|
||||
parser.add_argument("tree_oid")
|
||||
args = parser.parse_args()
|
||||
print(json.dumps(build(args.source, args.output, args.source_oid, args.tree_oid)))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Evaluate AIOS V2R1C4 dormant release evidence.
|
||||
|
||||
This command performs no Git, network, deployment, tag, Registry or Baseline
|
||||
write. It evaluates explicit evidence supplied by a governed execution agent
|
||||
and writes a deterministic JSON decision report.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from typing import Any, Mapping
|
||||
|
||||
|
||||
REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
|
||||
PACKAGE_ROOT = REPOSITORY_ROOT / "services" / "aios-autonomous-release-candidate"
|
||||
if str(PACKAGE_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PACKAGE_ROOT))
|
||||
|
||||
from aios_autonomous_release_candidate.acceptance import ( # noqa: E402
|
||||
AcceptancePhase,
|
||||
AcceptanceStatus,
|
||||
DormantAcceptanceGate,
|
||||
DormantAcceptanceInput,
|
||||
FinalDormantAcceptanceGate,
|
||||
FinalDormantAcceptanceInput,
|
||||
)
|
||||
from aios_autonomous_release_candidate.models import sha256_json # noqa: E402
|
||||
|
||||
|
||||
REPORT_SCHEMA = "aios.dormant-release.acceptance/v1"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RunnerDecision:
|
||||
phase: str
|
||||
status: str
|
||||
report: Mapping[str, Any]
|
||||
exit_code: int
|
||||
|
||||
|
||||
def read_json_object(path: Path) -> Mapping[str, Any]:
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
except FileNotFoundError as exc:
|
||||
raise ValueError(f"input file does not exist: {path}") from exc
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ValueError(f"input is not valid JSON: {exc.msg}") from exc
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError("input must contain one JSON object")
|
||||
return value
|
||||
|
||||
|
||||
def atomic_write_json(path: Path, payload: Mapping[str, Any]) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
temporary = path.with_suffix(path.suffix + ".tmp")
|
||||
temporary.write_text(
|
||||
json.dumps(payload, sort_keys=True, indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
temporary.chmod(0o600)
|
||||
temporary.replace(path)
|
||||
|
||||
|
||||
def evaluate(phase: AcceptancePhase, evidence: Mapping[str, Any]) -> RunnerDecision:
|
||||
if phase is AcceptancePhase.CANDIDATE:
|
||||
parsed = DormantAcceptanceInput.from_mapping(evidence)
|
||||
result = DormantAcceptanceGate().evaluate(parsed)
|
||||
else:
|
||||
parsed = FinalDormantAcceptanceInput.from_mapping(evidence)
|
||||
result = FinalDormantAcceptanceGate().evaluate(parsed)
|
||||
result_payload = dict(result.payload())
|
||||
report = {
|
||||
"schema": REPORT_SCHEMA,
|
||||
"phase": result.phase.value,
|
||||
"status": result.status.value,
|
||||
"decision": result_payload,
|
||||
"input_sha256": sha256_json(evidence),
|
||||
"production_action_authorized": False,
|
||||
}
|
||||
return RunnerDecision(
|
||||
phase=result.phase.value,
|
||||
status=result.status.value,
|
||||
report=report,
|
||||
exit_code=0 if result.status is AcceptanceStatus.READY else 3,
|
||||
)
|
||||
|
||||
|
||||
def invalid_report(phase: str, message: str) -> RunnerDecision:
|
||||
report = {
|
||||
"schema": REPORT_SCHEMA,
|
||||
"phase": phase,
|
||||
"status": AcceptanceStatus.BLOCKED.value,
|
||||
"decision": {
|
||||
"blocking_code": "INVALID_ACCEPTANCE_INPUT",
|
||||
"blocking_message": message,
|
||||
},
|
||||
"production_action_authorized": False,
|
||||
}
|
||||
return RunnerDecision(
|
||||
phase=phase,
|
||||
status=AcceptanceStatus.BLOCKED.value,
|
||||
report=report,
|
||||
exit_code=2,
|
||||
)
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(prog="run_acceptance.py")
|
||||
parser.add_argument(
|
||||
"--phase",
|
||||
required=True,
|
||||
choices=[phase.value for phase in AcceptancePhase],
|
||||
)
|
||||
parser.add_argument("--input", required=True, type=Path)
|
||||
parser.add_argument("--output", required=True, type=Path)
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = build_parser().parse_args(argv)
|
||||
try:
|
||||
phase = AcceptancePhase(args.phase)
|
||||
evidence = read_json_object(args.input)
|
||||
decision = evaluate(phase, evidence)
|
||||
except (OSError, TypeError, ValueError) as exc:
|
||||
decision = invalid_report(args.phase, str(exc))
|
||||
|
||||
envelope = dict(decision.report)
|
||||
envelope["generated_at"] = datetime.now(timezone.utc).isoformat()
|
||||
atomic_write_json(args.output, envelope)
|
||||
print(json.dumps(envelope, sort_keys=True))
|
||||
return decision.exit_code
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,356 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
OBSERVATION_SCHEMA = "aios.v2r1c4-validation-observations/v3"
|
||||
BRANCH = "patch/v2r1c4-security-invariants-v3-20260729"
|
||||
REQUIRED_RAW = {
|
||||
"compileall.log", "pytest-main.xml", "pytest-legacy.xml",
|
||||
"nonroot-focused.xml", "systemd-verify.log", "gitleaks.json",
|
||||
"scans.json", "wheel-repro.json", "candidate-live.json", "validator.json",
|
||||
}
|
||||
REQUIRED_COMMANDS = {
|
||||
"compileall": {"compileall.log"},
|
||||
"pytest_main": {"pytest-main.xml"},
|
||||
"pytest_legacy": {"pytest-legacy.xml"},
|
||||
"non_root_focused": {"nonroot-focused.xml"},
|
||||
"systemd_verify": {"systemd-verify.log"},
|
||||
"secret_scan": {"gitleaks.json"},
|
||||
"structural_scans": {"scans.json"},
|
||||
"artifact_reproducibility": {"wheel-repro.json"},
|
||||
"candidate_live": {"candidate-live.json"},
|
||||
"independent_validator": {"validator.json"},
|
||||
}
|
||||
COMMAND_TEXT = {
|
||||
"compileall": "python3 -m compileall -q active-runtime services scripts tests",
|
||||
"pytest_main": "python3 -m pytest -q tests --ignore=tests/autonomous-release --junitxml=pytest-main.xml",
|
||||
"pytest_legacy": "python3 -m pytest -q tests/autonomous-release --junitxml=pytest-legacy.xml",
|
||||
"non_root_focused": "python3 -m pytest -q test_non_root_execution_v3.py test_writer_revoker_security_v3.py test_systemd_security_v3.py test_candidate_integration_v3.py --junitxml=nonroot-focused.xml",
|
||||
"systemd_verify": "systemd-analyze --root=STAGED_ROOT verify SIX_CANDIDATE_UNITS",
|
||||
"secret_scan": "gitleaks detect --redact --log-opts=EXPECTED_BASE..SOURCE_COMMIT --report-format=json",
|
||||
"structural_scans": "scan tracked paths and changed source for traversal symlinks credentials and dependencies",
|
||||
"artifact_reproducibility": "SOURCE_DATE_EPOCH=SOURCE_COMMIT python3 -m build --wheel --no-isolation twice",
|
||||
"candidate_live": "run UID1000 candidate health tag-plan and systemd-socket-activate merge probes",
|
||||
"independent_validator": "independent read-only validator audit SOURCE_COMMIT",
|
||||
}
|
||||
INVARIANTS = {
|
||||
1: ("owner_manifest_release_envelope_full_binding", ["approval_binding.py", "release.py", "release_action.py"]),
|
||||
2: ("all_digests_recomputed_before_side_effects", ["release_inputs.py", "release_action.py"]),
|
||||
3: ("transactional_deploy_compensation_and_resume", ["release.py", "release_deploy.py"]),
|
||||
4: ("rollback_restore_approved_release_root_binding", ["release_deploy.py"]),
|
||||
5: ("writer_repo_base_worktree_remote_credential_closure", ["grant.py", "writer.py", "writer_server.py"]),
|
||||
6: ("expiry_aware_crash_recoverable_revoker", ["revoker.py"]),
|
||||
7: ("non_root_writer_publisher_directory_ownership", ["deploy/systemd", "deploy/tmpfiles.d"]),
|
||||
8: ("structured_loopback_and_redirect_denial", ["release_transport.py", "gitea_api.py"]),
|
||||
9: ("candidate_only_live_integration_fixtures", ["candidate_fixture.py", "candidate systemd units"]),
|
||||
}
|
||||
INVARIANT_RAW = {
|
||||
1: ["pytest-main.xml"],
|
||||
2: ["pytest-main.xml"],
|
||||
3: ["pytest-main.xml"],
|
||||
4: ["pytest-main.xml"],
|
||||
5: ["nonroot-focused.xml", "pytest-main.xml"],
|
||||
6: ["nonroot-focused.xml", "pytest-main.xml"],
|
||||
7: ["nonroot-focused.xml", "pytest-main.xml", "systemd-verify.log"],
|
||||
8: ["pytest-main.xml"],
|
||||
9: ["candidate-live.json", "nonroot-focused.xml", "pytest-main.xml"],
|
||||
}
|
||||
FAILURE_TESTS = {
|
||||
"deploy_mid_restart_resume": "test_deploy_mid_step_failure_resumes_without_losing_rollback_target",
|
||||
"deploy_compensation": "test_executor_persists_failed_activation_compensation_and_resumes",
|
||||
"rollback_binding_mismatch": "test_artifact_rejects_unmanifested_files_and_rollback_escape",
|
||||
"revoker_receipt_checkpoint_crash": "test_revoker_recovers_across_receipt_checkpoint_crash",
|
||||
"loopback_parser_bypass": "test_loopback_url_validation_is_structured",
|
||||
"writer_binding_mismatch": "test_writer_server_rejects_credential_identity_mismatch",
|
||||
}
|
||||
|
||||
|
||||
def run(root: Path, *args: str) -> str:
|
||||
return subprocess.check_output(
|
||||
["git", "-c", f"safe.directory={root}", "-C", str(root), *args], text=True,
|
||||
).strip()
|
||||
|
||||
|
||||
def write_json(path: Path, value: object) -> None:
|
||||
path.write_text(json.dumps(value, sort_keys=True, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def fail(code: str) -> None:
|
||||
raise SystemExit(code)
|
||||
|
||||
|
||||
def exact_dict(value: object, keys: set[str], code: str) -> dict:
|
||||
if not isinstance(value, dict) or set(value) != keys:
|
||||
fail(code)
|
||||
return value
|
||||
|
||||
|
||||
def junit_totals(paths: list[Path]) -> dict[str, int]:
|
||||
totals = {"tests": 0, "failures": 0, "errors": 0, "skipped": 0}
|
||||
for path in paths:
|
||||
try:
|
||||
root = ET.parse(path).getroot()
|
||||
except (ET.ParseError, OSError):
|
||||
fail("JUNIT_RAW_INVALID")
|
||||
suites = [root] if root.tag == "testsuite" else list(root.findall("testsuite"))
|
||||
if not suites:
|
||||
fail("JUNIT_RAW_INVALID")
|
||||
for suite in suites:
|
||||
for key in totals:
|
||||
try:
|
||||
totals[key] += int(suite.attrib.get(key, "0"))
|
||||
except ValueError:
|
||||
fail("JUNIT_RAW_INVALID")
|
||||
return totals
|
||||
|
||||
|
||||
def passing_junit_names(path: Path) -> set[str]:
|
||||
try:
|
||||
return {
|
||||
node.attrib.get("name", "") for node in ET.parse(path).getroot().iter("testcase")
|
||||
if not any(node.find(tag) is not None for tag in ("failure", "error", "skipped"))
|
||||
}
|
||||
except (ET.ParseError, OSError):
|
||||
fail("JUNIT_RAW_INVALID")
|
||||
|
||||
|
||||
def junit_property(path: Path, name: str) -> str:
|
||||
try:
|
||||
values = [
|
||||
node.attrib.get("value", "") for node in ET.parse(path).getroot().iter("property")
|
||||
if node.attrib.get("name") == name
|
||||
]
|
||||
except (ET.ParseError, OSError):
|
||||
fail("JUNIT_RAW_INVALID")
|
||||
if len(values) != 1:
|
||||
fail("JUNIT_PROPERTY_INVALID")
|
||||
return values[0]
|
||||
|
||||
|
||||
def load_json(path: Path, code: str) -> object:
|
||||
try:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, UnicodeError, json.JSONDecodeError):
|
||||
fail(code)
|
||||
|
||||
|
||||
def validate_observations(observations: object, source: str) -> dict[str, Path]:
|
||||
required_keys = {
|
||||
"schema", "source_commit_oid", "commands", "raw_evidence", "security_invariants",
|
||||
"test_results", "failure_injection_results", "systemd_validation",
|
||||
"non_root_permission_results", "dependency_scan", "secret_scan",
|
||||
"path_traversal_scan", "credential_leak_scan", "artifact_reproducibility",
|
||||
"validator_report", "unresolved_issues",
|
||||
}
|
||||
obs = exact_dict(observations, required_keys, "OBSERVATION_SCHEMA_INVALID")
|
||||
if obs["schema"] != OBSERVATION_SCHEMA:
|
||||
fail("OBSERVATION_SCHEMA_INVALID")
|
||||
if obs["source_commit_oid"] != source:
|
||||
fail("OBSERVATION_SOURCE_COMMIT_MISMATCH")
|
||||
|
||||
raw = exact_dict(obs["raw_evidence"], REQUIRED_RAW, "RAW_EVIDENCE_SET_INVALID")
|
||||
raw_paths: dict[str, Path] = {}
|
||||
for name, binding in raw.items():
|
||||
item = exact_dict(binding, {"path", "sha256"}, "RAW_EVIDENCE_SCHEMA_INVALID")
|
||||
path = Path(item["path"])
|
||||
if not path.is_absolute() or not path.is_file() or path.is_symlink():
|
||||
fail("RAW_EVIDENCE_PATH_INVALID")
|
||||
if sha256(path) != item["sha256"]:
|
||||
fail("RAW_EVIDENCE_SHA256_MISMATCH")
|
||||
raw_paths[name] = path
|
||||
|
||||
commands = obs["commands"]
|
||||
if not isinstance(commands, list) or len(commands) != len(REQUIRED_COMMANDS):
|
||||
fail("OBSERVED_COMMAND_SET_INVALID")
|
||||
seen: set[str] = set()
|
||||
for value in commands:
|
||||
item = exact_dict(value, {"id", "command", "returncode", "raw_evidence"}, "OBSERVED_COMMAND_SCHEMA_INVALID")
|
||||
command_id = item["id"]
|
||||
if command_id in seen or command_id not in REQUIRED_COMMANDS or item["returncode"] != 0:
|
||||
fail("OBSERVED_COMMAND_FAILURE")
|
||||
if item["command"] != COMMAND_TEXT[command_id]:
|
||||
fail("OBSERVED_COMMAND_MISMATCH")
|
||||
if set(item["raw_evidence"]) != REQUIRED_COMMANDS[command_id]:
|
||||
fail("COMMAND_RAW_BINDING_INVALID")
|
||||
seen.add(command_id)
|
||||
if seen != set(REQUIRED_COMMANDS):
|
||||
fail("OBSERVED_COMMAND_SET_INVALID")
|
||||
|
||||
invariants = obs["security_invariants"]
|
||||
if not isinstance(invariants, list) or len(invariants) != len(INVARIANTS):
|
||||
fail("SECURITY_INVARIANTS_NOT_CLOSED")
|
||||
invariant_ids: set[int] = set()
|
||||
for value in invariants:
|
||||
item = exact_dict(value, {"id", "name", "status", "locations", "raw_evidence"}, "SECURITY_INVARIANT_SCHEMA_INVALID")
|
||||
invariant_id = item["id"]
|
||||
if invariant_id in invariant_ids or invariant_id not in INVARIANTS:
|
||||
fail("SECURITY_INVARIANT_ID_INVALID")
|
||||
name, locations = INVARIANTS[invariant_id]
|
||||
if item["name"] != name or item["status"] != "PASS" or item["locations"] != locations:
|
||||
fail("SECURITY_INVARIANTS_NOT_CLOSED")
|
||||
if item["raw_evidence"] != INVARIANT_RAW[invariant_id]:
|
||||
fail("SECURITY_INVARIANT_RAW_BINDING_INVALID")
|
||||
invariant_ids.add(invariant_id)
|
||||
if invariant_ids != set(INVARIANTS):
|
||||
fail("SECURITY_INVARIANTS_NOT_CLOSED")
|
||||
|
||||
junit = junit_totals([raw_paths["pytest-main.xml"], raw_paths["pytest-legacy.xml"]])
|
||||
tests = exact_dict(obs["test_results"], {"tests_passed", "tests_failed", "tests_skipped", "collection_errors", "junit_tests"}, "TEST_RESULTS_SCHEMA_INVALID")
|
||||
derived_passed = junit["tests"] - junit["failures"] - junit["errors"] - junit["skipped"]
|
||||
if tests != {"tests_passed": derived_passed, "tests_failed": junit["failures"] + junit["errors"], "tests_skipped": junit["skipped"], "collection_errors": junit["errors"], "junit_tests": junit["tests"]}:
|
||||
fail("TEST_RESULTS_RAW_MISMATCH")
|
||||
if tests["tests_failed"] != 0 or tests["collection_errors"] != 0:
|
||||
fail("TEST_RESULTS_FAILED")
|
||||
|
||||
failure = exact_dict(obs["failure_injection_results"], {"status", "passed", "failed", "scenarios", "raw_evidence"}, "FAILURE_INJECTION_SCHEMA_INVALID")
|
||||
test_names = passing_junit_names(raw_paths["pytest-main.xml"])
|
||||
derived_scenarios = [
|
||||
scenario for scenario, test_name in FAILURE_TESTS.items()
|
||||
if any(name == test_name or name.startswith(test_name + "[") for name in test_names)
|
||||
]
|
||||
if failure != {"status": "PASS", "passed": len(FAILURE_TESTS), "failed": 0, "scenarios": list(FAILURE_TESTS), "raw_evidence": ["pytest-main.xml"]} or derived_scenarios != list(FAILURE_TESTS):
|
||||
fail("FAILURE_INJECTION_NOT_CLOSED")
|
||||
|
||||
systemd = exact_dict(obs["systemd_validation"], {"status", "units_verified", "warnings", "raw_evidence"}, "SYSTEMD_RESULT_SCHEMA_INVALID")
|
||||
try:
|
||||
systemd_lines = [line for line in raw_paths["systemd-verify.log"].read_text(encoding="utf-8").splitlines() if line]
|
||||
compileall_text = raw_paths["compileall.log"].read_text(encoding="utf-8")
|
||||
except (OSError, UnicodeError):
|
||||
fail("COMMAND_RAW_INVALID")
|
||||
denied_systemd = ("failed", "error", "invalid", "not found", "not executable", "unknown key")
|
||||
if any(any(term in line.lower() for term in denied_systemd) for line in systemd_lines):
|
||||
fail("SYSTEMD_RAW_FAILED")
|
||||
if systemd != {"status": "PASS", "units_verified": 6, "warnings": systemd_lines, "raw_evidence": "systemd-verify.log"}:
|
||||
fail("SYSTEMD_VALIDATION_FAILED")
|
||||
if compileall_text:
|
||||
fail("COMPILEALL_RAW_FAILED")
|
||||
nonroot = exact_dict(obs["non_root_permission_results"], {"status", "uid", "tests_passed", "raw_evidence"}, "NON_ROOT_RESULT_SCHEMA_INVALID")
|
||||
nonroot_junit = junit_totals([raw_paths["nonroot-focused.xml"]])
|
||||
nonroot_passed = nonroot_junit["tests"] - nonroot_junit["failures"] - nonroot_junit["errors"] - nonroot_junit["skipped"]
|
||||
try:
|
||||
observed_uid = int(junit_property(raw_paths["nonroot-focused.xml"], "process_uid"))
|
||||
except ValueError:
|
||||
fail("NON_ROOT_UID_INVALID")
|
||||
if nonroot != {"status": "PASS", "uid": observed_uid, "tests_passed": nonroot_passed, "raw_evidence": "nonroot-focused.xml"} or observed_uid == 0 or nonroot_passed <= 0 or nonroot_junit["failures"] or nonroot_junit["errors"] or nonroot_junit["skipped"]:
|
||||
fail("NON_ROOT_VALIDATION_FAILED")
|
||||
|
||||
scans = exact_dict(load_json(raw_paths["scans.json"], "SCAN_RAW_INVALID"), {"credential_pattern_hits", "dependency_scan", "hardcoded_credentials", "path_traversal_entries", "source_commit_oid", "symlink_entries", "tracked_path_count"}, "SCAN_RAW_SCHEMA_INVALID")
|
||||
if scans["source_commit_oid"] != source or scans["path_traversal_entries"] or scans["symlink_entries"] or scans["hardcoded_credentials"] != 0:
|
||||
fail("STRUCTURAL_SCAN_FAILED")
|
||||
dependency = exact_dict(obs["dependency_scan"], {"status", "declared_runtime_dependencies", "raw_evidence"}, "DEPENDENCY_RESULT_SCHEMA_INVALID")
|
||||
if dependency != {"status": "PASS", "declared_runtime_dependencies": 0, "raw_evidence": "scans.json"} or scans["dependency_scan"] != {"declared_runtime_dependencies": 0, "status": "PASS"}:
|
||||
fail("DEPENDENCY_SCAN_FAILED")
|
||||
path_scan = exact_dict(obs["path_traversal_scan"], {"status", "entries", "symlinks", "raw_evidence"}, "PATH_SCAN_SCHEMA_INVALID")
|
||||
if path_scan != {"status": "PASS", "entries": 0, "symlinks": 0, "raw_evidence": "scans.json"}:
|
||||
fail("PATH_TRAVERSAL_SCAN_FAILED")
|
||||
credential = exact_dict(obs["credential_leak_scan"], {"status", "hardcoded_credentials", "raw_evidence"}, "CREDENTIAL_SCAN_SCHEMA_INVALID")
|
||||
if credential != {"status": "PASS", "hardcoded_credentials": 0, "raw_evidence": "scans.json"}:
|
||||
fail("CREDENTIAL_SCAN_FAILED")
|
||||
|
||||
leaks = load_json(raw_paths["gitleaks.json"], "SECRET_RAW_INVALID")
|
||||
secret = exact_dict(obs["secret_scan"], {"status", "leaks", "raw_evidence"}, "SECRET_RESULT_SCHEMA_INVALID")
|
||||
if not isinstance(leaks, list) or leaks or secret != {"status": "PASS", "leaks": 0, "raw_evidence": "gitleaks.json"}:
|
||||
fail("SECRET_SCAN_FAILED")
|
||||
|
||||
wheel = exact_dict(load_json(raw_paths["wheel-repro.json"], "ARTIFACT_RAW_INVALID"), {"first_sha256", "second_sha256", "source_commit_oid", "status"}, "ARTIFACT_RAW_SCHEMA_INVALID")
|
||||
artifact = exact_dict(obs["artifact_reproducibility"], {"status", "sha256", "raw_evidence"}, "ARTIFACT_RESULT_SCHEMA_INVALID")
|
||||
if wheel["source_commit_oid"] != source or wheel["status"] != "PASS" or wheel["first_sha256"] != wheel["second_sha256"] or artifact != {"status": "PASS", "sha256": wheel["first_sha256"], "raw_evidence": "wheel-repro.json"}:
|
||||
fail("ARTIFACT_REPRODUCIBILITY_FAILED")
|
||||
|
||||
live = exact_dict(load_json(raw_paths["candidate-live.json"], "CANDIDATE_RAW_INVALID"), {"candidate_only", "health", "merge_socket", "process_uid", "production_modified", "source_commit_oid", "status", "tag_execute"}, "CANDIDATE_RAW_SCHEMA_INVALID")
|
||||
if live != {"candidate_only": True, "health": "PASS", "merge_socket": "PASS", "process_uid": 1000, "production_modified": False, "source_commit_oid": source, "status": "PASS", "tag_execute": False}:
|
||||
fail("CANDIDATE_LIVE_VALIDATION_FAILED")
|
||||
|
||||
validator_raw = exact_dict(load_json(raw_paths["validator.json"], "VALIDATOR_RAW_INVALID"), {"blocking_layer", "product_invariants_passed", "source_commit_oid", "status"}, "VALIDATOR_RAW_SCHEMA_INVALID")
|
||||
validator = exact_dict(obs["validator_report"], {"status", "source_commit_oid", "product_invariants_passed", "blocking_layer", "raw_evidence"}, "VALIDATOR_RESULT_SCHEMA_INVALID")
|
||||
if validator_raw != {"blocking_layer": "NONE", "product_invariants_passed": 9, "source_commit_oid": source, "status": "PASS"} or validator != {**validator_raw, "raw_evidence": "validator.json"}:
|
||||
fail("INDEPENDENT_VALIDATOR_FAILED")
|
||||
|
||||
issues = obs["unresolved_issues"]
|
||||
if not isinstance(issues, list) or any(not isinstance(item, dict) or set(item) != {"id", "status", "description"} or item["status"] not in {"ACCEPTED_BOUNDARY", "RESOLVED"} for item in issues):
|
||||
fail("UNRESOLVED_ISSUES_INVALID")
|
||||
return raw_paths
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--repo-root", type=Path, required=True)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument("--source-commit", required=True)
|
||||
parser.add_argument("--expected-base", required=True)
|
||||
parser.add_argument("--authority-main", required=True)
|
||||
parser.add_argument("--executed-at", required=True)
|
||||
parser.add_argument("--observations", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
root = args.repo_root.resolve(strict=True)
|
||||
output = args.output
|
||||
if not output.is_absolute() or root not in output.resolve().parents or output.exists():
|
||||
fail("OUTPUT_PATH_DENIED")
|
||||
source = run(root, "rev-parse", args.source_commit)
|
||||
tree = run(root, "rev-parse", f"{source}^{{tree}}")
|
||||
if run(root, "merge-base", args.expected_base, source) != args.expected_base:
|
||||
fail("EXPECTED_BASE_NOT_ANCESTOR")
|
||||
observations = load_json(args.observations, "OBSERVATION_JSON_INVALID")
|
||||
raw_paths = validate_observations(observations, source)
|
||||
changed = run(root, "diff", "--name-only", f"{args.expected_base}..{source}").splitlines()
|
||||
numstat = run(root, "diff", "--numstat", f"{args.expected_base}..{source}").splitlines()
|
||||
|
||||
# No output is created until every observation and raw semantic binding passes.
|
||||
output.mkdir(parents=True, exist_ok=False)
|
||||
for name, path in raw_paths.items():
|
||||
shutil.copyfile(path, output / name)
|
||||
environment = {
|
||||
"executed_at": args.executed_at,
|
||||
"git_version": subprocess.check_output(["git", "--version"], text=True).strip(),
|
||||
"platform": platform.platform(),
|
||||
"python_executable": str(Path(sys.executable).resolve(strict=True)),
|
||||
"python_version": platform.python_version(),
|
||||
"generator": "scripts/generate_v2r1c4_v3_evidence.py",
|
||||
"generator_schema": "aios.v2r1c4-security-evidence/v3",
|
||||
}
|
||||
write_json(output / "SOURCE_MANIFEST.json", {**environment, "authority_main_oid": args.authority_main, "expected_base_oid": args.expected_base, "release_branch": BRANCH, "source_commit_oid": source, "source_tree_oid": tree})
|
||||
write_json(output / "RELEASE_IDENTIFIERS.json", {"artifact_sha256": "NOT_BUILT", "expected_base_oid": args.expected_base, "production_baseline": "V2R1C3_UNCHANGED", "registry_version": "V2R1C3_UNCHANGED", "release_commit_oid": source, "release_tag": "NOT_CREATED", "release_tree_oid": tree, "rollback_sha256": "NOT_BUILT"})
|
||||
write_json(output / "DIFF_SUMMARY.json", {"changed_file_count": len(changed), "numstat": numstat, "range": f"{args.expected_base}..{source}"})
|
||||
write_json(output / "CHANGED_FILES.json", {"files": changed})
|
||||
write_json(output / "SECURITY_INVARIANT_MATRIX.json", {"fixed": 9, "total": 9, "invariants": observations["security_invariants"]})
|
||||
(output / "THREAT_MODEL.md").write_text("# V2R1C4 v3 threat model\n\nAssets: Owner approval provenance, release objects, rollback root, Writer credentials, candidate-only state.\n\nTrust boundaries: Gitea/Git authority, non-root Writer and Publisher, local loopback HTTP, Unix socket activation, systemd state directories.\n\nClosed threats: manifest substitution, digest-only declaration, partial activation, rollback target substitution, worktree/remote/credential confusion, early or partial revocation, root-owned directory denial, loopback parser bypass/redirect leak, and candidate state escaping into production.\n\nResidual boundary: the real live-tag test remains intentionally disabled because this task forbids formal tag creation.\n", encoding="utf-8")
|
||||
outputs = {
|
||||
"TEST_COMMANDS.json": {"commands": observations["commands"]},
|
||||
"TEST_RESULTS.json": observations["test_results"],
|
||||
"FAILURE_INJECTION_RESULTS.json": observations["failure_injection_results"],
|
||||
"SYSTEMD_VALIDATION.json": observations["systemd_validation"],
|
||||
"NON_ROOT_PERMISSION_RESULTS.json": observations["non_root_permission_results"],
|
||||
"DEPENDENCY_SCAN.json": observations["dependency_scan"],
|
||||
"SECRET_SCAN.json": observations["secret_scan"],
|
||||
"PATH_TRAVERSAL_SCAN.json": observations["path_traversal_scan"],
|
||||
"CREDENTIAL_LEAK_SCAN.json": observations["credential_leak_scan"],
|
||||
"ARTIFACT_REPRODUCIBILITY.json": observations["artifact_reproducibility"],
|
||||
"VALIDATOR_REPORT.json": observations["validator_report"],
|
||||
"UNRESOLVED_ISSUES.json": observations["unresolved_issues"],
|
||||
}
|
||||
for name, value in outputs.items():
|
||||
write_json(output / name, value)
|
||||
payload_files = sorted(path for path in output.iterdir() if path.is_file())
|
||||
write_json(output / "FILE_SHA256.json", {"files": {path.name: sha256(path) for path in payload_files}})
|
||||
registered = sorted(path for path in output.iterdir() if path.is_file())
|
||||
write_json(output / "EVIDENCE_MANIFEST.json", {**environment, "files": {path.name: sha256(path) for path in registered}, "source_commit_oid": source, "source_tree_oid": tree, "status": "CANDIDATE_READY_FOR_INDEPENDENT_VALIDATION"})
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -16,7 +16,7 @@ required=(
|
||||
"config/runtime_selector.yaml"
|
||||
)
|
||||
for f in "${required[@]}"; do test -f "$ROOT/$f" || { echo "MISSING $f"; exit 1; }; done
|
||||
grep -Eq "^V1R1C[0-9]+$" "$ROOT/VERSION"
|
||||
grep -Eq "^V[0-9]+R[0-9]+C[0-9]+$" "$ROOT/VERSION"
|
||||
python3 -m py_compile \
|
||||
"$ROOT/active-runtime/app/aios_gitops_model_api.py" \
|
||||
"$ROOT/active-runtime/app/aios_chatgpt_actions_api.py" \
|
||||
|
||||
Executable
+652
@@ -0,0 +1,652 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Hardened WeChat outbound backend for AIOS.
|
||||
|
||||
Only the dedicated UCC adapter is a supported send backend. OpenClaw channel
|
||||
discovery is retained for diagnostics, but generic OpenClaw chat or agent
|
||||
delivery is never promoted to WeChat delivery.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import selectors
|
||||
import signal
|
||||
import subprocess
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
|
||||
OPENCLAW_BIN = os.environ.get("AIOS_OPENCLAW_BIN", "openclaw")
|
||||
WECHAT_ACCOUNT_ID = os.environ.get("AIOS_WECHAT_ACCOUNT_ID", "")
|
||||
WECHAT_CHANNEL = os.environ.get("AIOS_WECHAT_CHANNEL", "")
|
||||
SEND_BACKEND = os.environ.get("AIOS_WECHAT_SEND_BACKEND", "")
|
||||
UCC_ADAPTER_URL = os.environ.get("AIOS_UCC_WECHAT_ADAPTER_URL", "http://127.0.0.1:18800")
|
||||
UCC_ADAPTER_TOKEN = os.environ.get("AIOS_UCC_WECHAT_ADAPTER_TOKEN") or os.environ.get(
|
||||
"AIOS_WECHAT_QR_ADAPTER_TOKEN", ""
|
||||
)
|
||||
UCC_SEND_PATH = os.environ.get("AIOS_UCC_WECHAT_SEND_PATH", "/api/internal/wechat/send")
|
||||
_allowed_origins = os.environ.get("AIOS_UCC_WECHAT_ALLOWED_ORIGINS", "").strip()
|
||||
UCC_ADAPTER_ALLOWED_ORIGINS = tuple(
|
||||
origin.strip() for origin in _allowed_origins.split(",") if origin.strip()
|
||||
) or (
|
||||
"http://127.0.0.1:18800",
|
||||
"http://[::1]:18800",
|
||||
)
|
||||
|
||||
MAX_TARGET_BYTES = 512
|
||||
MAX_CONTENT_BYTES = 32 * 1024
|
||||
MAX_IDENTIFIER_BYTES = 512
|
||||
MAX_METADATA_BYTES = 8 * 1024
|
||||
MAX_METADATA_DEPTH = 32
|
||||
MAX_METADATA_NODES = 1024
|
||||
MAX_RESPONSE_BYTES = 64 * 1024
|
||||
MAX_ENDPOINT_BYTES = 256
|
||||
_DEFAULT_TIMEOUT_SECONDS = 10
|
||||
_MAX_TIMEOUT_SECONDS = 30
|
||||
_LEGACY_SEND_PATH = "/internal/channels/wechat/send"
|
||||
_SAFE_PATH = re.compile(r"^/[A-Za-z0-9._~!$&'()*+,;=:@%/-]+$")
|
||||
_MAX_PATH_DECODE_ROUNDS = 4
|
||||
_OPENCLAW_TIMEOUT_RETURN_CODE = 124
|
||||
_OPENCLAW_OUTPUT_LIMIT_RETURN_CODE = 125
|
||||
_OPENCLAW_SPAWN_ERROR_RETURN_CODE = 126
|
||||
|
||||
|
||||
def _bounded_timeout(value: str) -> int:
|
||||
try:
|
||||
parsed = int(value)
|
||||
except (TypeError, ValueError):
|
||||
return _DEFAULT_TIMEOUT_SECONDS
|
||||
return min(max(parsed, 1), _MAX_TIMEOUT_SECONDS)
|
||||
|
||||
|
||||
REQUEST_TIMEOUT_SECONDS = _bounded_timeout(os.environ.get("AIOS_UCC_WECHAT_TIMEOUT_SECONDS", "10"))
|
||||
|
||||
|
||||
class BackendResult:
|
||||
def __init__(self, ok: bool, backend: str, status: str, data: Dict[str, Any]) -> None:
|
||||
self.ok = ok
|
||||
self.backend = backend
|
||||
self.status = status
|
||||
self.data = data
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {"ok": self.ok, "backend": self.backend, "status": self.status, **self.data}
|
||||
|
||||
|
||||
class _BackendProtocolError(Exception):
|
||||
"""Internal marker whose details must never cross the module boundary."""
|
||||
|
||||
|
||||
def _openclaw_base_args() -> List[str]:
|
||||
"""Return the OpenClaw 2026.6.11-compatible command prefix.
|
||||
|
||||
Gateway/auth global flags are not supported by the installed CLI and would
|
||||
also expose credentials through the process argument vector.
|
||||
"""
|
||||
|
||||
return [OPENCLAW_BIN]
|
||||
|
||||
|
||||
def _terminate_process_tree(process: Any) -> None:
|
||||
if os.name == "posix":
|
||||
try:
|
||||
os.killpg(process.pid, signal.SIGTERM)
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
process.wait(timeout=0.5)
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
pass
|
||||
time.sleep(0.05)
|
||||
try:
|
||||
os.killpg(process.pid, signal.SIGKILL)
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
process.wait(timeout=0.5)
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
pass
|
||||
return
|
||||
if process.poll() is not None:
|
||||
return
|
||||
try:
|
||||
process.terminate()
|
||||
process.wait(timeout=1)
|
||||
return
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
pass
|
||||
try:
|
||||
process.kill()
|
||||
process.wait(timeout=1)
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
pass
|
||||
|
||||
|
||||
def _run_openclaw(
|
||||
args: List[str], timeout: int = 15
|
||||
) -> subprocess.CompletedProcess[str]:
|
||||
bounded_timeout = min(max(int(timeout), 1), _MAX_TIMEOUT_SECONDS)
|
||||
popen_options: Dict[str, Any] = {
|
||||
"stdout": subprocess.PIPE,
|
||||
"stderr": subprocess.PIPE,
|
||||
}
|
||||
if os.name == "posix":
|
||||
popen_options["start_new_session"] = True
|
||||
elif hasattr(subprocess, "CREATE_NEW_PROCESS_GROUP"):
|
||||
popen_options["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
|
||||
try:
|
||||
process = subprocess.Popen(args, **popen_options)
|
||||
except (OSError, ValueError):
|
||||
return subprocess.CompletedProcess(args, _OPENCLAW_SPAWN_ERROR_RETURN_CODE, "", "")
|
||||
|
||||
selector = selectors.DefaultSelector()
|
||||
streams = {"stdout": process.stdout, "stderr": process.stderr}
|
||||
buffers = {"stdout": bytearray(), "stderr": bytearray()}
|
||||
try:
|
||||
for name, stream in streams.items():
|
||||
if stream is not None:
|
||||
selector.register(stream, selectors.EVENT_READ, name)
|
||||
except (OSError, ValueError):
|
||||
selector.close()
|
||||
_terminate_process_tree(process)
|
||||
for stream in streams.values():
|
||||
if stream is not None:
|
||||
stream.close()
|
||||
return subprocess.CompletedProcess(args, _OPENCLAW_SPAWN_ERROR_RETURN_CODE, "", "")
|
||||
|
||||
deadline = time.monotonic() + bounded_timeout
|
||||
total_bytes = 0
|
||||
return_code: Optional[int] = None
|
||||
try:
|
||||
while selector.get_map():
|
||||
remaining = deadline - time.monotonic()
|
||||
if remaining <= 0:
|
||||
return_code = _OPENCLAW_TIMEOUT_RETURN_CODE
|
||||
break
|
||||
events = selector.select(min(remaining, 0.1))
|
||||
for key, _mask in events:
|
||||
allowance = MAX_RESPONSE_BYTES - total_bytes
|
||||
try:
|
||||
chunk = os.read(key.fileobj.fileno(), min(8192, allowance + 1))
|
||||
except OSError:
|
||||
return_code = _OPENCLAW_SPAWN_ERROR_RETURN_CODE
|
||||
break
|
||||
if not chunk:
|
||||
selector.unregister(key.fileobj)
|
||||
key.fileobj.close()
|
||||
continue
|
||||
if len(chunk) > allowance:
|
||||
return_code = _OPENCLAW_OUTPUT_LIMIT_RETURN_CODE
|
||||
break
|
||||
buffers[key.data].extend(chunk)
|
||||
total_bytes += len(chunk)
|
||||
if return_code is not None:
|
||||
break
|
||||
finally:
|
||||
selector.close()
|
||||
|
||||
if return_code is not None:
|
||||
_terminate_process_tree(process)
|
||||
for stream in streams.values():
|
||||
if stream is not None and not stream.closed:
|
||||
stream.close()
|
||||
return subprocess.CompletedProcess(args, return_code, "", "")
|
||||
try:
|
||||
completed_return_code = process.wait(timeout=1)
|
||||
except subprocess.TimeoutExpired:
|
||||
_terminate_process_tree(process)
|
||||
return subprocess.CompletedProcess(args, _OPENCLAW_TIMEOUT_RETURN_CODE, "", "")
|
||||
stdout = buffers["stdout"].decode("utf-8", errors="replace")
|
||||
stderr = buffers["stderr"].decode("utf-8", errors="replace")
|
||||
return subprocess.CompletedProcess(args, completed_return_code, stdout, stderr)
|
||||
|
||||
|
||||
def _json_dict(text: str) -> Dict[str, Any]:
|
||||
try:
|
||||
value = json.loads(text or "{}")
|
||||
except (RecursionError, TypeError, ValueError, json.JSONDecodeError):
|
||||
return {}
|
||||
return value if isinstance(value, dict) else {}
|
||||
|
||||
|
||||
def discover_openclaw_wechat_channel() -> Optional[str]:
|
||||
"""Discover an explicitly typed WeChat/Weixin channel for diagnostics."""
|
||||
|
||||
if WECHAT_CHANNEL:
|
||||
return WECHAT_CHANNEL
|
||||
try:
|
||||
proc = _run_openclaw(_openclaw_base_args() + ["channels", "list", "--json"], timeout=15)
|
||||
except (OSError, subprocess.SubprocessError, ValueError):
|
||||
return None
|
||||
if proc.returncode != 0:
|
||||
return None
|
||||
data = _json_dict(proc.stdout[:MAX_RESPONSE_BYTES])
|
||||
candidates: List[str] = []
|
||||
|
||||
def walk(value: Any) -> None:
|
||||
if isinstance(value, dict):
|
||||
name = value.get("name") or value.get("channel") or value.get("id")
|
||||
kind = value.get("type") or value.get("kind")
|
||||
if isinstance(name, str) and isinstance(kind, str):
|
||||
label = f"{name} {kind}".lower()
|
||||
if "wechat" in label or "weixin" in label:
|
||||
candidates.append(name)
|
||||
for child in value.values():
|
||||
walk(child)
|
||||
elif isinstance(value, list):
|
||||
for child in value:
|
||||
walk(child)
|
||||
|
||||
walk(data)
|
||||
return candidates[0] if candidates else None
|
||||
|
||||
|
||||
def _auth_headers() -> Dict[str, str]:
|
||||
headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
||||
if UCC_ADAPTER_TOKEN:
|
||||
headers["Authorization"] = "Bearer " + UCC_ADAPTER_TOKEN
|
||||
return headers
|
||||
|
||||
|
||||
def _safe_path(value: Any) -> Optional[str]:
|
||||
if not isinstance(value, str):
|
||||
return None
|
||||
path = value if value.startswith("/") else "/" + value
|
||||
try:
|
||||
encoded_path = path.encode("utf-8")
|
||||
except UnicodeEncodeError:
|
||||
return None
|
||||
if len(encoded_path) > MAX_ENDPOINT_BYTES:
|
||||
return None
|
||||
if not _SAFE_PATH.fullmatch(path):
|
||||
return None
|
||||
decoded = path
|
||||
for _ in range(_MAX_PATH_DECODE_ROUNDS):
|
||||
try:
|
||||
next_decoded = urllib.parse.unquote_to_bytes(decoded).decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
return None
|
||||
if next_decoded == decoded:
|
||||
break
|
||||
decoded = next_decoded
|
||||
if "%" in decoded or "\\" in decoded or "\x00" in decoded:
|
||||
return None
|
||||
if not _SAFE_PATH.fullmatch(decoded):
|
||||
return None
|
||||
if any(segment in {".", ".."} for segment in decoded.split("/")):
|
||||
return None
|
||||
return path
|
||||
|
||||
|
||||
def _unique_paths(paths: List[str]) -> List[str]:
|
||||
out: List[str] = []
|
||||
for candidate in paths:
|
||||
path = _safe_path(candidate)
|
||||
if path and path not in out:
|
||||
out.append(path)
|
||||
if len(out) == 2:
|
||||
break
|
||||
return out
|
||||
|
||||
|
||||
def _ucc_send_paths() -> List[str]:
|
||||
canonical = _safe_path(UCC_SEND_PATH) or "/api/internal/wechat/send"
|
||||
return _unique_paths([canonical, _LEGACY_SEND_PATH])
|
||||
|
||||
|
||||
def _canonical_origin(value: Any) -> Optional[str]:
|
||||
if not isinstance(value, str) or not value or len(value) > 2048:
|
||||
return None
|
||||
if "\\" in value or any(ord(character) < 32 for character in value):
|
||||
return None
|
||||
try:
|
||||
parsed = urllib.parse.urlsplit(value)
|
||||
port = parsed.port
|
||||
except (UnicodeError, ValueError):
|
||||
return None
|
||||
scheme = parsed.scheme.lower()
|
||||
if scheme not in {"http", "https"} or not parsed.hostname:
|
||||
return None
|
||||
if parsed.username or parsed.password or parsed.query or parsed.fragment or parsed.path not in {"", "/"}:
|
||||
return None
|
||||
try:
|
||||
host = parsed.hostname.rstrip(".").encode("idna").decode("ascii").lower()
|
||||
except UnicodeError:
|
||||
return None
|
||||
if not host:
|
||||
return None
|
||||
if port is None:
|
||||
port = 80 if scheme == "http" else 443
|
||||
rendered_host = f"[{host}]" if ":" in host else host
|
||||
return f"{scheme}://{rendered_host}:{port}"
|
||||
|
||||
|
||||
def _adapter_base_url() -> Optional[str]:
|
||||
origin = _canonical_origin(UCC_ADAPTER_URL)
|
||||
allowed = {
|
||||
canonical
|
||||
for canonical in (_canonical_origin(value) for value in UCC_ADAPTER_ALLOWED_ORIGINS)
|
||||
if canonical
|
||||
}
|
||||
return origin if origin and origin in allowed else None
|
||||
|
||||
|
||||
class _NoRedirectHandler(urllib.request.HTTPRedirectHandler):
|
||||
def redirect_request(
|
||||
self,
|
||||
req: Any,
|
||||
fp: Any,
|
||||
code: int,
|
||||
msg: str,
|
||||
headers: Any,
|
||||
newurl: str,
|
||||
) -> None:
|
||||
del req, fp, code, msg, headers, newurl
|
||||
return None
|
||||
|
||||
|
||||
def _open_no_redirect(request: urllib.request.Request, timeout: int) -> Any:
|
||||
opener = urllib.request.build_opener(_NoRedirectHandler())
|
||||
return opener.open(request, timeout=timeout)
|
||||
|
||||
|
||||
def ucc_adapter_alive() -> bool:
|
||||
base_url = _adapter_base_url()
|
||||
if not base_url:
|
||||
return False
|
||||
request = urllib.request.Request(
|
||||
base_url + "/health", headers=_auth_headers(), method="GET"
|
||||
)
|
||||
try:
|
||||
with _open_no_redirect(request, timeout=min(5, REQUEST_TIMEOUT_SECONDS)) as response:
|
||||
return 200 <= int(response.status) < 300
|
||||
except (OSError, ValueError, urllib.error.URLError):
|
||||
return False
|
||||
|
||||
|
||||
def select_backend() -> Dict[str, Any]:
|
||||
explicit = SEND_BACKEND.strip().lower()
|
||||
if explicit:
|
||||
if explicit == "ucc-adapter":
|
||||
return {"backend": "ucc-adapter", "reason": "explicit"}
|
||||
return {"backend": "unavailable", "reason": "unsupported_backend"}
|
||||
if ucc_adapter_alive():
|
||||
return {"backend": "ucc-adapter", "reason": "ucc_adapter_alive"}
|
||||
return {"backend": "unavailable", "reason": "ucc_adapter_unavailable"}
|
||||
|
||||
|
||||
def _post_ucc(path: str, payload: Dict[str, Any]) -> Tuple[int, Dict[str, Any]]:
|
||||
base_url = _adapter_base_url()
|
||||
safe_path = _safe_path(path)
|
||||
if not base_url or not safe_path:
|
||||
raise _BackendProtocolError("invalid adapter endpoint")
|
||||
try:
|
||||
body = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise _BackendProtocolError("invalid request payload") from exc
|
||||
request = urllib.request.Request(
|
||||
base_url + safe_path,
|
||||
data=body,
|
||||
headers=_auth_headers(),
|
||||
method="POST",
|
||||
)
|
||||
with _open_no_redirect(request, timeout=REQUEST_TIMEOUT_SECONDS) as response:
|
||||
raw = response.read(MAX_RESPONSE_BYTES + 1)
|
||||
if len(raw) > MAX_RESPONSE_BYTES:
|
||||
raise _BackendProtocolError("adapter response too large")
|
||||
try:
|
||||
text = raw.decode("utf-8")
|
||||
except UnicodeDecodeError as exc:
|
||||
raise _BackendProtocolError("adapter response encoding invalid") from exc
|
||||
data = _json_dict(text)
|
||||
if not data:
|
||||
raise _BackendProtocolError("adapter response invalid")
|
||||
return int(response.status), data
|
||||
|
||||
|
||||
def _ucc_response_sent(data: Dict[str, Any]) -> bool:
|
||||
return isinstance(data, dict) and data.get("ok") is True and data.get("status") == "sent"
|
||||
|
||||
|
||||
def _ucc_result_is_ok(data: Dict[str, Any], dry_run: bool = False) -> bool:
|
||||
expected_status = "dry_run" if dry_run else "sent"
|
||||
return isinstance(data, dict) and data.get("ok") is True and data.get("status") == expected_status
|
||||
|
||||
|
||||
def _utf8_size(value: str) -> Optional[int]:
|
||||
try:
|
||||
return len(value.encode("utf-8"))
|
||||
except UnicodeEncodeError:
|
||||
return None
|
||||
|
||||
|
||||
def _valid_identifier(value: Any, required: bool = False) -> bool:
|
||||
if not isinstance(value, str):
|
||||
return False
|
||||
if required and not value.strip():
|
||||
return False
|
||||
if not value:
|
||||
return not required
|
||||
if len(value) > MAX_IDENTIFIER_BYTES:
|
||||
return False
|
||||
size = _utf8_size(value)
|
||||
return size is not None and size <= MAX_IDENTIFIER_BYTES and not any(
|
||||
ord(character) < 32 or ord(character) == 127 for character in value
|
||||
)
|
||||
|
||||
|
||||
def _validate_metadata(metadata: Any) -> Optional[str]:
|
||||
if not isinstance(metadata, dict):
|
||||
return "invalid_input"
|
||||
nodes = 0
|
||||
stack: List[Tuple[Any, int]] = [(metadata, 0)]
|
||||
while stack:
|
||||
value, depth = stack.pop()
|
||||
nodes += 1
|
||||
if depth > MAX_METADATA_DEPTH or nodes > MAX_METADATA_NODES:
|
||||
return "input_too_large"
|
||||
if isinstance(value, dict):
|
||||
if len(value) + nodes + len(stack) > MAX_METADATA_NODES:
|
||||
return "input_too_large"
|
||||
for key, child in value.items():
|
||||
if (
|
||||
not isinstance(key, str)
|
||||
or len(key) > MAX_METADATA_BYTES
|
||||
or _utf8_size(key) is None
|
||||
):
|
||||
return "invalid_input"
|
||||
stack.append((child, depth + 1))
|
||||
elif isinstance(value, list):
|
||||
if len(value) + nodes + len(stack) > MAX_METADATA_NODES:
|
||||
return "input_too_large"
|
||||
stack.extend((child, depth + 1) for child in value)
|
||||
elif isinstance(value, str):
|
||||
if len(value) > MAX_METADATA_BYTES:
|
||||
return "input_too_large"
|
||||
if _utf8_size(value) is None:
|
||||
return "invalid_input"
|
||||
elif value is not None and not isinstance(value, (bool, int, float)):
|
||||
return "invalid_input"
|
||||
try:
|
||||
encoded = json.dumps(
|
||||
metadata,
|
||||
ensure_ascii=False,
|
||||
allow_nan=False,
|
||||
separators=(",", ":"),
|
||||
).encode("utf-8")
|
||||
except (RecursionError, TypeError, UnicodeEncodeError, ValueError):
|
||||
return "invalid_input"
|
||||
return "input_too_large" if len(encoded) > MAX_METADATA_BYTES else None
|
||||
|
||||
|
||||
def _validate_payload(
|
||||
target: Any,
|
||||
content: Any,
|
||||
account_id: Any,
|
||||
message_id: Any,
|
||||
metadata: Any,
|
||||
dry_run: Any,
|
||||
) -> Optional[str]:
|
||||
if not isinstance(target, str) or not isinstance(content, str):
|
||||
return "invalid_input"
|
||||
if not target.strip() or not content.strip() or not isinstance(dry_run, bool):
|
||||
return "invalid_input"
|
||||
if any(ord(character) < 32 or ord(character) == 127 for character in target):
|
||||
return "invalid_input"
|
||||
if "\x00" in content or not _valid_identifier(account_id) or not _valid_identifier(message_id):
|
||||
return "invalid_input"
|
||||
if len(target) > MAX_TARGET_BYTES or len(content) > MAX_CONTENT_BYTES:
|
||||
return "input_too_large"
|
||||
target_size = _utf8_size(target)
|
||||
content_size = _utf8_size(content)
|
||||
if target_size is None or content_size is None:
|
||||
return "invalid_input"
|
||||
if target_size > MAX_TARGET_BYTES or content_size > MAX_CONTENT_BYTES:
|
||||
return "input_too_large"
|
||||
return _validate_metadata(metadata)
|
||||
|
||||
|
||||
def _safe_provider_message_id(value: Any) -> Optional[str]:
|
||||
if not isinstance(value, str) or not value:
|
||||
return None
|
||||
if len(value) > MAX_IDENTIFIER_BYTES:
|
||||
return None
|
||||
size = _utf8_size(value)
|
||||
if size is None or size > MAX_IDENTIFIER_BYTES:
|
||||
return None
|
||||
if any(ord(character) < 32 or ord(character) == 127 for character in value):
|
||||
return None
|
||||
return value
|
||||
|
||||
|
||||
def send_ucc_adapter(
|
||||
target: str,
|
||||
content: str,
|
||||
account_id: str = "",
|
||||
message_id: str = "",
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
dry_run: bool = False,
|
||||
) -> BackendResult:
|
||||
effective_account = account_id or WECHAT_ACCOUNT_ID
|
||||
effective_metadata: Any = {} if metadata is None else metadata
|
||||
error_code = _validate_payload(
|
||||
target,
|
||||
content,
|
||||
effective_account,
|
||||
message_id,
|
||||
effective_metadata,
|
||||
dry_run,
|
||||
)
|
||||
if error_code:
|
||||
return BackendResult(False, "ucc-adapter", "failed", {"code": error_code})
|
||||
|
||||
payload = {
|
||||
"target": target,
|
||||
"content": content,
|
||||
"account_id": effective_account,
|
||||
"message_id": message_id,
|
||||
"metadata": effective_metadata,
|
||||
"dry_run": dry_run,
|
||||
}
|
||||
last_endpoint: Optional[str] = None
|
||||
for path in _ucc_send_paths():
|
||||
last_endpoint = path
|
||||
try:
|
||||
http_status, data = _post_ucc(path, payload)
|
||||
except urllib.error.HTTPError as exc:
|
||||
if exc.code in {404, 405}:
|
||||
continue
|
||||
return BackendResult(
|
||||
False,
|
||||
"ucc-adapter",
|
||||
"failed",
|
||||
{"code": "ucc_adapter_http_error", "http_status": int(exc.code), "endpoint": path},
|
||||
)
|
||||
except (OSError, ValueError, urllib.error.URLError, _BackendProtocolError):
|
||||
return BackendResult(
|
||||
False,
|
||||
"ucc-adapter",
|
||||
"failed",
|
||||
{"code": "ucc_adapter_unavailable", "endpoint": path},
|
||||
)
|
||||
except Exception:
|
||||
# Fail closed without returning exception text, type, or chained cause.
|
||||
return BackendResult(
|
||||
False,
|
||||
"ucc-adapter",
|
||||
"failed",
|
||||
{"code": "ucc_adapter_unavailable", "endpoint": path},
|
||||
)
|
||||
|
||||
if not (200 <= http_status < 300):
|
||||
return BackendResult(
|
||||
False,
|
||||
"ucc-adapter",
|
||||
"failed",
|
||||
{"code": "ucc_adapter_http_error", "http_status": http_status, "endpoint": path},
|
||||
)
|
||||
if not _ucc_result_is_ok(data, dry_run):
|
||||
return BackendResult(
|
||||
False,
|
||||
"ucc-adapter",
|
||||
"failed",
|
||||
{"code": "ucc_adapter_rejected", "http_status": http_status, "endpoint": path},
|
||||
)
|
||||
confirmed_status = "dry_run" if dry_run else "sent"
|
||||
result_data: Dict[str, Any] = {
|
||||
"code": confirmed_status,
|
||||
"http_status": http_status,
|
||||
"endpoint": path,
|
||||
}
|
||||
provider_message_id = _safe_provider_message_id(data.get("provider_message_id"))
|
||||
if provider_message_id:
|
||||
result_data["provider_message_id"] = provider_message_id
|
||||
return BackendResult(True, "ucc-adapter", confirmed_status, result_data)
|
||||
|
||||
data: Dict[str, Any] = {"code": "ucc_adapter_unavailable"}
|
||||
if last_endpoint:
|
||||
data["endpoint"] = last_endpoint
|
||||
return BackendResult(False, "ucc-adapter", "failed", data)
|
||||
|
||||
|
||||
def send_openclaw_channel(
|
||||
target: str,
|
||||
content: str,
|
||||
account_id: str = "",
|
||||
message_id: str = "",
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
dry_run: bool = False,
|
||||
) -> BackendResult:
|
||||
del target, content, account_id, message_id, metadata, dry_run
|
||||
return BackendResult(
|
||||
False,
|
||||
"openclaw-channel",
|
||||
"failed",
|
||||
{"code": "unsupported_backend"},
|
||||
)
|
||||
|
||||
|
||||
def send_message(
|
||||
target: str,
|
||||
content: str,
|
||||
account_id: str = "",
|
||||
message_id: str = "",
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
dry_run: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
selected = select_backend()
|
||||
if selected.get("backend") == "ucc-adapter":
|
||||
return send_ucc_adapter(
|
||||
target, content, account_id, message_id, metadata, dry_run
|
||||
).to_dict()
|
||||
code = "unsupported_backend" if selected.get("reason") == "unsupported_backend" else "wechat_outbound_backend_unavailable"
|
||||
return BackendResult(False, "unavailable", "failed", {"code": code}).to_dict()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(json.dumps(select_backend(), ensure_ascii=False, separators=(",", ":")))
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user