From 63798d38a35fa14e9b7f899f638eb8fdcf077d46 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 10:17:43 +0900 Subject: [PATCH 1/7] test(sensitive): require identity-bound reservation settlement --- .../tests/sensitive_handle_settlement.rs | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 crates/originweave-policy/tests/sensitive_handle_settlement.rs diff --git a/crates/originweave-policy/tests/sensitive_handle_settlement.rs b/crates/originweave-policy/tests/sensitive_handle_settlement.rs new file mode 100644 index 00000000..87d37757 --- /dev/null +++ b/crates/originweave-policy/tests/sensitive_handle_settlement.rs @@ -0,0 +1,128 @@ +#![allow(clippy::expect_used)] + +use originweave_core::Origin; +use originweave_policy::{ + DataClassification, HandleRevocationReason, HandleUseDecision, SensitiveDataAuthority, + SensitiveHandleUseState, SensitiveValueHandleScope, +}; + +const TENANT: &str = "tenant_alpha"; +const TASK: &str = "task_ship_order"; +const FIELD: &str = "shipping_address"; +const PURPOSE: &str = "fulfill_order"; +const DESTINATION: &str = "https://shipping.example"; +const AUDIENCE: &str = "trusted_browser_adapter"; + +fn authority(destination: &str) -> SensitiveDataAuthority { + SensitiveDataAuthority::new( + TENANT, + TASK, + FIELD, + PURPOSE, + Origin::parse(destination).expect("test origin must be valid"), + DataClassification::PersonalData, + ) +} + +fn scope(max_uses: u32) -> SensitiveValueHandleScope { + SensitiveValueHandleScope::new(authority(DESTINATION), AUDIENCE, 2_000, max_uses) +} + +#[test] +fn compensating_exact_failed_reservation_restores_capacity_without_replay() { + let mut state = SensitiveHandleUseState::new(scope(1)); + + let first = state + .reserve_tracked_use(authority(DESTINATION), AUDIENCE, 1_999) + .expect("first reservation must be authorized"); + assert_eq!(state.reserved_uses(), 1); + assert_eq!(state.outstanding_reservations(), 1); + assert_eq!(state.completed_uses(), 0); + assert_eq!( + state.reserve_tracked_use(authority(DESTINATION), AUDIENCE, 1_999), + Err(HandleUseDecision::UseLimitReached) + ); + + assert!(state.compensate_reservation(first)); + assert_eq!(state.reserved_uses(), 0); + assert_eq!(state.outstanding_reservations(), 0); + assert_eq!(state.completed_uses(), 0); + + let replacement = state + .reserve_tracked_use(authority(DESTINATION), AUDIENCE, 1_999) + .expect("compensation must restore one use of capacity"); + assert_ne!(replacement, first); + assert!(!state.compensate_reservation(first)); + assert_eq!(state.reserved_uses(), 1); + assert_eq!(state.outstanding_reservations(), 1); +} + +#[test] +fn committed_reservation_remains_consumed_and_cannot_be_compensated() { + let mut state = SensitiveHandleUseState::new(scope(1)); + let reservation = state + .reserve_tracked_use(authority(DESTINATION), AUDIENCE, 1_999) + .expect("reservation must be authorized"); + + assert!(state.commit_reservation(reservation)); + assert_eq!(state.reserved_uses(), 1); + assert_eq!(state.outstanding_reservations(), 0); + assert_eq!(state.completed_uses(), 1); + assert!(!state.commit_reservation(reservation)); + assert!(!state.compensate_reservation(reservation)); + assert_eq!( + state.reserve_tracked_use(authority(DESTINATION), AUDIENCE, 1_999), + Err(HandleUseDecision::UseLimitReached) + ); +} + +#[test] +fn settlement_is_identity_bound_when_multiple_reservations_are_outstanding() { + let mut state = SensitiveHandleUseState::new(scope(3)); + let first = state + .reserve_tracked_use(authority(DESTINATION), AUDIENCE, 1_999) + .expect("first reservation must be authorized"); + let second = state + .reserve_tracked_use(authority(DESTINATION), AUDIENCE, 1_999) + .expect("second reservation must be authorized"); + + assert_ne!(first, second); + assert!(state.compensate_reservation(first)); + assert_eq!(state.reserved_uses(), 1); + assert_eq!(state.outstanding_reservations(), 1); + assert!(state.commit_reservation(second)); + assert_eq!(state.reserved_uses(), 1); + assert_eq!(state.outstanding_reservations(), 0); + assert_eq!(state.completed_uses(), 1); +} + +#[test] +fn denied_or_revoked_state_never_creates_tracked_reservation() { + let mut state = SensitiveHandleUseState::new(scope(2)); + + assert_eq!( + state.reserve_tracked_use(authority("https://other.example"), AUDIENCE, 1_999), + Err(HandleUseDecision::ScopeMismatch) + ); + assert_eq!(state.outstanding_reservations(), 0); + assert!(state.revoke(HandleRevocationReason::PolicyChanged)); + assert_eq!( + state.reserve_tracked_use(authority(DESTINATION), AUDIENCE, 1_999), + Err(HandleUseDecision::Revoked) + ); + assert_eq!(state.reserved_uses(), 0); +} + +#[test] +fn revocation_does_not_prevent_compensating_an_undisclosed_reservation() { + let mut state = SensitiveHandleUseState::new(scope(1)); + let reservation = state + .reserve_tracked_use(authority(DESTINATION), AUDIENCE, 1_999) + .expect("reservation must be authorized before revocation"); + + assert!(state.revoke(HandleRevocationReason::SessionTerminated)); + assert!(state.compensate_reservation(reservation)); + assert_eq!(state.reserved_uses(), 0); + assert_eq!(state.completed_uses(), 0); + assert_eq!(state.outstanding_reservations(), 0); +} From 01005f4941c3518dfb8fdc50e099e9ba9a385670 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 10:20:35 +0900 Subject: [PATCH 2/7] feat(sensitive): add identity-bound reservation settlement --- .../originweave-policy/src/sensitive_data.rs | 185 +++++++++++++++--- 1 file changed, 160 insertions(+), 25 deletions(-) diff --git a/crates/originweave-policy/src/sensitive_data.rs b/crates/originweave-policy/src/sensitive_data.rs index 89427a98..3523fad3 100644 --- a/crates/originweave-policy/src/sensitive_data.rs +++ b/crates/originweave-policy/src/sensitive_data.rs @@ -4,6 +4,8 @@ //! protected value itself, performs no I/O, and grants no authority from ambient //! session, network, repository, or model state. +use std::collections::BTreeSet; + use originweave_core::Origin; const MAX_AUTHORITY_IDENTIFIER_BYTES: usize = 128; @@ -167,8 +169,10 @@ pub enum HandleUseDecision { AudienceMismatch, /// The handle is no longer valid at the supplied trusted time. Expired, - /// The bounded use count has already been consumed. + /// The bounded use count has already been consumed or reserved. UseLimitReached, + /// The in-process reservation identity space is exhausted and no safe identity can be issued. + ReservationIdentityExhausted, } /// Reason that authoritative in-process handle state was revoked. @@ -256,25 +260,41 @@ impl HandleUseRequest { } } -/// In-process authoritative use-count and revocation state for one opaque sensitive-value handle scope. +/// Opaque in-process identity for one unsettled sensitive-handle use reservation. +/// +/// This is not the sensitive-value handle and carries no protected value or +/// authority fields. It exists only so a trusted broker can settle the exact +/// reservation that it previously created instead of decrementing a shared count +/// ambiguously. Reservation identities are never reused within one state value. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct SensitiveHandleUseReservation { + sequence: u64, +} + +/// In-process authoritative use-count, reservation-settlement, and revocation state +/// for one opaque sensitive-value handle scope. /// -/// This value removes the caller-supplied prior-use count from the reservation -/// operation. A successful reservation compares the exact authority, exact -/// non-transferable audience, trusted time, expiry, revocation state, and current -/// count and then increments the count while the caller holds an exclusive mutable -/// borrow of this state. Denied reservations never consume a use. +/// The state supports two reservation modes. [`Self::reserve_use`] retains the +/// earlier conservative behavior and immediately consumes a use without offering +/// compensation. [`Self::reserve_tracked_use`] creates an identity-bound unsettled +/// reservation that a trusted broker must later commit after disclosure or +/// compensate only when it has authoritative proof that no disclosure occurred. +/// Denied reservations never consume capacity. /// /// This is a policy-state primitive, not the trusted broker itself. It contains /// neither the opaque handle token nor protected data and provides no authenticated /// workload identity, durable or cross-process transaction, value resolution, -/// compensation, or persistence. A shared or durable broker must derive the -/// audience from authenticated caller identity, place the state behind its own -/// transactional or locking boundary, persist lifecycle state, and recheck it -/// before disclosure. +/// persistence, or proof that compensation is truthful. A shared or durable broker +/// must derive audience from authenticated caller identity, place reserve/recheck/ +/// disclose/settle behind its own transactional or locking boundary, persist +/// lifecycle state, and recheck authority immediately before disclosure. #[derive(Debug, PartialEq, Eq)] pub struct SensitiveHandleUseState { scope: SensitiveValueHandleScope, reserved_uses: u32, + completed_uses: u32, + outstanding_reservations: BTreeSet, + next_reservation_sequence: Option, revocation_reason: Option, } @@ -285,16 +305,31 @@ impl SensitiveHandleUseState { Self { scope, reserved_uses: 0, + completed_uses: 0, + outstanding_reservations: BTreeSet::new(), + next_reservation_sequence: Some(1), revocation_reason: None, } } - /// Return the number of uses already reserved through this state value. + /// Return uses that are either permanently consumed or currently reserved. #[must_use] pub const fn reserved_uses(&self) -> u32 { self.reserved_uses } + /// Return uses known to have completed or been conservatively consumed. + #[must_use] + pub const fn completed_uses(&self) -> u32 { + self.completed_uses + } + + /// Return the number of tracked reservations awaiting commit or compensation. + #[must_use] + pub fn outstanding_reservations(&self) -> usize { + self.outstanding_reservations.len() + } + /// Return the first authoritative revocation reason, if this state was revoked. #[must_use] pub const fn revocation_reason(&self) -> Option { @@ -305,6 +340,8 @@ impl SensitiveHandleUseState { /// /// Returns `true` only for the state transition from active to revoked. A /// later duplicate call is a no-op and cannot rewrite the original reason. + /// Existing tracked reservations remain settleable so a broker can record a + /// completed disclosure or compensate a failed pre-disclosure attempt. pub fn revoke(&mut self, reason: HandleRevocationReason) -> bool { if self.revocation_reason.is_some() { false @@ -314,19 +351,93 @@ impl SensitiveHandleUseState { } } - /// Reserve one use from the current authoritative count when policy permits it. + /// Reserve and immediately consume one use when policy permits it. /// - /// The audience must be derived by the trusted broker from authenticated caller - /// identity, and the supplied time must come from the broker's trusted clock. - /// Revocation is authoritative and is checked before later request details so a - /// revoked handle cannot expose whether a different scope, audience, expiry, or - /// use-limit condition would otherwise have matched. Every denial leaves the - /// authoritative count unchanged. + /// This compatibility path is deliberately non-compensatable. Callers that + /// need pre-disclosure rollback must use [`Self::reserve_tracked_use`] and + /// settle the returned identity explicitly. Revocation is checked before + /// later request details, and every denial leaves the authoritative count + /// unchanged. pub fn reserve_use( &mut self, authority: SensitiveDataAuthority, audience_id: &str, now_epoch_seconds: u64, + ) -> HandleUseDecision { + let decision = self.evaluate_reservation(authority, audience_id, now_epoch_seconds); + if decision != HandleUseDecision::Authorized { + return decision; + } + self.reserved_uses += 1; + self.completed_uses += 1; + HandleUseDecision::Authorized + } + + /// Reserve one identity-bound use without yet claiming that disclosure completed. + /// + /// The returned reservation is caller-unforgeable only to the extent that this + /// state object itself is protected by the trusted broker. It carries no secret + /// data. Reservation identities never recycle, including after compensation, + /// so a stale settlement token cannot affect a later use. Exhausting the local + /// identity sequence fails closed without consuming capacity. + pub fn reserve_tracked_use( + &mut self, + authority: SensitiveDataAuthority, + audience_id: &str, + now_epoch_seconds: u64, + ) -> Result { + let decision = self.evaluate_reservation(authority, audience_id, now_epoch_seconds); + if decision != HandleUseDecision::Authorized { + return Err(decision); + } + let Some(sequence) = self.next_reservation_sequence else { + return Err(HandleUseDecision::ReservationIdentityExhausted); + }; + self.next_reservation_sequence = sequence.checked_add(1); + let reservation = SensitiveHandleUseReservation { sequence }; + let inserted = self.outstanding_reservations.insert(reservation); + debug_assert!(inserted, "reservation sequence must never be reused"); + self.reserved_uses += 1; + Ok(reservation) + } + + /// Mark one exact tracked reservation as a completed, permanently consumed use. + /// + /// This method records settlement only; it does not authorize disclosure. A + /// trusted broker must already have performed the required immediate authority + /// recheck and must call this only after the protected value was actually + /// disclosed. Returns `false` for an unknown, stale, compensated, or already + /// committed reservation and leaves all counters unchanged. + pub fn commit_reservation(&mut self, reservation: SensitiveHandleUseReservation) -> bool { + if self.outstanding_reservations.remove(&reservation) { + self.completed_uses += 1; + true + } else { + false + } + } + + /// Release one exact tracked reservation after authoritative pre-disclosure failure. + /// + /// A trusted broker may call this only when it knows the protected value did + /// not cross the disclosure boundary. Compensation removes only the supplied + /// outstanding identity and restores one unit of capacity. Unknown, stale, + /// committed, or already compensated identities return `false` without + /// changing state. Revocation does not block cleanup compensation. + pub fn compensate_reservation(&mut self, reservation: SensitiveHandleUseReservation) -> bool { + if self.outstanding_reservations.remove(&reservation) { + self.reserved_uses -= 1; + true + } else { + false + } + } + + fn evaluate_reservation( + &self, + authority: SensitiveDataAuthority, + audience_id: &str, + now_epoch_seconds: u64, ) -> HandleUseDecision { if self.revocation_reason.is_some() { return HandleUseDecision::Revoked; @@ -337,12 +448,7 @@ impl SensitiveHandleUseState { now_epoch_seconds, self.reserved_uses, ); - let decision = evaluate_handle_use(&request, &self.scope); - if decision != HandleUseDecision::Authorized { - return decision; - } - self.reserved_uses += 1; - HandleUseDecision::Authorized + evaluate_handle_use(&request, &self.scope) } } @@ -379,3 +485,32 @@ pub fn evaluate_handle_use( HandleUseDecision::Authorized } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn exhausted_reservation_identity_fails_closed_without_consuming_capacity() { + let authority = SensitiveDataAuthority::new( + "tenant_alpha", + "task_alpha", + "field_alpha", + "purpose_alpha", + Origin::parse("https://example.com").unwrap_or_else(|error| { + panic!("unit-test origin must parse: {error:?}") + }), + DataClassification::PersonalData, + ); + let scope = SensitiveValueHandleScope::new(authority.clone(), "adapter_alpha", 2_000, 2); + let mut state = SensitiveHandleUseState::new(scope); + state.next_reservation_sequence = None; + + assert_eq!( + state.reserve_tracked_use(authority, "adapter_alpha", 1_999), + Err(HandleUseDecision::ReservationIdentityExhausted) + ); + assert_eq!(state.reserved_uses(), 0); + assert_eq!(state.outstanding_reservations(), 0); + } +} From 78327bdaae6452c940febcdd476a7b34a9a74b2c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 10:21:16 +0900 Subject: [PATCH 3/7] feat(sensitive): export tracked reservation identity --- crates/originweave-policy/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/originweave-policy/src/lib.rs b/crates/originweave-policy/src/lib.rs index 8cab72bb..abb84862 100644 --- a/crates/originweave-policy/src/lib.rs +++ b/crates/originweave-policy/src/lib.rs @@ -12,7 +12,8 @@ mod sensitive_data; pub use sensitive_data::{ DataClassification, DisclosureDecision, DisclosureScope, HandleRevocationReason, HandleUseDecision, HandleUseRequest, SensitiveDataAuthority, SensitiveDataRequest, - SensitiveHandleUseState, SensitiveValueHandleScope, evaluate_disclosure, evaluate_handle_use, + SensitiveHandleUseReservation, SensitiveHandleUseState, SensitiveValueHandleScope, + evaluate_disclosure, evaluate_handle_use, }; use originweave_core::{ From add3599bee784c58dfaa4275d17c477eaed781a9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 10:25:26 +0900 Subject: [PATCH 4/7] fix(sensitive): format and harden reservation settlement --- crates/originweave-policy/src/sensitive_data.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/originweave-policy/src/sensitive_data.rs b/crates/originweave-policy/src/sensitive_data.rs index 3523fad3..fca8b713 100644 --- a/crates/originweave-policy/src/sensitive_data.rs +++ b/crates/originweave-policy/src/sensitive_data.rs @@ -395,8 +395,7 @@ impl SensitiveHandleUseState { }; self.next_reservation_sequence = sequence.checked_add(1); let reservation = SensitiveHandleUseReservation { sequence }; - let inserted = self.outstanding_reservations.insert(reservation); - debug_assert!(inserted, "reservation sequence must never be reused"); + let _ = self.outstanding_reservations.insert(reservation); self.reserved_uses += 1; Ok(reservation) } @@ -488,6 +487,8 @@ pub fn evaluate_handle_use( #[cfg(test)] mod tests { + #![allow(clippy::expect_used)] + use super::*; #[test] @@ -497,9 +498,7 @@ mod tests { "task_alpha", "field_alpha", "purpose_alpha", - Origin::parse("https://example.com").unwrap_or_else(|error| { - panic!("unit-test origin must parse: {error:?}") - }), + Origin::parse("https://example.com").expect("unit-test origin must parse"), DataClassification::PersonalData, ); let scope = SensitiveValueHandleScope::new(authority.clone(), "adapter_alpha", 2_000, 2); From c672e59242d4b4a7cc9c3fc13ac30e6456e3356b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 11:12:08 +0900 Subject: [PATCH 5/7] fix(sensitive): use allocation-bound reservation identities --- .../originweave-policy/src/sensitive_data.rs | 96 ++++++++----------- 1 file changed, 41 insertions(+), 55 deletions(-) diff --git a/crates/originweave-policy/src/sensitive_data.rs b/crates/originweave-policy/src/sensitive_data.rs index fca8b713..b62791e2 100644 --- a/crates/originweave-policy/src/sensitive_data.rs +++ b/crates/originweave-policy/src/sensitive_data.rs @@ -4,7 +4,7 @@ //! protected value itself, performs no I/O, and grants no authority from ambient //! session, network, repository, or model state. -use std::collections::BTreeSet; +use std::sync::Arc; use originweave_core::Origin; @@ -171,8 +171,6 @@ pub enum HandleUseDecision { Expired, /// The bounded use count has already been consumed or reserved. UseLimitReached, - /// The in-process reservation identity space is exhausted and no safe identity can be issued. - ReservationIdentityExhausted, } /// Reason that authoritative in-process handle state was revoked. @@ -263,14 +261,23 @@ impl HandleUseRequest { /// Opaque in-process identity for one unsettled sensitive-handle use reservation. /// /// This is not the sensitive-value handle and carries no protected value or -/// authority fields. It exists only so a trusted broker can settle the exact -/// reservation that it previously created instead of decrementing a shared count -/// ambiguously. Reservation identities are never reused within one state value. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +/// authority fields. Each returned token and the corresponding state entry retain +/// the same allocation-bound identity. A surviving stale token therefore keeps its +/// allocation alive, so a later reservation cannot alias it even after compensation. +/// The token is intentionally in-process-only, non-serializable, and non-copyable. +#[derive(Debug)] pub struct SensitiveHandleUseReservation { - sequence: u64, + identity: Arc<[u8; 1]>, } +impl PartialEq for SensitiveHandleUseReservation { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.identity, &other.identity) + } +} + +impl Eq for SensitiveHandleUseReservation {} + /// In-process authoritative use-count, reservation-settlement, and revocation state /// for one opaque sensitive-value handle scope. /// @@ -293,8 +300,7 @@ pub struct SensitiveHandleUseState { scope: SensitiveValueHandleScope, reserved_uses: u32, completed_uses: u32, - outstanding_reservations: BTreeSet, - next_reservation_sequence: Option, + outstanding_reservations: Vec, revocation_reason: Option, } @@ -306,8 +312,7 @@ impl SensitiveHandleUseState { scope, reserved_uses: 0, completed_uses: 0, - outstanding_reservations: BTreeSet::new(), - next_reservation_sequence: Some(1), + outstanding_reservations: Vec::new(), revocation_reason: None, } } @@ -377,9 +382,10 @@ impl SensitiveHandleUseState { /// /// The returned reservation is caller-unforgeable only to the extent that this /// state object itself is protected by the trusted broker. It carries no secret - /// data. Reservation identities never recycle, including after compensation, - /// so a stale settlement token cannot affect a later use. Exhausting the local - /// identity sequence fails closed without consuming capacity. + /// data and no caller-controlled identifier. The state retains a second strong + /// reference to the same allocation while the reservation is outstanding. If a + /// settled token survives, its allocation remains live and therefore cannot be + /// reused by a later reservation. pub fn reserve_tracked_use( &mut self, authority: SensitiveDataAuthority, @@ -390,14 +396,13 @@ impl SensitiveHandleUseState { if decision != HandleUseDecision::Authorized { return Err(decision); } - let Some(sequence) = self.next_reservation_sequence else { - return Err(HandleUseDecision::ReservationIdentityExhausted); - }; - self.next_reservation_sequence = sequence.checked_add(1); - let reservation = SensitiveHandleUseReservation { sequence }; - let _ = self.outstanding_reservations.insert(reservation); + let identity = Arc::new([0_u8]); + self.outstanding_reservations + .push(SensitiveHandleUseReservation { + identity: Arc::clone(&identity), + }); self.reserved_uses += 1; - Ok(reservation) + Ok(SensitiveHandleUseReservation { identity }) } /// Mark one exact tracked reservation as a completed, permanently consumed use. @@ -407,8 +412,13 @@ impl SensitiveHandleUseState { /// recheck and must call this only after the protected value was actually /// disclosed. Returns `false` for an unknown, stale, compensated, or already /// committed reservation and leaves all counters unchanged. - pub fn commit_reservation(&mut self, reservation: SensitiveHandleUseReservation) -> bool { - if self.outstanding_reservations.remove(&reservation) { + pub fn commit_reservation(&mut self, reservation: &SensitiveHandleUseReservation) -> bool { + if let Some(index) = self + .outstanding_reservations + .iter() + .position(|candidate| candidate == reservation) + { + self.outstanding_reservations.swap_remove(index); self.completed_uses += 1; true } else { @@ -423,8 +433,13 @@ impl SensitiveHandleUseState { /// outstanding identity and restores one unit of capacity. Unknown, stale, /// committed, or already compensated identities return `false` without /// changing state. Revocation does not block cleanup compensation. - pub fn compensate_reservation(&mut self, reservation: SensitiveHandleUseReservation) -> bool { - if self.outstanding_reservations.remove(&reservation) { + pub fn compensate_reservation(&mut self, reservation: &SensitiveHandleUseReservation) -> bool { + if let Some(index) = self + .outstanding_reservations + .iter() + .position(|candidate| candidate == reservation) + { + self.outstanding_reservations.swap_remove(index); self.reserved_uses -= 1; true } else { @@ -484,32 +499,3 @@ pub fn evaluate_handle_use( HandleUseDecision::Authorized } } - -#[cfg(test)] -mod tests { - #![allow(clippy::expect_used)] - - use super::*; - - #[test] - fn exhausted_reservation_identity_fails_closed_without_consuming_capacity() { - let authority = SensitiveDataAuthority::new( - "tenant_alpha", - "task_alpha", - "field_alpha", - "purpose_alpha", - Origin::parse("https://example.com").expect("unit-test origin must parse"), - DataClassification::PersonalData, - ); - let scope = SensitiveValueHandleScope::new(authority.clone(), "adapter_alpha", 2_000, 2); - let mut state = SensitiveHandleUseState::new(scope); - state.next_reservation_sequence = None; - - assert_eq!( - state.reserve_tracked_use(authority, "adapter_alpha", 1_999), - Err(HandleUseDecision::ReservationIdentityExhausted) - ); - assert_eq!(state.reserved_uses(), 0); - assert_eq!(state.outstanding_reservations(), 0); - } -} From 17bc00790e75424afd97c8a73800d9b16c766300 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 11:12:40 +0900 Subject: [PATCH 6/7] test(sensitive): exercise allocation-bound settlement tokens --- .../tests/sensitive_handle_settlement.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/originweave-policy/tests/sensitive_handle_settlement.rs b/crates/originweave-policy/tests/sensitive_handle_settlement.rs index 87d37757..cd780c55 100644 --- a/crates/originweave-policy/tests/sensitive_handle_settlement.rs +++ b/crates/originweave-policy/tests/sensitive_handle_settlement.rs @@ -43,7 +43,7 @@ fn compensating_exact_failed_reservation_restores_capacity_without_replay() { Err(HandleUseDecision::UseLimitReached) ); - assert!(state.compensate_reservation(first)); + assert!(state.compensate_reservation(&first)); assert_eq!(state.reserved_uses(), 0); assert_eq!(state.outstanding_reservations(), 0); assert_eq!(state.completed_uses(), 0); @@ -52,7 +52,7 @@ fn compensating_exact_failed_reservation_restores_capacity_without_replay() { .reserve_tracked_use(authority(DESTINATION), AUDIENCE, 1_999) .expect("compensation must restore one use of capacity"); assert_ne!(replacement, first); - assert!(!state.compensate_reservation(first)); + assert!(!state.compensate_reservation(&first)); assert_eq!(state.reserved_uses(), 1); assert_eq!(state.outstanding_reservations(), 1); } @@ -64,12 +64,12 @@ fn committed_reservation_remains_consumed_and_cannot_be_compensated() { .reserve_tracked_use(authority(DESTINATION), AUDIENCE, 1_999) .expect("reservation must be authorized"); - assert!(state.commit_reservation(reservation)); + assert!(state.commit_reservation(&reservation)); assert_eq!(state.reserved_uses(), 1); assert_eq!(state.outstanding_reservations(), 0); assert_eq!(state.completed_uses(), 1); - assert!(!state.commit_reservation(reservation)); - assert!(!state.compensate_reservation(reservation)); + assert!(!state.commit_reservation(&reservation)); + assert!(!state.compensate_reservation(&reservation)); assert_eq!( state.reserve_tracked_use(authority(DESTINATION), AUDIENCE, 1_999), Err(HandleUseDecision::UseLimitReached) @@ -87,10 +87,10 @@ fn settlement_is_identity_bound_when_multiple_reservations_are_outstanding() { .expect("second reservation must be authorized"); assert_ne!(first, second); - assert!(state.compensate_reservation(first)); + assert!(state.compensate_reservation(&first)); assert_eq!(state.reserved_uses(), 1); assert_eq!(state.outstanding_reservations(), 1); - assert!(state.commit_reservation(second)); + assert!(state.commit_reservation(&second)); assert_eq!(state.reserved_uses(), 1); assert_eq!(state.outstanding_reservations(), 0); assert_eq!(state.completed_uses(), 1); @@ -121,7 +121,7 @@ fn revocation_does_not_prevent_compensating_an_undisclosed_reservation() { .expect("reservation must be authorized before revocation"); assert!(state.revoke(HandleRevocationReason::SessionTerminated)); - assert!(state.compensate_reservation(reservation)); + assert!(state.compensate_reservation(&reservation)); assert_eq!(state.reserved_uses(), 0); assert_eq!(state.completed_uses(), 0); assert_eq!(state.outstanding_reservations(), 0); From aa46d982b2bf786fe297744ac99f88b6c4c5f4cf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 11:18:12 +0900 Subject: [PATCH 7/7] test(sensitive): reject cross-state reservation settlement --- .../tests/sensitive_handle_settlement.rs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/crates/originweave-policy/tests/sensitive_handle_settlement.rs b/crates/originweave-policy/tests/sensitive_handle_settlement.rs index cd780c55..e94585c6 100644 --- a/crates/originweave-policy/tests/sensitive_handle_settlement.rs +++ b/crates/originweave-policy/tests/sensitive_handle_settlement.rs @@ -96,6 +96,30 @@ fn settlement_is_identity_bound_when_multiple_reservations_are_outstanding() { assert_eq!(state.completed_uses(), 1); } +#[test] +fn reservation_token_cannot_settle_a_different_state() { + let mut first_state = SensitiveHandleUseState::new(scope(1)); + let mut second_state = SensitiveHandleUseState::new(scope(1)); + let first = first_state + .reserve_tracked_use(authority(DESTINATION), AUDIENCE, 1_999) + .expect("first-state reservation must be authorized"); + let second = second_state + .reserve_tracked_use(authority(DESTINATION), AUDIENCE, 1_999) + .expect("second-state reservation must be authorized"); + + assert!(!second_state.commit_reservation(&first)); + assert!(!second_state.compensate_reservation(&first)); + assert_eq!(second_state.reserved_uses(), 1); + assert_eq!(second_state.completed_uses(), 0); + assert_eq!(second_state.outstanding_reservations(), 1); + + assert!(first_state.compensate_reservation(&first)); + assert!(second_state.commit_reservation(&second)); + assert_eq!(first_state.reserved_uses(), 0); + assert_eq!(second_state.completed_uses(), 1); + assert_eq!(second_state.outstanding_reservations(), 0); +} + #[test] fn denied_or_revoked_state_never_creates_tracked_reservation() { let mut state = SensitiveHandleUseState::new(scope(2));