artifact_seal — deterministic SHA-256 seal manifests over a file tree
Lane: Codeforge. Stdlib only. No third-party dependencies.
artifact_seal.py the engine + CLI (~14 KB)
selftest_artifact_seal.py 44 checks, 3 layers
verify_artifact_seal_oracle.py independent oracle (sha256sum, git blob)
Why
AI-generated artifacts drift silently: an editor strips a trailing newline, a
manually edited file changes by one character, a git checkout restores an
older revision. A single "total hash of the whole directory" is useless here --
it changes whenever ANY file changes, so it cannot answer "which file moved?".
This answers that with a per-file digest plus a whole-set seal, and reports missing / extra / digest-mismatch per path.
Design decisions that are load-bearing
- Deterministic. Same bytes, same seal, on any platform. No timestamps, no inode data, no absolute paths in the digest input. Verified: sealing from two different CWDs yields an identical manifest.
- Tamper-evident. Flipping one bit in one file changes that file's digest AND the whole-set seal.
- Order-invariant. The whole-set seal is computed over entries sorted by
(path, name). Reordering the
entriesarray inside the manifest does not change the seal and does not affect verification. This matters because the manifest is a JSON file that editors andjqwill reorder. - Length-prefixed canonical encoding. The whole-set digest is NOT a
concatenation of the per-file digests. A plain join is ambiguous across field
boundaries (
["ab","c"]and["a","bc"]hash the same joined string); each field is emitted aslen:value, which removes the ambiguity. A test asserts two structurally different trees do not share a seal. - A trailing newline is CONTENT.
hash_textdoes not normalise line endings. A file ending in\nand the same file without it get different digests -- a stripped trailing newline is exactly the drift this exists to catch.str.splitlines()is never used. - Size is evidence, not a verdict. A size mismatch is reported, but the
digest is still computed and still compared. An earlier version
continued after a size mismatch, which silently swallowed a same-length content change (see "Bugs this suite caught" below). - Renames are visible. The digest covers the path as written in the
manifest, so a rename surfaces as
missing: old+extra: new, never as a silent digest match. - Symlinks and non-regular files are skipped. Hashing a FIFO is either
infinite or meaningless. Hidden files (dot-prefixed) ARE included:
.gitignoreis content too. parse_manifestraises;verify_manifestreturns problems. A structurally broken manifest (bad JSON, wrongformat, wrongcount, non-hex digest) is not a "verification result" -- there is nothing sensible to compare against, so it raisesSealError. Content problems come back as a problem list.
CLI
python3 artifact_seal.py seal DIR [--out FILE] [--prefix P]
python3 artifact_seal.py seal FILE ... --out FILE
python3 artifact_seal.py verify --manifest FILE [--target DIR]
verify reads the tree from --target (default: CWD). The directory is
deliberately NOT accepted as a bare positional: that was ambiguous with the
manifest's own path and was a real source of silent wrong-tree verification.
Exit codes: 0 ok / 1 verification failed or unreadable manifest / 2 usage.
Checks
Layer 1 -- 24 hand cases: trailing newline, empty file (== sha256('')), empty
tree, order invariance, rename detection, length-prefix ambiguity, manifest
forgery, count mismatch, text round-trip, size/digest interaction, five
malformed manifests.
Layer 2 -- 60 randomised trees. Each is sealed, verified (must pass), then mutated in exactly one of six ways -- bitflip, append, delete, truncate, added, renamed. Every mutation must be caught: 60/60 caught, and all six mutation classes were hit at least 9 times.
Layer 3 -- 3 mutants of the verifier. Each must FAIL the property suite, otherwise the suite proves nothing: a seal-blind verifier accepts an internally-inconsistent manifest; an extras-blind verifier accepts an injected file; an order-dependent verifier rejects a reordered manifest. The real verifier rejects all three forgeries and accepts the reordering.
Plus 6 CLI subprocess checks.
Bugs this suite caught (all real, all fixed)
- CLI positional filter was broken.
rest.index(a)matched the FIRST occurrence, so--manifest's value was never excluded and the directory was parsed as an option value.verify DIR --manifest FILEverified CWD instead ofDIR-- a silent wrong-tree pass. Fixed by an explicit state-machine_positional(), and the directory is no longer accepted positionally at all. - Size check swallowed the digest check.
continueafter a size mismatch meant an appended edit to a 4-byte file produced only "size mismatch" and the digest was never compared. Fixed: both are reported; a regression test asserts a same-length content change is caught by the digest check alone.
Both were found by running the suite, not by reading the code.
Oracle
verify_artifact_seal_oracle.py cross-checks against two independent
implementations:
sha256sum-- 40 real repo files, 0 disagreements.git's blob hasher (sha1("blob <len>\0" + content)) -- different hash, same content, used to prove the bytes are what the digest claims.- A one-byte tamper to
wizard-work/null/diffkit/diffkit.pyin the real repo is caught, and the tree verifies again after restoring the original bytes.