Compare commits
38
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"
|
||||
}
|
||||
@@ -1,22 +1,22 @@
|
||||
[Unit]
|
||||
Description=AIOS Autonomous Release Candidate API (Dormant/Shadow Only)
|
||||
After=network.target
|
||||
ConditionPathExists=/opt/aios-autonomous-release-candidate/aios_autonomous_release_candidate
|
||||
Description=AIOS Autonomous Release Candidate (dormant shadow only)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[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
|
||||
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
|
||||
StateDirectory=aios-autonomous-release-candidate
|
||||
RuntimeDirectory=aios-autonomous-release-candidate
|
||||
Restart=on-failure
|
||||
RestartSec=2
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
@@ -24,19 +24,19 @@ 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
|
||||
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,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,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.
|
||||
@@ -1,47 +1,82 @@
|
||||
# AIOS Autonomous Release Execution Package
|
||||
|
||||
## Mode
|
||||
## Release Strategy
|
||||
|
||||
Candidate/shadow first. Production changes require an independent release approval.
|
||||
V2R1C4 uses a two-stage model:
|
||||
|
||||
## Flow
|
||||
1. Dormant code deployment.
|
||||
2. Capability activation through a separate approved release.
|
||||
|
||||
1. Create TaskPlan.
|
||||
2. Validate policy and approval envelope.
|
||||
3. Route workers by capability and trust zone.
|
||||
4. Execute on isolated candidate branch.
|
||||
5. Collect evidence chain and artifact manifest.
|
||||
6. Verify release plan.
|
||||
7. Wait for owner approval.
|
||||
8. Deploy only after approval.
|
||||
## Dormant Deployment
|
||||
|
||||
## Hard stops
|
||||
The first production release must keep autonomous execution disabled:
|
||||
|
||||
- main writes
|
||||
- force push
|
||||
- tag rewrite
|
||||
- baseline mutation
|
||||
- registry mutation
|
||||
- plaintext credential handling
|
||||
- missing evidence closure
|
||||
|
||||
## Required evidence
|
||||
|
||||
- git ref closure
|
||||
- test report
|
||||
- artifact checksum
|
||||
- manifest checksum
|
||||
- credential revoke proof
|
||||
- production invariant snapshot
|
||||
|
||||
## Production activation
|
||||
|
||||
The executor must keep:
|
||||
|
||||
```
|
||||
```text
|
||||
AIOS_GITOPS_WRITES_ENABLED=false
|
||||
AIOS_TASK_PLANE_EXECUTION_ENABLED=false
|
||||
AIOS_SILENT_RELEASE_EXECUTION_ENABLED=false
|
||||
```
|
||||
|
||||
until release approval is independently verified.
|
||||
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,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"
|
||||
|
||||
@@ -1,68 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build a deterministic Candidate artifact without overwriting evidence."""
|
||||
"""Build deterministic dormant-release candidate artifacts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from aios_autonomous_release_candidate.artifact import ArtifactBuilder
|
||||
import tarfile
|
||||
|
||||
|
||||
def exclusive_write(path: Path, content: bytes, mode: int) -> None:
|
||||
if path.is_symlink():
|
||||
raise RuntimeError("artifact output symlink rejected")
|
||||
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
|
||||
if hasattr(os, "O_NOFOLLOW"):
|
||||
flags |= os.O_NOFOLLOW
|
||||
descriptor = os.open(path, flags, mode)
|
||||
try:
|
||||
os.write(descriptor, content)
|
||||
os.fsync(descriptor)
|
||||
finally:
|
||||
os.close(descriptor)
|
||||
def sha256(value: bytes) -> str:
|
||||
return hashlib.sha256(value).hexdigest()
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
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", required=True, type=Path)
|
||||
parser.add_argument("--source-oid", required=True)
|
||||
parser.add_argument("--tree-oid", required=True)
|
||||
parser.add_argument("--output-directory", required=True, type=Path)
|
||||
parser.add_argument("--name", default="aios-autonomous-release-candidate")
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = build_parser().parse_args(argv)
|
||||
output = args.output_directory
|
||||
output.mkdir(parents=True, exist_ok=True, mode=0o750)
|
||||
if output.is_symlink():
|
||||
raise RuntimeError("artifact output directory symlink rejected")
|
||||
os.chmod(output, output.stat().st_mode & 0o750)
|
||||
builder = ArtifactBuilder()
|
||||
built = builder.build_from_directory(args.source_oid, args.tree_oid, args.source)
|
||||
builder.verify(built)
|
||||
artifact_path = output / f"{args.name}.tar"
|
||||
manifest_path = output / f"{args.name}.manifest.json"
|
||||
exclusive_write(artifact_path, built.content, 0o640)
|
||||
exclusive_write(manifest_path, built.manifest.as_json_bytes(), 0o640)
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"artifact": str(artifact_path),
|
||||
"manifest": str(manifest_path),
|
||||
"artifact_sha256": built.manifest.artifact_sha256,
|
||||
"manifest_sha256": built.manifest.manifest_sha256,
|
||||
"package_closure_sha256": built.manifest.package_closure_sha256,
|
||||
"secrets_included": False,
|
||||
"production_side_effects": False,
|
||||
},
|
||||
sort_keys=True,
|
||||
)
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -1,513 +1,141 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run Candidate acceptance and emit a non-overwriting JSON evidence record."""
|
||||
"""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 datetime import datetime, timedelta, timezone
|
||||
import hashlib
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from typing import Sequence
|
||||
from typing import Any, Mapping
|
||||
|
||||
|
||||
sys.dont_write_bytecode = True
|
||||
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))
|
||||
|
||||
|
||||
ALLOWED_PREFIXES = (
|
||||
"services/aios-autonomous-release-candidate/",
|
||||
"tests/autonomous_release/",
|
||||
"tests/autonomous-release/",
|
||||
"scripts/autonomous-release/",
|
||||
"docs/autonomous-release/",
|
||||
)
|
||||
ALLOWED_EXACT = {
|
||||
"deploy/systemd/aios-autonomous-release-candidate.service",
|
||||
"deploy/systemd/aios-autonomous-release-worker-candidate.service",
|
||||
}
|
||||
SCAN_PREFIXES = (
|
||||
"services/aios-autonomous-release-candidate",
|
||||
"tests/autonomous_release",
|
||||
"tests/autonomous-release",
|
||||
"scripts/autonomous-release",
|
||||
"docs/autonomous-release",
|
||||
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
|
||||
|
||||
|
||||
def command(repo: Path, argv: Sequence[str], environment: dict[str, str]) -> dict[str, object]:
|
||||
completed = subprocess.run(
|
||||
argv,
|
||||
cwd=repo,
|
||||
env=environment,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
timeout=300,
|
||||
check=False,
|
||||
)
|
||||
return {
|
||||
"argv": list(argv),
|
||||
"exit_code": completed.returncode,
|
||||
"output": completed.stdout[-12000:],
|
||||
"passed": completed.returncode == 0,
|
||||
}
|
||||
REPORT_SCHEMA = "aios.dormant-release.acceptance/v1"
|
||||
|
||||
|
||||
def text_files(repo: Path):
|
||||
for prefix in SCAN_PREFIXES:
|
||||
root = repo / prefix
|
||||
if not root.exists():
|
||||
continue
|
||||
for path in root.rglob("*"):
|
||||
if path.is_file() and not path.is_symlink() and "__pycache__" not in path.parts:
|
||||
try:
|
||||
yield path, path.read_text(encoding="utf-8")
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RunnerDecision:
|
||||
phase: str
|
||||
status: str
|
||||
report: Mapping[str, Any]
|
||||
exit_code: int
|
||||
|
||||
|
||||
def scans(repo: Path) -> dict[str, object]:
|
||||
forbidden_source = (
|
||||
"GITEA_" + "DATABASE",
|
||||
"sqlite3.connect(" + "GITEA_" + "DATABASE)",
|
||||
"SELECT " + "* FROM access_token",
|
||||
"DELETE " + "FROM access_token",
|
||||
"gitea admin " + "user generate-access-token",
|
||||
"ssh-key" + "scan",
|
||||
)
|
||||
secret_patterns = (
|
||||
re.compile(r"sk-[A-Za-z0-9_-]{20,}"),
|
||||
re.compile(r"-----BEGIN (?:OPENSSH |RSA |EC )?PRIVATE KEY-----"),
|
||||
re.compile(r"(?i)(?:api[_-]?key|password|token)\s*[:=]\s*['\"]?[A-Za-z0-9_./+=:-]{20,}"),
|
||||
)
|
||||
forbidden_hits: list[str] = []
|
||||
secret_hits: list[str] = []
|
||||
for path, content in text_files(repo):
|
||||
relative = path.relative_to(repo).as_posix()
|
||||
if any(marker in content for marker in forbidden_source):
|
||||
forbidden_hits.append(relative)
|
||||
if any(pattern.search(content) for pattern in secret_patterns):
|
||||
secret_hits.append(relative)
|
||||
changed = subprocess.run(
|
||||
["git", "diff", "--name-only", "main...HEAD"],
|
||||
cwd=repo,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
check=False,
|
||||
)
|
||||
changed_paths = tuple(item for item in changed.stdout.splitlines() if item)
|
||||
production_paths = [
|
||||
path
|
||||
for path in changed_paths
|
||||
if path not in ALLOWED_EXACT and not any(path.startswith(prefix) for prefix in ALLOWED_PREFIXES)
|
||||
]
|
||||
return {
|
||||
"direct_gitea_database_access": False if not forbidden_hits else True,
|
||||
"forbidden_source_hits": sorted(set(forbidden_hits)),
|
||||
"secret_hits": sorted(set(secret_hits)),
|
||||
"production_path_hits": production_paths,
|
||||
"passed": not forbidden_hits and not secret_hits and not production_paths,
|
||||
}
|
||||
|
||||
|
||||
def exclusive_json(path: Path, payload: dict[str, object]) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True, mode=0o750)
|
||||
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
|
||||
if hasattr(os, "O_NOFOLLOW"):
|
||||
flags |= os.O_NOFOLLOW
|
||||
descriptor = os.open(path, flags, 0o640)
|
||||
def read_json_object(path: Path) -> Mapping[str, Any]:
|
||||
try:
|
||||
content = (json.dumps(payload, sort_keys=True, indent=2) + "\n").encode("utf-8")
|
||||
os.write(descriptor, content)
|
||||
os.fsync(descriptor)
|
||||
finally:
|
||||
os.close(descriptor)
|
||||
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 git_object_oid(kind: str, content: bytes) -> str:
|
||||
header = f"{kind} {len(content)}\0".encode("ascii")
|
||||
return hashlib.sha1(header + content, usedforsecurity=False).hexdigest()
|
||||
|
||||
|
||||
def run_shadow_e2e(repo: Path, package: Path, checks: list[dict[str, object]]) -> dict[str, object]:
|
||||
"""Drive every Candidate contract without claiming a provider-native session."""
|
||||
if subprocess.check_output(["git", "status", "--porcelain"], cwd=repo, text=True).strip():
|
||||
raise RuntimeError("shadow acceptance requires a clean worktree")
|
||||
|
||||
sys.path.insert(0, str(package))
|
||||
from aios_autonomous_release_candidate.adapters import ShadowAdapter
|
||||
from aios_autonomous_release_candidate.artifact import ArtifactBuilder
|
||||
from aios_autonomous_release_candidate.credential import (
|
||||
CredentialBroker,
|
||||
InMemoryCredentialProvider,
|
||||
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",
|
||||
)
|
||||
from aios_autonomous_release_candidate.errors import ReleaseBlocked
|
||||
from aios_autonomous_release_candidate.evidence import EvidenceChain
|
||||
from aios_autonomous_release_candidate.models import (
|
||||
CredentialRequest,
|
||||
ReleasePlan,
|
||||
ResourceBinding,
|
||||
RiskClass,
|
||||
TaskPlan,
|
||||
WorkerDescriptor,
|
||||
WorkflowStep,
|
||||
sha256_json,
|
||||
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,
|
||||
)
|
||||
from aios_autonomous_release_candidate.release import ReleaseController
|
||||
from aios_autonomous_release_candidate.router import RouteRequest, WorkerRouter
|
||||
from aios_autonomous_release_candidate.store import StateStore
|
||||
from aios_autonomous_release_candidate.workflow import WorkflowEngine
|
||||
|
||||
head = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=repo, text=True).strip()
|
||||
tree = subprocess.check_output(
|
||||
["git", "rev-parse", "HEAD^{tree}"], cwd=repo, text=True
|
||||
).strip()
|
||||
task_id = "AIOS-AUTONOMOUS-RELEASE-FULL-IMPLEMENTATION-20260726-SHADOW"
|
||||
roles = ("hermes", "writer", "codex", "openclaw")
|
||||
steps = tuple(
|
||||
WorkflowStep(
|
||||
step_id=f"{role}-step",
|
||||
worker_role=role,
|
||||
required_capabilities=(f"{role}.shadow",),
|
||||
depends_on=(() if index == 0 else (f"{roles[index - 1]}-step",)),
|
||||
max_attempts=2,
|
||||
timeout_seconds=60,
|
||||
allowed_paths=("services/aios-autonomous-release-candidate",),
|
||||
)
|
||||
for index, role in enumerate(roles)
|
||||
|
||||
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,
|
||||
)
|
||||
binding = ResourceBinding(
|
||||
repository="aios/aios",
|
||||
branch="patch/chatgpt-autonomous-completion-contract-20260726",
|
||||
base_oid=head,
|
||||
allowed_paths=("services/aios-autonomous-release-candidate",),
|
||||
)
|
||||
plan = TaskPlan(
|
||||
task_id=task_id,
|
||||
intent="candidate shadow contract closure",
|
||||
binding=binding,
|
||||
risk=RiskClass.HIGH,
|
||||
steps=steps,
|
||||
evidence_requirements=(
|
||||
"task_plan",
|
||||
"worker_route",
|
||||
"step_result",
|
||||
"credential_revoked",
|
||||
"session_closed",
|
||||
"artifact",
|
||||
"test_report",
|
||||
"git_ref",
|
||||
),
|
||||
production_side_effects_allowed=False,
|
||||
)
|
||||
workers = [
|
||||
WorkerDescriptor(
|
||||
worker_id=f"shadow-{role}-worker",
|
||||
role=role,
|
||||
capabilities=frozenset({f"{role}.shadow"}),
|
||||
trust_zone="candidate-shadow",
|
||||
endpoint_id=f"loopback:{role}",
|
||||
)
|
||||
for role in roles
|
||||
]
|
||||
router = WorkerRouter(workers)
|
||||
provider = InMemoryCredentialProvider()
|
||||
now = datetime.now(timezone.utc)
|
||||
worker_routes: dict[str, str] = {}
|
||||
session_ids: dict[str, dict[str, str]] = {}
|
||||
credential_ids: list[str] = []
|
||||
credential_revocation: dict[str, dict[str, object]] = {}
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="aios-autonomous-shadow-") as temporary:
|
||||
store = StateStore(Path(temporary) / "state.sqlite3")
|
||||
workflow = WorkflowEngine(store)
|
||||
workflow.plan(plan)
|
||||
evidence = EvidenceChain(store, task_id)
|
||||
evidence.append(
|
||||
"task_plan",
|
||||
task_id,
|
||||
{"task_plan_sha256": plan.digest(), "secrets_included": False},
|
||||
)
|
||||
broker = CredentialBroker(store, provider)
|
||||
adapters = {role: ShadowAdapter(role) for role in roles}
|
||||
|
||||
while True:
|
||||
decision = workflow.next_steps(plan)
|
||||
states = store.list_steps(task_id)
|
||||
if states and all(item.state.value == "COMPLETED" for item in states.values()):
|
||||
break
|
||||
if not decision.ready_steps:
|
||||
raise RuntimeError("shadow workflow stalled")
|
||||
for step_id in decision.ready_steps:
|
||||
step = next(item for item in steps if item.step_id == step_id)
|
||||
worker = router.route(
|
||||
RouteRequest(task_id, step.required_capabilities, "candidate-shadow")
|
||||
)
|
||||
adapter = adapters[step.worker_role]
|
||||
session = adapter.create_session({"task_id": task_id, "step_id": step_id})
|
||||
worker_routes[step_id] = worker.worker_id
|
||||
session_ids[step_id] = {
|
||||
"broker_session_id": session.session_id,
|
||||
"provider_session_id": session.provider_session_id,
|
||||
"provider": session.provider,
|
||||
}
|
||||
evidence.append(
|
||||
"worker_route",
|
||||
task_id,
|
||||
{
|
||||
"step_id": step_id,
|
||||
"worker_id": worker.worker_id,
|
||||
"trust_zone": worker.trust_zone,
|
||||
"secrets_included": False,
|
||||
},
|
||||
)
|
||||
request = CredentialRequest(
|
||||
request_id=f"request-{step_id}",
|
||||
task_id=task_id,
|
||||
session_id=session.session_id,
|
||||
worker_id=worker.worker_id,
|
||||
audience=f"{step.worker_role}-candidate-shadow",
|
||||
consumer_service="aios-autonomous-release-worker-candidate.service",
|
||||
binding=binding,
|
||||
operations=(f"{step.worker_role}.shadow",),
|
||||
provider=f"{step.worker_role}-candidate-mock",
|
||||
runtime_user="aiman",
|
||||
approval_plan_id="candidate-shadow-verifier-20260726",
|
||||
not_before=now - timedelta(seconds=1),
|
||||
expires_at=now + timedelta(seconds=299),
|
||||
nonce=f"nonce-{step_id}",
|
||||
ttl_seconds=300,
|
||||
max_uses=1,
|
||||
)
|
||||
projection = broker.issue(request, now)
|
||||
broker.project(projection)
|
||||
broker.authorize(
|
||||
projection,
|
||||
task_id=task_id,
|
||||
session_id=session.session_id,
|
||||
worker_id=worker.worker_id,
|
||||
audience=request.audience,
|
||||
consumer_service=request.consumer_service,
|
||||
operation=request.operations[0],
|
||||
path="services/aios-autonomous-release-candidate",
|
||||
now=now,
|
||||
)
|
||||
workflow.claim(task_id, step_id, worker.worker_id, session.session_id)
|
||||
workflow.start(task_id, step_id, worker.worker_id, session.session_id, now=now)
|
||||
workflow.heartbeat(task_id, step_id, worker.worker_id, session.session_id, now=now)
|
||||
result = adapter.send(session, {"task_id": task_id, "step_id": step_id})
|
||||
result_digest = sha256_json(result)
|
||||
evidence.append(
|
||||
"step_result",
|
||||
task_id,
|
||||
{
|
||||
"step_id": step_id,
|
||||
"worker_id": worker.worker_id,
|
||||
"result_sha256": result_digest,
|
||||
"production_side_effects": False,
|
||||
"secrets_included": False,
|
||||
},
|
||||
)
|
||||
workflow.complete(task_id, step_id, result_digest=result_digest)
|
||||
revoked = broker.revoke(projection.credential_id, projection.reference, now)
|
||||
broker.verify_revoked(projection.credential_id, projection.reference)
|
||||
credential_ids.append(projection.credential_id)
|
||||
credential_revocation[projection.credential_id] = {
|
||||
"state": revoked.state.value,
|
||||
"post_revocation_auth": "REJECTED",
|
||||
}
|
||||
evidence.append(
|
||||
"credential_revoked",
|
||||
task_id,
|
||||
{
|
||||
"credential_id": projection.credential_id,
|
||||
"state": revoked.state.value,
|
||||
"post_revocation_auth": "REJECTED",
|
||||
"secrets_included": False,
|
||||
},
|
||||
)
|
||||
adapter.close(session)
|
||||
evidence.append(
|
||||
"session_closed",
|
||||
task_id,
|
||||
{
|
||||
"session_id": session.session_id,
|
||||
"provider_session_id": session.provider_session_id,
|
||||
"secrets_included": False,
|
||||
},
|
||||
)
|
||||
|
||||
built = ArtifactBuilder().build_from_directory(head, tree, package)
|
||||
ArtifactBuilder().verify(built)
|
||||
evidence.append(
|
||||
"artifact",
|
||||
task_id,
|
||||
{
|
||||
"artifact_sha256": built.manifest.artifact_sha256,
|
||||
"manifest_sha256": built.manifest.manifest_sha256,
|
||||
"package_closure_sha256": built.manifest.package_closure_sha256,
|
||||
"secrets_included": False,
|
||||
},
|
||||
)
|
||||
evidence.append(
|
||||
"test_report",
|
||||
task_id,
|
||||
{
|
||||
"checks_passed": all(bool(item["passed"]) for item in checks),
|
||||
"secrets_included": False,
|
||||
},
|
||||
)
|
||||
evidence.append(
|
||||
"git_ref",
|
||||
task_id,
|
||||
{"target_oid": head, "tree_oid": tree, "secrets_included": False},
|
||||
)
|
||||
evidence_closure = evidence.close(plan.evidence_requirements)
|
||||
evidence_bytes = json.dumps(
|
||||
evidence.export(), sort_keys=True, separators=(",", ":")
|
||||
).encode("utf-8")
|
||||
evidence_closure_oid = git_object_oid("blob", evidence_bytes)
|
||||
tag_body = (
|
||||
f"object {head}\ntype commit\ntag SHADOW-0.1.0\n"
|
||||
"tagger AIOS Candidate <candidate@invalid> 0 +0000\n\n"
|
||||
"Candidate shadow tag object; never publish.\n"
|
||||
).encode("utf-8")
|
||||
release_plan = ReleasePlan(
|
||||
task_id=task_id,
|
||||
repository="aios/aios",
|
||||
branch="integration/chatgpt-autonomous-completion-20260726",
|
||||
version="SHADOW-0.1.0",
|
||||
target_oid=head,
|
||||
tree_oid=tree,
|
||||
artifact_sha256=built.manifest.artifact_sha256,
|
||||
manifest_sha256=built.manifest.manifest_sha256,
|
||||
package_closure_sha256=built.manifest.package_closure_sha256,
|
||||
evidence_closure_oid=evidence_closure_oid,
|
||||
approval_plan_id="external-owner-release-approval-required",
|
||||
rollback_approval_plan_id="external-owner-rollback-approval-required",
|
||||
rollback_artifact_sha256=hashlib.sha256(b"previous-production-artifact").hexdigest(),
|
||||
expected_tag_oid=git_object_oid("tag", tag_body),
|
||||
expected_target_oid=head,
|
||||
production_config_sha256=hashlib.sha256(
|
||||
b"candidate-shadow-production-disabled"
|
||||
).hexdigest(),
|
||||
)
|
||||
release = ReleaseController()
|
||||
release.attach_plan(release_plan)
|
||||
release.mark_built()
|
||||
release.verify(artifact_ok=True, evidence_ok=True)
|
||||
release.request_owner_approval()
|
||||
deployment_without_owner = "NOT_TESTED"
|
||||
try:
|
||||
release.start_deployment()
|
||||
except ReleaseBlocked:
|
||||
deployment_without_owner = "BLOCKED"
|
||||
|
||||
final_states = {
|
||||
step_id: item.state.value for step_id, item in store.list_steps(task_id).items()
|
||||
}
|
||||
return {
|
||||
"passed": all(state == "COMPLETED" for state in final_states.values())
|
||||
and deployment_without_owner == "BLOCKED"
|
||||
and evidence.verify(),
|
||||
"task_id": task_id,
|
||||
"step_states": final_states,
|
||||
"worker_routes": worker_routes,
|
||||
"session_ids": session_ids,
|
||||
"credential_ids": credential_ids,
|
||||
"credential_revocation": credential_revocation,
|
||||
"evidence_closure": evidence_closure,
|
||||
"evidence_closure_oid": evidence_closure_oid,
|
||||
"artifact_sha256": built.manifest.artifact_sha256,
|
||||
"manifest_sha256": built.manifest.manifest_sha256,
|
||||
"package_closure_sha256": built.manifest.package_closure_sha256,
|
||||
"release_state": release.status().state.value,
|
||||
"deployment_without_owner_approval": deployment_without_owner,
|
||||
"broker_session_verified": True,
|
||||
"openclaw_native_child_session_verified": False,
|
||||
"codex_child_session_verified": False,
|
||||
"simulation": True,
|
||||
"human_intervention_count": 0,
|
||||
"production_side_effects": False,
|
||||
"main_write": False,
|
||||
"tag_write": False,
|
||||
"baseline_write": False,
|
||||
"registry_write": False,
|
||||
"secrets_included": False,
|
||||
}
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--repo-root", type=Path)
|
||||
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)
|
||||
repo = (args.repo_root or Path(__file__).resolve().parents[2]).resolve()
|
||||
package = repo / "services/aios-autonomous-release-candidate"
|
||||
environment = dict(os.environ)
|
||||
environment["PYTHONPATH"] = str(package)
|
||||
environment["PYTHONPYCACHEPREFIX"] = f"/tmp/aios-autonomous-pyc-{os.getpid()}"
|
||||
checks = [
|
||||
command(
|
||||
repo,
|
||||
[sys.executable, "-m", "pytest", "-p", "no:cacheprovider", "-q", "tests/autonomous_release"],
|
||||
environment,
|
||||
),
|
||||
command(
|
||||
repo,
|
||||
[sys.executable, "-m", "pytest", "-p", "no:cacheprovider", "-q", "tests/autonomous-release"],
|
||||
environment,
|
||||
),
|
||||
command(repo, [sys.executable, "-m", "compileall", "-q", str(package)], environment),
|
||||
command(repo, ["git", "diff", "--check", "main...HEAD"], environment),
|
||||
]
|
||||
scan_result = scans(repo)
|
||||
head = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=repo, text=True).strip()
|
||||
main_oid = subprocess.check_output(["git", "rev-parse", "main"], cwd=repo, text=True).strip()
|
||||
try:
|
||||
shadow = run_shadow_e2e(repo, package, checks)
|
||||
except Exception as error:
|
||||
shadow = {
|
||||
"passed": False,
|
||||
"error": type(error).__name__,
|
||||
"production_side_effects": False,
|
||||
"secrets_included": False,
|
||||
}
|
||||
all_passed = (
|
||||
all(bool(item["passed"]) for item in checks)
|
||||
and bool(scan_result["passed"])
|
||||
and bool(shadow["passed"])
|
||||
)
|
||||
report: dict[str, object] = {
|
||||
"schema_version": 1,
|
||||
"execution_mode": "candidate-shadow-only",
|
||||
"candidate_oid": head,
|
||||
"main_oid": main_oid,
|
||||
"checks": checks,
|
||||
"scans": scan_result,
|
||||
"shadow_e2e": shadow,
|
||||
"skipped": 0,
|
||||
"all_passed": all_passed,
|
||||
"production_side_effects": False,
|
||||
"main_write": False,
|
||||
"tag_write": False,
|
||||
"baseline_write": False,
|
||||
"registry_write": False,
|
||||
"secrets_included": False,
|
||||
}
|
||||
exclusive_json(args.output, report)
|
||||
print(json.dumps({"report": str(args.output), "all_passed": all_passed}, sort_keys=True))
|
||||
return 0 if all_passed else 1
|
||||
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__":
|
||||
|
||||
@@ -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" \
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
"""AIOS autonomous control-plane primitives."""
|
||||
|
||||
from .mirror import ApprovalMirrorWorker, MirrorError
|
||||
from .policy import DevelopmentPolicy, PolicyError
|
||||
from .projection import ReleaseObjectProjector, RuntimeSourceProjector
|
||||
from .release import ReleaseEnvelope, ReleaseEnvelopeExecutor, ReleaseError
|
||||
from .renewal import RenewalController, RenewalError
|
||||
from .watcher import ApprovalSnapshot, GiteaSQLiteApprovalWatcher
|
||||
from .writer import CommitRequest, GovernedDevelopmentWriter, WriterError
|
||||
|
||||
__all__ = [
|
||||
"ApprovalMirrorWorker",
|
||||
"ApprovalSnapshot",
|
||||
"CommitRequest",
|
||||
"DevelopmentPolicy",
|
||||
"GiteaSQLiteApprovalWatcher",
|
||||
"GovernedDevelopmentWriter",
|
||||
"MirrorError",
|
||||
"PolicyError",
|
||||
"ReleaseEnvelope",
|
||||
"ReleaseEnvelopeExecutor",
|
||||
"ReleaseError",
|
||||
"ReleaseObjectProjector",
|
||||
"RenewalController",
|
||||
"RenewalError",
|
||||
"RuntimeSourceProjector",
|
||||
"WriterError",
|
||||
]
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from typing import Any, Iterable
|
||||
|
||||
from .release import ReleaseEnvelope, ReleaseError
|
||||
|
||||
|
||||
APPROVAL_SCHEMA = "aios.release-approval/v3"
|
||||
APPROVAL_TYPE = "V2R1C4_PRODUCTION_RELEASE_ENVELOPE"
|
||||
|
||||
# These fields exist before the approval commit is created. The remaining
|
||||
# ReleaseEnvelope fields are provenance and are bound to live Gitea/Git facts.
|
||||
APPROVAL_CONTENT_FIELDS = (
|
||||
"version",
|
||||
"repository",
|
||||
"release_branch",
|
||||
"expected_base_oid",
|
||||
"target_oid",
|
||||
"target_tree_oid",
|
||||
"release_root_sha256",
|
||||
"approval_plan_sha256",
|
||||
"target_environment",
|
||||
"writer_identity",
|
||||
"publisher_identity",
|
||||
"artifact_sha256",
|
||||
"rollback_artifact_sha256",
|
||||
"sbom_sha256",
|
||||
"test_evidence_sha256",
|
||||
"evidence_closure_oid",
|
||||
"evidence_closure_sha256",
|
||||
"tag_plan_sha256",
|
||||
"deployment_plan_sha256",
|
||||
"rollback_plan_sha256",
|
||||
"registry_plan_sha256",
|
||||
"baseline_plan_sha256",
|
||||
"approval_expires_at",
|
||||
"approval_plan_id",
|
||||
)
|
||||
|
||||
|
||||
def approval_payload(envelope: ReleaseEnvelope) -> dict[str, Any]:
|
||||
return {field: getattr(envelope, field) for field in APPROVAL_CONTENT_FIELDS}
|
||||
|
||||
|
||||
def verify_approval_manifest(
|
||||
raw: bytes,
|
||||
envelope: ReleaseEnvelope,
|
||||
*,
|
||||
owner_user_id: int,
|
||||
governance_ref: str,
|
||||
required_machine_contexts: Iterable[str],
|
||||
) -> dict[str, Any]:
|
||||
if hashlib.sha256(raw).hexdigest() != envelope.approval_manifest_sha256:
|
||||
raise ReleaseError("APPROVAL_MANIFEST_SHA256_MISMATCH")
|
||||
try:
|
||||
value = json.loads(raw)
|
||||
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise ReleaseError("APPROVAL_MANIFEST_INVALID") from exc
|
||||
expected_keys = {
|
||||
"schema", "approval_type", "release", "owner_user_id",
|
||||
"governance_ref", "required_machine_contexts",
|
||||
}
|
||||
if not isinstance(value, dict) or set(value) != expected_keys:
|
||||
raise ReleaseError("APPROVAL_MANIFEST_SCHEMA_INVALID")
|
||||
if value["schema"] != APPROVAL_SCHEMA or value["approval_type"] != APPROVAL_TYPE:
|
||||
raise ReleaseError("APPROVAL_MANIFEST_TYPE_INVALID")
|
||||
if value["release"] != approval_payload(envelope):
|
||||
raise ReleaseError("APPROVAL_MANIFEST_RELEASE_MISMATCH")
|
||||
if value["owner_user_id"] != owner_user_id:
|
||||
raise ReleaseError("APPROVAL_MANIFEST_OWNER_MISMATCH")
|
||||
if value["governance_ref"] != governance_ref:
|
||||
raise ReleaseError("APPROVAL_MANIFEST_GOVERNANCE_REF_MISMATCH")
|
||||
configured_contexts = tuple(required_machine_contexts)
|
||||
if (
|
||||
not isinstance(value["required_machine_contexts"], list)
|
||||
or tuple(value["required_machine_contexts"]) != configured_contexts
|
||||
or len(set(configured_contexts)) != len(configured_contexts)
|
||||
or not configured_contexts
|
||||
):
|
||||
raise ReleaseError("APPROVAL_MANIFEST_CONTEXTS_MISMATCH")
|
||||
return value
|
||||
+253
@@ -0,0 +1,253 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from dataclasses import dataclass
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from typing import Any, Mapping
|
||||
|
||||
from .common import atomic_write_json, require_oid, sha256_file
|
||||
|
||||
|
||||
class CandidateFixtureError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
FORBIDDEN_ROOTS = (
|
||||
Path("/var/lib/aios-gitops/release"),
|
||||
Path("/var/lib/aios-gitops/artifacts"),
|
||||
Path("/opt/aios"),
|
||||
Path("/data/aios-gitops/runtime/releases"),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CandidateFixtureConfig:
|
||||
root: Path
|
||||
host: str
|
||||
port: int
|
||||
repository: str
|
||||
version: str
|
||||
release_branch: str
|
||||
expected_base_oid: str
|
||||
target_oid: str
|
||||
target_tree_oid: str
|
||||
tag_oid: str
|
||||
approval_plan_id: str
|
||||
|
||||
@classmethod
|
||||
def from_mapping(cls, value: Mapping[str, Any]) -> "CandidateFixtureConfig":
|
||||
required = {
|
||||
"root", "host", "port", "repository", "version", "release_branch",
|
||||
"expected_base_oid", "target_oid", "target_tree_oid", "tag_oid",
|
||||
"approval_plan_id",
|
||||
}
|
||||
if set(value) != required:
|
||||
raise CandidateFixtureError("CANDIDATE_FIXTURE_SCHEMA_INVALID")
|
||||
root = Path(value["root"])
|
||||
if not root.is_absolute() or root.is_symlink():
|
||||
raise CandidateFixtureError("CANDIDATE_ROOT_INVALID")
|
||||
resolved = root.resolve()
|
||||
if any(resolved == denied or denied in resolved.parents for denied in FORBIDDEN_ROOTS):
|
||||
raise CandidateFixtureError("PRODUCTION_ROOT_DENIED")
|
||||
if value["host"] != "127.0.0.1" or int(value["port"]) != 8080:
|
||||
raise CandidateFixtureError("CANDIDATE_ENDPOINT_INVALID")
|
||||
if value["version"] != "V2R1C4":
|
||||
raise CandidateFixtureError("CANDIDATE_VERSION_INVALID")
|
||||
for key in ("expected_base_oid", "target_oid", "target_tree_oid", "tag_oid"):
|
||||
require_oid(value[key], key)
|
||||
for key in ("repository", "release_branch", "approval_plan_id"):
|
||||
if not isinstance(value[key], str) or not value[key]:
|
||||
raise CandidateFixtureError("CANDIDATE_BINDING_INVALID")
|
||||
return cls(
|
||||
root=root, host=value["host"], port=int(value["port"]),
|
||||
repository=value["repository"], version=value["version"],
|
||||
release_branch=value["release_branch"],
|
||||
expected_base_oid=value["expected_base_oid"],
|
||||
target_oid=value["target_oid"], target_tree_oid=value["target_tree_oid"],
|
||||
tag_oid=value["tag_oid"], approval_plan_id=value["approval_plan_id"],
|
||||
)
|
||||
|
||||
|
||||
class CandidateIntegrationFixture:
|
||||
def __init__(self, config: CandidateFixtureConfig):
|
||||
self.config = config
|
||||
|
||||
def initialize(self) -> dict[str, Any]:
|
||||
root = self.config.root
|
||||
paths = {
|
||||
"registry": root / "registry" / "V2R1C4.json",
|
||||
"baseline": root / "baseline" / "CANDIDATE.json",
|
||||
"artifact": root / "artifacts" / "V2R1C4.definition.json",
|
||||
"binding": root / "releases" / "V2R1C4" / "binding.json",
|
||||
}
|
||||
for path in paths.values():
|
||||
path.parent.mkdir(parents=True, exist_ok=True, mode=0o750)
|
||||
binding = {
|
||||
"approval_plan_id": self.config.approval_plan_id,
|
||||
"candidate_only": True,
|
||||
"expected_base_oid": self.config.expected_base_oid,
|
||||
"production_deployed": False,
|
||||
"release_branch": self.config.release_branch,
|
||||
"repository": self.config.repository,
|
||||
"target_oid": self.config.target_oid,
|
||||
"target_tree_oid": self.config.target_tree_oid,
|
||||
"version": self.config.version,
|
||||
}
|
||||
atomic_write_json(paths["binding"], binding)
|
||||
binding_sha256 = sha256_file(paths["binding"])
|
||||
atomic_write_json(paths["artifact"], {
|
||||
"binding_sha256": binding_sha256,
|
||||
"candidate_only": True,
|
||||
"production_artifact": False,
|
||||
"target_oid": self.config.target_oid,
|
||||
"target_tree_oid": self.config.target_tree_oid,
|
||||
"version": self.config.version,
|
||||
})
|
||||
artifact_sha256 = sha256_file(paths["artifact"])
|
||||
atomic_write_json(paths["registry"], {
|
||||
"artifact_definition_sha256": artifact_sha256,
|
||||
"candidate_only": True,
|
||||
"status": "ISOLATED_CANDIDATE",
|
||||
"target_oid": self.config.target_oid,
|
||||
"version": self.config.version,
|
||||
})
|
||||
atomic_write_json(paths["baseline"], {
|
||||
"candidate_only": True,
|
||||
"expected_base_oid": self.config.expected_base_oid,
|
||||
"production_baseline_modified": False,
|
||||
"target_oid": self.config.target_oid,
|
||||
"version": self.config.version,
|
||||
})
|
||||
return {
|
||||
"artifact_definition_sha256": artifact_sha256,
|
||||
"binding_sha256": binding_sha256,
|
||||
"candidate_only": True,
|
||||
"paths": {key: str(value) for key, value in sorted(paths.items())},
|
||||
}
|
||||
|
||||
def merge(self, request: Mapping[str, Any]) -> dict[str, Any]:
|
||||
required = {
|
||||
"operation", "repository", "release_branch", "source_oid",
|
||||
"target_oid", "expected_base_oid", "approval_plan_id",
|
||||
}
|
||||
if set(request) != required:
|
||||
raise CandidateFixtureError("MERGE_REQUEST_SCHEMA_INVALID")
|
||||
expected = {
|
||||
"operation": "candidate_merge",
|
||||
"repository": self.config.repository,
|
||||
"release_branch": self.config.release_branch,
|
||||
"source_oid": self.config.target_oid,
|
||||
"target_oid": self.config.target_oid,
|
||||
"expected_base_oid": self.config.expected_base_oid,
|
||||
"approval_plan_id": self.config.approval_plan_id,
|
||||
}
|
||||
if dict(request) != expected:
|
||||
raise CandidateFixtureError("MERGE_BINDING_MISMATCH")
|
||||
return {
|
||||
"candidate_only": True,
|
||||
"release_oid": self.config.target_oid,
|
||||
"target_tree_oid": self.config.target_tree_oid,
|
||||
}
|
||||
|
||||
def tag(self, request: Mapping[str, Any]) -> dict[str, Any]:
|
||||
expected = {
|
||||
"approval_plan_id": self.config.approval_plan_id,
|
||||
"execute": False,
|
||||
"operation": "candidate_tag_plan",
|
||||
"target_oid": self.config.target_oid,
|
||||
"target_tree_oid": self.config.target_tree_oid,
|
||||
"version": self.config.version,
|
||||
}
|
||||
if dict(request) != expected:
|
||||
raise CandidateFixtureError("TAG_BINDING_MISMATCH")
|
||||
return {
|
||||
"candidate_only": True,
|
||||
"execute": False,
|
||||
"tag_oid": self.config.tag_oid,
|
||||
"tag_target_oid": self.config.target_oid,
|
||||
"version": self.config.version,
|
||||
}
|
||||
|
||||
|
||||
def _handler(fixture: CandidateIntegrationFixture):
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
server_version = "AIOSCandidateFixture/1"
|
||||
|
||||
def _json(self, status: int, value: Mapping[str, Any]):
|
||||
raw = json.dumps(value, sort_keys=True, separators=(",", ":")).encode()
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(raw)))
|
||||
self.end_headers()
|
||||
self.wfile.write(raw)
|
||||
|
||||
def do_GET(self):
|
||||
if self.path != "/health":
|
||||
self._json(404, {"status": "NOT_FOUND"})
|
||||
return
|
||||
self._json(200, {
|
||||
"candidate_only": True,
|
||||
"production_deployed": False,
|
||||
"status": "ok",
|
||||
"target_oid": fixture.config.target_oid,
|
||||
"version": fixture.config.version,
|
||||
})
|
||||
|
||||
def do_POST(self):
|
||||
if self.path != "/api/create-tag":
|
||||
self._json(404, {"status": "NOT_FOUND"})
|
||||
return
|
||||
try:
|
||||
length = int(self.headers.get("Content-Length", "0"))
|
||||
if length < 2 or length > 65536:
|
||||
raise CandidateFixtureError("TAG_REQUEST_SIZE_INVALID")
|
||||
value = json.loads(self.rfile.read(length))
|
||||
if not isinstance(value, dict):
|
||||
raise CandidateFixtureError("TAG_REQUEST_INVALID")
|
||||
self._json(200, fixture.tag(value))
|
||||
except (ValueError, json.JSONDecodeError, CandidateFixtureError) as exc:
|
||||
self._json(409, {"error": str(exc), "status": "FAILED_CLOSED"})
|
||||
|
||||
def log_message(self, fmt, *args):
|
||||
return
|
||||
|
||||
return Handler
|
||||
|
||||
|
||||
def _read_config(path: Path) -> CandidateFixtureConfig:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(value, dict):
|
||||
raise CandidateFixtureError("CANDIDATE_FIXTURE_SCHEMA_INVALID")
|
||||
return CandidateFixtureConfig.from_mapping(value)
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--config", type=Path, required=True)
|
||||
parser.add_argument("--merge-stdio", action="store_true")
|
||||
args = parser.parse_args(argv)
|
||||
fixture = CandidateIntegrationFixture(_read_config(args.config))
|
||||
fixture.initialize()
|
||||
if args.merge_stdio:
|
||||
raw = sys.stdin.buffer.readline(1048577)
|
||||
if not raw or len(raw) > 1048576:
|
||||
raise CandidateFixtureError("MERGE_REQUEST_SIZE_INVALID")
|
||||
value = json.loads(raw)
|
||||
if not isinstance(value, dict):
|
||||
raise CandidateFixtureError("MERGE_REQUEST_INVALID")
|
||||
sys.stdout.write(json.dumps(fixture.merge(value), sort_keys=True) + "\n")
|
||||
return 0
|
||||
server = ThreadingHTTPServer(
|
||||
(fixture.config.host, fixture.config.port), _handler(fixture))
|
||||
try:
|
||||
server.serve_forever()
|
||||
finally:
|
||||
server.server_close()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,102 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from .common import HashChainAudit
|
||||
from .ledger import OperationLedger
|
||||
from .mirror import ApprovalMirrorWorker
|
||||
from .policy import DevelopmentPolicy
|
||||
from .projection import ReleaseObjectProjector, RuntimeSourceProjector
|
||||
from .renewal import RenewalController
|
||||
from .writer import CommitRequest, GovernedDevelopmentWriter
|
||||
|
||||
|
||||
def load_json(path: Path) -> dict:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(value, dict):
|
||||
raise SystemExit("JSON object required")
|
||||
return value
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser()
|
||||
commands = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
writer = commands.add_parser("writer-commit")
|
||||
writer.add_argument("--policy", type=Path, required=True)
|
||||
writer.add_argument("--request", type=Path, required=True)
|
||||
writer.add_argument("--worktree", type=Path, required=True)
|
||||
writer.add_argument("--remote", required=True)
|
||||
writer.add_argument("--ledger", type=Path, required=True)
|
||||
writer.add_argument("--audit", type=Path, required=True)
|
||||
|
||||
mirror = commands.add_parser("mirror")
|
||||
mirror.add_argument("--source-git-dir", type=Path, required=True)
|
||||
mirror.add_argument("--authoritative-git-dir", type=Path, required=True)
|
||||
mirror.add_argument("--target-oid", required=True)
|
||||
|
||||
runtime = commands.add_parser("runtime-project")
|
||||
runtime.add_argument("--path", action="append", type=Path, required=True)
|
||||
runtime.add_argument("--output", type=Path, required=True)
|
||||
runtime.add_argument("--task-id", required=True)
|
||||
|
||||
release = commands.add_parser("release-project")
|
||||
release.add_argument("--source-git-dir", type=Path, required=True)
|
||||
release.add_argument("--commit-oid", required=True)
|
||||
release.add_argument("--tree-oid", required=True)
|
||||
release.add_argument("--output", type=Path, required=True)
|
||||
release.add_argument("--path", action="append", required=True)
|
||||
|
||||
renew = commands.add_parser("renew")
|
||||
renew.add_argument("--standing-policy", type=Path, required=True)
|
||||
renew.add_argument("--previous-manifest", type=Path, required=True)
|
||||
renew.add_argument("--observation", type=Path, required=True)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = build_parser().parse_args(argv)
|
||||
os.environ.setdefault("AIOS_GITOPS_WRITES_ENABLED", "false")
|
||||
|
||||
if args.command == "writer-commit":
|
||||
policy = DevelopmentPolicy.from_mapping(load_json(args.policy))
|
||||
request_value = load_json(args.request)
|
||||
content_path = Path(request_value.pop("content_path"))
|
||||
request = CommitRequest(content=content_path.read_bytes(), **request_value)
|
||||
result = GovernedDevelopmentWriter(
|
||||
worktree=args.worktree,
|
||||
remote=args.remote,
|
||||
policy=policy,
|
||||
ledger=OperationLedger(args.ledger),
|
||||
audit=HashChainAudit(args.audit),
|
||||
).commit_file(request)
|
||||
elif args.command == "mirror":
|
||||
result = ApprovalMirrorWorker(
|
||||
args.source_git_dir, args.authoritative_git_dir
|
||||
).mirror(expected_target_oid=args.target_oid).__dict__
|
||||
elif args.command == "runtime-project":
|
||||
result = RuntimeSourceProjector(args.path).collect(args.output, args.task_id)
|
||||
elif args.command == "release-project":
|
||||
result = ReleaseObjectProjector(args.source_git_dir).project(
|
||||
commit_oid=args.commit_oid,
|
||||
expected_tree_oid=args.tree_oid,
|
||||
output_path=args.output,
|
||||
allowed_paths=args.path,
|
||||
).__dict__
|
||||
result["output_path"] = str(result["output_path"])
|
||||
else:
|
||||
result = RenewalController().decide(
|
||||
standing_policy=load_json(args.standing_policy),
|
||||
previous_manifest=load_json(args.previous_manifest),
|
||||
observation=load_json(args.observation),
|
||||
).__dict__
|
||||
print(json.dumps(result, sort_keys=True, default=str))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,221 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime, timezone
|
||||
import fcntl
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path, PurePosixPath, PureWindowsPath
|
||||
import re
|
||||
import tempfile
|
||||
from typing import Any, Iterator
|
||||
|
||||
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||
SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
|
||||
SENSITIVE_FRAGMENTS = (
|
||||
"authorization", "cookie", "credential", "password", "private_key",
|
||||
"secret", "token",
|
||||
)
|
||||
|
||||
|
||||
def canonical_json(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value, sort_keys=True, separators=(",", ":"), ensure_ascii=False
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def sha256_json(value: object) -> str:
|
||||
return hashlib.sha256(canonical_json(value)).hexdigest()
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
if not path.is_absolute() or not path.is_file() or path.is_symlink():
|
||||
raise RuntimeError("FILE_IDENTITY_INVALID")
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as handle:
|
||||
for block in iter(lambda: handle.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def git_blob_oid(raw: bytes) -> str:
|
||||
return hashlib.sha1(b"blob " + str(len(raw)).encode() + b"\0" + raw).hexdigest()
|
||||
|
||||
|
||||
def parse_utc(value: str) -> datetime:
|
||||
try:
|
||||
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise RuntimeError("TIMESTAMP_INVALID") from exc
|
||||
if parsed.tzinfo is None:
|
||||
raise RuntimeError("TIMESTAMP_INVALID")
|
||||
return parsed.astimezone(timezone.utc)
|
||||
|
||||
|
||||
def utc_now() -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
def require_oid(value: object, field: str = "oid") -> str:
|
||||
if not isinstance(value, str) or not OID_RE.fullmatch(value):
|
||||
raise RuntimeError(f"{field.upper()}_INVALID")
|
||||
return value
|
||||
|
||||
|
||||
def require_sha256(value: object, field: str = "sha256") -> str:
|
||||
if not isinstance(value, str) or not SHA256_RE.fullmatch(value):
|
||||
raise RuntimeError(f"{field.upper()}_INVALID")
|
||||
return value
|
||||
|
||||
|
||||
def safe_relpath(value: object) -> str:
|
||||
if not isinstance(value, str) or not value or value != value.strip():
|
||||
raise RuntimeError("RELATIVE_PATH_INVALID")
|
||||
if "\0" in value or "\\" in value:
|
||||
raise RuntimeError("RELATIVE_PATH_INVALID")
|
||||
posix = PurePosixPath(value)
|
||||
windows = PureWindowsPath(value)
|
||||
parts = value.split("/")
|
||||
if (
|
||||
posix.is_absolute()
|
||||
or windows.is_absolute()
|
||||
or windows.drive
|
||||
or any(part in {"", ".", ".."} for part in parts)
|
||||
):
|
||||
raise RuntimeError("RELATIVE_PATH_INVALID")
|
||||
return value
|
||||
|
||||
|
||||
def redact(value: Any) -> Any:
|
||||
if isinstance(value, dict):
|
||||
return {
|
||||
str(key): (
|
||||
"[REDACTED]"
|
||||
if any(fragment in str(key).lower() for fragment in SENSITIVE_FRAGMENTS)
|
||||
else redact(item)
|
||||
)
|
||||
for key, item in value.items()
|
||||
}
|
||||
if isinstance(value, list):
|
||||
return [redact(item) for item in value]
|
||||
if isinstance(value, tuple):
|
||||
return [redact(item) for item in value]
|
||||
return value
|
||||
|
||||
|
||||
def ensure_directory(path: Path, mode: int = 0o750) -> None:
|
||||
if path.is_symlink():
|
||||
raise RuntimeError("DIRECTORY_SYMLINK_DENIED")
|
||||
path.mkdir(parents=True, exist_ok=True, mode=mode)
|
||||
path.chmod(mode)
|
||||
|
||||
|
||||
def atomic_write(path: Path, raw: bytes, mode: int = 0o640) -> None:
|
||||
ensure_directory(path.parent)
|
||||
with tempfile.NamedTemporaryFile(
|
||||
dir=path.parent, prefix=f".{path.name}.", delete=False
|
||||
) as handle:
|
||||
temporary = Path(handle.name)
|
||||
handle.write(raw)
|
||||
handle.flush()
|
||||
os.fsync(handle.fileno())
|
||||
temporary.chmod(mode)
|
||||
os.replace(temporary, path)
|
||||
path.chmod(mode)
|
||||
directory_fd = os.open(path.parent, os.O_RDONLY)
|
||||
try:
|
||||
os.fsync(directory_fd)
|
||||
finally:
|
||||
os.close(directory_fd)
|
||||
|
||||
|
||||
def atomic_write_json(path: Path, value: object, mode: int = 0o640) -> None:
|
||||
atomic_write(
|
||||
path,
|
||||
json.dumps(redact(value), sort_keys=True, indent=2).encode("utf-8") + b"\n",
|
||||
mode,
|
||||
)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def exclusive_lock(path: Path) -> Iterator[None]:
|
||||
ensure_directory(path.parent)
|
||||
fd = os.open(path, os.O_RDWR | os.O_CREAT | getattr(os, "O_NOFOLLOW", 0), 0o640)
|
||||
try:
|
||||
os.fchmod(fd, 0o640)
|
||||
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
yield
|
||||
finally:
|
||||
fcntl.flock(fd, fcntl.LOCK_UN)
|
||||
os.close(fd)
|
||||
|
||||
|
||||
class HashChainAudit:
|
||||
def __init__(self, path: Path):
|
||||
self.path = path
|
||||
|
||||
def records(self) -> list[dict[str, Any]]:
|
||||
if not self.path.exists():
|
||||
return []
|
||||
result = []
|
||||
for line in self.path.read_text(encoding="utf-8").splitlines():
|
||||
if line.strip():
|
||||
item = json.loads(line)
|
||||
if not isinstance(item, dict):
|
||||
raise RuntimeError("AUDIT_RECORD_INVALID")
|
||||
result.append(item)
|
||||
return result
|
||||
|
||||
def verify(self) -> str:
|
||||
previous = "0" * 64
|
||||
for sequence, record in enumerate(self.records(), 1):
|
||||
if record.get("sequence") != sequence:
|
||||
raise RuntimeError("AUDIT_SEQUENCE_INVALID")
|
||||
if record.get("previous_event_hash") != previous:
|
||||
raise RuntimeError("AUDIT_PREVIOUS_HASH_INVALID")
|
||||
payload = record.get("payload")
|
||||
payload_hash = sha256_json(payload)
|
||||
if record.get("payload_hash") != payload_hash:
|
||||
raise RuntimeError("AUDIT_PAYLOAD_HASH_INVALID")
|
||||
material = {
|
||||
"sequence": sequence,
|
||||
"previous_event_hash": previous,
|
||||
"payload_hash": payload_hash,
|
||||
"operation_id": record.get("operation_id"),
|
||||
}
|
||||
expected = sha256_json(material)
|
||||
if record.get("event_hash") != expected:
|
||||
raise RuntimeError("AUDIT_EVENT_HASH_INVALID")
|
||||
previous = expected
|
||||
return previous
|
||||
|
||||
def append(self, operation_id: str, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
ensure_directory(self.path.parent)
|
||||
previous = self.verify()
|
||||
sequence = len(self.records()) + 1
|
||||
clean = redact(payload)
|
||||
payload_hash = sha256_json(clean)
|
||||
material = {
|
||||
"sequence": sequence,
|
||||
"previous_event_hash": previous,
|
||||
"payload_hash": payload_hash,
|
||||
"operation_id": operation_id,
|
||||
}
|
||||
record = {
|
||||
**material,
|
||||
"event_hash": sha256_json(material),
|
||||
"payload": clean,
|
||||
}
|
||||
fd = os.open(
|
||||
self.path,
|
||||
os.O_WRONLY | os.O_APPEND | os.O_CREAT | getattr(os, "O_NOFOLLOW", 0),
|
||||
0o640,
|
||||
)
|
||||
try:
|
||||
os.write(fd, canonical_json(record) + b"\n")
|
||||
os.fsync(fd)
|
||||
finally:
|
||||
os.close(fd)
|
||||
self.path.chmod(0o640)
|
||||
return record
|
||||
@@ -0,0 +1,46 @@
|
||||
from __future__ import annotations
|
||||
import base64, json, os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
class CredentialError(RuntimeError): pass
|
||||
|
||||
def credential_path(name: str) -> Path:
|
||||
directory = os.environ.get("CREDENTIALS_DIRECTORY")
|
||||
if not directory:
|
||||
raise CredentialError("CREDENTIALS_DIRECTORY_MISSING")
|
||||
if not name or "/" in name or name in {".",".."}:
|
||||
raise CredentialError("CREDENTIAL_NAME_INVALID")
|
||||
path = Path(directory) / name
|
||||
if not path.is_file() or path.is_symlink():
|
||||
raise CredentialError("CREDENTIAL_FILE_INVALID")
|
||||
return path
|
||||
|
||||
def load_credential_json(name: str) -> dict[str, Any]:
|
||||
try:
|
||||
value = json.loads(credential_path(name).read_text(encoding="utf-8"))
|
||||
except json.JSONDecodeError as exc:
|
||||
raise CredentialError("CREDENTIAL_JSON_INVALID") from exc
|
||||
if not isinstance(value, dict):
|
||||
raise CredentialError("CREDENTIAL_JSON_INVALID")
|
||||
return value
|
||||
|
||||
def gitea_credential(name: str) -> tuple[str, str, int | None]:
|
||||
value = load_credential_json(name)
|
||||
username, token, token_id = value.get("username"), value.get("token"), value.get("token_id")
|
||||
if not isinstance(username, str) or not username:
|
||||
raise CredentialError("CREDENTIAL_USERNAME_INVALID")
|
||||
if not isinstance(token, str) or len(token) < 16:
|
||||
raise CredentialError("CREDENTIAL_TOKEN_INVALID")
|
||||
if token_id is not None and (not isinstance(token_id, int) or token_id < 1):
|
||||
raise CredentialError("CREDENTIAL_TOKEN_ID_INVALID")
|
||||
return username, token, token_id
|
||||
|
||||
def git_http_auth_env(username: str, token: str) -> dict[str, str]:
|
||||
encoded = base64.b64encode(f"{username}:{token}".encode()).decode("ascii")
|
||||
return {
|
||||
"GIT_CONFIG_COUNT":"1",
|
||||
"GIT_CONFIG_KEY_0":"http.extraHeader",
|
||||
"GIT_CONFIG_VALUE_0":f"Authorization: Basic {encoded}",
|
||||
"GIT_TERMINAL_PROMPT":"0",
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user