Skip to content

fix(receipts): reject empty-quote receipts instead of verifying them#513

Open
davion-knight wants to merge 2 commits into
vouchdev:testfrom
davion-knight:fix/receipt-empty-quote-verifies
Open

fix(receipts): reject empty-quote receipts instead of verifying them#513
davion-knight wants to merge 2 commits into
vouchdev:testfrom
davion-knight:fix/receipt-empty-quote-verifies

Conversation

@davion-knight

@davion-knight davion-knight commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

summary

verify_receipt()'s null-guard only checked quote is None, not an empty string. an Evidence with quote="" and byte_start == byte_end (a zero-length span) decodes to "", trivially equals the empty quote, and returns VERIFIED — despite carrying no actual quoted text.

this contradicts the function's own docstring: "Returns NO_RECEIPT when the evidence carries no verifiable span (either offset missing, or no quote to compare)." the sibling locate_span() already refuses to mint a receipt for an empty quote (if not needle: return None, with its own test test_locate_span_returns_none_for_empty_quote) — but verify_receipt had no equivalent guard on the verify side.

why it matters

Evidence.quote has no min-length constraint at the model level. the normal propose path can't trigger this today (proposals.py always builds evidence via receipt_for_quote, which delegates to locate_span's guard) — but bundle.py's import_apply and sync.py's sync_apply write evidence/*.yaml straight to disk after only pydantic-schema validation, no receipt-content check. a hand-crafted bundle/sync source can land an empty-quote, zero-length-span evidence, and any claim citing it would mechanically clear the auto-approval receipt gate (evaluate_claim_receipts) despite proving nothing about the source.

what changed

one line in src/vouch/receipts.py::verify_receipt: quote is Nonenot quote, so both None and "" hit the same NO_RECEIPT path. no other call site needs a change — verify_evidence and evaluate_claim_receipts both delegate to verify_receipt.

test plan

new test test_no_receipt_when_quote_is_empty_string in tests/test_receipts.py — asserts verify_receipt returns NO_RECEIPT for quote=""/byte_start == byte_end. fails on the old code (VERIFIED), passes with the fix.

.venv/bin/python -m ruff check src tests
.venv/bin/python -m mypy src
.venv/bin/python -m pytest tests/test_receipts.py -q
.venv/bin/python -m pytest tests/ -q --ignore=tests/embeddings

all clean. the full suite has 5 pre-existing order-dependent failures (test_fsck_clean_kb_prints_clean_and_exits_zero, test_search_fts5_backend_label, test_deindex_removes_fts_and_prov, and 2 parametrized cases in test_covered_methods_attach_sidebar_when_kb_has_recent_claims) that reproduce identically on a clean test checkout with no changes — confirmed via git stash + rerun, unrelated to this PR.

Summary by CodeRabbit

  • Bug Fixes

    • Receipt and evidence verification no longer treat receipts with empty quoted text (with a zero-length byte span) as verified.
    • Empty-quote cases are now correctly reported as “no receipt evidence” instead of being incorrectly marked as verified.
  • Tests

    • Added unit tests to ensure empty-string quotes are handled as “no receipt,” including when the referenced source is missing.

verify_receipt()'s guard only checked `quote is None`, not an empty
string. an Evidence with quote="" and byte_start == byte_end decodes
to "", trivially equals the empty quote, and returns VERIFIED --
despite carrying no actual quoted text. this contradicts the
function's own docstring ("no quote to compare" -> NO_RECEIPT) and the
sibling locate_span(), which already refuses to mint a receipt for an
empty quote on the propose path. the verify path had no equivalent
guard, so an empty-quote evidence landing on disk some other way
(bundle import, sync) would mechanically "verify" and clear the
auto-approval receipt gate.

`not quote` catches both None and "" with no other call sites needing
changes, since verify_evidence and evaluate_claim_receipts both
delegate to verify_receipt.
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 105d2078-767b-400f-ab5b-9a92e31697a3

📥 Commits

Reviewing files that changed from the base of the PR and between 4934d7d and f8b52b6.

📒 Files selected for processing (2)
  • src/vouch/receipts.py
  • tests/test_receipts.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/vouch/receipts.py
  • tests/test_receipts.py

Walkthrough

verify_receipt and verify_evidence now treat empty decoded quotes as missing receipts. Regression tests cover zero-length spans and missing sources, and the changelog documents the correction.

Changes

Receipt verification

Layer / File(s) Summary
Reject empty receipt quotes
src/vouch/receipts.py, tests/test_receipts.py, CHANGELOG.md
verify_receipt and verify_evidence return NO_RECEIPT for empty quotes, with regression tests and an updated unreleased changelog entry.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • vouchdev/vouch#486: Also changes receipt verification conditions around quote values and byte offsets.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main behavioral fix to receipt verification.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added docs documentation, specs, examples, and repo guidance tests tests and fixtures size: XS less than 50 changed non-doc lines labels Jul 16, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/vouch/receipts.py`:
- Line 61: Update the empty-quote guard in verify_evidence to use the same
falsy-quote handling as verify_receipt, so an empty evidence.quote with a
missing source returns the no-receipt result instead of forged. Preserve the
existing behavior for non-empty quotes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: aa88a531-b829-4529-b62f-3c4326e2f63a

📥 Commits

Reviewing files that changed from the base of the PR and between bfb6b0a and 4934d7d.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • src/vouch/receipts.py
  • tests/test_receipts.py

Comment thread src/vouch/receipts.py
verify_evidence carries its own copy of the empty-span guard ahead of
the source read (a missing source must never mask "nothing to compare"
as forged). it had the same quote is None gap verify_receipt did:
quote="" paired with a missing source fell through to FORGED instead
of NO_RECEIPT. same fix, one call site up.

per coderabbit review on vouchdev#513.
@davion-knight

Copy link
Copy Markdown
Contributor Author

fixed — verify_evidence carried its own copy of the same guard ahead of the source-read (so a missing source can't mask "nothing to compare" as forged), and had the identical quote is None gap. same fix applied there: quote="" paired with a missing source now correctly returns NO_RECEIPT instead of FORGED. added test_verify_evidence_no_receipt_for_empty_quote_and_missing_source to cover it.

@davion-knight

Copy link
Copy Markdown
Contributor Author

heads up — the coderabbit-gate / gate check on this PR is failing not because of anything in this diff, but because posting the coderabbit-approved commit status hit 403 Resource not accessible by integration (https://github.com/vouchdev/vouch/actions/runs/29531526715/job/87732715858), even though the job declares statuses: write. looks like a token-permission restriction on pull_request_review-triggered runs for fork-originated PRs. CodeRabbit's actual review is approved and all 10 substantive checks are green — just flagging the gate workflow itself in case it's silently blocking other contributors' merges too.

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

Labels

docs documentation, specs, examples, and repo guidance size: XS less than 50 changed non-doc lines tests tests and fixtures

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant