Files
ARCADIA-Studio/harness/experiments/navigation-prerelease-native-20260911/payload/helper.py
T

267 lines
12 KiB
Python

"""Packaged navigation trial; GUI input and release acceptance belong to root.
No execution is admitted until binding.json is completed and its SHA is pinned
after independent artifact review. Nothing here operates the GUI or compiles.
"""
import argparse
import ctypes
from ctypes import wintypes
from datetime import datetime, timezone
import hashlib
import json
import os
from pathlib import Path, PurePosixPath
import re
import shutil
import subprocess
import sys
import zipfile
ROOT = Path(__file__).resolve().parents[2]
BASE = Path(__file__).resolve().parent
TRIAL = BASE / 'trial'
BINDING_SHA = '82606e6b3a5fb5dada3e55adcd5cd76792c9f938e6907313539dae7fc5125657'
SOURCE = '33c6d83e9ba9a5b1310b1b627d7c40affcbcf15c'
NAME = 'ArcadiaStudio-0.2.0-preview.navigation.20260911-x86_64-pc-windows-gnullvm'
EXPECTED = {
'arcadia-studio.exe': 'b183ef31f16b3986538b3f2aba8435eab45978635c47f99ca01be6e72b168848',
'arcadia-server.exe': 'efeeb89c0d2e0f93df4472cb3e9f48b2b722a37ae1043c90a5a56f2e845fe665',
'libunwind.dll': 'f45604a12e8bcd839fd86f667c1be1cc7c4f9da2b3c69ff381ac708f0e1ae7d4',
}
FIXTURES = ROOT / '.local/text-navigation-native-20260911-preparation'
FIXTURE_SHA = 'edd4157fc31823f4494dee91862728b87f12ef51c49b8d1fd86a954bc9ecc1ed'
ORACLE = ROOT / '.local/staged-source-oracle-20260911/staged-source-authoring-contract.exe'
ORACLE_SHA = '967d9ba3c6fb3a7efb410833903ec94556ca2d48a67d5072c8bb160f40a00f9c'
def now():
return datetime.now(timezone.utc).isoformat()
def ordinary(path):
path = Path(path).absolute()
for p in (path, *path.parents):
if p.exists():
assert not p.is_symlink() and not (getattr(p.lstat(), 'st_file_attributes', 0) & 0x400), str(p)
return path
def sha(path):
with ordinary(path).open('rb') as stream:
return hashlib.file_digest(stream, 'sha256').hexdigest()
def load(path):
return json.loads(ordinary(path).read_text(encoding='utf-8-sig'))
def record(path, value):
ordinary(path)
with path.open('x', encoding='utf-8', newline='\n') as out:
json.dump(value, out, ensure_ascii=False, indent=2)
out.write('\n')
def relative(name):
p = PurePosixPath(name)
assert name == p.as_posix() and not p.is_absolute() and p.parts
assert all(x not in ('.', '..') for x in p.parts)
assert not any(c in name for c in '\\:\r\n')
return name
def local(name):
p = ordinary(ROOT / relative(name))
assert p.resolve().is_relative_to(ROOT / '.local')
return p
def inventory(folder):
ordinary(folder)
found = {}
for p in folder.rglob('*'):
ordinary(p)
if p.is_file():
found[p.relative_to(folder).as_posix()] = sha(p)
assert len({p.casefold() for p in found}) == len(found)
return found
def copy_exact(source, destination):
before = inventory(source)
assert not destination.exists()
shutil.copytree(source, destination)
assert inventory(source) == inventory(destination) == before
return before
def environment():
parent = {k.upper(): v for k, v in os.environ.items()}
standard = {k: parent[k.upper()] for k in ('SystemRoot', 'SystemDrive', 'ProgramData', 'ALLUSERSPROFILE', 'PUBLIC')}
assert all(v and '%' not in v for v in standard.values())
system = standard['SystemRoot']
return {**standard, 'WINDIR': system, 'PATH': system + '\\System32;' + system,
'USERPROFILE': str(TRIAL / 'profile'), 'APPDATA': str(TRIAL / 'profile/roaming'),
'LOCALAPPDATA': str(TRIAL / 'profile/local'), 'TEMP': str(TRIAL / 'tmp'), 'TMP': str(TRIAL / 'tmp')}
def process_identity(pid):
"""Read-only process query; invalid PID means absent, access errors refuse."""
api = ctypes.WinDLL('kernel32', use_last_error=True)
api.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
api.OpenProcess.restype = wintypes.HANDLE
api.CloseHandle.argtypes = [wintypes.HANDLE]
api.GetProcessTimes.argtypes = [wintypes.HANDLE] + [ctypes.POINTER(wintypes.FILETIME)] * 4
api.GetProcessTimes.restype = wintypes.BOOL
api.QueryFullProcessImageNameW.argtypes = [wintypes.HANDLE, wintypes.DWORD, wintypes.LPWSTR, ctypes.POINTER(wintypes.DWORD)]
api.QueryFullProcessImageNameW.restype = wintypes.BOOL
handle = api.OpenProcess(0x1000, False, pid)
if not handle:
error = ctypes.get_last_error()
assert error == 87, f'Process query failed: {error}'
return None
try:
created, exited, kernel, user = (wintypes.FILETIME() for _ in range(4))
assert api.GetProcessTimes(handle, ctypes.byref(created), ctypes.byref(exited), ctypes.byref(kernel), ctypes.byref(user))
buffer, count = ctypes.create_unicode_buffer(32768), wintypes.DWORD(32768)
assert api.QueryFullProcessImageNameW(handle, 0, buffer, ctypes.byref(count))
return {'path': str(Path(buffer.value).resolve()), 'creation_filetime': (created.dwHighDateTime << 32) | created.dwLowDateTime}
finally:
api.CloseHandle(handle)
def admission(verify_inventory=True):
assert BINDING_SHA and sha(BASE / 'binding.json') == BINDING_SHA, 'Package binding is pending or changed.'
binding = load(BASE / 'binding.json')
assert binding['status'] == 'READY' and binding['source_commit'] == SOURCE
archive, review_path, producer_path, package = (local(binding[k]) for k in ('archive_path', 'review_path', 'producer_readback_path', 'package_path'))
assert archive.name == NAME + '.zip' and package.name == NAME
assert package.resolve() != (producer_path.parent / 'packages' / NAME).resolve(), 'Use independent extraction, not producer stage.'
assert sha(archive) == binding['archive_sha256'] and archive.stat().st_size == binding['archive_bytes']
assert sha(review_path) == binding['review_sha256'] and sha(producer_path) == binding['producer_readback_sha256']
review, producer = load(review_path), load(producer_path)
assert review['status'] == producer['status'] == 'PASS'
assert review['archive']['sha256'] == producer['archive_sha256'] == binding['archive_sha256']
assert review['archive']['bytes'] == producer['archive_bytes'] == binding['archive_bytes']
assert Path(review['archive']['path']).resolve() == archive.resolve()
expected = review['archive']['files']
assert expected == {p: v['sha256'] for p, v in producer['files'].items()}
assert review['archive']['file_count'] == producer['file_count'] == len(expected)
if verify_inventory:
assert inventory(package) == expected
for p, h in EXPECTED.items():
assert expected[p] == h
zipped = {}
with zipfile.ZipFile(archive) as z:
for item in z.infolist():
assert item.filename.startswith(NAME + '/') and not item.is_dir()
p = relative(item.filename[len(NAME) + 1:])
assert p not in zipped
zipped[p] = hashlib.sha256(z.read(item)).hexdigest()
assert zipped == expected
return binding, package, expected
def fixture():
assert sha(FIXTURES / 'fixture-manifest.json') == FIXTURE_SHA
expected = {e['path'].removeprefix('valid-baseline/'): e['sha256'] for e in load(FIXTURES / 'fixture-manifest.json') if e['path'].startswith('valid-baseline/')}
assert len(expected) == 4 and inventory(FIXTURES / 'valid-baseline') == expected
assert sha(ORACLE) == ORACLE_SHA
return FIXTURES / 'valid-baseline', expected
def launch():
assert not TRIAL.exists(), 'Preserve old trial; never overwrite.'
binding, package, expected = admission()
baseline, files = fixture()
TRIAL.mkdir()
for folder in ('profile/local', 'profile/roaming', 'profile/Desktop', 'tmp', 'screenshots', 'checkpoints'):
(TRIAL / folder).mkdir(parents=True)
assert copy_exact(baseline, TRIAL / 'work') == files
record(TRIAL / 'package-before.json', expected)
record(TRIAL / 'source-before.json', files)
record(TRIAL / 'admission.json', {'atUtc': now(), 'binding_sha256': BINDING_SHA, 'binding': binding, 'fixture_sha256': FIXTURE_SHA, 'scope': 'Artifact/fixture admission, not native acceptance.'})
env = environment()
startup = subprocess.STARTUPINFO()
startup.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startup.wShowWindow = 0
with (TRIAL / 'stdout.log').open('xb') as out, (TRIAL / 'stderr.log').open('xb') as err:
process = subprocess.Popen([str(package / 'arcadia-studio.exe')], cwd=package, env=env, stdout=out, stderr=err, startupinfo=startup)
# Preserve PID immediately even if a subsequent read-only identity query fails.
receipt = {'atUtc': now(), 'pid': process.pid, 'package': str(package), 'environment': env,
'binding_sha256': BINDING_SHA, 'source': str(TRIAL / 'work/project.arcadia'), 'scope': 'Identified packaged GUI launched; native acceptance pending.'}
record(TRIAL / 'launch.json', receipt)
identity = process_identity(process.pid)
assert identity and Path(identity['path']) == (package / 'arcadia-studio.exe').resolve()
record(TRIAL / 'process-identity.json', identity)
print(json.dumps(receipt, ensure_ascii=False, indent=2))
def package_guard(label):
binding, package, expected = admission(verify_inventory=False)
launch_receipt = load(TRIAL / 'launch.json')
assert launch_receipt['binding_sha256'] == BINDING_SHA and Path(launch_receipt['package']).resolve() == package.resolve()
assert load(TRIAL / 'package-before.json') == expected
actual = inventory(package)
unchanged = actual == expected
record(TRIAL / (label + '-package.json'), {'atUtc': now(), 'archive_sha256': binding['archive_sha256'], 'files': actual,
'added': sorted(actual.keys() - expected.keys()), 'removed': sorted(expected.keys() - actual.keys()),
'changed': sorted(p for p in actual.keys() & expected.keys() if actual[p] != expected[p]),
'completeInventoryUnchanged': unchanged})
assert unchanged, 'Package changed; exact delta receipt preserved.'
return package
def saved(args):
assert args.after_visible_save, 'Root must observe successful Save first.'
assert re.fullmatch('[a-z0-9][a-z0-9-]{0,79}', args.label)
folder = TRIAL / 'checkpoints' / args.label
assert not folder.exists()
package = package_guard(args.label)
baseline, expected = fixture()
folder.mkdir()
actual = copy_exact(TRIAL / 'work', folder / 'sources')
record(folder / 'sources.json', actual)
assert actual == expected, 'Saved exact baseline files differ; preserve and inspect.'
env = environment()
env['PATH'] = str(package) + ';' + env['PATH']
command = [str(ORACLE), 'same', str(baseline / 'project.arcadia'), str(folder / 'sources/project.arcadia')]
result = subprocess.run(command, cwd=folder, env=env, capture_output=True)
(folder / 'stdout.log').write_bytes(result.stdout)
(folder / 'stderr.log').write_bytes(result.stderr)
record(folder / 'result.json', {'atUtc': now(), 'command': command, 'exitCode': result.returncode, 'oracle_sha256': ORACLE_SHA,
'scope': 'Saved disk/model oracle after root-observed Save; no independent UI assertion.'})
assert result.returncode == 0
package_guard(args.label + '-after-oracle')
print(json.dumps({'action': 'saved', 'oracle_exit': 0, 'source_files': len(actual)}))
def closed(args):
assert args.observed_absent, 'Root must first observe the GUI/window absent.'
launch_receipt = load(TRIAL / 'launch.json')
current = process_identity(launch_receipt['pid'])
original = load(TRIAL / 'process-identity.json')
assert current is None or current['creation_filetime'] != original['creation_filetime'], 'Original GUI process remains present.'
package = package_guard('after-close')
record(TRIAL / 'closed.json', {'atUtc': now(), 'pid': launch_receipt['pid'], 'originalProcessAbsent': True,
'pidReused': current is not None, 'completePackageUnchanged': True,
'scope': 'Root-observed window absence plus read-only process identity and package check; no numeric exit code observed.'})
print(json.dumps({'action': 'closed', 'package': str(package), 'result': 'PASS'}))
if __name__ == '__main__':
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')
parser = argparse.ArgumentParser()
parser.add_argument('action', choices=('launch', 'saved', 'closed'))
parser.add_argument('--label')
parser.add_argument('--after-visible-save', action='store_true')
parser.add_argument('--observed-absent', action='store_true')
args = parser.parse_args()
if args.action == 'launch':
launch()
elif args.action == 'saved':
saved(args)
else:
closed(args)