Skip to content

fix(longitudinal): re-check consent and bind tenant - #199

Closed
cursor[bot] wants to merge 4 commits into
mainfrom
cursor/bc-95406020-984a-402e-af29-facd39735626-35c9
Closed

fix(longitudinal): re-check consent and bind tenant#199
cursor[bot] wants to merge 4 commits into
mainfrom
cursor/bc-95406020-984a-402e-af29-facd39735626-35c9

Conversation

@cursor

@cursor cursor Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Why

#184 enrolled a Seoul clinic participant after a longitudinal grant, but Gyeot collection stayed authorized from EnrollmentState alone. A later revoke, or resume after that revoke, still allowed collection. Tenant was an unbound caller string, so a second clinic could enroll the same participant.

TDD

RED tests/longitudinal_enrollment_contract.rs revoked after enroll, resumed, and required authorize_collection to fail closed. A ParticipantRecord from another tenant was rejected.

GREEN replaces can_accept_observations with authorize_collection(&ConsentSnapshot) and binds enroll to &ParticipantRecord.

Local GREEN:

  • cargo test --test longitudinal_enrollment_contract
  • cargo test --test documentation_architecture_contract --test consent_identity_safety
  • cargo clippy --all-targets -- -D warnings

Scope

  • Current-consent collection gate.
  • Tenant/participant bind through ParticipantRecord.
  • TRACEABILITY §2 stays Target on evaluated main.
  • Logical ERD adds enrollment_membership_context so work/home are not flattened onto the enrollment row.

Out of scope

Operator next action

Review the revoke-after-enroll and cross-tenant contracts. Prefer this head over #184. Do not merge #184. Do not self-approve. Independent last-push approval and exact-head checks are required.

Open in Web View Automation 

cursoragent and others added 3 commits August 16, 2026 16:18
A Seoul clinic participant can join a 14-day mood diary only after
granting longitudinal observation consent. Work and home membership
stay distinct, research refusal does not block personal enrollment,
and pause/resume/withdraw keep the enrollment evidence.

Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Collection now fails closed after a later longitudinal revoke, even if
the enrollment is still Enrolled or was resumed. Enroll takes the
tenant-owned participant record so a second clinic cannot borrow the
Seoul participant under another tenant.

Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
@cursor
cursor Bot requested a review from seonghobae August 16, 2026 16:28
Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Collection still trusts a caller-chosen snapshot

#199 correctly stops collection when the caller passes a new revoked snapshot, and enroll correctly binds ParticipantRecord tenant. That is not enough for the next persist/HTTP/Gyeot caller.

ConsentSnapshot is an immutable enroll-time view. After revoke it remains Granted. authorize_collection(&snapshot) only inspects that view, so:

enrolled.authorize_collection(&enroll_time_snapshot) // Ok(()) after revoke

EnrollmentState::Enrolled still documents that Gyeot may collect. Resume after revoke returns Enrolled. Any caller that gates on state() or reloads consent_snapshot_ref recreates the #184 bug.

Collection also never re-binds tenant. The live Gyeot path stays tenant-unbound.

Required before this slice is the landing vehicle

  1. authorize_collection(&ParticipantRecord, &ConsentLedger) — current ledger head, not enroll-time snapshot.
  2. CrossTenantDenied when the caller record tenant ≠ enrollment tenant.
  3. Document Enrolled as membership only; consent_snapshot_ref as enroll evidence only.
  4. Operator copy: grant or restore consent before enrollment or collection.
  5. Tests: enroll-time snapshot still looks Granted after revoke and must not authorize; other-clinic record fails closed while consent remains granted.

Do not merge #184. Do not merge this head as the collection gate. Do not add persistence onto this snapshot-only API. Independent last-push approval and exact-head checks are still required.

A successor branch implements the ledger + tenant collection gate.

Open in Web View Automation 

Sent by Cursor Automation: Fix Issues

Comment thread src/longitudinal.rs
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum EnrollmentState {
/// The participant is enrolled and Gyeot may collect observations.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Enrolled still says Gyeot may collect. resume after revoke returns this state. Document membership only: collection requires authorize_collection on the current ledger and tenant-owned record.

Comment thread src/longitudinal.rs
Self::ParticipantMismatch => {
"use the consent snapshot that belongs to this participant"
}
Self::LongitudinalConsentRequired => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This copy is reused after enroll + revoke. Tell the operator to grant or restore longitudinal consent before enrollment or collection.

Comment thread src/longitudinal.rs
/// Returns [`LongitudinalEnrollmentError`] when the snapshot belongs to
/// another participant, the enrollment is paused or withdrawn, or
/// longitudinal observation consent is missing or revoked.
pub fn authorize_collection(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Caller-supplied ConsentSnapshot is not a current-consent gate. After revoke, the enroll-time snapshot stays Granted. Take &ParticipantRecord + &ConsentLedger, fail closed unless the latest LongitudinalObservation event is Granted, and return CrossTenantDenied when the record tenant does not own this enrollment.

fn revoke_after_enroll_stops_collection_even_after_resume() {
let (mut ledger, snapshot) = granted_longitudinal_ledger();
let enrolled = enroll_seoul(&snapshot);
assert_eq!(enrolled.authorize_collection(&snapshot), Ok(()));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This only proves a new revoked snapshot fails. Add the realistic case: enroll-time snapshot remains Granted after revoke, and collection must still fail when the current ledger is consulted. Also bind tenant at collection time.

Copy link
Copy Markdown
Contributor

Closing as a proven superseded predecessor. Fresh compare shows #199 head 0e77397c is the merge base/ancestor of #226 head 8c54fccb, ahead by 2 and behind by 0. #226 keeps this enrollment/tenant line but fixes the remaining authority defect: collection now consults the current consent ledger rather than a stale enroll-time ConsentSnapshot, so revoke and cross-tenant collection fail closed. Continue exact-head CI/review on #226; do not merge #199 separately.

@seonghobae seonghobae closed this Aug 16, 2026
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