tricu

An interpreted language for exploring Tree Calculus
Log | Files | Refs | README | LICENSE

commit b49e05e66a98cf0a87c15ee7f08000f48ab4e5f3
parent a3b1cc20776e20ddcb160db9d7ba520caa92e91b
Author: James Eversole <james@eversole.co>
Date:   Fri, 16 Oct 2026 12:51:58 -0500

Give isolated generations matching procfs and preserve prelaunch continuation evidence

Native_math parity probe passes under nested user/PID/mount isolation. Continue only after re-auditing completed jobs and identical frozen input/runtime checks; never resume partially executed generation stores.

Diffstat:
Mext/zig/README.md | 12+++++++++++-
Mtest/bootstrap_cross_host.py | 66+++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
2 files changed, 64 insertions(+), 14 deletions(-)

diff --git a/ext/zig/README.md b/ext/zig/README.md @@ -285,7 +285,8 @@ comparisons, and no timing threshold is a test gate. `test/bootstrap_cross_host.py --probe` runs the frozen `native_math` closure on both hosts, with identical kits and a 1 GiB/120-second budget per job. It requires -Linux `bwrap` with working user/PID namespaces and a Nix Python environment: +Linux `bwrap` and util-linux `unshare` with working user/PID/mount namespaces, +and a Nix Python environment: ```sh nix develop -c python3 test/bootstrap_cross_host.py --probe \ @@ -302,3 +303,12 @@ independently audited object inventories. Its experiment definition records the source commit/archive, mapped-input hashes, runtimes, Python, tooling, limits and comparison boundaries. Historical `ext/zig/result` is the sole excluded archive entry: a tracked absolute build-output symlink, not a compiler input. + +The launcher first creates a private PID namespace with a matching procfs mount, +then starts the filesystem sandbox. This avoids assuming that the caller's `/proc` +uses its PID numbering. Both isolation tool identities are recorded. A full +experiment stopped before a job creates its generation directory can explicitly +use `--continue-experiment`: frozen sources/runtimes and completed generations +are reverified, the failed launch and original tooling are preserved, and the +pending job still starts fresh. Incomplete executed generations are never resumed +by this acceptance route. diff --git a/test/bootstrap_cross_host.py b/test/bootstrap_cross_host.py @@ -13,6 +13,7 @@ import shutil import subprocess import sys import tarfile +import time from bootstrap_acceptance import ARTIFACTS, sha, verify, verify_generation from bootstrap_generations import source_map, snapshot @@ -74,7 +75,8 @@ def verify_cross(first, second, control, zig, runtime, library, source): def sandbox_command(source, kit, stage, runtime, library, host, root, memory, seconds): - command = ['bwrap','--die-with-parent','--unshare-all', + command = ['unshare','--user','--map-root-user','--mount','--pid','--fork', + '--kill-child','--mount-proc','--','bwrap','--die-with-parent','--unshare-all', '--ro-bind','/nix/store','/nix/store','--proc','/proc','--dev','/dev', '--tmpfs','/tmp','--ro-bind',str(source),'/source', '--ro-bind',str(kit),'/kit','--bind',str(stage),'/work', @@ -99,6 +101,7 @@ def main(): p.add_argument('--zig-library',type=Path,required=True) p.add_argument('--probe',action='store_true',help='native_math on both hosts, 1 GiB/120s; no full generations') p.add_argument('--verify-only',action='store_true') + p.add_argument('--continue-experiment',action='store_true',help='re-audit completed jobs; continue only jobs that never entered execution') args=p.parse_args() directory=args.directory.resolve() runtime,library=args.tricu.resolve(),args.zig_library.resolve() @@ -109,19 +112,33 @@ def main(): runtime,library,source) print(json.dumps(evidence,indent=2)) return - directory.mkdir(parents=True,exist_ok=False) commit=subprocess.check_output(['git','rev-parse',args.source_commit+'^{commit}'],cwd=ROOT,text=True).strip() archive=directory/'source.tar' - with archive.open('wb') as out: - subprocess.run(['git','archive',commit],cwd=ROOT,stdout=out,check=True) - source.mkdir() - with tarfile.open(archive) as tar: - # Historical tracked build-output symlink, excluded by recovery archives - # too. Keep all other extraction safety checks; never follow it. - tar.extractall(source,members=(m for m in tar if m.name!='ext/zig/result'),filter='data') + if args.continue_experiment: + original=json.loads((directory/'experiment.json').read_text()) + if (args.probe or original['source_commit']!=commit or original['source_archive_sha256']!=sha(archive) + or original['runtime']!=dict(path=str(runtime),sha256=sha(runtime)) + or original['zig_library']!=dict(path=str(library),sha256=sha(library))): + raise ValueError('continuation requires identical frozen sources and runtimes') + with tarfile.open(archive) as tar: + for member in tar: + if member.isfile() and (source/member.name).read_bytes()!=tar.extractfile(member).read(): + raise ValueError('frozen source changed: '+member.name) + # Preserve the failed attempt, including its exact original tooling. + shutil.copyfile(directory/'report.json',directory/('report-before-'+str(time.time_ns())+'.json')) + else: + directory.mkdir(parents=True,exist_ok=False) + with archive.open('wb') as out: + subprocess.run(['git','archive',commit],cwd=ROOT,stdout=out,check=True) + source.mkdir() + with tarfile.open(archive) as tar: + # Historical tracked build-output symlink, excluded by recovery + # archives too. Keep all other extraction safety checks. + tar.extractall(source,members=(m for m in tar if m.name!='ext/zig/result'),filter='data') # The sandbox also mounts this snapshot read-only; never use live sources. map_text,entries,required=source_map(source/'bootstrap/compiler-source-map.v1',source,'source_map') - harness=directory/'harness/test' + attempt='continuation-'+str(time.time_ns()) if args.continue_experiment else 'harness' + harness=directory/attempt/'test' harness.mkdir(parents=True) for path in (ROOT/'test').glob('*.py'): shutil.copyfile(path,harness/path.name) tooling={path.name:sha(path) for path in harness.glob('*.py')} @@ -132,12 +149,12 @@ def main(): zig_library=dict(path=str(library),sha256=sha(library)), python=dict(path=sys.executable,sha256=sha(Path(sys.executable))), harness_sha256=sha(Path(__file__)), tooling_sha256=tooling, + isolation_tools={name:dict(path=str(Path(shutil.which(name)).resolve()),sha256=sha(Path(shutil.which(name)))) for name in ('unshare','bwrap')}, limits=dict(memory_mib=memory,wall_seconds=seconds,capabilities=8), expected='H2 = H3 = Z3: complete objects, receipts, checkpoints and three bundles', comparison_exclusions=['runtime/transport provenance','timings','logs','filesystem locations','invocation records'], **snapshot(map_text,entries,source)) - save(directory/'experiment.json',definition) - shutil.copyfile(__file__,directory/'experiment-harness.py') + save(directory/(attempt+'-definition.json' if args.continue_experiment else 'experiment.json'),definition) report=dict(complete=False,scope='probe' if args.probe else 'full',stages=[]) save(directory/'report.json',report) def job(label,kit,host): @@ -145,6 +162,18 @@ def main(): # independently captures/authenticates it again inside. load_kit(kit,'stable') stage=directory/label + if args.continue_experiment and stage.exists(): + if (stage/'generation/status.json').is_file(): + inputs,_,_=verify_generation(stage/'generation',runtime if host=='haskell' else library,source,host) + if inputs['compiler_bundle_sha256']!=sha(kit/'rebuilt-compiler.arboricx') or inputs['adapter_sha256']!=sha(kit/'io-adapter.arboricx'): + raise ValueError('completed job used a different kit') + report['stages'].append(label) + save(directory/'report.json',report) + print('Re-audited completed '+label,flush=True) + return stage/'generation' + if (stage/'generation').exists(): + raise ValueError('cannot resume a partially executed generation; select a fresh experiment') + stage.rename(directory/(label+'-launch-failed-'+str(time.time_ns()))) stage.mkdir() command=sandbox_command(source,kit,stage,runtime,library,host, 'native_math' if args.probe else 'source_map',memory,seconds) @@ -163,6 +192,12 @@ def main(): return stage/'generation' def kit_from(run,label): kit=directory/(label+'-kit') + if kit.exists(): + load_kit(kit,'stable') + for name in ('rebuilt-compiler.arboricx','io-adapter.arboricx'): + if (kit/name).read_bytes()!=(run/name).read_bytes(): + raise ValueError('retained kit differs from parent generation') + return kit kit.mkdir() artifacts={} for role,name,export in [('compiler','rebuilt-compiler.arboricx','sourceMapBuildStable'), @@ -178,7 +213,12 @@ def main(): if inventory(first)!=inventory(zig): raise ValueError('probe complete object inventories differ') else: second=job('H2',kit_from(first,'H1'),'haskell') - save(directory/'accepted-pair.json',verify(first,second,runtime,source)) + accepted=verify(first,second,runtime,source) + if (directory/'accepted-pair.json').exists(): + if json.loads((directory/'accepted-pair.json').read_text())!=accepted: + raise ValueError('existing pair acceptance changed') + else: + save(directory/'accepted-pair.json',accepted) kit=kit_from(second,'H2') control=job('H3',kit,'haskell') zig=job('Z3',kit,'zig')