Seal Phase 4AN exact continuation engineering evidence
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
"""Verify AN publication bytes in Git's index or one immutable commit.
|
||||
|
||||
No working-tree payload fallback, Git writes, network, bulk verification or
|
||||
scientific execution. The publication receipt SHA must be externally trusted.
|
||||
Only the 21 registered engineering sources are allowed in the AN model scope.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path, PurePosixPath
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
PROJECT = '/data/digital-drosophila'
|
||||
MODEL = 'models/male-cns-lif/olfactory-conditioning-lab-v1'
|
||||
EXPERIMENT = 'experiments/phase4an-engineering-validation'
|
||||
REGISTRY = 'registry/phase4an-engineering-validation'
|
||||
MODULES = ('runtime_light', 'runtime_continuation', 'engineering_inputs', 'engineering_plan',
|
||||
'engineering_compare', 'engineering_queue_audit', 'engineering_prepare',
|
||||
'engineering_workflow', 'engineering_scopes', 'engineering_analysis')
|
||||
SOURCES = tuple(sorted([m + '.py' for m in MODULES] + ['test_' + m + '.py' for m in MODULES] + ['ENGINEERING_GATE_DRAFT.md']))
|
||||
MAX_BYTES = 4 * 1024**2
|
||||
SUFFIXES = frozenset(('.json', '.csv', '.md', '.log', '.sha256'))
|
||||
CONTROL = frozenset(('manifest.json', 'checksums.sha256'))
|
||||
SIDE = frozenset(('evidence-index.json', 'source-identity.json', 'seal-verification.json'))
|
||||
SHA = re.compile(r'[0-9a-f]{64}\Z')
|
||||
OID = re.compile(r'(?:[0-9a-f]{40}|[0-9a-f]{64})\Z')
|
||||
STATUSES = frozenset(('PASS-EXACT-LIGHT-CONTINUATION-ENGINEERING-NOT-LEARNING', 'FAIL-NUMERICAL', 'INCOMPLETE-COVERAGE'))
|
||||
|
||||
|
||||
def require(value, message):
|
||||
if not value:
|
||||
raise ValueError(message)
|
||||
|
||||
|
||||
def sha(data):
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def relative(name):
|
||||
require(type(name) is str and name and not any(c in name for c in '\\\x00\r\n\t'), 'Invalid relative path')
|
||||
path = PurePosixPath(name)
|
||||
require(not path.is_absolute() and str(path) == name and all(p not in ('', '.', '..') and ':' not in p for p in path.parts),
|
||||
'Noncanonical or escaping path: ' + name)
|
||||
return name
|
||||
|
||||
|
||||
def parse(data):
|
||||
def pairs(rows):
|
||||
result = {}
|
||||
for name, value in rows:
|
||||
require(name not in result, 'Duplicate JSON field: ' + name)
|
||||
result[name] = value
|
||||
return result
|
||||
return json.loads(data.decode('utf-8'), object_pairs_hook=pairs,
|
||||
parse_constant=lambda value: (_ for _ in ()).throw(ValueError('Nonfinite JSON: ' + value)))
|
||||
|
||||
|
||||
def inventory(rows):
|
||||
require(type(rows) is list and rows, 'Missing payload inventory')
|
||||
indexed = {}
|
||||
for row in rows:
|
||||
require(type(row) is dict and set(row) == {'path', 'bytes', 'sha256'}, 'Invalid inventory row schema')
|
||||
path = relative(row['path'])
|
||||
require(path not in indexed and '__pycache__' not in PurePosixPath(path).parts and
|
||||
type(row['bytes']) is int and row['bytes'] >= 0 and type(row['sha256']) is str and SHA.fullmatch(row['sha256']),
|
||||
'Invalid/duplicate inventory row: ' + path)
|
||||
indexed[path] = row
|
||||
return indexed
|
||||
|
||||
|
||||
class GitBlobs:
|
||||
def __init__(self, repo, staged, commit):
|
||||
require(type(staged) is bool and staged != (commit is not None), 'Select exactly one of staged or commit')
|
||||
self.repo = Path(repo).resolve(strict=True)
|
||||
env = os.environ.copy()
|
||||
for key in ('GIT_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE', 'GIT_COMMON_DIR', 'GIT_OBJECT_DIRECTORY', 'GIT_ALTERNATE_OBJECT_DIRECTORIES'):
|
||||
env.pop(key, None)
|
||||
env.update(GIT_OPTIONAL_LOCKS='0', GIT_NO_REPLACE_OBJECTS='1', GIT_NO_LAZY_FETCH='1')
|
||||
self.env = env
|
||||
self.base = ['git', '--no-optional-locks', '-c', 'core.fsmonitor=false', '-c', 'gc.auto=0', '-C', str(self.repo)]
|
||||
require(Path(self.command('rev-parse', '--show-toplevel').decode().strip()).resolve() == self.repo, 'Supply repository top-level')
|
||||
self.staged, self.commit = staged, None
|
||||
if not staged:
|
||||
require(type(commit) is str and commit and not commit.startswith('-') and not any(c in commit for c in '\x00\r\n'), 'Invalid commit selector')
|
||||
self.commit = self.command('rev-parse', '--verify', '--end-of-options', commit + '^{commit}').decode('ascii').strip()
|
||||
require(OID.fullmatch(self.commit), 'Unresolved commit identity')
|
||||
self.snapshot = self.listing()
|
||||
self.entries, self.verified = {}, {}
|
||||
require(self.snapshot.endswith(b'\0'), 'Empty or malformed scoped Git inventory')
|
||||
for raw in self.snapshot[:-1].split(b'\0'):
|
||||
metadata, path = raw.split(b'\t', 1); fields = metadata.decode('ascii').split(' ')
|
||||
require(len(fields) == 3, 'Malformed Git inventory entry')
|
||||
mode, second, third = fields
|
||||
oid = second if staged else third
|
||||
require((third == '0' if staged else second == 'blob') and mode in ('100644', '100755') and OID.fullmatch(oid),
|
||||
'Unmerged, symlink, gitlink or nonregular blob is forbidden')
|
||||
name = relative(path.decode('utf-8'))
|
||||
require(name.startswith(MODEL + '/') or name.startswith(REGISTRY + '/'), 'Out-of-scope Git blob')
|
||||
require(name not in self.entries, 'Duplicate Git path')
|
||||
self.entries[name] = (mode, oid)
|
||||
self.batch = subprocess.Popen(self.base + ['cat-file', '--batch'], env=self.env,
|
||||
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
def command(self, *args):
|
||||
result = subprocess.run(self.base + list(args), env=self.env, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
require(result.returncode == 0, 'Git read failed: ' + result.stderr.decode('utf-8', 'replace').strip())
|
||||
return result.stdout
|
||||
|
||||
def listing(self):
|
||||
return (self.command('ls-files', '--stage', '--full-name', '-z', '--', MODEL, REGISTRY) if self.staged else
|
||||
self.command('ls-tree', '-r', '-z', '--full-tree', self.commit, '--', MODEL, REGISTRY))
|
||||
|
||||
def read(self, path, expected=None):
|
||||
require(path in self.entries, 'Missing Git blob: ' + path)
|
||||
mode, oid = self.entries[path]
|
||||
self.batch.stdin.write((oid + '\n').encode('ascii')); self.batch.stdin.flush()
|
||||
fields = self.batch.stdout.readline(256).decode('ascii').strip().split(' ')
|
||||
require(len(fields) == 3 and fields[:2] == [oid, 'blob'] and fields[2].isdigit(), 'Invalid Git blob response')
|
||||
size = int(fields[2]); require(size <= MAX_BYTES, 'Selected Git blob exceeds 4 MiB: ' + path)
|
||||
data = self.batch.stdout.read(size)
|
||||
require(len(data) == size and self.batch.stdout.read(1) == b'\n', 'Truncated Git object')
|
||||
if expected is not None:
|
||||
require(size == expected['bytes'] and sha(data) == expected['sha256'], 'Git blob SHA/bytes mismatch: ' + path)
|
||||
self.verified[path] = dict(path=path, bytes=size, sha256=sha(data), git_oid=oid, git_mode=mode)
|
||||
return data
|
||||
|
||||
def close(self):
|
||||
# On a malformed/oversize object the child might still be writing.
|
||||
# Terminating this read-only batch process cannot alter the repository.
|
||||
if self.batch.poll() is None:
|
||||
self.batch.terminate()
|
||||
self.batch.communicate(timeout=10)
|
||||
|
||||
|
||||
def _seal(manifest_bytes, checksum_bytes, receipt):
|
||||
require(sha(manifest_bytes) == receipt['experiment_manifest_sha256'] and
|
||||
sha(checksum_bytes) == receipt['experiment_checksums_sha256'], 'Receipt-bound seal control mismatch')
|
||||
manifest = parse(manifest_bytes)
|
||||
require(type(manifest) is dict and set(manifest) == {'root', 'files'} and manifest['root'] == PROJECT + '/' + EXPERIMENT,
|
||||
'Wrong experiment seal root/schema')
|
||||
rows = inventory(manifest['files']); require(not (set(rows) & CONTROL), 'Seal controls cannot be payload rows')
|
||||
checks = {}
|
||||
for line in checksum_bytes.decode('utf-8').splitlines():
|
||||
require(len(line) >= 67 and SHA.fullmatch(line[:64]) and line[64:66] == ' ', 'Malformed checksum row')
|
||||
name = relative(line[66:]); require(name not in checks, 'Duplicate checksum row'); checks[name] = line[:64]
|
||||
require(checks == {**{k: v['sha256'] for k, v in rows.items()}, 'manifest.json': sha(manifest_bytes)},
|
||||
'Checksum inventory or manifest self-SHA mismatch')
|
||||
return rows
|
||||
|
||||
|
||||
def _selection(row):
|
||||
path = PurePosixPath(row['path'])
|
||||
if path.parts[0] == 'extension-build':
|
||||
return None, 'build_artifact'
|
||||
if row['bytes'] > MAX_BYTES:
|
||||
return None, 'exceeds_4_MiB'
|
||||
if path.parts[:2] == ('protocol', 'source-snapshot') and len(path.parts) == 3 and path.name in SOURCES:
|
||||
return 'source-snapshot/' + path.name, None
|
||||
if path.suffix.lower() not in SUFFIXES:
|
||||
return None, 'bulk_binary_or_unselected_suffix_including_jsonl'
|
||||
return 'evidence/' + row['path'], None
|
||||
|
||||
|
||||
def verify(repo, receipt_sha, *, staged=False, commit=None):
|
||||
require(type(receipt_sha) is str and SHA.fullmatch(receipt_sha), 'Externally trusted receipt SHA-256 is mandatory')
|
||||
git = GitBlobs(repo, staged, commit)
|
||||
try:
|
||||
raw = git.read(REGISTRY + '/publication-receipt.json')
|
||||
require(sha(raw) == receipt_sha, 'External receipt SHA-256 mismatch')
|
||||
receipt = parse(raw)
|
||||
require(receipt.get('schema') == 'phase4an-engineering-publication-v1' and receipt.get('publication_complete') is True and
|
||||
receipt.get('registry') == REGISTRY and receipt.get('project_root') == PROJECT and receipt.get('bulk_remains_at') == EXPERIMENT and
|
||||
receipt.get('declared_status') in STATUSES and receipt.get('model_directory_sealed') is False and
|
||||
type(receipt.get('used_source_count')) is int and receipt['used_source_count'] == 21 and
|
||||
receipt.get('model_files_copied') is False and receipt.get('complete_bulk_backup') is False and
|
||||
receipt.get('training') is False and receipt.get('candidate_generated') is False and
|
||||
receipt.get('learning_pass_claimed') is False, 'Wrong AN publication scope or declarations')
|
||||
rows = inventory(receipt['files']); require('publication-receipt.json' not in rows, 'Receipt cannot self-hash')
|
||||
actual = {p[len(REGISTRY) + 1:] for p in git.entries if p.startswith(REGISTRY + '/')}
|
||||
require(actual == set(rows) | {'publication-receipt.json'}, 'Registry has unlisted/missing Git files')
|
||||
required = SIDE | {'evidence/manifest.json', 'evidence/checksums.sha256', 'evidence/protocol/preregistered.json',
|
||||
'evidence/audit/final-status.json', 'evidence/audit/engineering-analysis.json'}
|
||||
require(required <= rows.keys(), 'Missing registry controls/evidence')
|
||||
retained = {}
|
||||
for name, row in rows.items():
|
||||
require(row['bytes'] <= MAX_BYTES, 'Registry file exceeds publication cap')
|
||||
data = git.read(REGISTRY + '/' + name, row)
|
||||
if name in required:
|
||||
retained[name] = data
|
||||
payload = _seal(retained['evidence/manifest.json'], retained['evidence/checksums.sha256'], receipt)
|
||||
require({name for name in payload if name.startswith('protocol/source-snapshot/')} ==
|
||||
{'protocol/source-snapshot/' + name for name in SOURCES}, 'Sealed source snapshots are not exactly the 21 used sources')
|
||||
controls = {name: dict(path=name, bytes=len(retained['evidence/' + name]), sha256=sha(retained['evidence/' + name])) for name in CONTROL}
|
||||
expected_index, omitted = {}, []
|
||||
for row in [*payload.values(), *controls.values()]:
|
||||
destination, reason = _selection(row)
|
||||
if destination is None:
|
||||
omitted.append(dict(row, reason=reason))
|
||||
else:
|
||||
require(destination not in expected_index, 'Duplicate publication destination')
|
||||
expected_index[destination] = dict(source=EXPERIMENT + '/' + row['path'], copy=destination, bytes=row['bytes'], sha256=row['sha256'])
|
||||
indexed = {}
|
||||
index = parse(retained['evidence-index.json']); require(type(index) is list, 'Malformed evidence index')
|
||||
for row in index:
|
||||
require(type(row) is dict and set(row) == {'source', 'copy', 'bytes', 'sha256'}, 'Invalid evidence-index row')
|
||||
require(type(row['bytes']) is int and row['bytes'] >= 0 and type(row['sha256']) is str and SHA.fullmatch(row['sha256']), 'Invalid evidence-index size/hash')
|
||||
name = relative(row['copy']); relative(row['source']); require(name not in indexed, 'Duplicate evidence-index destination')
|
||||
indexed[name] = row
|
||||
require(indexed == expected_index and set(rows) == set(indexed) | SIDE, 'Evidence index does not match seal/selection/receipt')
|
||||
for name, row in indexed.items():
|
||||
require(rows[name] == dict(path=name, bytes=row['bytes'], sha256=row['sha256']), 'Evidence receipt row mismatch')
|
||||
require(type(receipt.get('copied_evidence_files')) is int and receipt['copied_evidence_files'] == len(indexed) and
|
||||
type(receipt.get('copied_evidence_bytes')) is int and receipt['copied_evidence_bytes'] == sum(r['bytes'] for r in indexed.values()) and
|
||||
sorted(receipt.get('experiment_payload_not_copied', []), key=lambda r: r['path']) == sorted(omitted, key=lambda r: r['path']),
|
||||
'Copied/omitted payload accounting mismatch')
|
||||
proof = parse(retained['seal-verification.json'])
|
||||
require(proof.get('pass_all') is True and proof.get('root') == PROJECT + '/' + EXPERIMENT and
|
||||
proof.get('manifest_sha256') == receipt['experiment_manifest_sha256'] and
|
||||
proof.get('checksums_sha256') == receipt['experiment_checksums_sha256'] and
|
||||
proof.get('payload_file_count') == len(payload) and proof.get('payload_bytes') == sum(r['bytes'] for r in payload.values()),
|
||||
'Seal-verification sidecar contradicts controls')
|
||||
reg = parse(retained['evidence/protocol/preregistered.json'])
|
||||
require(reg.get('schema') == 'phase4an-engineering-registration-v1' and reg.get('training') is False and
|
||||
sha(retained['evidence/protocol/preregistered.json']) == receipt['preregistration_sha256'], 'Registration anchor mismatch')
|
||||
identities = parse(retained['source-identity.json'])
|
||||
require(type(identities) is list and len(identities) == 21 and receipt.get('source_identity') == 'source-identity.json', 'Missing source identity inventory')
|
||||
require(set(reg['source_sha256']) == {PROJECT + '/' + MODEL + '/' + name for name in SOURCES}, 'Registration does not bind exactly 21 engineering sources')
|
||||
observed_model = {p[len(MODEL) + 1:] for p in git.entries if p.startswith(MODEL + '/')}
|
||||
require(observed_model == set(SOURCES), 'Model Git scope must contain exactly the 21 registered used sources, not drafts')
|
||||
by_copy = {}
|
||||
for row in identities:
|
||||
require(type(row) is dict and set(row) == {'model_source', 'sealed_source', 'copy', 'bytes', 'sha256'}, 'Invalid source identity row')
|
||||
require(type(row['bytes']) is int and row['bytes'] >= 0 and type(row['sha256']) is str and SHA.fullmatch(row['sha256']), 'Invalid source identity size/hash')
|
||||
name = relative(row['copy']); require(name not in by_copy, 'Duplicate source identity'); by_copy[name] = row
|
||||
require(set(by_copy) == {'source-snapshot/' + name for name in SOURCES}, 'Source identity coverage mismatch')
|
||||
for name in SOURCES:
|
||||
copy_name, sealed = 'source-snapshot/' + name, 'protocol/source-snapshot/' + name
|
||||
row, manifest_row = by_copy[copy_name], payload[sealed]
|
||||
expected = dict(model_source=MODEL + '/' + name, sealed_source=EXPERIMENT + '/' + sealed,
|
||||
copy=copy_name, bytes=manifest_row['bytes'], sha256=manifest_row['sha256'])
|
||||
require(row == expected and rows[copy_name] == dict(path=copy_name, bytes=row['bytes'], sha256=row['sha256']) and
|
||||
reg['source_sha256'][PROJECT + '/' + MODEL + '/' + name] == row['sha256'] and
|
||||
reg['input_sha256'].get(PROJECT + '/' + EXPERIMENT + '/' + sealed) == row['sha256'], 'Source/snapshot/registration disagreement: ' + name)
|
||||
git.read(MODEL + '/' + name, row)
|
||||
for name, key in [('audit/final-status.json', 'final_sha256'), ('audit/engineering-analysis.json', 'analysis_sha256')]:
|
||||
require(sha(retained['evidence/' + name]) == receipt[key], 'Final/analysis anchor mismatch')
|
||||
require(parse(retained['evidence/' + name]).get('status') == receipt['declared_status'], 'Declared status disagreement')
|
||||
require(git.listing() == git.snapshot, 'Scoped Git inventory changed during verification')
|
||||
return dict(pass_all=True, verification='AN actual Git blob publication integrity', source='staged-index' if staged else 'commit',
|
||||
commit=git.commit, receipt_sha256=receipt_sha, declared_status=receipt['declared_status'], model_source_files=21,
|
||||
registry_files=len(rows) + 1, verified_blob_count=len(git.verified), verified_bytes=sum(v['bytes'] for v in git.verified.values()),
|
||||
scoped_git_inventory_sha256=sha(git.snapshot), working_tree_payload_used=False, git_index_modified=False,
|
||||
experiment_bulk_verified_by_this_tool=False, scientific_gate_executed=False, model_directory_declared_sealed=False,
|
||||
publication_integrity_is_not_scientific_pass=True)
|
||||
finally:
|
||||
git.close()
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--repo', required=True)
|
||||
parser.add_argument('--receipt-sha256', required=True)
|
||||
source = parser.add_mutually_exclusive_group(required=True)
|
||||
source.add_argument('--staged', action='store_true'); source.add_argument('--commit')
|
||||
args = parser.parse_args(argv)
|
||||
try:
|
||||
result = verify(args.repo, args.receipt_sha256, staged=args.staged, commit=args.commit)
|
||||
except (ValueError, KeyError, TypeError, AttributeError, OSError, UnicodeError, EOFError) as error:
|
||||
print(json.dumps(dict(pass_all=False, error_type=type(error).__name__, error=str(error), scientific_gate_executed=False)))
|
||||
return 1
|
||||
print(json.dumps(result, ensure_ascii=False, allow_nan=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user