Seal AO interface failure and verified isolated postprocessing diagnosis
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
"""Publish the sealed AO interface evidence, not a backup or learning verdict.
|
||||
|
||||
All three external anchors are mandatory. The original complete experiment is
|
||||
verified before and after copying; only this new exclusive registry is written.
|
||||
The registry then receives its own independently verified small-file seal.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path, PurePosixPath
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
ROOT = Path('/data/digital-drosophila')
|
||||
MODEL_REL = Path('models/male-cns-lif/olfactory-conditioning-lab-v1')
|
||||
EXP_REL = Path('experiments/phase4ao-conditioning-interface')
|
||||
REGISTRY_REL = Path('registry/phase4ao-conditioning-interface')
|
||||
HELPER_REL = Path('models/male-cns-lif/pn-recruitment-audit-v1/publication.py')
|
||||
HELPER_SHA = '7a79c499098b74163e7bb16adabcbc54e7069dafa204d0b99c7f4c03d79f8016'
|
||||
REGISTRATION = 'protocol/preregistered.json'
|
||||
FINAL = 'audit/final-status.json'
|
||||
ANALYSIS = 'audit/interface-analysis.json'
|
||||
FAILURE_ANALYSIS = 'audit/failure-analysis.json'
|
||||
FULL_PLAN = 'protocol/full-trial-plan.json'
|
||||
SNAPSHOT = 'protocol/source-snapshot/'
|
||||
MAX_BYTES = 4 * 1024 ** 2
|
||||
SUFFIXES = frozenset(('.json', '.md', '.log', '.sha256'))
|
||||
SHA = re.compile(r'^[0-9a-f]{64}$')
|
||||
|
||||
|
||||
def require(ok, message):
|
||||
if not ok:
|
||||
raise ValueError(message)
|
||||
|
||||
|
||||
def digest(path):
|
||||
value = hashlib.sha256()
|
||||
with Path(path).open('rb') as handle:
|
||||
for block in iter(lambda: handle.read(8 << 20), b''):
|
||||
value.update(block)
|
||||
return value.hexdigest()
|
||||
|
||||
|
||||
def load_helper(path):
|
||||
raw = Path(path).read_bytes()
|
||||
require(hashlib.sha256(raw).hexdigest() == HELPER_SHA, 'Frozen seal helper SHA mismatch')
|
||||
spec = importlib.util.spec_from_file_location('phase4ao_pinned_seal_helper', path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
# Execute only the exact pinned helper; never import old experiment runners
|
||||
# or change any helper globals. No import-generated __pycache__ is written.
|
||||
exec(compile(raw, str(path), 'exec'), module.__dict__)
|
||||
return module
|
||||
|
||||
|
||||
def write_json(path, value):
|
||||
raw = (json.dumps(value, sort_keys=True, indent=2, ensure_ascii=False,
|
||||
allow_nan=False) + '\n').encode('utf-8')
|
||||
require(len(raw) <= MAX_BYTES, 'Generated evidence exceeds the 4 MiB limit')
|
||||
with Path(path).open('xb') as handle:
|
||||
handle.write(raw)
|
||||
|
||||
|
||||
def small_json(helper, experiment, rows, name):
|
||||
require(name in rows and rows[name]['bytes'] <= MAX_BYTES, 'Missing/oversize required JSON: ' + name)
|
||||
path = helper._regular_file(experiment, name)
|
||||
require(digest(path) == rows[name]['sha256'], 'Evidence changed: ' + name)
|
||||
value = helper._json_bytes(path.read_bytes())
|
||||
require(isinstance(value, dict), 'Expected JSON object: ' + name)
|
||||
return value
|
||||
|
||||
|
||||
def source_identity(helper, root, experiment, rows, registration, registration_sha):
|
||||
model = root / MODEL_REL
|
||||
source_pins = registration.get('source_sha256')
|
||||
prepared = registration.get('prepared_file_sha256')
|
||||
require(isinstance(source_pins, dict) and source_pins and isinstance(prepared, dict),
|
||||
'Missing registered source/prepared inventory')
|
||||
files, sealed_names = [], set()
|
||||
for text, checksum in sorted(source_pins.items()):
|
||||
require(isinstance(text, str) and isinstance(checksum, str) and SHA.fullmatch(checksum),
|
||||
'Malformed registered source identity')
|
||||
path = Path(text)
|
||||
require(path.is_absolute() and path.parent == model and path.suffix in ('.py', '.md') and
|
||||
path.name not in ('', '.', '..') and '..' not in path.parts,
|
||||
'Registered source escapes the fixed model directory')
|
||||
original = helper._regular_file(root, path.relative_to(root).as_posix())
|
||||
name = SNAPSHOT + path.name
|
||||
require(name not in sealed_names and name in rows, 'Missing/duplicate registered snapshot')
|
||||
sealed_names.add(name)
|
||||
snapshot = helper._regular_file(experiment, name)
|
||||
row = rows[name]
|
||||
require(prepared.get(str(snapshot)) == checksum and row['sha256'] == checksum and
|
||||
row['bytes'] <= MAX_BYTES and original.stat().st_size == row['bytes'] and
|
||||
digest(original) == checksum and digest(snapshot) == checksum,
|
||||
'Model, registration, prepared inventory and sealed snapshot differ: ' + path.name)
|
||||
files.append(dict(model_source=path.relative_to(root).as_posix(), sealed_source=name,
|
||||
copy='source-snapshot/' + path.name, bytes=row['bytes'], sha256=checksum))
|
||||
actual = {name for name in rows if name.startswith(SNAPSHOT)}
|
||||
require(actual == sealed_names, 'Source snapshot inventory differs from registration')
|
||||
# Prepared AO files must also be the same sealed payload, not merely listed
|
||||
# in a registration whose sources happen to match.
|
||||
for text, checksum in prepared.items():
|
||||
require(isinstance(text, str) and isinstance(checksum, str) and SHA.fullmatch(checksum),
|
||||
'Malformed prepared-file identity')
|
||||
path = Path(text)
|
||||
require(path.is_absolute() and path.is_relative_to(experiment) and '..' not in path.parts,
|
||||
'Prepared file escapes this experiment')
|
||||
name = path.relative_to(experiment).as_posix()
|
||||
helper._relative_name(name)
|
||||
require(name in rows and rows[name]['sha256'] == checksum, 'Prepared file differs from seal: ' + name)
|
||||
require(FULL_PLAN in rows and registration.get('full_trial_plan_file_sha256') == rows[FULL_PLAN]['sha256'] and
|
||||
prepared.get(str(experiment / FULL_PLAN)) == rows[FULL_PLAN]['sha256'],
|
||||
'Full plan identity is not bound to the registration')
|
||||
require(type(registration.get('committed_source_blobs_verified')) is int and
|
||||
registration['committed_source_blobs_verified'] == len(files) and
|
||||
isinstance(registration.get('git_code_commit'), str) and
|
||||
re.fullmatch(r'[0-9a-f]{40}', registration['git_code_commit']), 'Missing registered source version')
|
||||
return dict(registration_sha256=registration_sha, source_count=len(files), files=files,
|
||||
git_code_commit=registration['git_code_commit'],
|
||||
original_snapshot_registration_exact=True,
|
||||
git_blobs_reverified_by_publisher=False, model_directory_sealed=False)
|
||||
|
||||
|
||||
def selection(row):
|
||||
name = row['path']
|
||||
parsed = PurePosixPath(name)
|
||||
if name == FULL_PLAN:
|
||||
return None, 'full_trial_plan_intentionally_not_exported'
|
||||
if 'extension-build' in parsed.parts or 'build' in parsed.parts or '__pycache__' in parsed.parts:
|
||||
return None, 'build_or_cache_artifact'
|
||||
if row['bytes'] > MAX_BYTES:
|
||||
return None, 'exceeds_4_MiB'
|
||||
if name.startswith(SNAPSHOT):
|
||||
return 'source-snapshot/' + parsed.name, None
|
||||
if parsed.suffix.lower() in SUFFIXES:
|
||||
return 'evidence/' + name, None
|
||||
return None, 'binary_or_non_selected_extension'
|
||||
|
||||
|
||||
def _check_roots(root, paths):
|
||||
for path in paths:
|
||||
current = root
|
||||
for part in path.relative_to(root).parts:
|
||||
current /= part
|
||||
require(not current.is_symlink(), 'Symlink in publication path: ' + str(current))
|
||||
|
||||
|
||||
def _registry_seal(helper, registry):
|
||||
files = [dict(path=p.relative_to(registry).as_posix(), bytes=p.stat().st_size, sha256=digest(p))
|
||||
for p in sorted(registry.rglob('*')) if p.is_file()]
|
||||
write_json(registry / 'manifest.json', dict(root=str(registry), files=files))
|
||||
raw = ''.join(row['sha256'] + ' ' + row['path'] + '\n' for row in files)
|
||||
raw += digest(registry / 'manifest.json') + ' manifest.json\n'
|
||||
require(len(raw.encode('utf-8')) <= MAX_BYTES, 'Registry checksums exceed small-file limit')
|
||||
with (registry / 'checksums.sha256').open('xb') as handle:
|
||||
handle.write(raw.encode('utf-8'))
|
||||
return helper.verify_seal(registry)
|
||||
|
||||
|
||||
def publish(registration_sha256, experiment_manifest_sha256, final_sha256, *, root=ROOT):
|
||||
"""Create only the fixed AO registry; verify integrity, not science.
|
||||
|
||||
The root override exists for isolated synthetic tests. Existing and partial
|
||||
registries are never overwritten, removed, or resumed.
|
||||
"""
|
||||
require(sys.dont_write_bytecode, 'Use Python -B')
|
||||
for value in (registration_sha256, experiment_manifest_sha256, final_sha256):
|
||||
require(isinstance(value, str) and SHA.fullmatch(value), 'Three external SHA-256 anchors are mandatory')
|
||||
root = Path(root)
|
||||
require(root.is_absolute() and root.is_dir() and not root.is_symlink(), 'Invalid project root')
|
||||
root = root.resolve(strict=True)
|
||||
experiment, registry, helper_path = (root / part for part in (EXP_REL, REGISTRY_REL, HELPER_REL))
|
||||
_check_roots(root, (experiment, root / MODEL_REL, registry, helper_path))
|
||||
require(not registry.exists(), 'Registry already exists; no overwrite or resume')
|
||||
helper = load_helper(helper_path)
|
||||
publisher_sha = digest(__file__)
|
||||
for relative, expected in ((REGISTRATION, registration_sha256), ('manifest.json', experiment_manifest_sha256),
|
||||
(FINAL, final_sha256)):
|
||||
require(digest(helper._regular_file(experiment, relative)) == expected, 'External anchor mismatch: ' + relative)
|
||||
verified = helper.verify_seal(experiment)
|
||||
rows = {row['path']: row for row in verified['files']}
|
||||
require(verified['manifest_sha256'] == experiment_manifest_sha256 and
|
||||
rows.get(REGISTRATION, {}).get('sha256') == registration_sha256 and
|
||||
rows.get(FINAL, {}).get('sha256') == final_sha256,
|
||||
'Verified seal differs from external registration/manifest/final anchors')
|
||||
registration = small_json(helper, experiment, rows, REGISTRATION)
|
||||
require(registration.get('schema') == 'phase4ao-conditioning-interface-registration-v1', 'Wrong phase registration')
|
||||
final = small_json(helper, experiment, rows, FINAL)
|
||||
status = final.get('status')
|
||||
require(isinstance(status, str) and re.fullmatch(r'(PASS|FAIL|INCOMPLETE)(-[A-Z0-9]+)*', status),
|
||||
'Unknown finalized status family')
|
||||
require(final.get('registration_sha256') == registration_sha256, 'Final status names another registration')
|
||||
for flag in ('learned_candidate', 'learning_success_claimed', 'full_conditioning_executed'):
|
||||
require(final.get(flag) is False, 'Final scope flag is not explicitly false: ' + flag)
|
||||
analysis_sha = final.get('analysis_sha256')
|
||||
require(isinstance(analysis_sha, str) and SHA.fullmatch(analysis_sha), 'Missing finalized analysis SHA')
|
||||
linked = [name for name in (ANALYSIS, FAILURE_ANALYSIS)
|
||||
if name in rows and rows[name]['sha256'] == analysis_sha]
|
||||
require(len(linked) == 1, 'Final analysis link must identify one sealed interface/failure analysis')
|
||||
analysis_path = linked[0]
|
||||
if 'analysis_path' in final:
|
||||
require(final['analysis_path'] == analysis_path, 'Explicit analysis path disagrees with SHA')
|
||||
analysis = small_json(helper, experiment, rows, analysis_path)
|
||||
require(analysis.get('status') == status and analysis.get('registration_sha256') == registration_sha256,
|
||||
'Final and analysis identities differ')
|
||||
sources = source_identity(helper, root, experiment, rows, registration, registration_sha256)
|
||||
inventory, plans = [], []
|
||||
for row in verified['files'] + helper._control_rows(experiment, verified):
|
||||
target, reason = selection(row)
|
||||
inventory.append(dict(**row, disposition='copied' if target else 'omitted', copy=target, reason=reason))
|
||||
if target:
|
||||
plans.append((row, target))
|
||||
targets = [target for _, target in plans]
|
||||
require(len(set(targets)) == len(targets), 'Publication destination collision')
|
||||
required = {REGISTRATION, FINAL, 'manifest.json', 'checksums.sha256'}
|
||||
required.add(analysis_path)
|
||||
require(all('evidence/' + name in targets for name in required), 'Mandatory evidence is missing/oversize')
|
||||
require(all(row['copy'] in targets for row in sources['files']), 'A registered source is not exportable')
|
||||
registry.parent.mkdir(parents=True, exist_ok=True)
|
||||
registry.mkdir(exist_ok=False)
|
||||
try:
|
||||
records = []
|
||||
for row, target in plans:
|
||||
source = helper._regular_file(experiment, row['path'])
|
||||
helper._copy_verified(source, registry / target, row['bytes'], row['sha256'])
|
||||
records.append(dict(source=source.relative_to(root).as_posix(), copy=target,
|
||||
bytes=row['bytes'], sha256=row['sha256']))
|
||||
after = helper.verify_seal(experiment)
|
||||
require(after == verified, 'Experiment changed during publication')
|
||||
require(source_identity(helper, root, experiment, rows, registration, registration_sha256) == sources,
|
||||
'Registered source changed during publication')
|
||||
require(digest(helper_path) == HELPER_SHA and digest(__file__) == publisher_sha, 'Publication/helper source changed')
|
||||
write_json(registry / 'seal-verification.json', {k: v for k, v in verified.items() if k != 'files'})
|
||||
write_json(registry / 'evidence-index.json', records)
|
||||
write_json(registry / 'source-identity.json', sources)
|
||||
write_json(registry / 'artifact-inventory.json', dict(
|
||||
experiment_manifest_sha256=experiment_manifest_sha256, files=inventory,
|
||||
omitted_files=sum(row['disposition'] == 'omitted' for row in inventory),
|
||||
omitted_bytes=sum(row['bytes'] for row in inventory if row['disposition'] == 'omitted'),
|
||||
inventory_covers_complete_experiment_and_controls=True, complete_bulk_backup=False))
|
||||
receipt_files = [dict(path=p.relative_to(registry).as_posix(), bytes=p.stat().st_size, sha256=digest(p))
|
||||
for p in sorted(registry.rglob('*')) if p.is_file()]
|
||||
receipt = dict(schema='phase4ao-publication-v1', publication_complete=True,
|
||||
registry=REGISTRY_REL.as_posix(), project_root=str(root), declared_audit_status=status,
|
||||
registration_sha256=registration_sha256, experiment_manifest_sha256=experiment_manifest_sha256,
|
||||
experiment_checksums_sha256=verified['checksums_sha256'], final_sha256=final_sha256,
|
||||
analysis_sha256=analysis_sha, analysis_path=analysis_path,
|
||||
helper_sha256=HELPER_SHA, publisher_sha256=publisher_sha,
|
||||
helper_globals_modified=False, source_count=sources['source_count'],
|
||||
copied_evidence_files=len(records), copied_evidence_bytes=sum(row['bytes'] for row in records),
|
||||
files=receipt_files, bulk_remains_at=EXP_REL.as_posix(), complete_bulk_backup=False,
|
||||
model_directory_sealed=False, scientific_gate_executed_by_publisher=False,
|
||||
scientific_gate_certified_by_publication=False, candidate_generated=False, learning_achieved=False,
|
||||
existing_files_modified=False, new_brain_run=False,
|
||||
scope='Complete source experiment byte seal verified; small evidence copied. Original status preserved, not reassessed.',
|
||||
receipt_self_hash='Returned externally; registry manifest separately seals this receipt.',
|
||||
registry_controls='manifest.json and checksums.sha256 are created after this receipt and independently verified.',
|
||||
limits=dict(max_selected_file_bytes=MAX_BYTES, selected_suffixes=sorted(SUFFIXES),
|
||||
full_trial_plan_always_omitted=True, registered_source_snapshot_exception=True))
|
||||
write_json(registry / 'publication-receipt.json', receipt)
|
||||
small_seal = _registry_seal(helper, registry)
|
||||
return dict(publication_complete=True, registry=str(registry), declared_audit_status=status,
|
||||
registration_sha256=registration_sha256, receipt_sha256=digest(registry / 'publication-receipt.json'),
|
||||
registry_manifest_sha256=small_seal['manifest_sha256'], registry_checksums_sha256=small_seal['checksums_sha256'],
|
||||
registry_payload_files=small_seal['payload_file_count'], source_count=sources['source_count'],
|
||||
copied_evidence_files=len(records), complete_bulk_backup=False,
|
||||
scientific_gate_executed_by_publisher=False, scientific_gate_certified_by_publication=False)
|
||||
except Exception as error:
|
||||
if not (registry / 'publication-failure.json').exists():
|
||||
write_json(registry / 'publication-failure.json', dict(publication_complete=False,
|
||||
error_type=type(error).__name__, error=str(error), partial_outputs_preserved=True,
|
||||
existing_files_modified=False))
|
||||
raise
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--registration-sha256', required=True)
|
||||
parser.add_argument('--experiment-manifest-sha256', required=True)
|
||||
parser.add_argument('--final-sha256', required=True)
|
||||
args = parser.parse_args()
|
||||
result = publish(args.registration_sha256, args.experiment_manifest_sha256, args.final_sha256)
|
||||
print(json.dumps(result, ensure_ascii=False, sort_keys=True, allow_nan=False), flush=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user