174 lines
9.9 KiB
Python
174 lines
9.9 KiB
Python
"""Verify pinned State source receipts against sibling capture capsules; optionally compare Git blobs."""
|
|
from pathlib import Path, PurePosixPath
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import zipfile
|
|
import io
|
|
import stat
|
|
|
|
HERE = Path(__file__).resolve().parent
|
|
COMPARISON = "Exact committed Git blobs equal every captured input; same complete filtered inventory"
|
|
EXPLICIT_FIXTURES = {
|
|
"harness/experiments/edge-attachments-20260922/native-fixture.arcadia",
|
|
"harness/experiments/textual-authoring-20260911/alarm-response.arcadia",
|
|
}
|
|
INCLUDE_FIXTURES = False
|
|
|
|
|
|
def need(ok, message):
|
|
if not ok:
|
|
raise ValueError(message)
|
|
|
|
|
|
def pin(data):
|
|
return {"bytes": len(data), "sha256": hashlib.sha256(data).hexdigest()}
|
|
|
|
|
|
def parsed(data):
|
|
def unique(items):
|
|
result = {}
|
|
for key, value in items:
|
|
need(key not in result, "duplicate JSON key: " + key)
|
|
result[key] = value
|
|
return result
|
|
return json.loads(data, object_pairs_hook=unique)
|
|
|
|
|
|
def safe(name):
|
|
return (isinstance(name, str) and bool(name) and "\\" not in name and ":" not in name
|
|
and not PurePosixPath(name).is_absolute()
|
|
and all(part not in ("", ".", "..") for part in name.split("/")))
|
|
|
|
|
|
def relevant(path):
|
|
return (INCLUDE_FIXTURES and path in EXPLICIT_FIXTURES) or path in ("Cargo.toml", "Cargo.lock", "rust-toolchain.toml") or path.startswith(
|
|
("crates/", "build/", ".cargo/", "scripts/", "docs/", "examples/", "harness/fixtures/", "harness/prototypes/"))
|
|
|
|
|
|
def git(repo, *arguments, data=None):
|
|
environment = dict(os.environ, GIT_NO_LAZY_FETCH="1")
|
|
return subprocess.run(["git", *arguments], cwd=repo, input=data, stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE, check=True, env=environment).stdout
|
|
|
|
|
|
def compare_commit(repo, commit, inputs):
|
|
need(git(repo, "rev-parse", "--verify", commit + "^{commit}").decode().strip() == commit, "named commit unavailable")
|
|
entries = git(repo, "ls-tree", "-r", "--full-tree", "-z", commit).split(b"\0")
|
|
blobs = {}
|
|
for entry in filter(None, entries):
|
|
header, path_bytes = entry.split(b"\t", 1)
|
|
path = path_bytes.decode("utf-8")
|
|
if relevant(path):
|
|
_mode, kind, oid = header.split()
|
|
need(kind == b"blob", "relevant non-blob tree entry: " + path)
|
|
need(path not in blobs, "duplicate tree path")
|
|
blobs[path] = oid
|
|
need(set(blobs) == set(inputs), "complete filtered Git inventory differs; missing="
|
|
+ repr(sorted(set(inputs) - set(blobs))) + "; additional=" + repr(sorted(set(blobs) - set(inputs))))
|
|
# Object IDs avoid path quoting and commit:path interpretation in batch input.
|
|
paths = sorted(blobs)
|
|
data = git(repo, "cat-file", "--batch", data=b"".join(blobs[path] + b"\n" for path in paths))
|
|
offset, actual = 0, {}
|
|
for path in paths:
|
|
end = data.index(b"\n", offset)
|
|
oid, kind, size_bytes = data[offset:end].split()
|
|
size = int(size_bytes)
|
|
need(oid == blobs[path] and kind == b"blob" and size >= 0, "Git batch object relationship")
|
|
content = data[end + 1:end + 1 + size]
|
|
need(len(content) == size and data[end + 1 + size:end + 2 + size] == b"\n", "Git batch framing")
|
|
actual[path] = pin(content)
|
|
offset = end + 2 + size
|
|
need(offset == len(data), "trailing Git batch bytes")
|
|
different = [path for path in paths if actual[path] != inputs[path]]
|
|
need(not different, "captured bytes differ from named commit: " + repr(different))
|
|
|
|
|
|
|
|
SPECS = {'desktop-2': {'commit': '2a3b9ba822a49ead233c4b5f98412a07c1c1da12', 'inputCount': 733, 'inputMap': {'bytes': 122682, 'sha256': '9cf85879dfb0bb5ec3b15c66b23df316e6bfe8451b6cf0c895bf7b9b5fbdde4a'}, 'explicitCompileTimeFixtures': [], 'receiptPin': {'bytes': 428, 'sha256': '6012bdc52a3843c9cfd5f3c80ec4270a7c0f1e13ef4738b0e3f4ed048ff8d885'}}, 'lint-2': {'commit': '3480b54201a5f361cca0bcd0d45dd6546a7a7bc9', 'inputCount': 735, 'inputMap': {'bytes': 123053, 'sha256': '4215d0ca0319282768f1acdf05c2c094ea7432e17ed3a715df73aa05cef851bf'}, 'explicitCompileTimeFixtures': ['harness/experiments/edge-attachments-20260922/native-fixture.arcadia', 'harness/experiments/textual-authoring-20260911/alarm-response.arcadia'], 'receiptPin': {'bytes': 583, 'sha256': 'e0a6deab68683278965643f6f34a1038fcea25c1f242b6f75f5559b7697ec7d6'}}}
|
|
TOOLS = {".gitattributes", "prepare.py", "verify.py", "verify-committed-inputs.py", "README.md"}
|
|
FILES = TOOLS | {"source-" + run + ".json" for run in SPECS}
|
|
|
|
def inventory(archive, expected):
|
|
names = archive.namelist()
|
|
need(len(names) == len(set(names)) and set(names) == set(expected), "exact ZIP inventory")
|
|
for item in archive.infolist():
|
|
need(safe(item.filename) and not item.is_dir() and not stat.S_ISLNK(item.external_attr >> 16), "safe ZIP member")
|
|
need(pin(archive.read(item.filename)) == expected[item.filename], "ZIP member pin: " + item.filename)
|
|
|
|
def verify_data(receipts, captured, repo=None):
|
|
global INCLUDE_FIXTURES
|
|
need(set(receipts) == set(captured) == set(SPECS), "exact source run inventory")
|
|
for run, spec in SPECS.items():
|
|
receipt_bytes = receipts[run]
|
|
need(pin(receipt_bytes) == spec["receiptPin"], run + ": exact historical receipt bytes")
|
|
receipt = parsed(receipt_bytes)
|
|
need(receipt["gate"] == run and receipt["comparison"] == COMPARISON, run + ": receipt identity")
|
|
need(all(receipt[key] == spec[key] for key in ("commit", "inputCount", "inputMap", "explicitCompileTimeFixtures")), run + ": pinned receipt relationships")
|
|
map_bytes, result, archive_bytes = captured[run]
|
|
need(pin(map_bytes) == spec["inputMap"] == result["inputs"], run + ": exact captured input map")
|
|
need(result["exitCode"] == 0 and result["inputsUnchanged"] is True, run + ": accepted unchanged gate")
|
|
inputs = parsed(map_bytes)
|
|
need(len(inputs) == spec["inputCount"] == result["inputCount"], run + ": input counts")
|
|
INCLUDE_FIXTURES = bool(spec["explicitCompileTimeFixtures"])
|
|
need(spec["explicitCompileTimeFixtures"] == (sorted(EXPLICIT_FIXTURES) if INCLUDE_FIXTURES else []), run + ": exact fixture scope")
|
|
need(all(safe(name) and relevant(name) for name in inputs), run + ": input scope")
|
|
need((EXPLICIT_FIXTURES & set(inputs)) == (EXPLICIT_FIXTURES if INCLUDE_FIXTURES else set()), run + ": explicit fixture membership")
|
|
need(sum(name.startswith("crates/vendor/iced_widget/") for name in inputs) == 71, run + ": vendor count")
|
|
need(pin(archive_bytes) == result["archive"], run + ": source capsule pin")
|
|
with zipfile.ZipFile(io.BytesIO(archive_bytes)) as archive:
|
|
inventory(archive, inputs)
|
|
if repo is not None:
|
|
compare_commit(repo, spec["commit"], inputs)
|
|
print("PASS", run, spec["commit"], len(inputs), "source pins" + (" and exact Git blobs" if repo is not None else " (Git comparison not rerun)"))
|
|
return {"runs": len(SPECS), "gitCompared": repo is not None}
|
|
|
|
def raw_captures(gates):
|
|
return {run: ((gates/run/"inputs.json").read_bytes(), parsed((gates/run/"result.json").read_bytes()),
|
|
(gates/run/"inputs.zip").read_bytes()) for run in SPECS}
|
|
|
|
def packaged(checks):
|
|
files = parsed((checks/"files.json").read_bytes())
|
|
need(set(files) == {"prepare.py", "verify.py", "README.md", "manifest.json", "evidence.zip"}, "checks outer inventory")
|
|
for name, expected in files.items():
|
|
need(pin((checks/name).read_bytes()) == expected, "checks outer pin: " + name)
|
|
manifest = parsed((checks/"manifest.json").read_bytes())
|
|
need(manifest["schema"] == 1 and manifest["kind"] == "multiselect-cargo-gates", "checks kind")
|
|
logical, members = manifest["logicalFiles"], manifest["members"]
|
|
need(all(safe(name) and safe(target) for name,target in logical.items()) and set(logical.values()) == set(members), "checks logical inventory")
|
|
captured = {}
|
|
with zipfile.ZipFile(checks/"evidence.zip") as archive:
|
|
inventory(archive, members)
|
|
for run in SPECS:
|
|
prefix = "cargo/" + run + "/"
|
|
result = parsed(archive.read(logical[prefix + "result.json"]))
|
|
spec = manifest["runs"][run]
|
|
need(all(result[key] == spec[key] for key in ("command", "head", "exitCode", "inputCount")), run + ": checks relationships")
|
|
for name in ("inputs.json", "inputs.zip"):
|
|
member = logical[prefix+name]
|
|
need(member == "capsules/" + members[member]["sha256"] + Path(name).suffix, run + ": source capsule mapping")
|
|
captured[run] = (archive.read(logical[prefix+"inputs.json"]), result, archive.read(logical[prefix+"inputs.zip"]))
|
|
return captured
|
|
|
|
def verify_package(package, checks, repo=None):
|
|
files = parsed((package/"files.json").read_bytes())
|
|
need(set(files) == FILES, "source outer inventory")
|
|
need({p.name for p in package.iterdir()} == FILES | {"files.json"}, "source exact package inventory")
|
|
for name, expected in files.items():
|
|
need((package/name).is_file() and not (package/name).is_symlink() and pin((package/name).read_bytes()) == expected, "source outer pin: " + name)
|
|
return verify_data({run:(package/("source-"+run+".json")).read_bytes() for run in SPECS}, packaged(checks), repo)
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("package", nargs="?", type=Path, default=HERE)
|
|
parser.add_argument("--checks", type=Path, help="sibling checks package; default PACKAGE/../checks")
|
|
parser.add_argument("--repo", type=Path, help="optional repository containing both exact commits, for renewed Git-blob comparison")
|
|
args = parser.parse_args()
|
|
print("PASS source evidence:", verify_package(args.package, args.checks or args.package.parent/"checks", args.repo))
|
|
print("No builds, checkouts, helper execution or native acceptance claim. Run the checks verifier separately for full gate relationships.")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|