Skip to content

fix(storage): stop requiring an impossible index-tier backup attestation - #3596

Merged
Sinity merged 2 commits into
masterfrom
fix/storage/derived-tier-backup-manifest-attestation
Aug 3, 2026
Merged

fix(storage): stop requiring an impossible index-tier backup attestation#3596
Sinity merged 2 commits into
masterfrom
fix/storage/derived-tier-backup-manifest-attestation

Conversation

@Sinity

@Sinity Sinity commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Summary

agent_meta_sidecar_purge_apply.py (polylogue-ioz7) and attachment_reacquisition.py both require a verified backup manifest covering index.db before mutating it, but the validation call unconditionally requires a cryptographic HMAC attestation for that tier -- an attestation the backup system never produces for derived tiers. No backup manifest, however fresh or complete, could ever satisfy it, so --apply was permanently broken for both actuators.

Problem

Discovered running the operator-authorized polylogue-ioz7 live purge against a same-day, all-5-tiers-present, verdict=success backup manifest. polylogue/maintenance/agent_meta_sidecar_purge_apply.py:201 calls validate_migration_backup_manifest(backup_manifest, ArchiveTier.INDEX, connection=validate_conn), which unconditionally requires exactly one tier="index" HMAC attestation entry in the receipt's attestations list. But polylogue/daemon/backup.py's _write_successful_verification_receipt hardcodes authority_paths to only ever include {"source", "user"} tiers -- by design, matching the durable-tier architecture where only source.db/user.db are durable and cryptographically attestable; index.db/embeddings.db are derived/rebuildable and were never wired for one. attachment_reacquisition.py had the identical bug at two call sites.

Solution

Split migration_runner.py's validation logic into a shared internal helper plus two entry points:

  • validate_migration_backup_manifest keeps its existing behavior (attestation required) for durable-tier migrations (migrate_archive_tier, unchanged).
  • New validate_backup_manifest_covers_derived_tier skips the attestation check for non-durable tiers while keeping every other guarantee: manifest/receipt shape, tier inclusion, receipt/manifest byte match, and a byte-exact live fingerprint (sha256/size/user_version) recomputed from the current on-disk file.

Both actuators now call the new function for their index.db check. Durable-tier callers (raw_authority.py, raw_append_chain_backfill_apply.py, raw_membership_writeback_apply.py, raw_live_source_reconciliation_apply.py) are untouched -- they correctly pass ArchiveTier.SOURCE.

Anti-vacuity

Every existing test for both actuators monkeypatched the validator entirely, so none of them ever exercised a manifest actually produced by polylogue ops backup --profile full_evidence --verify -- exactly the gap that hid this bug. Added test_apply_accepts_a_real_backup_manifest_from_ops_backup, which builds the fixture archive, runs the real backup_archive() entry point against it, and lets apply_agent_meta_sidecar_purge validate that real manifest with no monkeypatching.

Verification

  • devtools test tests/unit/maintenance/test_agent_meta_sidecar_purge_apply.py tests/unit/storage/test_attachment_reacquisition.py -- 14 passed.
  • devtools test on the untouched durable-tier siblings (raw_authority, raw_append_chain_backfill_apply, raw_membership_writeback_apply, raw_live_source_reconciliation_apply, durable_migrations) -- 55 passed, confirming the durable-tier path is unaffected.
  • mypy --strict on all 5 touched files -- clean.
  • devtools verify --quick (pre-push gate) -- clean.

Ref polylogue-5kmn7, polylogue-ioz7

Summary by CodeRabbit

  • Bug Fixes

    • Improved validation of rebuildable index backups during purge and attachment recovery operations.
    • Added comprehensive checks for backup coverage, inventory, artifacts, receipts, and live fingerprints.
    • Durable backup validation continues to require receipt authentication.
  • Tests

    • Added regression coverage confirming valid backups allow purging only the intended sidecar sessions and record receipts.
    • Updated recovery and maintenance validation tests.

Problem: two mutation actuators that protect a derived-tier (index.db)
change behind a backup manifest -- agent_meta_sidecar_purge_apply.py
(polylogue-ioz7) and attachment_reacquisition.py -- called
validate_migration_backup_manifest(..., ArchiveTier.INDEX, ...), which
unconditionally requires a cryptographic HMAC attestation for that tier.
daemon/backup.py's _write_successful_verification_receipt only ever mints
attestations for durable tiers (source, user), by design -- index.db and
embeddings.db were never wired for one, matching the architecture's
durable-vs-derived split. So no backup manifest, however fresh or
complete, could ever satisfy the check: --apply against either actuator
was permanently broken. Found running the operator-authorized ioz7 live
purge against a same-day, all-5-tiers-present, verdict=success manifest.

What changed: migration_runner.py's validation logic is split into a
shared internal helper plus two entry points. validate_migration_backup_manifest
keeps its existing behavior (attestation required) for durable-tier
migrations (migrate_archive_tier, unchanged). The new
validate_backup_manifest_covers_derived_tier skips the attestation check
for non-durable tiers, while keeping every other guarantee: manifest/
receipt shape, tier inclusion, receipt/manifest byte match, and a
byte-exact live fingerprint (sha256/size/user_version) recomputed from the
current on-disk file. Both purge_apply.py and attachment_reacquisition.py
now call the new function for their index.db check.

