From fd57ad4672a5f98c29050f253bd87fa99b1c4c82 Mon Sep 17 00:00:00 2001 From: Sinity Date: Mon, 13 Jul 2026 07:29:51 +0200 Subject: [PATCH 1/2] fix(storage): prove quarantined browser repair witness Quarantined browser captures retain an undecided membership census. Require that census together with an exact immutable unknown-key selected-baseline receipt before copy-forward, so stale or incompatible membership/application evidence cannot be promoted.\n\nRef polylogue-lkrc.\n\nCo-Authored-By: Claude --- polylogue/storage/repair.py | 35 +++++++++++++++++-- .../test_browser_capture_origin_repair.py | 33 +++++++++++++++-- 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/polylogue/storage/repair.py b/polylogue/storage/repair.py index df51fff110..7b7e9c7101 100644 --- a/polylogue/storage/repair.py +++ b/polylogue/storage/repair.py @@ -1175,7 +1175,7 @@ def _verify_browser_origin_copy_forward_source_stage( len(projection.message_hashes), 0, RawRevisionAuthority.QUARANTINED.value, - "applied", + None, ) or census is None or tuple(census) != ("complete", 1) @@ -1417,6 +1417,17 @@ def _inspect_browser_capture_origin_mismatch( "SELECT session_id, origin, native_id, content_hash FROM sessions WHERE session_id = ?", (session_id,), ).fetchone() + old_applications = conn.execute( + """ + SELECT session_id, logical_source_key, source_revision, acquisition_generation, + decision, accepted_raw_id, accepted_source_revision, + accepted_content_hash, baseline_raw_id, decided_at_ms + FROM raw_revision_applications + WHERE raw_id = ? AND logical_source_key = ? AND decision = 'selected_baseline' + ORDER BY decision_id + """, + (raw_id, old_key), + ).fetchall() if ( head is None or indexed is None @@ -1428,6 +1439,21 @@ def _inspect_browser_capture_origin_mismatch( or int(head["accepted_frontier"]) != blob_size or int(head["acquisition_generation"]) != int(raw["acquisition_generation"]) or _bytes_value(indexed["content_hash"]) != accepted_hash + or len(old_applications) != 1 + or tuple(old_applications[0])[:-1] + != ( + session_id, + old_key, + blob_hash_hex, + int(raw["acquisition_generation"]), + ApplicationDecision.SELECTED_BASELINE.value, + raw_id, + blob_hash_hex, + accepted_hash, + raw_id, + ) + or not isinstance(old_applications[0][-1], int) + or int(old_applications[0][-1]) < 0 ): return _browser_origin_ineligible(raw_id, "current accepted head does not exactly prove the normalized session") membership = conn.execute( @@ -1453,7 +1479,12 @@ def _inspect_browser_capture_origin_mismatch( or int(membership["message_count"]) != len(projection.message_hashes) or int(membership["acquisition_generation"]) != int(raw["acquisition_generation"]) or str(membership["revision_authority"]) != RawRevisionAuthority.QUARANTINED.value - or str(membership["decision"]) != "applied" + # A quarantined source raw has not been admitted to replay, so its + # singleton census witness must remain undecided. The old + # selected-baseline receipt above is the separate, immutable authority + # witness for the current unknown-key head. A non-null membership + # decision would be incompatible with this narrow recovery shape. + or membership["decision"] is not None or str(census["status"]) != "complete" or int(census["member_count"]) != 1 ): diff --git a/tests/unit/storage/test_browser_capture_origin_repair.py b/tests/unit/storage/test_browser_capture_origin_repair.py index a5e2010383..7c627cd50f 100644 --- a/tests/unit/storage/test_browser_capture_origin_repair.py +++ b/tests/unit/storage/test_browser_capture_origin_repair.py @@ -115,6 +115,7 @@ def _seed_mismatched_browser_head(root: Path) -> str: accepted_content_hash=accepted_hash, accepted_frontier_kind="byte", accepted_frontier=len(payload), + baseline_raw_id=raw_id, detail="historical mismatched browser head", ), decided_at_ms=2, @@ -139,7 +140,7 @@ def _seed_mismatched_browser_head(root: Path) -> str: normalized_content_hash, message_count, acquisition_generation, revision_authority, decision, decided_at_ms ) VALUES (?, 'chatgpt:browser-origin-one', 'browser-origin-one', ?, ?, 1, 0, - 'quarantined', 'applied', 2) + 'quarantined', NULL, NULL) """, (raw_id, accepted_hash.hex(), accepted_hash), ) @@ -319,6 +320,24 @@ def test_browser_capture_origin_copy_forward_preserves_old_evidence_and_is_idemp assert receipt.read_text().count("\n") == 2 +def test_browser_capture_origin_rejects_decided_quarantined_membership(tmp_path: Path) -> None: + raw_id = _seed_mismatched_browser_head(tmp_path) + with sqlite3.connect(tmp_path / "source.db") as source: + source.execute( + """ + UPDATE raw_session_memberships + SET decision = 'ambiguous', decided_at_ms = 2 + WHERE raw_id = ? + """, + (raw_id,), + ) + + report = repair_browser_capture_origin_mismatches(_config(tmp_path), [raw_id]) + + assert report.ineligible_count == 1 + assert report.items[0].reason == "membership census does not exactly reproduce the accepted session" + + def test_browser_capture_origin_rebuild_keeps_copy_forward_head(tmp_path: Path) -> None: raw_id = _seed_mismatched_browser_head(tmp_path) dry_run = repair_browser_capture_origin_mismatches(_config(tmp_path), [raw_id]) @@ -340,7 +359,7 @@ def test_browser_capture_origin_rebuild_keeps_copy_forward_head(tmp_path: Path) ).fetchone() == (copy_raw_id,) -@pytest.mark.parametrize("mutation", ["blob", "head", "origin", "canonical_head"]) +@pytest.mark.parametrize("mutation", ["blob", "head", "origin", "application", "canonical_head"]) def test_browser_capture_origin_copy_forward_mutations_fail_closed(tmp_path: Path, mutation: str) -> None: raw_id = _seed_mismatched_browser_head(tmp_path) with sqlite3.connect(tmp_path / "source.db") as source, sqlite3.connect(tmp_path / "index.db") as index: @@ -350,6 +369,11 @@ def test_browser_capture_origin_copy_forward_mutations_fail_closed(tmp_path: Pat index.execute("UPDATE raw_revision_heads SET accepted_frontier = accepted_frontier + 1") elif mutation == "origin": source.execute("UPDATE raw_sessions SET origin = 'chatgpt-export' WHERE raw_id = ?", (raw_id,)) + elif mutation == "application": + index.execute( + "DELETE FROM raw_revision_applications WHERE raw_id = ? AND decision = 'selected_baseline'", + (raw_id,), + ) else: index.execute( """ @@ -371,7 +395,10 @@ def test_browser_capture_origin_copy_forward_mutations_fail_closed(tmp_path: Pat def test_browser_capture_origin_rejects_unresolved_source_membership(tmp_path: Path) -> None: raw_id = _seed_mismatched_browser_head(tmp_path) with sqlite3.connect(tmp_path / "source.db") as source: - source.execute("UPDATE raw_session_memberships SET decision = 'ambiguous' WHERE raw_id = ?", (raw_id,)) + source.execute( + "UPDATE raw_session_memberships SET decision = 'ambiguous', decided_at_ms = 2 WHERE raw_id = ?", + (raw_id,), + ) report = repair_browser_capture_origin_mismatches(_config(tmp_path), [raw_id]) From 050dab5e5989a80d028de4b4e9d0fd7738fbc447 Mon Sep 17 00:00:00 2001 From: Sinity Date: Mon, 13 Jul 2026 07:43:39 +0200 Subject: [PATCH 2/2] fix(storage): bound browser origin recovery generation Restrict copy-forward candidates to generation-zero quarantined raws, matching the staged source contract. Preserve explicit fail-closed coverage for byte-proven authority and nonzero generation witnesses.\n\nRef polylogue-lkrc.\n\nCo-Authored-By: Claude --- polylogue/storage/repair.py | 1 + .../test_browser_capture_origin_repair.py | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/polylogue/storage/repair.py b/polylogue/storage/repair.py index 7b7e9c7101..7816dda1fa 100644 --- a/polylogue/storage/repair.py +++ b/polylogue/storage/repair.py @@ -1336,6 +1336,7 @@ def _inspect_browser_capture_origin_mismatch( or str(raw["revision_authority"]) != RawRevisionAuthority.QUARANTINED.value or raw["source_revision"] is None or raw["acquisition_generation"] is None + or int(raw["acquisition_generation"]) != 0 or any( raw[name] is not None for name in ( diff --git a/tests/unit/storage/test_browser_capture_origin_repair.py b/tests/unit/storage/test_browser_capture_origin_repair.py index 7c627cd50f..dba068007c 100644 --- a/tests/unit/storage/test_browser_capture_origin_repair.py +++ b/tests/unit/storage/test_browser_capture_origin_repair.py @@ -359,7 +359,9 @@ def test_browser_capture_origin_rebuild_keeps_copy_forward_head(tmp_path: Path) ).fetchone() == (copy_raw_id,) -@pytest.mark.parametrize("mutation", ["blob", "head", "origin", "application", "canonical_head"]) +@pytest.mark.parametrize( + "mutation", ["blob", "head", "origin", "application", "generation", "authority", "canonical_head"] +) def test_browser_capture_origin_copy_forward_mutations_fail_closed(tmp_path: Path, mutation: str) -> None: raw_id = _seed_mismatched_browser_head(tmp_path) with sqlite3.connect(tmp_path / "source.db") as source, sqlite3.connect(tmp_path / "index.db") as index: @@ -374,6 +376,18 @@ def test_browser_capture_origin_copy_forward_mutations_fail_closed(tmp_path: Pat "DELETE FROM raw_revision_applications WHERE raw_id = ? AND decision = 'selected_baseline'", (raw_id,), ) + elif mutation == "generation": + source.execute("UPDATE raw_sessions SET acquisition_generation = 1 WHERE raw_id = ?", (raw_id,)) + source.execute("UPDATE raw_session_memberships SET acquisition_generation = 1 WHERE raw_id = ?", (raw_id,)) + index.execute( + "UPDATE raw_revision_heads SET acquisition_generation = 1 WHERE accepted_raw_id = ?", (raw_id,) + ) + index.execute( + "UPDATE raw_revision_applications SET acquisition_generation = 1 WHERE raw_id = ?", + (raw_id,), + ) + elif mutation == "authority": + source.execute("UPDATE raw_sessions SET revision_authority = 'byte_proven' WHERE raw_id = ?", (raw_id,)) else: index.execute( """