From 40515a275f3dc72495c26d1322b573edd988663f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 15:39:18 +0900 Subject: [PATCH 01/18] test(sensitive): require exact model route authority --- .../tests/sensitive_model_route.rs | 208 ++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 crates/originweave-policy/tests/sensitive_model_route.rs diff --git a/crates/originweave-policy/tests/sensitive_model_route.rs b/crates/originweave-policy/tests/sensitive_model_route.rs new file mode 100644 index 00000000..0ee70a64 --- /dev/null +++ b/crates/originweave-policy/tests/sensitive_model_route.rs @@ -0,0 +1,208 @@ +#![allow(clippy::expect_used)] + +//! Fail-closed policy contracts for model-route admission of sensitive data. +//! +//! Route admission is intentionally separate from disclosure authority: authorizing a +//! provider/model/region/retention tuple must never imply that raw protected values may be +//! disclosed. A later broker/orchestrator must independently authorize the value form and derive +//! the actual route identity from trusted runtime configuration. + +use originweave_core::Origin; +use originweave_policy::{ + DataClassification, ModelRouteDecision, ModelRouteRequest, ModelRouteScope, + SensitiveDataAuthority, evaluate_model_route, +}; + +fn authority(destination: &str) -> SensitiveDataAuthority { + SensitiveDataAuthority::new( + "tenant-alpha", + "task-42", + "customer-email", + "case-resolution", + Origin::parse(destination).expect("valid destination origin"), + DataClassification::PersonalData, + ) +} + +fn scope() -> ModelRouteScope { + ModelRouteScope::new( + authority("https://model-gateway.example"), + "provider-private", + "model-reviewed-v1", + "kr-central", + "no-training-ephemeral", + ) +} + +fn request() -> ModelRouteRequest { + ModelRouteRequest::new( + authority("https://model-gateway.example"), + "provider-private", + "model-reviewed-v1", + "kr-central", + "no-training-ephemeral", + ) +} + +#[test] +fn exact_sensitive_authority_and_model_route_are_admitted() { + assert_eq!( + evaluate_model_route(&request(), &scope()), + ModelRouteDecision::Authorized + ); +} + +#[test] +fn sensitive_authority_mismatch_is_distinct_from_route_mismatch() { + let wrong_authority = ModelRouteRequest::new( + authority("https://different-gateway.example"), + "provider-private", + "model-reviewed-v1", + "kr-central", + "no-training-ephemeral", + ); + + assert_eq!( + evaluate_model_route(&wrong_authority, &scope()), + ModelRouteDecision::AuthorityMismatch + ); +} + +#[test] +fn every_model_route_dimension_is_exact_and_non_transferable() { + let cases = [ + ModelRouteRequest::new( + authority("https://model-gateway.example"), + "provider-other", + "model-reviewed-v1", + "kr-central", + "no-training-ephemeral", + ), + ModelRouteRequest::new( + authority("https://model-gateway.example"), + "provider-private", + "model-other-v2", + "kr-central", + "no-training-ephemeral", + ), + ModelRouteRequest::new( + authority("https://model-gateway.example"), + "provider-private", + "model-reviewed-v1", + "us-east", + "no-training-ephemeral", + ), + ModelRouteRequest::new( + authority("https://model-gateway.example"), + "provider-private", + "model-reviewed-v1", + "kr-central", + "provider-default-retention", + ), + ]; + + for candidate in cases { + assert_eq!( + evaluate_model_route(&candidate, &scope()), + ModelRouteDecision::RouteMismatch + ); + } +} + +#[test] +fn malformed_request_route_identifiers_fail_closed() { + let malformed_values = ["", "contains space", "🚫", &"x".repeat(129)]; + + for malformed in malformed_values { + let candidates = [ + ModelRouteRequest::new( + authority("https://model-gateway.example"), + malformed, + "model-reviewed-v1", + "kr-central", + "no-training-ephemeral", + ), + ModelRouteRequest::new( + authority("https://model-gateway.example"), + "provider-private", + malformed, + "kr-central", + "no-training-ephemeral", + ), + ModelRouteRequest::new( + authority("https://model-gateway.example"), + "provider-private", + "model-reviewed-v1", + malformed, + "no-training-ephemeral", + ), + ModelRouteRequest::new( + authority("https://model-gateway.example"), + "provider-private", + "model-reviewed-v1", + "kr-central", + malformed, + ), + ]; + for candidate in candidates { + assert_eq!( + evaluate_model_route(&candidate, &scope()), + ModelRouteDecision::RouteMismatch + ); + } + } +} + +#[test] +fn malformed_scope_route_identifiers_fail_closed_even_when_request_matches() { + let malformed_scope = ModelRouteScope::new( + authority("https://model-gateway.example"), + "provider-private", + "model-reviewed-v1", + "kr-central", + "invalid retention", + ); + let matching_invalid_request = ModelRouteRequest::new( + authority("https://model-gateway.example"), + "provider-private", + "model-reviewed-v1", + "kr-central", + "invalid retention", + ); + + assert_eq!( + evaluate_model_route(&matching_invalid_request, &malformed_scope), + ModelRouteDecision::RouteMismatch + ); +} + +#[test] +fn malformed_sensitive_authority_fails_closed_even_when_both_sides_match() { + let invalid_authority = SensitiveDataAuthority::new( + "invalid tenant", + "task-42", + "customer-email", + "case-resolution", + Origin::parse("https://model-gateway.example").expect("valid destination origin"), + DataClassification::PersonalData, + ); + let invalid_request = ModelRouteRequest::new( + invalid_authority.clone(), + "provider-private", + "model-reviewed-v1", + "kr-central", + "no-training-ephemeral", + ); + let invalid_scope = ModelRouteScope::new( + invalid_authority, + "provider-private", + "model-reviewed-v1", + "kr-central", + "no-training-ephemeral", + ); + + assert_eq!( + evaluate_model_route(&invalid_request, &invalid_scope), + ModelRouteDecision::AuthorityMismatch + ); +} From 2bc00e1ac17625c92dd4cee72dadf951026cda53 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 15:43:28 +0900 Subject: [PATCH 02/18] feat(sensitive): add exact model route policy primitive --- crates/originweave-policy/src/model_route.rs | 144 +++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 crates/originweave-policy/src/model_route.rs diff --git a/crates/originweave-policy/src/model_route.rs b/crates/originweave-policy/src/model_route.rs new file mode 100644 index 00000000..1a03cdc1 --- /dev/null +++ b/crates/originweave-policy/src/model_route.rs @@ -0,0 +1,144 @@ +//! Exact provider/model/region/retention route admission for sensitive-data workflows. +//! +//! This module evaluates route metadata only. An [`ModelRouteDecision::Authorized`] result does +//! not authorize disclosure of a protected value, authenticate a provider, prove the provider's +//! physical region, invoke a model, or choose a fallback. A trusted broker/orchestrator must +//! independently authorize the permitted value form and derive the actual route identity from +//! trusted runtime configuration. + +use crate::sensitive_data::{ + DisclosureDecision, DisclosureScope, SensitiveDataAuthority, SensitiveDataRequest, + evaluate_disclosure, +}; + +const MAX_ROUTE_IDENTIFIER_BYTES: usize = 128; + +/// Result of comparing one requested model route with its exact sensitive-data route authority. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ModelRouteDecision { + /// The exact sensitive-data authority and every route identifier match. + Authorized, + /// The sensitive-data authority is malformed or does not match the route scope. + AuthorityMismatch, + /// Provider, model, region, or retention-policy route metadata is malformed or does not match. + RouteMismatch, +} + +/// One proposed provider/model/region/retention route for an already classified sensitive field. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ModelRouteRequest { + authority: SensitiveDataAuthority, + provider_id: String, + model_id: String, + region_id: String, + retention_policy_id: String, +} + +impl ModelRouteRequest { + /// Build a requested model route without granting disclosure authority. + #[must_use] + pub fn new( + authority: SensitiveDataAuthority, + provider_id: &str, + model_id: &str, + region_id: &str, + retention_policy_id: &str, + ) -> Self { + Self { + authority, + provider_id: provider_id.to_owned(), + model_id: model_id.to_owned(), + region_id: region_id.to_owned(), + retention_policy_id: retention_policy_id.to_owned(), + } + } + + fn route_identifiers_are_valid(&self) -> bool { + route_identifier_is_valid(&self.provider_id) + && route_identifier_is_valid(&self.model_id) + && route_identifier_is_valid(&self.region_id) + && route_identifier_is_valid(&self.retention_policy_id) + } +} + +/// Exact model-route authority for one existing sensitive-data authority tuple. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ModelRouteScope { + authority: SensitiveDataAuthority, + provider_id: String, + model_id: String, + region_id: String, + retention_policy_id: String, +} + +impl ModelRouteScope { + /// Build a route scope from trusted policy metadata. + /// + /// Route identifiers are validated when the scope is evaluated so malformed policy state + /// remains fail-closed rather than becoming authority merely because request and scope match. + #[must_use] + pub fn new( + authority: SensitiveDataAuthority, + provider_id: &str, + model_id: &str, + region_id: &str, + retention_policy_id: &str, + ) -> Self { + Self { + authority, + provider_id: provider_id.to_owned(), + model_id: model_id.to_owned(), + region_id: region_id.to_owned(), + retention_policy_id: retention_policy_id.to_owned(), + } + } + + fn route_identifiers_are_valid(&self) -> bool { + route_identifier_is_valid(&self.provider_id) + && route_identifier_is_valid(&self.model_id) + && route_identifier_is_valid(&self.region_id) + && route_identifier_is_valid(&self.retention_policy_id) + } +} + +fn route_identifier_is_valid(identifier: &str) -> bool { + !identifier.is_empty() + && identifier.len() <= MAX_ROUTE_IDENTIFIER_BYTES + && identifier.bytes().any(|byte| byte.is_ascii_alphanumeric()) + && identifier + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b':' | b'-')) +} + +/// Evaluate exact model-route admission without authorizing protected-value disclosure. +/// +/// The existing sensitive-data disclosure comparator is reused only to validate that request and +/// scope carry the same complete [`SensitiveDataAuthority`]. Route admission then independently +/// requires valid, exact provider, model, region, and retention-policy identifiers. All route +/// identifiers use bounded 1–128 byte ASCII policy tokens containing alphanumeric characters plus +/// `.`, `_`, `:`, and `-`. +#[must_use] +pub fn evaluate_model_route( + request: &ModelRouteRequest, + scope: &ModelRouteScope, +) -> ModelRouteDecision { + let authority_decision = evaluate_disclosure( + &SensitiveDataRequest::new(request.authority.clone()), + &DisclosureScope::new(scope.authority.clone(), DisclosureDecision::OpaqueHandleOnly), + ); + if authority_decision == DisclosureDecision::DenyAccess { + return ModelRouteDecision::AuthorityMismatch; + } + + if !request.route_identifiers_are_valid() + || !scope.route_identifiers_are_valid() + || request.provider_id != scope.provider_id + || request.model_id != scope.model_id + || request.region_id != scope.region_id + || request.retention_policy_id != scope.retention_policy_id + { + ModelRouteDecision::RouteMismatch + } else { + ModelRouteDecision::Authorized + } +} From bc8528477636e9d6a5a12ce1967adf755147b774 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 15:44:17 +0900 Subject: [PATCH 03/18] feat(sensitive): expose model route authority --- crates/originweave-policy/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/originweave-policy/src/lib.rs b/crates/originweave-policy/src/lib.rs index abb84862..26addac0 100644 --- a/crates/originweave-policy/src/lib.rs +++ b/crates/originweave-policy/src/lib.rs @@ -7,8 +7,10 @@ #![forbid(unsafe_code)] #![deny(missing_docs)] +mod model_route; mod sensitive_data; +pub use model_route::{ModelRouteDecision, ModelRouteRequest, ModelRouteScope, evaluate_model_route}; pub use sensitive_data::{ DataClassification, DisclosureDecision, DisclosureScope, HandleRevocationReason, HandleUseDecision, HandleUseRequest, SensitiveDataAuthority, SensitiveDataRequest, From 0d137a8898b68463662de87977676701d095766c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 15:47:27 +0900 Subject: [PATCH 04/18] style(sensitive): apply canonical rustfmt exports --- crates/originweave-policy/src/lib.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/originweave-policy/src/lib.rs b/crates/originweave-policy/src/lib.rs index 26addac0..52290f9d 100644 --- a/crates/originweave-policy/src/lib.rs +++ b/crates/originweave-policy/src/lib.rs @@ -10,7 +10,9 @@ mod model_route; mod sensitive_data; -pub use model_route::{ModelRouteDecision, ModelRouteRequest, ModelRouteScope, evaluate_model_route}; +pub use model_route::{ + ModelRouteDecision, ModelRouteRequest, ModelRouteScope, evaluate_model_route, +}; pub use sensitive_data::{ DataClassification, DisclosureDecision, DisclosureScope, HandleRevocationReason, HandleUseDecision, HandleUseRequest, SensitiveDataAuthority, SensitiveDataRequest, From c33e258ebad8933e74d376e21d66bbb071eed1be Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 15:48:17 +0900 Subject: [PATCH 05/18] style(sensitive): apply canonical rustfmt model route --- crates/originweave-policy/src/model_route.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/originweave-policy/src/model_route.rs b/crates/originweave-policy/src/model_route.rs index 1a03cdc1..1236d6f0 100644 --- a/crates/originweave-policy/src/model_route.rs +++ b/crates/originweave-policy/src/model_route.rs @@ -124,7 +124,10 @@ pub fn evaluate_model_route( ) -> ModelRouteDecision { let authority_decision = evaluate_disclosure( &SensitiveDataRequest::new(request.authority.clone()), - &DisclosureScope::new(scope.authority.clone(), DisclosureDecision::OpaqueHandleOnly), + &DisclosureScope::new( + scope.authority.clone(), + DisclosureDecision::OpaqueHandleOnly, + ), ); if authority_decision == DisclosureDecision::DenyAccess { return ModelRouteDecision::AuthorityMismatch; From 762009a1f36016327107f3cf17681893c8e63236 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 16:00:57 +0900 Subject: [PATCH 06/18] test(sensitive): cover malformed route policy scope --- .../tests/sensitive_model_route.rs | 56 ++++++++++++------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/crates/originweave-policy/tests/sensitive_model_route.rs b/crates/originweave-policy/tests/sensitive_model_route.rs index 0ee70a64..fd8d80e4 100644 --- a/crates/originweave-policy/tests/sensitive_model_route.rs +++ b/crates/originweave-policy/tests/sensitive_model_route.rs @@ -154,26 +154,44 @@ fn malformed_request_route_identifiers_fail_closed() { } #[test] -fn malformed_scope_route_identifiers_fail_closed_even_when_request_matches() { - let malformed_scope = ModelRouteScope::new( - authority("https://model-gateway.example"), - "provider-private", - "model-reviewed-v1", - "kr-central", - "invalid retention", - ); - let matching_invalid_request = ModelRouteRequest::new( - authority("https://model-gateway.example"), - "provider-private", - "model-reviewed-v1", - "kr-central", - "invalid retention", - ); +fn malformed_scope_route_identifiers_fail_closed_against_a_valid_request() { + let cases = [ + ModelRouteScope::new( + authority("https://model-gateway.example"), + "invalid provider", + "model-reviewed-v1", + "kr-central", + "no-training-ephemeral", + ), + ModelRouteScope::new( + authority("https://model-gateway.example"), + "provider-private", + "invalid model", + "kr-central", + "no-training-ephemeral", + ), + ModelRouteScope::new( + authority("https://model-gateway.example"), + "provider-private", + "model-reviewed-v1", + "invalid region", + "no-training-ephemeral", + ), + ModelRouteScope::new( + authority("https://model-gateway.example"), + "provider-private", + "model-reviewed-v1", + "kr-central", + "invalid retention", + ), + ]; - assert_eq!( - evaluate_model_route(&matching_invalid_request, &malformed_scope), - ModelRouteDecision::RouteMismatch - ); + for malformed_scope in cases { + assert_eq!( + evaluate_model_route(&request(), &malformed_scope), + ModelRouteDecision::RouteMismatch + ); + } } #[test] From 3e04503e51fe89d4ee9a465de2516069b426e24d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 16:01:54 +0900 Subject: [PATCH 07/18] docs(sensitive): record exact model route authority --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09c1db80..24e598ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Added +- Exact sensitive-data model-route admission that binds the complete existing sensitive authority to bounded provider, model, region, and retention-policy identifiers while remaining explicitly separate from protected-value disclosure, provider authentication, runtime region attestation, model invocation, and fallback selection. - In-process authoritative sensitive-handle use reservation and first-revocation-wins lifecycle state that owns the bounded use count, records task-completion/policy-change/key-rotation/session-termination/suspicious-use revocation causes, blocks all future reservations after revocation, increments only after exact scope/classification/expiry/use-limit authorization, and leaves denied reservations unconsumed; this is a policy primitive only and does not claim durable broker storage, protected-value resolution, or cross-process transactionality. - Rust workspace for independently reusable core, policy, destination, network, TLS, resource, and evidence modules. - Canonical HTTPS and loopback-origin boundary with case-normalized schemes and hosts, default-port normalization, IPv4/IPv6 handling, browser-special numeric-host rejection, and explicit malformed-input errors. From 94130414efc9ae84b2e711a6ed0c5987aab264ec Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 16:09:34 +0900 Subject: [PATCH 08/18] test(sensitive): separate model training route authority --- .../tests/sensitive_model_route.rs | 75 ++++++++++++++----- 1 file changed, 58 insertions(+), 17 deletions(-) diff --git a/crates/originweave-policy/tests/sensitive_model_route.rs b/crates/originweave-policy/tests/sensitive_model_route.rs index fd8d80e4..28685a9b 100644 --- a/crates/originweave-policy/tests/sensitive_model_route.rs +++ b/crates/originweave-policy/tests/sensitive_model_route.rs @@ -3,9 +3,9 @@ //! Fail-closed policy contracts for model-route admission of sensitive data. //! //! Route admission is intentionally separate from disclosure authority: authorizing a -//! provider/model/region/retention tuple must never imply that raw protected values may be -//! disclosed. A later broker/orchestrator must independently authorize the value form and derive -//! the actual route identity from trusted runtime configuration. +//! provider/model/region/retention/training tuple must never imply that raw protected values may +//! be disclosed. A later broker/orchestrator must independently authorize the value form and +//! derive the actual route identity from trusted runtime configuration. use originweave_core::Origin; use originweave_policy::{ @@ -30,7 +30,8 @@ fn scope() -> ModelRouteScope { "provider-private", "model-reviewed-v1", "kr-central", - "no-training-ephemeral", + "ephemeral-retention", + "no-training", ) } @@ -40,7 +41,8 @@ fn request() -> ModelRouteRequest { "provider-private", "model-reviewed-v1", "kr-central", - "no-training-ephemeral", + "ephemeral-retention", + "no-training", ) } @@ -59,7 +61,8 @@ fn sensitive_authority_mismatch_is_distinct_from_route_mismatch() { "provider-private", "model-reviewed-v1", "kr-central", - "no-training-ephemeral", + "ephemeral-retention", + "no-training", ); assert_eq!( @@ -76,21 +79,24 @@ fn every_model_route_dimension_is_exact_and_non_transferable() { "provider-other", "model-reviewed-v1", "kr-central", - "no-training-ephemeral", + "ephemeral-retention", + "no-training", ), ModelRouteRequest::new( authority("https://model-gateway.example"), "provider-private", "model-other-v2", "kr-central", - "no-training-ephemeral", + "ephemeral-retention", + "no-training", ), ModelRouteRequest::new( authority("https://model-gateway.example"), "provider-private", "model-reviewed-v1", "us-east", - "no-training-ephemeral", + "ephemeral-retention", + "no-training", ), ModelRouteRequest::new( authority("https://model-gateway.example"), @@ -98,6 +104,15 @@ fn every_model_route_dimension_is_exact_and_non_transferable() { "model-reviewed-v1", "kr-central", "provider-default-retention", + "no-training", + ), + ModelRouteRequest::new( + authority("https://model-gateway.example"), + "provider-private", + "model-reviewed-v1", + "kr-central", + "ephemeral-retention", + "training-allowed", ), ]; @@ -120,21 +135,24 @@ fn malformed_request_route_identifiers_fail_closed() { malformed, "model-reviewed-v1", "kr-central", - "no-training-ephemeral", + "ephemeral-retention", + "no-training", ), ModelRouteRequest::new( authority("https://model-gateway.example"), "provider-private", malformed, "kr-central", - "no-training-ephemeral", + "ephemeral-retention", + "no-training", ), ModelRouteRequest::new( authority("https://model-gateway.example"), "provider-private", "model-reviewed-v1", malformed, - "no-training-ephemeral", + "ephemeral-retention", + "no-training", ), ModelRouteRequest::new( authority("https://model-gateway.example"), @@ -142,6 +160,15 @@ fn malformed_request_route_identifiers_fail_closed() { "model-reviewed-v1", "kr-central", malformed, + "no-training", + ), + ModelRouteRequest::new( + authority("https://model-gateway.example"), + "provider-private", + "model-reviewed-v1", + "kr-central", + "ephemeral-retention", + malformed, ), ]; for candidate in candidates { @@ -161,21 +188,24 @@ fn malformed_scope_route_identifiers_fail_closed_against_a_valid_request() { "invalid provider", "model-reviewed-v1", "kr-central", - "no-training-ephemeral", + "ephemeral-retention", + "no-training", ), ModelRouteScope::new( authority("https://model-gateway.example"), "provider-private", "invalid model", "kr-central", - "no-training-ephemeral", + "ephemeral-retention", + "no-training", ), ModelRouteScope::new( authority("https://model-gateway.example"), "provider-private", "model-reviewed-v1", "invalid region", - "no-training-ephemeral", + "ephemeral-retention", + "no-training", ), ModelRouteScope::new( authority("https://model-gateway.example"), @@ -183,6 +213,15 @@ fn malformed_scope_route_identifiers_fail_closed_against_a_valid_request() { "model-reviewed-v1", "kr-central", "invalid retention", + "no-training", + ), + ModelRouteScope::new( + authority("https://model-gateway.example"), + "provider-private", + "model-reviewed-v1", + "kr-central", + "ephemeral-retention", + "invalid training", ), ]; @@ -209,14 +248,16 @@ fn malformed_sensitive_authority_fails_closed_even_when_both_sides_match() { "provider-private", "model-reviewed-v1", "kr-central", - "no-training-ephemeral", + "ephemeral-retention", + "no-training", ); let invalid_scope = ModelRouteScope::new( invalid_authority, "provider-private", "model-reviewed-v1", "kr-central", - "no-training-ephemeral", + "ephemeral-retention", + "no-training", ); assert_eq!( From 901fa3f83a7adff16326251047198f549336d3e4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 16:11:40 +0900 Subject: [PATCH 09/18] feat(sensitive): bind model training policy separately --- crates/originweave-policy/src/model_route.rs | 23 +++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/crates/originweave-policy/src/model_route.rs b/crates/originweave-policy/src/model_route.rs index 1236d6f0..5e2c6814 100644 --- a/crates/originweave-policy/src/model_route.rs +++ b/crates/originweave-policy/src/model_route.rs @@ -1,4 +1,4 @@ -//! Exact provider/model/region/retention route admission for sensitive-data workflows. +//! Exact provider/model/region/retention/training route admission for sensitive-data workflows. //! //! This module evaluates route metadata only. An [`ModelRouteDecision::Authorized`] result does //! not authorize disclosure of a protected value, authenticate a provider, prove the provider's @@ -20,11 +20,11 @@ pub enum ModelRouteDecision { Authorized, /// The sensitive-data authority is malformed or does not match the route scope. AuthorityMismatch, - /// Provider, model, region, or retention-policy route metadata is malformed or does not match. + /// Provider, model, region, retention, or training route metadata is malformed or mismatched. RouteMismatch, } -/// One proposed provider/model/region/retention route for an already classified sensitive field. +/// One proposed model route for an already classified sensitive field. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ModelRouteRequest { authority: SensitiveDataAuthority, @@ -32,6 +32,7 @@ pub struct ModelRouteRequest { model_id: String, region_id: String, retention_policy_id: String, + training_policy_id: String, } impl ModelRouteRequest { @@ -43,6 +44,7 @@ impl ModelRouteRequest { model_id: &str, region_id: &str, retention_policy_id: &str, + training_policy_id: &str, ) -> Self { Self { authority, @@ -50,6 +52,7 @@ impl ModelRouteRequest { model_id: model_id.to_owned(), region_id: region_id.to_owned(), retention_policy_id: retention_policy_id.to_owned(), + training_policy_id: training_policy_id.to_owned(), } } @@ -58,6 +61,7 @@ impl ModelRouteRequest { && route_identifier_is_valid(&self.model_id) && route_identifier_is_valid(&self.region_id) && route_identifier_is_valid(&self.retention_policy_id) + && route_identifier_is_valid(&self.training_policy_id) } } @@ -69,6 +73,7 @@ pub struct ModelRouteScope { model_id: String, region_id: String, retention_policy_id: String, + training_policy_id: String, } impl ModelRouteScope { @@ -83,6 +88,7 @@ impl ModelRouteScope { model_id: &str, region_id: &str, retention_policy_id: &str, + training_policy_id: &str, ) -> Self { Self { authority, @@ -90,6 +96,7 @@ impl ModelRouteScope { model_id: model_id.to_owned(), region_id: region_id.to_owned(), retention_policy_id: retention_policy_id.to_owned(), + training_policy_id: training_policy_id.to_owned(), } } @@ -98,6 +105,7 @@ impl ModelRouteScope { && route_identifier_is_valid(&self.model_id) && route_identifier_is_valid(&self.region_id) && route_identifier_is_valid(&self.retention_policy_id) + && route_identifier_is_valid(&self.training_policy_id) } } @@ -114,9 +122,11 @@ fn route_identifier_is_valid(identifier: &str) -> bool { /// /// The existing sensitive-data disclosure comparator is reused only to validate that request and /// scope carry the same complete [`SensitiveDataAuthority`]. Route admission then independently -/// requires valid, exact provider, model, region, and retention-policy identifiers. All route -/// identifiers use bounded 1–128 byte ASCII policy tokens containing alphanumeric characters plus -/// `.`, `_`, `:`, and `-`. +/// requires valid, exact provider, model, region, retention-policy, and training-policy +/// identifiers. Keeping retention and training authority distinct prevents a permitted data +/// lifetime from silently granting provider training rights, or vice versa. All route identifiers +/// use bounded 1–128 byte ASCII policy tokens containing alphanumeric characters plus `.`, `_`, +/// `:`, and `-`. #[must_use] pub fn evaluate_model_route( request: &ModelRouteRequest, @@ -139,6 +149,7 @@ pub fn evaluate_model_route( || request.model_id != scope.model_id || request.region_id != scope.region_id || request.retention_policy_id != scope.retention_policy_id + || request.training_policy_id != scope.training_policy_id { ModelRouteDecision::RouteMismatch } else { From 83a3dba24116f4116a0a6a75e15cabfb2a4d7ac6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 16:12:46 +0900 Subject: [PATCH 10/18] docs(sensitive): record distinct training route policy --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24e598ae..8acc9c38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Added -- Exact sensitive-data model-route admission that binds the complete existing sensitive authority to bounded provider, model, region, and retention-policy identifiers while remaining explicitly separate from protected-value disclosure, provider authentication, runtime region attestation, model invocation, and fallback selection. +- Exact sensitive-data model-route admission that binds the complete existing sensitive authority to bounded provider, model, region, retention-policy, and training-policy identifiers while remaining explicitly separate from protected-value disclosure, provider authentication, runtime region attestation, model invocation, and fallback selection. - In-process authoritative sensitive-handle use reservation and first-revocation-wins lifecycle state that owns the bounded use count, records task-completion/policy-change/key-rotation/session-termination/suspicious-use revocation causes, blocks all future reservations after revocation, increments only after exact scope/classification/expiry/use-limit authorization, and leaves denied reservations unconsumed; this is a policy primitive only and does not claim durable broker storage, protected-value resolution, or cross-process transactionality. - Rust workspace for independently reusable core, policy, destination, network, TLS, resource, and evidence modules. - Canonical HTTPS and loopback-origin boundary with case-normalized schemes and hosts, default-port normalization, IPv4/IPv6 handling, browser-special numeric-host rejection, and explicit malformed-input errors. From 7a7f441f446f38328ededbe4e1cf33c5bb74971f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 16:16:16 +0900 Subject: [PATCH 11/18] test(sensitive): bind reviewed model subprocessors --- .../tests/sensitive_model_route.rs | 53 +++++++++++++++++-- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/crates/originweave-policy/tests/sensitive_model_route.rs b/crates/originweave-policy/tests/sensitive_model_route.rs index 28685a9b..0eaf64f7 100644 --- a/crates/originweave-policy/tests/sensitive_model_route.rs +++ b/crates/originweave-policy/tests/sensitive_model_route.rs @@ -3,9 +3,9 @@ //! Fail-closed policy contracts for model-route admission of sensitive data. //! //! Route admission is intentionally separate from disclosure authority: authorizing a -//! provider/model/region/retention/training tuple must never imply that raw protected values may -//! be disclosed. A later broker/orchestrator must independently authorize the value form and -//! derive the actual route identity from trusted runtime configuration. +//! provider/model/region/retention/training/subprocessor-policy tuple must never imply that raw +//! protected values may be disclosed. A later broker/orchestrator must independently authorize +//! the value form and derive the actual route identity from trusted runtime configuration. use originweave_core::Origin; use originweave_policy::{ @@ -32,6 +32,7 @@ fn scope() -> ModelRouteScope { "kr-central", "ephemeral-retention", "no-training", + "subprocessors-reviewed-v1", ) } @@ -43,6 +44,7 @@ fn request() -> ModelRouteRequest { "kr-central", "ephemeral-retention", "no-training", + "subprocessors-reviewed-v1", ) } @@ -63,6 +65,7 @@ fn sensitive_authority_mismatch_is_distinct_from_route_mismatch() { "kr-central", "ephemeral-retention", "no-training", + "subprocessors-reviewed-v1", ); assert_eq!( @@ -81,6 +84,7 @@ fn every_model_route_dimension_is_exact_and_non_transferable() { "kr-central", "ephemeral-retention", "no-training", + "subprocessors-reviewed-v1", ), ModelRouteRequest::new( authority("https://model-gateway.example"), @@ -89,6 +93,7 @@ fn every_model_route_dimension_is_exact_and_non_transferable() { "kr-central", "ephemeral-retention", "no-training", + "subprocessors-reviewed-v1", ), ModelRouteRequest::new( authority("https://model-gateway.example"), @@ -97,6 +102,7 @@ fn every_model_route_dimension_is_exact_and_non_transferable() { "us-east", "ephemeral-retention", "no-training", + "subprocessors-reviewed-v1", ), ModelRouteRequest::new( authority("https://model-gateway.example"), @@ -105,6 +111,7 @@ fn every_model_route_dimension_is_exact_and_non_transferable() { "kr-central", "provider-default-retention", "no-training", + "subprocessors-reviewed-v1", ), ModelRouteRequest::new( authority("https://model-gateway.example"), @@ -113,6 +120,16 @@ fn every_model_route_dimension_is_exact_and_non_transferable() { "kr-central", "ephemeral-retention", "training-allowed", + "subprocessors-reviewed-v1", + ), + ModelRouteRequest::new( + authority("https://model-gateway.example"), + "provider-private", + "model-reviewed-v1", + "kr-central", + "ephemeral-retention", + "no-training", + "subprocessors-other-v2", ), ]; @@ -137,6 +154,7 @@ fn malformed_request_route_identifiers_fail_closed() { "kr-central", "ephemeral-retention", "no-training", + "subprocessors-reviewed-v1", ), ModelRouteRequest::new( authority("https://model-gateway.example"), @@ -145,6 +163,7 @@ fn malformed_request_route_identifiers_fail_closed() { "kr-central", "ephemeral-retention", "no-training", + "subprocessors-reviewed-v1", ), ModelRouteRequest::new( authority("https://model-gateway.example"), @@ -153,6 +172,7 @@ fn malformed_request_route_identifiers_fail_closed() { malformed, "ephemeral-retention", "no-training", + "subprocessors-reviewed-v1", ), ModelRouteRequest::new( authority("https://model-gateway.example"), @@ -161,6 +181,7 @@ fn malformed_request_route_identifiers_fail_closed() { "kr-central", malformed, "no-training", + "subprocessors-reviewed-v1", ), ModelRouteRequest::new( authority("https://model-gateway.example"), @@ -169,6 +190,16 @@ fn malformed_request_route_identifiers_fail_closed() { "kr-central", "ephemeral-retention", malformed, + "subprocessors-reviewed-v1", + ), + ModelRouteRequest::new( + authority("https://model-gateway.example"), + "provider-private", + "model-reviewed-v1", + "kr-central", + "ephemeral-retention", + "no-training", + malformed, ), ]; for candidate in candidates { @@ -190,6 +221,7 @@ fn malformed_scope_route_identifiers_fail_closed_against_a_valid_request() { "kr-central", "ephemeral-retention", "no-training", + "subprocessors-reviewed-v1", ), ModelRouteScope::new( authority("https://model-gateway.example"), @@ -198,6 +230,7 @@ fn malformed_scope_route_identifiers_fail_closed_against_a_valid_request() { "kr-central", "ephemeral-retention", "no-training", + "subprocessors-reviewed-v1", ), ModelRouteScope::new( authority("https://model-gateway.example"), @@ -206,6 +239,7 @@ fn malformed_scope_route_identifiers_fail_closed_against_a_valid_request() { "invalid region", "ephemeral-retention", "no-training", + "subprocessors-reviewed-v1", ), ModelRouteScope::new( authority("https://model-gateway.example"), @@ -214,6 +248,7 @@ fn malformed_scope_route_identifiers_fail_closed_against_a_valid_request() { "kr-central", "invalid retention", "no-training", + "subprocessors-reviewed-v1", ), ModelRouteScope::new( authority("https://model-gateway.example"), @@ -222,6 +257,16 @@ fn malformed_scope_route_identifiers_fail_closed_against_a_valid_request() { "kr-central", "ephemeral-retention", "invalid training", + "subprocessors-reviewed-v1", + ), + ModelRouteScope::new( + authority("https://model-gateway.example"), + "provider-private", + "model-reviewed-v1", + "kr-central", + "ephemeral-retention", + "no-training", + "invalid subprocessors", ), ]; @@ -250,6 +295,7 @@ fn malformed_sensitive_authority_fails_closed_even_when_both_sides_match() { "kr-central", "ephemeral-retention", "no-training", + "subprocessors-reviewed-v1", ); let invalid_scope = ModelRouteScope::new( invalid_authority, @@ -258,6 +304,7 @@ fn malformed_sensitive_authority_fails_closed_even_when_both_sides_match() { "kr-central", "ephemeral-retention", "no-training", + "subprocessors-reviewed-v1", ); assert_eq!( From 0d6750173551ed39e884d5f47c7bcef56525fba4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 16:17:47 +0900 Subject: [PATCH 12/18] feat(sensitive): bind reviewed model subprocessors --- crates/originweave-policy/src/model_route.rs | 23 ++++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/crates/originweave-policy/src/model_route.rs b/crates/originweave-policy/src/model_route.rs index 5e2c6814..ac0f3d76 100644 --- a/crates/originweave-policy/src/model_route.rs +++ b/crates/originweave-policy/src/model_route.rs @@ -1,4 +1,4 @@ -//! Exact provider/model/region/retention/training route admission for sensitive-data workflows. +//! Exact provider/model/region/retention/training/subprocessor route admission for sensitive-data workflows. //! //! This module evaluates route metadata only. An [`ModelRouteDecision::Authorized`] result does //! not authorize disclosure of a protected value, authenticate a provider, prove the provider's @@ -20,7 +20,7 @@ pub enum ModelRouteDecision { Authorized, /// The sensitive-data authority is malformed or does not match the route scope. AuthorityMismatch, - /// Provider, model, region, retention, or training route metadata is malformed or mismatched. + /// Provider/model/region/retention/training/subprocessor metadata is malformed or mismatched. RouteMismatch, } @@ -33,6 +33,7 @@ pub struct ModelRouteRequest { region_id: String, retention_policy_id: String, training_policy_id: String, + subprocessor_policy_id: String, } impl ModelRouteRequest { @@ -45,6 +46,7 @@ impl ModelRouteRequest { region_id: &str, retention_policy_id: &str, training_policy_id: &str, + subprocessor_policy_id: &str, ) -> Self { Self { authority, @@ -53,6 +55,7 @@ impl ModelRouteRequest { region_id: region_id.to_owned(), retention_policy_id: retention_policy_id.to_owned(), training_policy_id: training_policy_id.to_owned(), + subprocessor_policy_id: subprocessor_policy_id.to_owned(), } } @@ -62,6 +65,7 @@ impl ModelRouteRequest { && route_identifier_is_valid(&self.region_id) && route_identifier_is_valid(&self.retention_policy_id) && route_identifier_is_valid(&self.training_policy_id) + && route_identifier_is_valid(&self.subprocessor_policy_id) } } @@ -74,6 +78,7 @@ pub struct ModelRouteScope { region_id: String, retention_policy_id: String, training_policy_id: String, + subprocessor_policy_id: String, } impl ModelRouteScope { @@ -89,6 +94,7 @@ impl ModelRouteScope { region_id: &str, retention_policy_id: &str, training_policy_id: &str, + subprocessor_policy_id: &str, ) -> Self { Self { authority, @@ -97,6 +103,7 @@ impl ModelRouteScope { region_id: region_id.to_owned(), retention_policy_id: retention_policy_id.to_owned(), training_policy_id: training_policy_id.to_owned(), + subprocessor_policy_id: subprocessor_policy_id.to_owned(), } } @@ -106,6 +113,7 @@ impl ModelRouteScope { && route_identifier_is_valid(&self.region_id) && route_identifier_is_valid(&self.retention_policy_id) && route_identifier_is_valid(&self.training_policy_id) + && route_identifier_is_valid(&self.subprocessor_policy_id) } } @@ -122,11 +130,11 @@ fn route_identifier_is_valid(identifier: &str) -> bool { /// /// The existing sensitive-data disclosure comparator is reused only to validate that request and /// scope carry the same complete [`SensitiveDataAuthority`]. Route admission then independently -/// requires valid, exact provider, model, region, retention-policy, and training-policy -/// identifiers. Keeping retention and training authority distinct prevents a permitted data -/// lifetime from silently granting provider training rights, or vice versa. All route identifiers -/// use bounded 1–128 byte ASCII policy tokens containing alphanumeric characters plus `.`, `_`, -/// `:`, and `-`. +/// requires valid, exact provider, model, region, retention-policy, training-policy, and reviewed +/// subprocessor-policy identifiers. Keeping retention, training, and subprocessor authority +/// distinct prevents one permitted provider contract dimension from silently authorizing another. +/// All route identifiers use bounded 1–128 byte ASCII policy tokens containing alphanumeric +/// characters plus `.`, `_`, `:`, and `-`. #[must_use] pub fn evaluate_model_route( request: &ModelRouteRequest, @@ -150,6 +158,7 @@ pub fn evaluate_model_route( || request.region_id != scope.region_id || request.retention_policy_id != scope.retention_policy_id || request.training_policy_id != scope.training_policy_id + || request.subprocessor_policy_id != scope.subprocessor_policy_id { ModelRouteDecision::RouteMismatch } else { From 2489a60f204172bf371b4b3fb037e9d7d1be2fe4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 16:18:43 +0900 Subject: [PATCH 13/18] docs(sensitive): record reviewed subprocessor route policy --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8acc9c38..e2cb36f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Added -- Exact sensitive-data model-route admission that binds the complete existing sensitive authority to bounded provider, model, region, retention-policy, and training-policy identifiers while remaining explicitly separate from protected-value disclosure, provider authentication, runtime region attestation, model invocation, and fallback selection. +- Exact sensitive-data model-route admission that binds the complete existing sensitive authority to bounded provider, model, region, retention-policy, training-policy, and reviewed subprocessor-policy identifiers while remaining explicitly separate from protected-value disclosure, provider authentication, runtime region attestation, model invocation, and fallback selection. - In-process authoritative sensitive-handle use reservation and first-revocation-wins lifecycle state that owns the bounded use count, records task-completion/policy-change/key-rotation/session-termination/suspicious-use revocation causes, blocks all future reservations after revocation, increments only after exact scope/classification/expiry/use-limit authorization, and leaves denied reservations unconsumed; this is a policy primitive only and does not claim durable broker storage, protected-value resolution, or cross-process transactionality. - Rust workspace for independently reusable core, policy, destination, network, TLS, resource, and evidence modules. - Canonical HTTPS and loopback-origin boundary with case-normalized schemes and hosts, default-port normalization, IPv4/IPv6 handling, browser-special numeric-host rejection, and explicit malformed-input errors. From 90bde2dba675be10abb34a5c2a8bf03bb34abcdf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 17:39:45 +0900 Subject: [PATCH 14/18] test(sensitive): require explicit model export policy --- .../tests/sensitive_model_export_route.rs | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 crates/originweave-policy/tests/sensitive_model_export_route.rs diff --git a/crates/originweave-policy/tests/sensitive_model_export_route.rs b/crates/originweave-policy/tests/sensitive_model_export_route.rs new file mode 100644 index 00000000..07c7e833 --- /dev/null +++ b/crates/originweave-policy/tests/sensitive_model_export_route.rs @@ -0,0 +1,78 @@ +#![allow(clippy::expect_used)] + +//! Export-policy binding regressions for sensitive model-route admission. +//! +//! Retention, training, reviewed subprocessors, and export are independent policy +//! dimensions. A route that is otherwise identical must not inherit export +//! authority merely because its provider/model/region tuple is approved. + +use originweave_core::Origin; +use originweave_policy::{ + DataClassification, ModelRouteDecision, ModelRouteRequest, ModelRouteScope, + SensitiveDataAuthority, evaluate_model_route, +}; + +fn authority() -> SensitiveDataAuthority { + SensitiveDataAuthority::new( + "tenant-alpha", + "task-42", + "customer-email", + "case-resolution", + Origin::parse("https://model-gateway.example").expect("valid destination origin"), + DataClassification::PersonalData, + ) +} + +fn request(export_policy_id: &str) -> ModelRouteRequest { + ModelRouteRequest::new_with_export_policy( + authority(), + "provider-private", + "model-reviewed-v1", + "kr-central", + "ephemeral-retention", + "no-training", + "subprocessors-reviewed-v1", + export_policy_id, + ) +} + +fn scope(export_policy_id: &str) -> ModelRouteScope { + ModelRouteScope::new_with_export_policy( + authority(), + "provider-private", + "model-reviewed-v1", + "kr-central", + "ephemeral-retention", + "no-training", + "subprocessors-reviewed-v1", + export_policy_id, + ) +} + +#[test] +fn exact_export_policy_is_part_of_model_route_authority() { + assert_eq!( + evaluate_model_route(&request("no-export"), &scope("no-export")), + ModelRouteDecision::Authorized + ); + assert_eq!( + evaluate_model_route(&request("approved-export"), &scope("no-export")), + ModelRouteDecision::RouteMismatch + ); +} + +#[test] +fn malformed_requested_export_policy_fails_closed() { + assert_eq!( + evaluate_model_route(&request("invalid export"), &scope("no-export")), + ModelRouteDecision::RouteMismatch + ); +} + +#[test] +fn malformed_scope_export_policy_fails_closed_against_valid_request() { + assert_eq!( + evaluate_model_route(&request("no-export"), &scope("invalid export")), + ModelRouteDecision::RouteMismatch + ); +} From 88b1fd46b6d2e83411682f97aa17de95aca90789 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 17:42:30 +0900 Subject: [PATCH 15/18] feat(sensitive): bind model route to export policy --- crates/originweave-policy/src/model_route.rs | 86 ++++++++++++++++++-- 1 file changed, 77 insertions(+), 9 deletions(-) diff --git a/crates/originweave-policy/src/model_route.rs b/crates/originweave-policy/src/model_route.rs index ac0f3d76..de9cab07 100644 --- a/crates/originweave-policy/src/model_route.rs +++ b/crates/originweave-policy/src/model_route.rs @@ -1,4 +1,4 @@ -//! Exact provider/model/region/retention/training/subprocessor route admission for sensitive-data workflows. +//! Exact provider/model/region/retention/training/subprocessor/export route admission for sensitive-data workflows. //! //! This module evaluates route metadata only. An [`ModelRouteDecision::Authorized`] result does //! not authorize disclosure of a protected value, authenticate a provider, prove the provider's @@ -12,6 +12,7 @@ use crate::sensitive_data::{ }; const MAX_ROUTE_IDENTIFIER_BYTES: usize = 128; +const DEFAULT_EXPORT_POLICY_ID: &str = "no-export"; /// Result of comparing one requested model route with its exact sensitive-data route authority. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -20,7 +21,7 @@ pub enum ModelRouteDecision { Authorized, /// The sensitive-data authority is malformed or does not match the route scope. AuthorityMismatch, - /// Provider/model/region/retention/training/subprocessor metadata is malformed or mismatched. + /// Provider/model/region/retention/training/subprocessor/export metadata is malformed or mismatched. RouteMismatch, } @@ -34,10 +35,14 @@ pub struct ModelRouteRequest { retention_policy_id: String, training_policy_id: String, subprocessor_policy_id: String, + export_policy_id: String, } impl ModelRouteRequest { - /// Build a requested model route without granting disclosure authority. + /// Build a requested model route that is explicitly non-exporting. + /// + /// Call [`Self::new_with_export_policy`] when a workflow requests a separately governed + /// export behavior. Neither constructor grants protected-value disclosure authority. #[must_use] pub fn new( authority: SensitiveDataAuthority, @@ -47,6 +52,34 @@ impl ModelRouteRequest { retention_policy_id: &str, training_policy_id: &str, subprocessor_policy_id: &str, + ) -> Self { + Self::new_with_export_policy( + authority, + provider_id, + model_id, + region_id, + retention_policy_id, + training_policy_id, + subprocessor_policy_id, + DEFAULT_EXPORT_POLICY_ID, + ) + } + + /// Build a requested model route with one explicit export-policy identifier. + /// + /// The identifier describes policy intent only. Matching an export-policy identifier does not + /// execute an export or grant access to protected bytes; the later broker/export boundary must + /// independently authorize and enforce the actual destination and value form. + #[must_use] + pub fn new_with_export_policy( + authority: SensitiveDataAuthority, + provider_id: &str, + model_id: &str, + region_id: &str, + retention_policy_id: &str, + training_policy_id: &str, + subprocessor_policy_id: &str, + export_policy_id: &str, ) -> Self { Self { authority, @@ -56,6 +89,7 @@ impl ModelRouteRequest { retention_policy_id: retention_policy_id.to_owned(), training_policy_id: training_policy_id.to_owned(), subprocessor_policy_id: subprocessor_policy_id.to_owned(), + export_policy_id: export_policy_id.to_owned(), } } @@ -66,6 +100,7 @@ impl ModelRouteRequest { && route_identifier_is_valid(&self.retention_policy_id) && route_identifier_is_valid(&self.training_policy_id) && route_identifier_is_valid(&self.subprocessor_policy_id) + && route_identifier_is_valid(&self.export_policy_id) } } @@ -79,13 +114,15 @@ pub struct ModelRouteScope { retention_policy_id: String, training_policy_id: String, subprocessor_policy_id: String, + export_policy_id: String, } impl ModelRouteScope { - /// Build a route scope from trusted policy metadata. + /// Build a non-exporting route scope from trusted policy metadata. /// /// Route identifiers are validated when the scope is evaluated so malformed policy state /// remains fail-closed rather than becoming authority merely because request and scope match. + /// Call [`Self::new_with_export_policy`] when policy explicitly governs another export mode. #[must_use] pub fn new( authority: SensitiveDataAuthority, @@ -95,6 +132,33 @@ impl ModelRouteScope { retention_policy_id: &str, training_policy_id: &str, subprocessor_policy_id: &str, + ) -> Self { + Self::new_with_export_policy( + authority, + provider_id, + model_id, + region_id, + retention_policy_id, + training_policy_id, + subprocessor_policy_id, + DEFAULT_EXPORT_POLICY_ID, + ) + } + + /// Build a route scope with one explicit export-policy identifier. + /// + /// This is route metadata only. The eventual export path must separately verify protected-value + /// disclosure, destination authority, retention, and any required human or dual-control approval. + #[must_use] + pub fn new_with_export_policy( + authority: SensitiveDataAuthority, + provider_id: &str, + model_id: &str, + region_id: &str, + retention_policy_id: &str, + training_policy_id: &str, + subprocessor_policy_id: &str, + export_policy_id: &str, ) -> Self { Self { authority, @@ -104,6 +168,7 @@ impl ModelRouteScope { retention_policy_id: retention_policy_id.to_owned(), training_policy_id: training_policy_id.to_owned(), subprocessor_policy_id: subprocessor_policy_id.to_owned(), + export_policy_id: export_policy_id.to_owned(), } } @@ -114,6 +179,7 @@ impl ModelRouteScope { && route_identifier_is_valid(&self.retention_policy_id) && route_identifier_is_valid(&self.training_policy_id) && route_identifier_is_valid(&self.subprocessor_policy_id) + && route_identifier_is_valid(&self.export_policy_id) } } @@ -130,11 +196,12 @@ fn route_identifier_is_valid(identifier: &str) -> bool { /// /// The existing sensitive-data disclosure comparator is reused only to validate that request and /// scope carry the same complete [`SensitiveDataAuthority`]. Route admission then independently -/// requires valid, exact provider, model, region, retention-policy, training-policy, and reviewed -/// subprocessor-policy identifiers. Keeping retention, training, and subprocessor authority -/// distinct prevents one permitted provider contract dimension from silently authorizing another. -/// All route identifiers use bounded 1–128 byte ASCII policy tokens containing alphanumeric -/// characters plus `.`, `_`, `:`, and `-`. +/// requires valid, exact provider, model, region, retention-policy, training-policy, reviewed +/// subprocessor-policy, and export-policy identifiers. Keeping retention, training, subprocessor, +/// and export authority distinct prevents one permitted provider contract dimension from silently +/// authorizing another. The compatibility constructor defaults export policy to `no-export`, so an +/// omitted export choice never widens route authority. All route identifiers use bounded 1–128 byte +/// ASCII policy tokens containing alphanumeric characters plus `.`, `_`, `:`, and `-`. #[must_use] pub fn evaluate_model_route( request: &ModelRouteRequest, @@ -159,6 +226,7 @@ pub fn evaluate_model_route( || request.retention_policy_id != scope.retention_policy_id || request.training_policy_id != scope.training_policy_id || request.subprocessor_policy_id != scope.subprocessor_policy_id + || request.export_policy_id != scope.export_policy_id { ModelRouteDecision::RouteMismatch } else { From e0527ffe5e461db778f0eb7ac37d7e3b81e1a7c5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 17:46:48 +0900 Subject: [PATCH 16/18] fix(sensitive): keep export routing within clippy API bounds --- crates/originweave-policy/src/model_route.rs | 86 ++++++-------------- 1 file changed, 26 insertions(+), 60 deletions(-) diff --git a/crates/originweave-policy/src/model_route.rs b/crates/originweave-policy/src/model_route.rs index de9cab07..bb02847f 100644 --- a/crates/originweave-policy/src/model_route.rs +++ b/crates/originweave-policy/src/model_route.rs @@ -41,8 +41,8 @@ pub struct ModelRouteRequest { impl ModelRouteRequest { /// Build a requested model route that is explicitly non-exporting. /// - /// Call [`Self::new_with_export_policy`] when a workflow requests a separately governed - /// export behavior. Neither constructor grants protected-value disclosure authority. + /// Call [`Self::with_export_policy`] when a workflow requests a separately governed export + /// behavior. Neither constructor nor export-policy selection grants disclosure authority. #[must_use] pub fn new( authority: SensitiveDataAuthority, @@ -52,34 +52,6 @@ impl ModelRouteRequest { retention_policy_id: &str, training_policy_id: &str, subprocessor_policy_id: &str, - ) -> Self { - Self::new_with_export_policy( - authority, - provider_id, - model_id, - region_id, - retention_policy_id, - training_policy_id, - subprocessor_policy_id, - DEFAULT_EXPORT_POLICY_ID, - ) - } - - /// Build a requested model route with one explicit export-policy identifier. - /// - /// The identifier describes policy intent only. Matching an export-policy identifier does not - /// execute an export or grant access to protected bytes; the later broker/export boundary must - /// independently authorize and enforce the actual destination and value form. - #[must_use] - pub fn new_with_export_policy( - authority: SensitiveDataAuthority, - provider_id: &str, - model_id: &str, - region_id: &str, - retention_policy_id: &str, - training_policy_id: &str, - subprocessor_policy_id: &str, - export_policy_id: &str, ) -> Self { Self { authority, @@ -89,10 +61,21 @@ impl ModelRouteRequest { retention_policy_id: retention_policy_id.to_owned(), training_policy_id: training_policy_id.to_owned(), subprocessor_policy_id: subprocessor_policy_id.to_owned(), - export_policy_id: export_policy_id.to_owned(), + export_policy_id: DEFAULT_EXPORT_POLICY_ID.to_owned(), } } + /// Select one explicit export-policy identifier for this requested route. + /// + /// The identifier describes policy intent only. Matching an export-policy identifier does not + /// execute an export or grant access to protected bytes; the later broker/export boundary must + /// independently authorize and enforce the actual destination and value form. + #[must_use] + pub fn with_export_policy(mut self, export_policy_id: &str) -> Self { + self.export_policy_id = export_policy_id.to_owned(); + self + } + fn route_identifiers_are_valid(&self) -> bool { route_identifier_is_valid(&self.provider_id) && route_identifier_is_valid(&self.model_id) @@ -122,7 +105,7 @@ impl ModelRouteScope { /// /// Route identifiers are validated when the scope is evaluated so malformed policy state /// remains fail-closed rather than becoming authority merely because request and scope match. - /// Call [`Self::new_with_export_policy`] when policy explicitly governs another export mode. + /// Call [`Self::with_export_policy`] when policy explicitly governs another export mode. #[must_use] pub fn new( authority: SensitiveDataAuthority, @@ -132,33 +115,6 @@ impl ModelRouteScope { retention_policy_id: &str, training_policy_id: &str, subprocessor_policy_id: &str, - ) -> Self { - Self::new_with_export_policy( - authority, - provider_id, - model_id, - region_id, - retention_policy_id, - training_policy_id, - subprocessor_policy_id, - DEFAULT_EXPORT_POLICY_ID, - ) - } - - /// Build a route scope with one explicit export-policy identifier. - /// - /// This is route metadata only. The eventual export path must separately verify protected-value - /// disclosure, destination authority, retention, and any required human or dual-control approval. - #[must_use] - pub fn new_with_export_policy( - authority: SensitiveDataAuthority, - provider_id: &str, - model_id: &str, - region_id: &str, - retention_policy_id: &str, - training_policy_id: &str, - subprocessor_policy_id: &str, - export_policy_id: &str, ) -> Self { Self { authority, @@ -168,10 +124,20 @@ impl ModelRouteScope { retention_policy_id: retention_policy_id.to_owned(), training_policy_id: training_policy_id.to_owned(), subprocessor_policy_id: subprocessor_policy_id.to_owned(), - export_policy_id: export_policy_id.to_owned(), + export_policy_id: DEFAULT_EXPORT_POLICY_ID.to_owned(), } } + /// Select one explicit export-policy identifier for this trusted route scope. + /// + /// This is route metadata only. The eventual export path must separately verify protected-value + /// disclosure, destination authority, retention, and any required human or dual-control approval. + #[must_use] + pub fn with_export_policy(mut self, export_policy_id: &str) -> Self { + self.export_policy_id = export_policy_id.to_owned(); + self + } + fn route_identifiers_are_valid(&self) -> bool { route_identifier_is_valid(&self.provider_id) && route_identifier_is_valid(&self.model_id) From a07e3eb33489e0ed26fdc2c381ae587edc41c503 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 17:47:38 +0900 Subject: [PATCH 17/18] test(sensitive): exercise bounded export-policy builder --- .../tests/sensitive_model_export_route.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/originweave-policy/tests/sensitive_model_export_route.rs b/crates/originweave-policy/tests/sensitive_model_export_route.rs index 07c7e833..0fd0a884 100644 --- a/crates/originweave-policy/tests/sensitive_model_export_route.rs +++ b/crates/originweave-policy/tests/sensitive_model_export_route.rs @@ -24,7 +24,7 @@ fn authority() -> SensitiveDataAuthority { } fn request(export_policy_id: &str) -> ModelRouteRequest { - ModelRouteRequest::new_with_export_policy( + ModelRouteRequest::new( authority(), "provider-private", "model-reviewed-v1", @@ -32,12 +32,12 @@ fn request(export_policy_id: &str) -> ModelRouteRequest { "ephemeral-retention", "no-training", "subprocessors-reviewed-v1", - export_policy_id, ) + .with_export_policy(export_policy_id) } fn scope(export_policy_id: &str) -> ModelRouteScope { - ModelRouteScope::new_with_export_policy( + ModelRouteScope::new( authority(), "provider-private", "model-reviewed-v1", @@ -45,8 +45,8 @@ fn scope(export_policy_id: &str) -> ModelRouteScope { "ephemeral-retention", "no-training", "subprocessors-reviewed-v1", - export_policy_id, ) + .with_export_policy(export_policy_id) } #[test] From 286f92aae9e298ab7dff1fd81c7850aabd5692ce Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 17:49:12 +0900 Subject: [PATCH 18/18] docs(sensitive): record export-bound model route --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2cb36f0..8b05db16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Added -- Exact sensitive-data model-route admission that binds the complete existing sensitive authority to bounded provider, model, region, retention-policy, training-policy, and reviewed subprocessor-policy identifiers while remaining explicitly separate from protected-value disclosure, provider authentication, runtime region attestation, model invocation, and fallback selection. +- Exact sensitive-data model-route admission that binds the complete existing sensitive authority to bounded provider, model, region, retention-policy, training-policy, reviewed subprocessor-policy, and export-policy identifiers; the compatibility constructor defaults export to `no-export`, and route admission remains explicitly separate from protected-value disclosure, export execution, provider authentication, runtime region attestation, model invocation, and fallback selection. - In-process authoritative sensitive-handle use reservation and first-revocation-wins lifecycle state that owns the bounded use count, records task-completion/policy-change/key-rotation/session-termination/suspicious-use revocation causes, blocks all future reservations after revocation, increments only after exact scope/classification/expiry/use-limit authorization, and leaves denied reservations unconsumed; this is a policy primitive only and does not claim durable broker storage, protected-value resolution, or cross-process transactionality. - Rust workspace for independently reusable core, policy, destination, network, TLS, resource, and evidence modules. - Canonical HTTPS and loopback-origin boundary with case-normalized schemes and hosts, default-port normalization, IPv4/IPv6 handling, browser-special numeric-host rejection, and explicit malformed-input errors.