Anti-vacuity: every existing test for both actuators monkeypatched the
validator entirely, so none of them ever exercised a manifest produced by
the real `polylogue ops backup --profile full_evidence --verify` path --
exactly the gap that hid this bug. Added
test_apply_accepts_a_real_backup_manifest_from_ops_backup, which builds
the fixture archive, runs the real backup_archive() entry point against
it, and lets apply_agent_meta_sidecar_purge validate that real manifest
with no monkeypatching.

Verification: devtools test on both actuators' test files (14 passed) +
the durable-tier siblings that still use validate_migration_backup_manifest
unchanged (raw_authority, raw_append_chain_backfill_apply,
raw_membership_writeback_apply, raw_live_source_reconciliation_apply,
durable_migrations -- 55 passed, confirming the durable-tier path is
untouched). mypy --strict on all 5 touched files -- clean.

Ref polylogue-5kmn7, polylogue-ioz7
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@Sinity, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 41 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: bbdd8617-9ebf-46d5-baba-e1b951629c42

📥 Commits

Reviewing files that changed from the base of the PR and between dd2b42f and 0af2eff.

📒 Files selected for processing (2)
  • polylogue/maintenance/agent_meta_sidecar_purge_apply.py
  • tests/unit/maintenance/test_agent_meta_sidecar_purge_apply.py
📝 Walkthrough

Walkthrough

Backup-manifest validation now uses a shared implementation with separate durable-tier and derived-tier modes. Index-tier callers use the derived-tier validator during preflight and apply. Tests add real-backup purge coverage and update validator patches.

Changes

Derived-Tier Backup Validation

Layer / File(s) Summary
Shared backup validation
polylogue/storage/sqlite/migration_runner.py
The module centralizes manifest validation, keeps attestation mandatory for durable tiers, adds derived-tier validation without attestation, rejects durable tiers in that mode, and exports the new validator.
Index-tier validation callers
polylogue/maintenance/agent_meta_sidecar_purge_apply.py, polylogue/storage/attachment_reacquisition.py
The purge and attachment reacquisition flows use derived-tier coverage checks for index-tier validation during preflight and apply. Apply documentation describes coverage and byte-exact fingerprint checks.
Validation regression coverage
tests/unit/maintenance/test_agent_meta_sidecar_purge_apply.py, tests/unit/storage/test_attachment_reacquisition.py
Tests patch the new validator. An integration test applies a real verified backup and checks sidecar removal, preserved sessions, and recorded receipts.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PurgeOrReacquisition
  participant DerivedTierValidator
  participant BackupEvidence
  participant IndexDB
  PurgeOrReacquisition->>DerivedTierValidator: validate index-tier backup coverage
  DerivedTierValidator->>BackupEvidence: verify manifest, receipt, artifact, and inventory
  DerivedTierValidator->>IndexDB: compare live fingerprint
  DerivedTierValidator-->>PurgeOrReacquisition: return validated backup path
  PurgeOrReacquisition->>IndexDB: apply purge or reacquisition
Loading

Possibly related PRs

  • Sinity/polylogue#2708: Introduces durable backup-manifest validation logic extended here for derived-tier checks.
  • Sinity/polylogue#2905: Also modifies backup-manifest validation and migration handling in migration_runner.py.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% 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
Title check ✅ Passed The title clearly identifies the fix for the impossible index-tier backup attestation requirement.
Description check ✅ Passed The description includes complete Summary, Problem, Solution, and Verification sections; omitted Changelog and Risks sections are not needed for this internal refactor.
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 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/storage/derived-tier-backup-manifest-attestation

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.

@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 `@polylogue/maintenance/agent_meta_sidecar_purge_apply.py`:
- Line 201: Both mutation flows revalidate the backup manifest too early; add a
post-lock fingerprint validation. In
polylogue/maintenance/agent_meta_sidecar_purge_apply.py:201, acquire the write
lock used by ArchiveStore.delete_sessions, then rerun
validate_backup_manifest_covers_derived_tier before deletion. In
polylogue/storage/attachment_reacquisition.py:557-563, execute BEGIN IMMEDIATE
before the second validation and preserve rollback handling when validation
fails. Add a concurrency regression that mutates index.db between precheck and
lock acquisition and verifies the apply operation rejects the stale backup.
🪄 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: ASSERTIVE

Plan: Pro Plus

Run ID: 48b0a921-b5dc-4361-830a-b54dcf69ef60

📥 Commits

Reviewing files that changed from the base of the PR and between 4beb886 and dd2b42f.

📒 Files selected for processing (5)
  • polylogue/maintenance/agent_meta_sidecar_purge_apply.py
  • polylogue/storage/attachment_reacquisition.py
  • polylogue/storage/sqlite/migration_runner.py
  • tests/unit/maintenance/test_agent_meta_sidecar_purge_apply.py
  • tests/unit/storage/test_attachment_reacquisition.py

