feat(v4): consolidate audited data recovery economic replay and optional archival

This commit is contained in:
ENELIX Agent
2026-10-03 10:40:18 +00:00
parent 6c91d6bcc6
commit 8eb9688942
26 changed files with 1747 additions and 38 deletions
@@ -0,0 +1,124 @@
"""Install the reviewed V4 release candidate as one transaction-like deployment.
Tests run in an isolated candidate image BEFORE replacing any running service.
No device control permission, forecast-source switch, telemetry rewrite or cursor
jump. Exactly proven numeric representations are registered for backlog recovery.
"""
from datetime import datetime, timezone
from pathlib import Path
import argparse,hashlib,json,os,subprocess,sys,tempfile
ROOT=Path(__file__).resolve().parents[1]
PACKAGE=ROOT/'commissioning/unified-release'
GUI=ROOT.parent/'license/public/netplan-v4.js'
def digest(p):
if p.is_symlink():raise ValueError('Symlink refused')
return hashlib.sha256(p.read_bytes()).hexdigest() if p.is_file() else None
def verify():
m=json.loads((PACKAGE/'RELEASE.json').read_text())
if m.get('scope')!='unified_v4_candidate' or not m.get('files'):raise ValueError('Unexpected release')
if digest(Path(__file__).resolve())!=m['installerSha256']:raise ValueError('Installer changed')
for n,v in m['files'].items():
if Path(n).is_absolute() or '..' in Path(n).parts:raise ValueError('Unsafe release path')
if digest(PACKAGE/'context'/n)!=v['after']:raise ValueError('Candidate drift: '+n)
if digest(ROOT/n) not in (v['before'],v['after']):raise ValueError('Concurrent runtime source edit: '+n)
if digest(GUI) not in (m['guiBefore'],m['files']['gui/netplan-v4.js']['after']):raise ValueError('Concurrent GUI edit')
if digest(PACKAGE/'mapping-proof.json')!=m['proofSha256']:raise ValueError('Compatibility proof changed')
if digest(ROOT/'commissioning/deploy_application.py')!=m['backupHelperSha256']:raise ValueError('Backup helper changed')
return m
BOOTSTRAP=r'''import json,os,sys,urllib.request
x=json.load(sys.stdin);base='http://127.0.0.1:9100'
health=json.load(urllib.request.urlopen(base+'/health',timeout=5))
assert health.get('applicationRelease')=='unified-rc1' and health.get('liveEnabled') is False
headers={'X-Enelix-Service-Token':os.environ['PROGNOSIS_SERVICE_TOKEN'],'Content-Type':'application/json'}
url=base+'/internal/v2/prognosis/'+x['plant']+'/planner'
def get():return json.load(urllib.request.urlopen(urllib.request.Request(url,headers=headers),timeout=20))
before=get()
receipt={'status':'not_registered_requires_explicit_approval'}
if x['approveMapping']:
request=urllib.request.Request(url+'/datasets/'+x['dataset']+'/mapping-compatibility',data=json.dumps(x['proof']).encode(),headers=headers,method='PUT')
receipt=json.load(urllib.request.urlopen(request,timeout=15))
after=get();assert before['settings']==after['settings'] and after['liveEnabled'] is False
print(json.dumps({'health':health,'mappingRecovery':receipt,'settingsUnchanged':True,'forecastSource':after['settings']['forecastSource'],'economicReplayConnected':after['economicComparison']['connected'],'dataSets':[{'datasetId':d['datasetId'],'records':d['records'],'lastCapture':d['lastCapture'],'lastReceived':d.get('lastReceived'),'status':d['status']} for d in after['dataPipeline']['datasets']]}))
'''
def run(plant,apply=False,approve_mapping=False):
m=verify()
if plant!=m['installationId']:raise ValueError('Wrong prepared installation')
if not apply:
print('UNIFIED RELEASE SOURCE CHECK PASSED; no installation.');return
if os.geteuid()!=0:raise ValueError('Run as root; do not widen Docker socket permissions')
from deploy_application import atomic,backup_database
folder=ROOT/'unified-releases'/datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%S.%fZ');folder.mkdir(parents=True,mode=0o700)
report={'scope':'unified_v4_candidate','releaseId':m['releaseId'],'startedAt':datetime.now(timezone.utc).isoformat(),'liveEnabled':False,'productionCommissioned':False,'steps':[]}
compose=['docker','compose','-f',str(ROOT/'compose.yaml')];env=dict(os.environ)
def cmd(args,timeout=180,capture=False,data=None):
return subprocess.run(args,cwd=ROOT,env=env,check=True,timeout=timeout,text=True,input=data,stdout=subprocess.PIPE if capture else None,stderr=subprocess.PIPE if capture else None)
changed={};old_gui=None;new_gui=None;replaced=False;old_image=None
try:
# Read only this non-secret allowlist; never print or persist the complete environment.
env['NETPLAN_V4_PLANTS']=plant
ids=cmd(compose+['ps','-q','netplan-v4'],capture=True).stdout.split()
if len(ids)!=1:raise ValueError('Expected existing V4 service')
allowed=cmd(['docker','exec',ids[0],'python','-c',"import os;print(os.getenv('NETPLAN_V4_PLANTS',''))"],capture=True).stdout.strip()
if plant not in allowed.split(','):raise ValueError('Installation not in running allowlist')
env['NETPLAN_V4_PLANTS']=allowed
old_image=cmd(['docker','inspect','--format','{{.Image}}',ids[0]],capture=True).stdout.strip();report['previousImage']=old_image
tag='enelix-netplan-v4:rc-'+m['releaseId'][:16]
cmd(['docker','build','-t',tag,str(PACKAGE/'context')],timeout=900)
candidate=cmd(['docker','image','inspect','--format','{{.Id}}',tag],capture=True).stdout.strip()
cmd(['docker','run','--rm','--network','none','--read-only','--cap-drop','ALL','--security-opt','no-new-privileges','--tmpfs','/tmp:rw,nosuid,nodev,noexec,size=256m','--entrypoint','python',candidate,'/app/run_tests.py'],timeout=600)
cmd(['node','--test',str(PACKAGE/'context/tests/portal.test.mjs')]);cmd(['node','--check',str(PACKAGE/'context/gui/netplan-v4.js')])
report['steps'].append('isolated_candidate_tests_passed');verify()
backup_database(ROOT/'data/netplan-v4.sqlite',folder/'before.sqlite');report['steps'].append('consistent_backup')
for name,v in m['files'].items():
target=ROOT/name;after=(PACKAGE/'context'/name).read_bytes();before=target.read_bytes() if target.is_file() else None
if before==after:continue
if digest(target)!=v['before']:raise ValueError('Source changed during tests')
backup=folder/'source-before'/name;backup.parent.mkdir(parents=True,exist_ok=True)
if before is not None:backup.write_bytes(before)
target.parent.mkdir(parents=True,exist_ok=True);atomic(target,after);changed[name]=(before,after)
override=folder/'candidate.yaml';override.write_text('services:\n netplan-v4:\n image: '+candidate+'\n')
rollback=folder/'rollback.yaml';rollback.write_text('services:\n netplan-v4:\n image: '+old_image+'\n')
replaced=True;cmd(compose+['-f',str(override),'up','-d','--no-deps','--no-build','--wait','netplan-v4'],timeout=180)
data=json.dumps({'plant':plant,'dataset':'lihrenmoos-physical-v1','proof':json.loads((PACKAGE/'mapping-proof.json').read_text()),'approveMapping':approve_mapping})
result=cmd(compose+['exec','-T','netplan-v4','python','-c',BOOTSTRAP],capture=True,data=data)
report['application']=json.loads(result.stdout)
old_gui=GUI.read_bytes();new_gui=(PACKAGE/'context/gui/netplan-v4.js').read_bytes()
if digest(GUI) not in (m['guiBefore'],m['files']['gui/netplan-v4.js']['after']):raise ValueError('Concurrent portal UI edit')
(folder/'gui-before.js').write_bytes(old_gui);atomic(GUI,new_gui)
report['steps'].append('audited_backlog_compatibility_and_application_verified');report['candidateImage']=candidate
report['status']='unified_candidate_installed_no_actuation'
except Exception as exc:
report['status']='needs_review';report['errorType']=type(exc).__name__
if old_gui is not None and GUI.read_bytes()==new_gui:atomic(GUI,old_gui)
for name,(before,after) in reversed(list(changed.items())):
target=ROOT/name
if target.read_bytes()!=after:continue
if before is None:target.unlink()
else:atomic(target,before)
if replaced and old_image:
try:
cmd(compose+['-f',str(folder/'rollback.yaml'),'up','-d','--no-deps','--no-build','--pull','never','--wait','netplan-v4'])
report['rollback']='previous_image_restored_additive_audit_tables_retained'
except Exception:report['rollback']='manual_review_required'
raise
finally:
report['finishedAt']=datetime.now(timezone.utc).isoformat();p=folder/'REPORT.json';p.write_text(json.dumps(report,indent=2)+'\n')
uid,gid=ROOT.stat().st_uid,ROOT.stat().st_gid
for x in (folder.parent,folder,p):os.chown(x,uid,gid)
p.chmod(0o640);print('UNIFIED RELEASE REPORT:',p)
print('V4 candidate and planner UI installed. Mapping equivalence is activated only with explicit approval. No data/cursor rewrite and no device-control grant.')
if __name__=='__main__':
p=argparse.ArgumentParser(description=__doc__);p.add_argument('--plant',required=True);p.add_argument('--apply',action='store_true');p.add_argument('--approve-numeric-mapping-compatibility',action='store_true',help='Explicitly approve only the audited 100 versus 100.0 mapping equivalence; never rewrite original records');a=p.parse_args()
try:run(a.plant,a.apply,a.approve_numeric_mapping_compatibility)
except Exception as exc:raise SystemExit('Stopped: '+type(exc).__name__+'. Review UNIFIED RELEASE REPORT; no actuator permission enabled.')