bootstrap_acceptance.py (6234B)
1 #!/usr/bin/env python3 2 """Strict opt-in stable bootstrap acceptance, independent of cached fixture checks. 3 Run only after two successful fresh-store generations. This audits existing 4 outputs; it does not compile source. Failed verification never accepts a baseline. 5 """ 6 import argparse 7 import hashlib 8 import json 9 from pathlib import Path 10 from process_rss import run_guarded 11 import sys 12 from bootstrap_generations import source_map, snapshot 13 from generation_io import version, versions, produced_adapter, check_pair, execution_flags, check_invocation 14 15 REPO = Path(__file__).resolve().parent.parent 16 ARTIFACTS = ('rebuilt-compiler.arboricx', 'build.arboricx', 'build-stable.arboricx') 17 18 19 def sha(path): 20 return hashlib.sha256(path.read_bytes()).hexdigest() 21 22 23 def verify_generation(run, runtime, source_root=None, execution_host='haskell'): 24 """Authenticate and independently audit one full fresh compiler generation. 25 26 Execution host is caller-selected, never inferred from untrusted provenance. 27 Pair acceptance below remains explicitly Haskell-only. 28 """ 29 status = json.loads((run / 'status.json').read_text()) 30 inputs = json.loads((run / 'inputs.json').read_text()) 31 if not status.get('complete') or status.get('backend') != 'stable': 32 raise ValueError('completed stable generations required') 33 if not inputs.get('fresh_store') or not status.get('fresh_store'): 34 raise ValueError('fresh-store execution evidence required; seeded checkpoints are not reproduction') 35 if any(status.get(key) != value for key, value in inputs.items()): 36 raise ValueError('status/input provenance mismatch') 37 if inputs.get('execution_host', 'haskell') != execution_host: 38 raise ValueError('execution host mismatch') 39 execution_io, _ = versions(inputs) 40 execution_flags(execution_io, execution_host) 41 if 'produced_io_version' not in inputs and (run / 'produced-io-adapter.arboricx').exists(): 42 raise ValueError('missing generation IO metadata') 43 if 'produced_io_version' in inputs: 44 produced_adapter(run, inputs) 45 invocation = json.loads((run / 'invocation.json').read_text()) 46 check_invocation(inputs, invocation, execution_host) 47 if execution_host == 'haskell-ffi': 48 request = json.loads((run / 'execution-request.json').read_text()) 49 if version(request.get('execution_io_version', 1)) != execution_io: 50 raise ValueError('execution request IO version mismatch') 51 for key in ('worker_export','compiler_bundle_sha256','adapter_sha256'): 52 if request.get(key) != inputs[key]: 53 raise ValueError('execution request identity mismatch: '+key) 54 root = source_root or Path(inputs['source_root']) 55 text, entries, _ = source_map(run / 'source-map.v1', root, 'source_map') 56 actual = snapshot(text, entries, root) 57 if inputs.get('root') != 'source_map' or any(inputs.get(k) != v for k, v in actual.items()): 58 raise ValueError('frozen source/map fingerprint mismatch') 59 if sha(runtime) != inputs['runtime_sha256']: 60 raise ValueError('runtime identity mismatch') 61 if sha(run / 'compiler.arboricx') != inputs['compiler_bundle_sha256']: 62 raise ValueError('compiler bundle identity mismatch') 63 if sha(run / 'io-adapter.arboricx') != inputs['adapter_sha256']: 64 raise ValueError('adapter identity mismatch') 65 for name in ARTIFACTS: 66 if sha(run / name) != status['artifacts'][name]['sha256']: 67 raise ValueError('packaged artifact identity mismatch: ' + name) 68 command = [sys.executable, str(REPO / 'test/bootstrap_store_check.py'), str(run)] 69 if source_root: 70 command += ['--source-root', str(source_root)] 71 audit, _ = run_guarded(command, memory_mib=8192, seconds=600) 72 return inputs, status, audit 73 74 75 def verify(first, second, tricu, source_root=None): 76 first, second = first.resolve(), second.resolve() 77 if first == second: 78 raise ValueError('two independent generation directories required') 79 records = [] 80 audits = [] 81 for run in (first, second): 82 inputs, _, audit = verify_generation(run, tricu, source_root) 83 audits.append(audit) 84 records.append(inputs) 85 for key in ('sources', 'map_sha256', 'backend', 'root', 'runtime_sha256'): 86 if records[0][key] != records[1][key]: 87 raise ValueError('generation inputs differ: ' + key) 88 check_pair(records[0], records[1]) 89 if sha(first / 'rebuilt-compiler.arboricx') != records[1]['compiler_bundle_sha256']: 90 raise ValueError('second generation did not execute the first rebuilt compiler') 91 for name in ARTIFACTS: 92 if sha(first / name) != sha(second / name): 93 raise ValueError('artifact reproduction failure: ' + name) 94 # Check actual manifests, not the runner's recorded comparison booleans. 95 for alias in records[0]['sources']: 96 a = (first / 'cas/cache/source-build' / alias).read_text().split('\t')[2] 97 b = (second / 'cas/cache/source-build' / alias).read_text().split('\t')[2] 98 if a != b: 99 raise ValueError('manifest reproduction failure: ' + alias) 100 return dict(accepted=True, backend='stable', generations=[str(first), str(second)], 101 inputs=records, audits=audits, 102 artifacts={name: sha(second / name) for name in ARTIFACTS}) 103 104 105 def main(): 106 parser = argparse.ArgumentParser(description=__doc__) 107 parser.add_argument('first', type=Path) 108 parser.add_argument('second', type=Path) 109 parser.add_argument('--tricu', type=Path, required=True) 110 parser.add_argument('--source-root', type=Path, help='explicit relocated frozen source for restore validation') 111 parser.add_argument('--output', type=Path, required=True, help='new acceptance evidence file; never overwritten') 112 args = parser.parse_args() 113 if args.output.exists(): 114 parser.error('acceptance evidence already exists; do not overwrite') 115 evidence = verify(args.first, args.second, args.tricu.resolve(), args.source_root) 116 args.output.write_text(json.dumps(evidence, indent=2) + '\n') 117 print('Accepted independently audited fresh-store stable reproduction: ' + str(args.output)) 118 119 120 if __name__ == '__main__': 121 main()