try:
_checkpoint_live_tier(validate_conn)
validate_migration_backup_manifest(backup_manifest, ArchiveTier.INDEX, connection=validate_conn)
validate_backup_manifest_covers_derived_tier(backup_manifest, ArchiveTier.INDEX, connection=validate_conn)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Validate the live fingerprint after the mutation lock is held.

Both flows validate index.db before the lock that protects the mutation. A concurrent writer can commit after validation. The subsequent delete or update can then act on an index tier that the backup no longer covers.

  • polylogue/maintenance/agent_meta_sidecar_purge_apply.py#L201-L201: acquire the write lock used for ArchiveStore.delete_sessions, then re-run validate_backup_manifest_covers_derived_tier before deletion.
  • polylogue/storage/attachment_reacquisition.py#L557-L563: run BEGIN IMMEDIATE before the second validation, and retain rollback handling if validation fails.

Add a concurrency regression that changes index.db between precheck and lock acquisition. The apply operation must refuse the stale backup.

📍 Affects 2 files
  • polylogue/maintenance/agent_meta_sidecar_purge_apply.py#L201-L201 (this comment)
  • polylogue/storage/attachment_reacquisition.py#L557-L563
🤖 Prompt for 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.

In `@polylogue/maintenance/agent_meta_sidecar_purge_apply.py` at line 201, Both
mutation flows revalidate the backup manifest too early; add a post-lock
fingerprint validation. In
polylogue/maintenance/agent_meta_sidecar_purge_apply.py:201, acquire the write
lock used by ArchiveStore.delete_sessions, then rerun
validate_backup_manifest_covers_derived_tier before deletion. In
polylogue/storage/attachment_reacquisition.py:557-563, execute BEGIN IMMEDIATE
before the second validation and preserve rollback handling when validation
fails. Add a concurrency regression that mutates index.db between precheck and
lock acquisition and verifies the apply operation rejects the stale backup.

Sinity added a commit that referenced this pull request Aug 3, 2026
…ed live

Live --apply attempt (branch fix/storage/derived-tier-backup-manifest-
attestation, PR #3596) confirms the 5kmn7 attestation fix works: the
backup manifest + live index.db fingerprint validation both passed
cleanly. Execution then failed safely (no mutation) at ArchiveStore
write-mode construction, refused by polylogue-9qnzy's exact schema-
currency gap (source.db v15 vs code's declared v20). Wires ioz7 as
blocked-by 9qnzy to reflect this confirmed dependency.

Co-Authored-By: Claude <noreply@anthropic.com>
CodeRabbit finding on #3596: agent_meta_sidecar_purge_apply.py validated
the backup manifest exactly once, before opening the write-mode
ArchiveStore -- unlike attachment_reacquisition.py's existing precheck +
authoritative-revalidation pattern. A concurrent write to index.db
between that single check and the eventual delete_sessions() call would
go undetected.

ArchiveStore.__init__ acquires the exclusive ActiveWriterLease
synchronously for read_only=False, so revalidating right after
construction (and before delete_sessions) closes the exact window:
nothing else can write once the lease is held. Adds that authoritative
revalidation, matching attachment_reacquisition.py's own pattern and
comment.

Added test_apply_refuses_when_backup_goes_stale_between_precheck_and_write_lease:
a fake validator that raises on its second call proves the revalidation
is actually wired in and enforced (not merely present as dead code) --
the apply aborts before any mutation, verified via unchanged session ids
and zero receipt rows. Updated the existing purges-only-the-phantoms test
to expect two validation calls instead of one.

attachment_reacquisition.py's own second-validation call already existed
before this PR (that's the pattern this change replicates) --
CodeRabbit's finer point about reordering BEGIN IMMEDIATE ahead of that
existing validation there is a separate, pre-existing design choice
unrelated to the attestation bug this PR fixes; left out of scope.

Verification: devtools test on both actuators' test files -- 15 passed.
mypy --strict -- clean.
@Sinity

Sinity commented Aug 3, 2026

Copy link
Copy Markdown
Owner Author

Fixed in 0af2eff: added the authoritative revalidation right after ArchiveStore acquires its exclusive writer lease (ArchiveStore.init acquires it synchronously for read_only=False), before delete_sessions runs -- matching attachment_reacquisition.py's existing precheck+revalidation pattern (which is where I copied it from). Added a concurrency regression test (fake validator raises on its second call) proving the revalidation actually aborts the apply before any mutation.

Left out of scope: your point about reordering BEGIN IMMEDIATE ahead of the validation in attachment_reacquisition.py itself. That's a pre-existing design choice in code this PR doesn't otherwise touch, not something the attestation bug this PR fixes introduced.

@Sinity
Sinity merged commit 3996106 into master Aug 3, 2026
3 checks passed
@Sinity
Sinity deleted the fix/storage/derived-tier-backup-manifest-attestation branch August 3, 2026 02:49
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.

1 participant