From 0c62a341af78008e8453121bc6727a6146a14b1c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 00:19:39 +0900 Subject: [PATCH 1/4] test(sensitive): require full-field authority for model disclosure --- .../tests/sensitive_model_disclosure.rs | 175 ++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 crates/originweave-policy/tests/sensitive_model_disclosure.rs diff --git a/crates/originweave-policy/tests/sensitive_model_disclosure.rs b/crates/originweave-policy/tests/sensitive_model_disclosure.rs new file mode 100644 index 00000000..6aecf8b3 --- /dev/null +++ b/crates/originweave-policy/tests/sensitive_model_disclosure.rs @@ -0,0 +1,175 @@ +#![allow(clippy::expect_used)] + +//! Fail-closed composition contract for one full protected field entering a reviewed model request. +//! +//! The model route and invocation policy are not raw-value disclosure authority. This contract +//! requires the exact sensitive-data authority to authorize full-field disclosure, requires that +//! authority to be the same authority carried by the reviewed model invocation, and then requires +//! the invocation policy itself to authorize. It carries no protected bytes and performs no model I/O. + +use originweave_core::Origin; +use originweave_policy::{ + DataClassification, DisclosureDecision, DisclosureScope, ModelDisclosureDecision, + ModelInvocationDecision, ModelInvocationRequest, ModelInvocationScope, ModelRouteDecision, + ModelRouteRequest, ModelRouteScope, SensitiveDataAuthority, SensitiveDataRequest, + evaluate_full_field_model_disclosure, +}; + +fn authority(task_id: &str) -> SensitiveDataAuthority { + SensitiveDataAuthority::new( + "tenant-alpha", + task_id, + "customer-email", + "case-resolution", + Origin::parse("https://model-gateway.example").expect("valid destination origin"), + DataClassification::PersonalData, + ) +} + +fn route_request(authority: SensitiveDataAuthority, provider_id: &str) -> ModelRouteRequest { + ModelRouteRequest::new( + authority, + provider_id, + "model-reviewed-v1", + "kr-central", + "ephemeral-retention", + "no-training", + "subprocessors-reviewed-v1", + ) +} + +fn route_scope(authority: SensitiveDataAuthority) -> ModelRouteScope { + ModelRouteScope::new( + authority, + "provider-private", + "model-reviewed-v1", + "kr-central", + "ephemeral-retention", + "no-training", + "subprocessors-reviewed-v1", + ) +} + +fn invocation_request(authority: SensitiveDataAuthority, provider_id: &str) -> ModelInvocationRequest { + ModelInvocationRequest::new( + route_request(authority, provider_id), + "case-resolution-prompt-v1", + "customer-email-summary-v1", + 4_096, + 1_024, + 0, + ) +} + +fn invocation_scope(authority: SensitiveDataAuthority) -> ModelInvocationScope { + ModelInvocationScope::new( + route_scope(authority), + "case-resolution-prompt-v1", + "customer-email-summary-v1", + 8_192, + 2_048, + 1_000, + ) +} + +#[test] +fn full_field_disclosure_and_exact_reviewed_invocation_are_authorized() { + let exact_authority = authority("task-42"); + let disclosure_request = SensitiveDataRequest::new(exact_authority.clone()); + let disclosure_scope = DisclosureScope::new( + exact_authority.clone(), + DisclosureDecision::FullFieldDisclosure, + ); + let invocation_request = invocation_request(exact_authority.clone(), "provider-private"); + let invocation_scope = invocation_scope(exact_authority); + + assert_eq!( + evaluate_full_field_model_disclosure( + &disclosure_request, + &disclosure_scope, + &invocation_request, + &invocation_scope, + 999, + ), + ModelDisclosureDecision::Authorized + ); +} + +#[test] +fn every_non_full_field_outcome_remains_non_authorizing_for_raw_model_input() { + for decision in [ + DisclosureDecision::DenyAccess, + DisclosureDecision::OpaqueHandleOnly, + DisclosureDecision::DerivedValueOnly, + DisclosureDecision::PartialFieldDisclosure, + DisclosureDecision::HumanApprovalRequired, + DisclosureDecision::DualControlRequired, + ] { + let exact_authority = authority("task-42"); + let disclosure_request = SensitiveDataRequest::new(exact_authority.clone()); + let disclosure_scope = DisclosureScope::new(exact_authority.clone(), decision); + let invocation_request = invocation_request(exact_authority.clone(), "provider-private"); + let invocation_scope = invocation_scope(exact_authority); + + assert_eq!( + evaluate_full_field_model_disclosure( + &disclosure_request, + &disclosure_scope, + &invocation_request, + &invocation_scope, + 999, + ), + ModelDisclosureDecision::DisclosureNotAuthorized(decision) + ); + } +} + +#[test] +fn disclosure_authority_cannot_be_composed_with_another_tasks_valid_invocation() { + let disclosed_authority = authority("task-42"); + let invocation_authority = authority("task-99"); + let disclosure_request = SensitiveDataRequest::new(disclosed_authority.clone()); + let disclosure_scope = DisclosureScope::new( + disclosed_authority, + DisclosureDecision::FullFieldDisclosure, + ); + let invocation_request = + invocation_request(invocation_authority.clone(), "provider-private"); + let invocation_scope = invocation_scope(invocation_authority); + + assert_eq!( + evaluate_full_field_model_disclosure( + &disclosure_request, + &disclosure_scope, + &invocation_request, + &invocation_scope, + 999, + ), + ModelDisclosureDecision::AuthorityMismatch + ); +} + +#[test] +fn invocation_denial_is_preserved_after_full_field_disclosure_authority() { + let exact_authority = authority("task-42"); + let disclosure_request = SensitiveDataRequest::new(exact_authority.clone()); + let disclosure_scope = DisclosureScope::new( + exact_authority.clone(), + DisclosureDecision::FullFieldDisclosure, + ); + let invocation_request = invocation_request(exact_authority.clone(), "provider-other"); + let invocation_scope = invocation_scope(exact_authority); + + assert_eq!( + evaluate_full_field_model_disclosure( + &disclosure_request, + &disclosure_scope, + &invocation_request, + &invocation_scope, + 999, + ), + ModelDisclosureDecision::InvocationDenied(ModelInvocationDecision::RouteDenied( + ModelRouteDecision::RouteMismatch + )) + ); +} From 1dcac8437e1011fdaed89aeea33bb793f7c3d869 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 00:22:31 +0900 Subject: [PATCH 2/4] style(sensitive): apply canonical model disclosure formatting --- .../tests/sensitive_model_disclosure.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/originweave-policy/tests/sensitive_model_disclosure.rs b/crates/originweave-policy/tests/sensitive_model_disclosure.rs index 6aecf8b3..be440877 100644 --- a/crates/originweave-policy/tests/sensitive_model_disclosure.rs +++ b/crates/originweave-policy/tests/sensitive_model_disclosure.rs @@ -50,7 +50,10 @@ fn route_scope(authority: SensitiveDataAuthority) -> ModelRouteScope { ) } -fn invocation_request(authority: SensitiveDataAuthority, provider_id: &str) -> ModelInvocationRequest { +fn invocation_request( + authority: SensitiveDataAuthority, + provider_id: &str, +) -> ModelInvocationRequest { ModelInvocationRequest::new( route_request(authority, provider_id), "case-resolution-prompt-v1", @@ -129,12 +132,9 @@ fn disclosure_authority_cannot_be_composed_with_another_tasks_valid_invocation() let disclosed_authority = authority("task-42"); let invocation_authority = authority("task-99"); let disclosure_request = SensitiveDataRequest::new(disclosed_authority.clone()); - let disclosure_scope = DisclosureScope::new( - disclosed_authority, - DisclosureDecision::FullFieldDisclosure, - ); - let invocation_request = - invocation_request(invocation_authority.clone(), "provider-private"); + let disclosure_scope = + DisclosureScope::new(disclosed_authority, DisclosureDecision::FullFieldDisclosure); + let invocation_request = invocation_request(invocation_authority.clone(), "provider-private"); let invocation_scope = invocation_scope(invocation_authority); assert_eq!( From 08e9133fc716b36352067a3e05c3ebf2a65c37e2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 01:07:20 +0900 Subject: [PATCH 3/4] feat(sensitive): compose full-field model disclosure authority --- crates/originweave-policy/src/model_route.rs | 64 ++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/crates/originweave-policy/src/model_route.rs b/crates/originweave-policy/src/model_route.rs index bb19a1d1..c2a3a74d 100644 --- a/crates/originweave-policy/src/model_route.rs +++ b/crates/originweave-policy/src/model_route.rs @@ -344,3 +344,67 @@ pub fn evaluate_model_invocation( ModelInvocationDecision::Authorized } } + +/// Result of composing an explicit full-field disclosure with a reviewed model invocation. +/// +/// This decision authorizes only the metadata composition boundary. It carries no protected field +/// bytes and does not authenticate or invoke a provider, resolve an opaque handle, validate model +/// output, enforce retention, or prove that a non-model execution path was unavailable. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ModelDisclosureDecision { + /// The exact full-field disclosure and reviewed model invocation both authorize the same field authority. + Authorized, + /// Sensitive-data policy did not explicitly authorize complete-field disclosure. + DisclosureNotAuthorized(DisclosureDecision), + /// The disclosure authority and model invocation belong to different exact authority tuples. + AuthorityMismatch, + /// Model-route or invocation policy denied the otherwise full-field-authorized request. + InvocationDenied(ModelInvocationDecision), +} + +/// Compose full-field sensitive-data disclosure with one exact reviewed model invocation. +/// +/// Full-field disclosure is evaluated first so every weaker, approval-pending, malformed, or denied +/// sensitive-data outcome remains non-authorizing for raw model input. The disclosure request must +/// then carry the same complete [`SensitiveDataAuthority`] as the model invocation request; this +/// prevents independently valid policy metadata for different tenant/task/field/purpose/destination/ +/// classification authorities from being combined. Only after those boundaries pass is the existing +/// route/context/prompt/schema/token/expiry evaluator consulted. `trusted_time` must come from the +/// same authoritative time domain as the reviewed invocation policy expiry. +/// +/// An [`ModelDisclosureDecision::Authorized`] result still does not carry or disclose protected +/// bytes. A trusted broker must independently prove that full-field model disclosure is necessary, +/// authenticate the runtime/provider, resolve the value inside its transaction boundary, execute only +/// the authorized route, validate output, and enforce retention/export policy. +#[must_use] +pub fn evaluate_full_field_model_disclosure( + disclosure_request: &SensitiveDataRequest, + disclosure_scope: &DisclosureScope, + invocation_request: &ModelInvocationRequest, + invocation_scope: &ModelInvocationScope, + trusted_time: u64, +) -> ModelDisclosureDecision { + let disclosure_decision = evaluate_disclosure(disclosure_request, disclosure_scope); + if disclosure_decision != DisclosureDecision::FullFieldDisclosure { + return ModelDisclosureDecision::DisclosureNotAuthorized(disclosure_decision); + } + + let invocation_authority_decision = evaluate_disclosure( + disclosure_request, + &DisclosureScope::new( + invocation_request.route.authority.clone(), + DisclosureDecision::FullFieldDisclosure, + ), + ); + if invocation_authority_decision != DisclosureDecision::FullFieldDisclosure { + return ModelDisclosureDecision::AuthorityMismatch; + } + + let invocation_decision = + evaluate_model_invocation(invocation_request, invocation_scope, trusted_time); + if invocation_decision == ModelInvocationDecision::Authorized { + ModelDisclosureDecision::Authorized + } else { + ModelDisclosureDecision::InvocationDenied(invocation_decision) + } +} From f2fbcae3f07cad722f43580aa5be0b4e691d2a9c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 01:08:06 +0900 Subject: [PATCH 4/4] feat(sensitive): export model disclosure composition --- crates/originweave-policy/src/lib.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/originweave-policy/src/lib.rs b/crates/originweave-policy/src/lib.rs index 8753f43a..e8d696b6 100644 --- a/crates/originweave-policy/src/lib.rs +++ b/crates/originweave-policy/src/lib.rs @@ -21,8 +21,9 @@ pub use model_output::{ evaluate_model_output, }; pub use model_route::{ - ModelInvocationDecision, ModelInvocationRequest, ModelInvocationScope, ModelRouteDecision, - ModelRouteRequest, ModelRouteScope, evaluate_model_invocation, evaluate_model_route, + ModelDisclosureDecision, ModelInvocationDecision, ModelInvocationRequest, ModelInvocationScope, + ModelRouteDecision, ModelRouteRequest, ModelRouteScope, evaluate_full_field_model_disclosure, + evaluate_model_invocation, evaluate_model_route, }; pub use sensitive_data::{ DataClassification, DisclosureDecision, DisclosureScope, HandleRevocationReason,