diff --git a/research/pr626-omitted-digest-audit/README.md b/research/pr626-omitted-digest-audit/README.md new file mode 100644 index 0000000000..eb9b2e238b --- /dev/null +++ b/research/pr626-omitted-digest-audit/README.md @@ -0,0 +1,74 @@ +# What the three omitted #626 digests measure + +**Verifier audit; no new kappa.** The [Linux reconciliation](../selected626-linux-reconciliation/README.md) +compares the preserved run `38317651752` with the committed #626 assembly after +removing three digest paths. It already says those differences are not known to +be gzip metadata, because the template, owner audit and invoice were not +uploaded. This note classifies the digests from the producers that wrote them. + +```sh +python3 -B research/pr626-omitted-digest-audit/verify.py +``` + +Expected: + +```text +PASS_PR626_OMITTED_DIGEST_AUDIT template=decompressed-payload owner_audit=indented-json-file fresh_invoice=indented-json-file differences=3 +``` + +Python 3.10+ and Git. The sprint base `66849e1475c930fd7732b1a0067ffee7e1e95f60` +must be present; a shallow clone has to fetch it first. No supplier replay. + +## Classification + +At that base, `research/complex-k492-owner-repair/code/invoice_allcold.py` +defines `filehash` as SHA-256 of the file bytes and `contenthash` as SHA-256 +of the decompressed payload when the file ends in `.gz`. + +| Omitted assembly path | Producer | What a difference means | +|---|---|---| +| `template_sha256` | `contenthash` of `GLOBAL-TEMPLATE.json.gz`, checked again by decompressing that file in `audit_allcold.py` | The template **payload** differs. Two gzip containers of one payload have the same `contenthash`, so this recorded mismatch is not a compression-header artifact. | +| `input_sha256.owner_audit` | SHA-256 of `OWNERS-AND-PROFILE.json`, written with `json.dumps(..., indent=2)` | The indented owner file differs. That file includes `source_artifacts`, and the invoice binds `CANONICAL-WORD.json.gz` with `filehash`, so the file can change because a nested container changed or because a mathematical field changed. | +| `input_sha256.fresh_invoice` | SHA-256 of `PREFIX-AND-INVOICE.json`, also `indent=2`, via `sha(invoice_path)` in `assemble.py` | Same kind of mixed file receipt for the invoice. | + +The recorded Linux and committed values differ on all three paths. They are +copied from the predecessor's `EXPECTED.json`; this package does not recompute +them from missing artifacts. + +The fixture shows the distinction directly. One payload compressed twice, at +different level and `mtime`, yields two containers and one payload hash. +Replacing a nested container hash inside an indented owner file changes +`owner_audit` while a projection that keeps only `paid_profile` stays equal. +Changing `paid_profile` still changes that projection. So dropping the file +hash can hide a container-only change, and keeping the reduced assembly +comparison still sees a profile change. + +`template_sha256` is the case that cannot be explained that way. Because it is +a payload hash, the recorded disagreement is a real template-content +disagreement. The omitted owner and invoice hashes stay unresolved: without +those files, a container change and a mathematical change are not separable. + +## Open next questions + +Can the omitted owner and invoice files be recovered from the retained stage +logs, or are they absent from run `38317651752`? That decides whether the two +file-hash disagreements are nested container receipts or changed audit fields. +The first step is to search the preserved logs for `OWNERS-AND-PROFILE.json` +and `PREFIX-AND-INVOICE.json` bytes or their payload hashes. It is blocked by +the reconciliation's own scope: the workflow did not upload those files, and +this audit does not invent them. + +## Credit and limits + +The reconciliation is Josh Snider's (`jsnider3`), with the selected #626 +composition. The gzip-container convention is the existing reproducibility +note. This package only classifies these three digests and adds the fixture +that separates container bytes from payload bytes. + +No selected certificate, supplier or kappa changes. The audit does not explain +the numerical template difference and does not rerun `make frontier626-verify`. +All-size, hosting, precision and analytic assumptions of the selected +construction remain open. + +Prepared by Riquelme Soares (@hevlyo) with substantial Cursor agent assistance. +Apache-2.0. No independent human peer review is asserted. diff --git a/research/pr626-omitted-digest-audit/SOURCES.json b/research/pr626-omitted-digest-audit/SOURCES.json new file mode 100644 index 0000000000..5bc4cd701a --- /dev/null +++ b/research/pr626-omitted-digest-audit/SOURCES.json @@ -0,0 +1,30 @@ +{ + "schema_version": 1, + "base_commit": "b5ea56e0a5ac4552166253b1fa4be0753bb38c30", + "files": [ + { + "path": "certificates/selected-result.json", + "usage": "provenance", + "git_blob": "ae6a99f28aa468649eb66a361e764d769363bf11", + "sha256": "10b9e340fd8e354a37a0c4813c90e5de1dd42044b5f6aca4a3fc5a42b685fa32" + }, + { + "path": "docs/research/agent-task-catalog.json", + "usage": "provenance", + "git_blob": "889b95d936746df124872133ae1e7cc339adcb92", + "sha256": "7d185cd8c2af35988d638d969748ad7b670f902606444dbfc748e4402c340df6" + }, + { + "path": "research/selected626-linux-reconciliation/EXPECTED.json", + "usage": "input", + "git_blob": "d3c578df6beee2e1f48a70149190a1aea3b01676", + "sha256": "33e4ee3a4927ff56b4526e451d5fbdff7316c7f25cdc6f262bb8a27b7e83161c" + }, + { + "path": "research/selected626-linux-reconciliation/verify.py", + "usage": "input", + "git_blob": "89f5d1bee94a1f63ee6081a05c293f63ec1b78ee", + "sha256": "d2e3999f1b0284d14070c148b15c8716b32521f9ffa3143f5dae2bea6f31ece8" + } + ] +} diff --git a/research/pr626-omitted-digest-audit/TASK.json b/research/pr626-omitted-digest-audit/TASK.json new file mode 100644 index 0000000000..e3593e6138 --- /dev/null +++ b/research/pr626-omitted-digest-audit/TASK.json @@ -0,0 +1,60 @@ +{ + "schema_version": 1, + "task_id": "portable-replay", + "priority_revision": 5, + "base_commit": "b5ea56e0a5ac4552166253b1fa4be0753bb38c30", + "original_card": { + "id": "portable-replay", + "question": "Can one reproducible environment or artifact-comparison failure be repaired?", + "starting_points": [ + "research/gzip-container-reproducibility/README.md", + "docs/ci-verification.md" + ], + "methods": [ + "Reproduce one portability failure and add a minimal cross-environment fix.", + "Audit one canonical-payload comparison for omitted proof-relevant fields." + ], + "acceptance_gate": "Preserve immutable source hashes and proof-relevant content; a different compressed byte stream alone is not a mathematical mismatch.", + "first_step": { + "command": [ + "python3", + "-B", + "scripts/research_baseline.py" + ], + "expected": "PASS_RESEARCH_BASELINE_BINDINGS_ONLY", + "inspect": "Read the gzip-container reproducibility examples and distinguish frozen source bytes from regenerated payloads.", + "scope": "Positive control / source-bound orientation only; this is not a new construction or full cold replay." + }, + "first_experiment": "Reproduce one specific mismatch or audit one canonical comparison for omitted proof-relevant fields.", + "bottleneck_link": "Portable exact checks prevent environment failures from blocking valid contributions.", + "allowed_variations": [ + "One interpreter/version mismatch or one payload field comparison." + ], + "scope_change_triggers": [ + "Removing immutable-source pins because generated compressed bytes differ." + ], + "overlap_terms": [ + "gzip container", + "reproducibility", + "canonical payload" + ] + }, + "scope_status": "refined", + "actual_scope": "Do the three digest paths omitted by the #626 Linux assembly comparison measure gzip containers, decompressed payloads, or raw indented JSON files?", + "scope_change_reason": "The card allows one payload-field comparison. The object is the three recorded differences in research/selected626-linux-reconciliation, classified from the hash helpers at sprint base 66849e1475c930fd7732b1a0067ffee7e1e95f60. It does not repair a supplier or drop an immutable pin.", + "bottleneck_link": "A portable check has to tell a compression difference from a changed proof object. Misreading these three digests would either block a valid replay or hide a real template change.", + "budget_minutes": 60, + "stop_rule": "Stop after classifying the three recorded paths and the container-versus-payload fixture. Do not rerun frontier626 or reconstruct the missing owner and invoice files.", + "outcome": "completed", + "evidence": "verify.py pins three producer blobs, shows one payload has two container hashes and one content hash, shows an indented owner file hash can change while paid_profile does not, and classifies the recorded Linux/committed pairs: template_sha256 is a decompressed payload, the other two are indented JSON files.", + "author": "Riquelme Soares (@hevlyo)", + "ai_assistance": "Substantial Cursor agent assistance in reading the pinned producers, the fixture, and the write-up. No proof assistant was used.", + "open_questions": [ + { + "question": "Can the omitted owner and invoice files be recovered from the retained stage logs of run 38317651752, or are their bytes absent?", + "why_it_matters": "Those two file hashes mix nested container receipts with mathematical fields, so only the files separate those causes.", + "first_step": "Search the preserved logs for OWNERS-AND-PROFILE.json and PREFIX-AND-INVOICE.json bytes or payload hashes.", + "blocked_by": "The frontier626 workflow did not upload those files. This audit does not invent them." + } + ] +} diff --git a/research/pr626-omitted-digest-audit/VERIFY.json b/research/pr626-omitted-digest-audit/VERIFY.json new file mode 100644 index 0000000000..3f304eb9f7 --- /dev/null +++ b/research/pr626-omitted-digest-audit/VERIFY.json @@ -0,0 +1,21 @@ +{ + "schema_version": 1, + "status": "ready", + "python": "3.12", + "requirements": [], + "inputs": [ + "research/selected626-linux-reconciliation/EXPECTED.json", + "research/selected626-linux-reconciliation/verify.py" + ], + "commands": [ + { + "argv": [ + "{python}", + "-B", + "{package}/verify.py" + ], + "timeout_seconds": 60, + "checks": "Pinned producer blobs, payload-versus-container fixture, owner-file fixture, and the three recorded digest classes." + } + ] +} diff --git a/research/pr626-omitted-digest-audit/verify.py b/research/pr626-omitted-digest-audit/verify.py new file mode 100644 index 0000000000..1589eab09e --- /dev/null +++ b/research/pr626-omitted-digest-audit/verify.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +"""Classify the three assembly digests omitted by the #626 Linux reconciliation. + +The reconciliation already refuses to call those differences gzip metadata. +This check says what each digest measures, using the producers at the pinned +sprint base. It does not rerun the supplier. Python 3.10+. +""" + +from hashlib import sha256 +import gzip +import json +from pathlib import Path +import subprocess + +ROOT = Path(__file__).resolve().parents[2] +PREDECESSOR = ROOT / "research/selected626-linux-reconciliation" +BASE = "66849e1475c930fd7732b1a0067ffee7e1e95f60" +BLOBS = { + "research/complex-k492-owner-repair/code/invoice_allcold.py": + "b244cd59940659465c07b013ebe3225077496f45", + "research/complex-k492-owner-repair/code/assemble.py": + "d1674e657ff046f9d55c917723002f270e05c42a", + "research/complex-k492-owner-repair/code/audit_allcold.py": + "e29d90e89af9d7535c96de6ebf7c4d03616f7c48", +} +SNIPPETS = { + "research/complex-k492-owner-repair/code/invoice_allcold.py": ( + "def filehash(path): return hashlib.sha256(path.read_bytes()).hexdigest()", + "return hashlib.sha256(gzip.decompress(data) if path.suffix=='.gz' else data).hexdigest()", + "assert replay['canonical_word_sha256']==filehash(cp)==registry['canonical_word_sha256']", + "template_hash=contenthash(template_path)", + "owner_audit_sha256=filehash(audit/'OWNERS-AND-PROFILE.json')", + "(out/'PREFIX-AND-INVOICE.json').write_text(json.dumps(receipt,indent=2)+'\\n')", + ), + "research/complex-k492-owner-repair/code/assemble.py": ( + "sha=lambda p:hashlib.sha256(p.read_bytes()).hexdigest()", + "input_hashes={'owner_audit':owner_sha,'fresh_invoice':invoice_sha,'inputs/PR543-EXPECTED.json':sha(bit_path)}", + "template_sha256=owner['template_sha256']", + "owner_path=C/'audit/OWNERS-AND-PROFILE.json';invoice_path=C/'audit/PREFIX-AND-INVOICE.json'", + "r=assemble(owner,invoice,sha(owner_path),sha(invoice_path))", + ), + "research/complex-k492-owner-repair/code/audit_allcold.py": ( + "raw=gzip.decompress((OUT/'GLOBAL-TEMPLATE.json.gz').read_bytes());assert hashlib.sha256(raw).hexdigest()==summary['template_sha256']", + "source_artifacts=summary['source_artifacts']", + "(REPORT.parent/'OWNERS-AND-PROFILE.json').write_text(json.dumps(owners,indent=2)+'\\n')", + ), +} + + +def need(condition, message): + if not condition: + raise ValueError(message) + + +def git_show(spec): + return subprocess.check_output(["git", "-C", str(ROOT), "show", spec]) + + +def git_rev_parse(spec): + return subprocess.check_output( + ["git", "-C", str(ROOT), "rev-parse", spec], text=True).strip() + + +def filehash(data): + return sha256(data).hexdigest() + + +def contenthash(data, gzip_wrapped): + payload = gzip.decompress(data) if gzip_wrapped else data + return sha256(payload).hexdigest() + + +def check_producers(): + sources = {} + for path, blob in BLOBS.items(): + need(git_rev_parse(BASE + ":" + path) == blob, "producer blob changed: " + path) + text = git_show(BASE + ":" + path).decode() + for snippet in SNIPPETS[path]: + need(snippet in text, "producer no longer defines: " + snippet) + sources[path] = text + return sources + + +def check_container_is_not_payload(): + payload = b'{"frames":[1,2,3],"paid":{"1":4}}\n' + first = gzip.compress(payload, compresslevel=9, mtime=0) + second = gzip.compress(payload, compresslevel=1, mtime=5) + need(first != second, "fixture failed to build two containers") + need(contenthash(first, True) == contenthash(second, True) == sha256(payload).hexdigest(), + "decompressed payload hash changed with the container") + need(filehash(first) != filehash(second), "container file hash ignored a real container change") + changed = payload.replace(b"[1,2,3]", b"[1,2,4]") + recompressed = gzip.compress(changed, compresslevel=9, mtime=0) + need(contenthash(recompressed, True) != contenthash(first, True), + "payload hash missed a template change") + return {"same_payload_containers_differ": True, + "payload_hash": contenthash(first, True), + "container_hashes_differ": True} + + +def owner_text(artifact_hash): + owner = { + "status": "PASS_INDEPENDENT_ASYNC_OWNERS_AND_PROFILE", + "template_sha256": "ab" * 32, + "paid_profile": {"1": 4}, + "source_artifacts": {"providers/p/CANONICAL-WORD.json.gz": artifact_hash}, + } + return (json.dumps(owner, indent=2) + "\n").encode() + + +def projected_assembly(owner_text_bytes): + """Fields the #626 assembly keeps after the three omitted digests are removed.""" + owner = json.loads(owner_text_bytes) + return {"complex_profile": owner["paid_profile"]} + + +def check_omitted_file_can_hide_container_change(container): + payload = b'{"frames":[1,2,3],"paid":{"1":4}}\n' + other = gzip.compress(payload, compresslevel=1, mtime=5) + left = owner_text(filehash(container)) + right = owner_text(filehash(other)) + need(filehash(left) != filehash(right), "owner file hash missed a nested container change") + need(projected_assembly(left) == projected_assembly(right), + "reduced assembly changed when only a container hash changed") + # A real profile change is still visible without the omitted file hash. + mutated = json.loads(left) + mutated["paid_profile"] = {"1": 5} + mutated_text = (json.dumps(mutated, indent=2) + "\n").encode() + need(projected_assembly(mutated_text) != projected_assembly(left), + "reduced assembly missed a profile change") + + +def classify_recorded_differences(): + expected = json.loads((PREDECESSOR / "EXPECTED.json").read_text()) + rows = expected["assembly_digest_differences"] + need([row["path"] for row in rows] == [ + ["template_sha256"], + ["input_sha256", "owner_audit"], + ["input_sha256", "fresh_invoice"], + ], "recorded difference paths changed") + classes = { + ("template_sha256",): "decompressed-payload", + ("input_sha256", "owner_audit"): "indented-json-file", + ("input_sha256", "fresh_invoice"): "indented-json-file", + } + classified = [] + for row in rows: + path = tuple(row["path"]) + need(row["linux"] != row["committed"], "recorded digests unexpectedly agree: " + str(path)) + need(len(row["linux"]) == len(row["committed"]) == 64, "digest length") + classified.append({"path": list(path), "measures": classes[path], + "linux": row["linux"], "committed": row["committed"]}) + predecessor = (PREDECESSOR / "verify.py").read_text() + need("DIFFERING_PATHS = [('template_sha256',), ('input_sha256', 'owner_audit'),\n" + " ('input_sha256', 'fresh_invoice')]" in predecessor, + "reconciliation no longer omits these three paths") + need("require(left == right, 'Mathematical assembly fields differ')" in predecessor, + "reconciliation equality check moved") + return classified + + +def main(): + check_producers() + containers = check_container_is_not_payload() + sample = gzip.compress(b'{"frames":[1,2,3],"paid":{"1":4}}\n', compresslevel=9, mtime=0) + check_omitted_file_can_hide_container_change(sample) + classified = classify_recorded_differences() + need([row["measures"] for row in classified] == [ + "decompressed-payload", "indented-json-file", "indented-json-file"], + "classification changed") + need(containers["same_payload_containers_differ"], "container fixture lost") + print("PASS_PR626_OMITTED_DIGEST_AUDIT " + "template=decompressed-payload owner_audit=indented-json-file " + "fresh_invoice=indented-json-file differences=3") + + +if __name__ == "__main__": + main()