fix(consent): fail closed on reordered stored history - #156
Conversation
A same-millisecond research revocation must remain the latest purpose decision after the runtime reloads PostgreSQL consent evidence. Missing participants stay absent, out-of-order stored history fails closed, and stronger isolation is rejected. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Reconstruct consent events by occurrence time, then insertion time, then event identity so a later-inserted same-millisecond revoke is not hidden behind a lexicographically later grant after process restart. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Traceability, ERD, UML, as-built schema, ADR-0015, and doctoring now distinguish persist-on-main from this reload slice and cite PostgreSQL READ COMMITTED isolation. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Reconstruct persisted consent events by physical created_at so a later-inserted same-millisecond revoke stays latest, and reject created_at collisions instead of inventing order from event identity. Out-of-order stored occurred_at now fails closed instead of being sorted into a newer grant. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
There was a problem hiding this comment.
Stale comment
Review (COMMENT, not approval)
This head closes the #140 fail-open. Reconstruct by physical
created_at, notoccurred_atplusevent_ref. A later-inserted same-millisecond research revoke stays latest. Acreated_atcollision or non-monotonic insertion-orderoccurred_atreturnsCorruptHistoryinstead of inventing a grant. Whitespace-padded participant aliases fail closed. Outbox and HTTP consent stay out of this slice.Do not merge #140. Do not self-approve this head. Independent last-push review and exact-head required checks still gate merge.
Residual risk (not a reconstruct defect)
created_atdefaults toclock_timestamp(). Two inserts in one persist transaction can theoretically share a microsecond and become unloadable after restart. That is fail-closed, not a silent grant. A monotonic insertion key belongs in a later persist-lock/outbox slice, not a third reload PR.There is still no PostgreSQL test that participant B’s events are absent when loading participant A. The
WHERE participant_ref = $1predicate is correct; add that isolation fixture when extending persist/outbox, not here.Next buyer-visible gap after this lands
Keep consent outbox on #142, then make
persist_consent_ledger_with_outboxthe sole write path. Consent HTTP stays later. This run continues with item-delivery restart reload on protected main: persist exists, but a restarted runtime cannot reconstruct already-shown items, so a mid-assessment restart can re-present or skip items.Sent by Cursor Automation: Fix Issues
There was a problem hiding this comment.
Stale comment
This is the correct consent-reload landing over #140. Reconstruct by physical
created_atinsertion order, fail closed on acreated_atcollision instead of sorting byevent_ref, and reject whitespace-padded participant aliases at the persist boundary. No HTTP or outbox leaked into this slice.Keep outbox composition on #142. Consent write-path landing is #170. Do not open another reload PR.
One later evidence gap, not a land blocker: persist participant A and B with different research decisions, load A, and prove B's events and research scope never appear. The
WHERE participant_ref = $1predicate already looks correct.Do not self-approve. Independent last-push review and exact-head required checks still gate merge.
Sent by Cursor Automation: Fix Issues
There was a problem hiding this comment.
Review (7fa917c)
SOUND. Prefer this head over closed #140 for consent-ledger reload. Do not open another consent-reload successor.
Verified
load_consent_ledgerreconstructs by physicalcreated_at, notoccurred_at/event_ref. Same-millisecond grant-then-revoke stays revoke-latest (same_millisecond_research_revoke_remains_the_latest_decision_after_reload).- Shared
created_atfails closed asCorruptHistoryinstead of ordering by event identity. - Non-monotonic insertion-order
occurred_atfails closed asCorruptHistoryrather than being reordered into a newer grant. - Missing ledger is
None, not an empty grant. - Caller aliases must already be canonical:
required_referencerejects whitespace-padded participant refs instead of trimming them into another identity. - Rustdoc states the
FOR SHAREheader lock does not serialize with persist-side header inserts. That honesty is correct for this slice.
Residual (later, not this PR)
Persist still inserts the ledger header without FOR UPDATE. A concurrent persist append can race the share lock. Do not fold #142 outbox composition or consent HTTP here.
Keep this ready head until exact-head required checks and an independent last-push review land. Never self-approve.
Sent by Cursor Automation: Fix Issues
Processing consumption rows now require claim_deadline_at after #81. Seed the wall-clock deadline and prove binary restore preserves it.
|
Pushed the #81 recovery fixture: processing restore rows now seed |


Prefer this head over #140 for consent-ledger reload.
#140 reconstructed stored events by
occurred_atthencreated_atthenevent_ref. That silently reorders a grant@20000 / revoke@19000 fixture into revoke-then-grant, soConsentLedger::recordaccepts both and the latest decision becomes a grant. The same identity tail can also invent grant-latest when two rows sharecreated_at.This successor keeps the reload slice (no outbox, no HTTP, no new tables) and:
created_atinsertion ordercreated_atcollision instead of ordering by event identityoccurred_atbecomeCorruptHistoryFOR SHAREdoes not serialize with persist-side header insertsLocal evidence:
cargo test --lib -- postgres_consent,postgres_consent_reload,postgres_consent_persistence, andpostgres_consent_error_contractare green against PostgreSQL 16.Do not fold #142 outbox composition or consent HTTP into this PR. Do not self-approve. Independent last-push review and exact-head required checks still gate merge.