Files
ARCADIA-Studio/harness/experiments/transition-picker-native-20260913/verify.py
T

64 lines
5.0 KiB
Python

"""Read-only portable receipt and frame verification, without archived execution."""
from pathlib import Path, PurePosixPath
from datetime import datetime
import hashlib,json,zipfile
ROOT=Path(__file__).resolve().parent
def pin(b):return {'bytes':len(b),'sha256':hashlib.sha256(b).hexdigest()}
def need(ok,msg):
if not ok:raise ValueError(msg)
def main():
files=json.loads((ROOT/'files.json').read_bytes())
for n,p in files.items():need(pin((ROOT/n).read_bytes())==p,'outer hash '+n)
manifest=json.loads((ROOT/'manifest.json').read_bytes())
with zipfile.ZipFile(ROOT/'evidence.zip') as z:
names=z.namelist()
need(len(names)==len(set(names)) and set(names)==set(manifest['members']),'exact ZIP membership')
need(all(not PurePosixPath(n).is_absolute() and '..' not in PurePosixPath(n).parts and '\\' not in n and ':' not in n for n in names),'safe members')
for n,p in manifest['members'].items():need(pin(z.read(n))==p,'member hash '+n)
def obj(n):return json.loads(z.read(n))
input_names={n[len('native/input/'):]:n for n in names if n.startswith('native/input/')}
reference=None;frame_count=0;image_count=0
for run,spec in manifest['runs'].items():
prefix='native/'+run+'/'
start=obj(prefix+'started.json');result=obj(prefix+'result.json');process=obj(prefix+'process.json')
need(all(result[k]==v for k,v in start.items()),run+' start/result relationship')
need(result['exitCode']==spec['exitCode'],run+' exit code')
need(isinstance(process['pid'],int) and process['pid']>0,run+' process ID')
need(datetime.fromisoformat(start['start'])<=datetime.fromisoformat(process['observed'])<=datetime.fromisoformat(result['finish']),run+' process time order')
need(result['helper']==pin(z.read(spec['helper'])),run+' launch helper')
need(result['inputs']==pin(z.read(prefix+'inputs.json')) and result['inputUnchanged'] is True,run+' input receipt')
inputs=obj(prefix+'inputs.json')
need(set(inputs)==set(input_names),run+' input inventory')
for n,p in inputs.items():need(pin(z.read(input_names[n]))==p,run+' input bytes '+n)
if reference is None:reference=inputs
need(inputs==reference,run+' all trials identical fixture')
for stream in ['stdout','stderr']:
b=z.read(prefix+stream+'.log');need(not b and result[stream]==pin(b),run+' empty stream '+stream)
build=obj(spec['build'])
need(result['buildReceipt']==pin(z.read(spec['build'])) and result['buildHead']==build['head'],run+' build receipt')
need(build['exitCode']==0 and build['inputsUnchanged'] is True,run+' build status')
need(datetime.fromisoformat(build['start'])<=datetime.fromisoformat(build['finish'])<=datetime.fromisoformat(start['start']),run+' build before launch')
gui=[a for a in build['artifacts'] if a['cargo']['target']['name']=='arcadia-studio']
need(len(gui)==1,run+' unique GUI artifact')
need(result['binary']==result['binaryAfter']==gui[0]['readback'],run+' unchanged GUI pin')
need(result['command']==[gui[0]['cargo']['executable']],run+' exact launch command')
images=[]
for metadata in spec['captures']:
frame=obj(metadata)
need(datetime.fromisoformat(start['start'])<=datetime.fromisoformat(frame['time'])<=datetime.fromisoformat(result['finish']),metadata+' capture interval')
need(frame['window']['app'].replace('\\','/')=='process:'+result['command'][0].replace('\\','/'),metadata+' window application path')
for image in frame['images']:
n=prefix+image['file'];need(n in manifest['members'] and z.read(n).startswith(b'\xff\xd8'),metadata+' JPEG reference')
need(image['width']>0 and image['height']>0,metadata+' frame dimensions')
images.append(n)
actual_images=[n for n in names if n.startswith(prefix) and n.endswith('.jpg')]
need(len(images)==len(set(images)) and set(images)==set(actual_images),run+' all images referenced once')
actual_metadata=[n for n in names if n.startswith(prefix) and n.endswith('.json') and 'images' in obj(n)]
need(set(actual_metadata)==set(spec['captures']) and len(actual_metadata)==len(spec['captures']),run+' all capture metadata')
frame_count+=len(actual_metadata);image_count+=len(images)
print(run,'exit',result['exitCode'],'captures',len(actual_metadata),'images',len(images))
need(frame_count==manifest['captureCount'] and image_count==manifest['imageCount'],'aggregate frame counts')
print('PASS:',len(names),'raw members;',len(reference),'unchanged fixture files;',frame_count,'capture records;',image_count,'original JPEGs.')
print('Build source inventories are external. Frames prove recorded states, not action execution or PID attestation. Capture helper was not start-pinned.')
if __name__=='__main__':main()