Skip to content

feat: let validate check a bundle against a digest from outside it - #38

Merged
utsab345 merged 1 commit into
utsab345:mainfrom
how2how2how2-arch:feat/validate-expect-digest
Oct 30, 2026
Merged

utsab345 merged 1 commit into
utsab345:mainfrom
how2how2how2-arch:feat/validate-expect-digest

Conversation

@how2how2how2-arch

Copy link
Copy Markdown
Contributor

What this adds

stepfork validate --expect-digest <sha256> — check a bundle against the bundle_digest that stepfork run --receipt recorded, i.e. a digest from outside the bundle.

Why

validate --verify-integrity checks each payload file against the integrity.json the bundle carries. That catches a file corrupted or edited on its own. It cannot catch a bundle whose record was rewritten to match the payload: the three SHA-256 values are recomputable by whoever edits events.jsonl, and the bundle then verifies clean. A file the record does not name is invisible to it too. So once a bundle travels — CI artifact, a colleague, a bug report — VERIFIED says the record agrees with the payload, not that this is the bundle a run produced.

export/receipt.py::bundle_digest already hashes every file in the bundle in sorted relative-path order, and stepfork run already writes it into the receipt. It was write-only: recorded, never checkable. This exposes the check without a second implementation of the standard — _enforce_expected_digest calls the same bundle_digest the receipt does, and is_sha256_digest in trace/integrity.py replaces the private regex use in _validate_record so the format is defined once.

Behaviour

Case Exit Output
digest matches 0 Bundle digest: MATCH <digest>
digest differs 1 both digests, plus the note that a matching integrity.json does not rule this out
--expect-digest is not 64 hex characters 2 nothing was compared, so it is not a verdict about the bundle
no --expect-digest unchanged —

Exit 2 for a malformed digest is deliberate: a check that could not run must not share an exit code with a check that ran and failed. Digests are trimmed and lower-cased before comparison, so a value copied out of JSON needs no cleanup.

Tests

  • matches; and matches with upper case and surrounding whitespace
  • an added file the integrity record does not name → caught. The test first asserts verify_bundle_integrity(...).status is VERIFIED, so the gap is demonstrated rather than assumed.
  • a payload edit plus a rewritten integrity.json → verify_bundle_integrity reports VERIFIED, and --expect-digest catches it
  • malformed digest → exit 2
  • --help lists the flag

Both catch-tests were run against a neutered comparison (if True: in place of hmac.compare_digest): exactly those two go red and the match-tests stay green.

Checks

uv run pytest 706 passed · uv run ruff check . · uv run ruff format --check . (196 files) · uv run mypy (132 source files) · uv build · uv run mkdocs build --strict · the three demos, each buggy FAIL (rc=1), fixed PASS.

Scope

Additive: no existing flag, exit code or output changes. docs/cli.md documents the flag and what the check does not prove — a digest that arrives inside the same archive as the bundle proves nothing, so it has to travel by a route the bundle's author cannot edit.

This is not tied to an issue; if you would rather it started as one, say so and I will move the discussion there.

@utsab345
utsab345 merged commit e687560 into utsab345:main Oct 30, 2026
10 checks passed
@utsab345

Copy link
Copy Markdown
Owner

Thanks @how2how2how2-arch for this. I verified it locally, approved it, and merged it. Appreciate the thorough tests and clear docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants