From eb7daf465f35b314b175a592cdb9d74962b64800 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 19:03:39 +0900 Subject: [PATCH 1/3] test(sensitive): require model invocation expiry authority --- .../tests/sensitive_model_invocation.rs | 70 +++++++++++++++---- 1 file changed, 56 insertions(+), 14 deletions(-) diff --git a/crates/originweave-policy/tests/sensitive_model_invocation.rs b/crates/originweave-policy/tests/sensitive_model_invocation.rs index 27b45a29..ecfffc35 100644 --- a/crates/originweave-policy/tests/sensitive_model_invocation.rs +++ b/crates/originweave-policy/tests/sensitive_model_invocation.rs @@ -1,10 +1,11 @@ #![allow(clippy::expect_used)] -//! Fail-closed prompt, output-schema, and token-budget contracts for sensitive model use. +//! Fail-closed prompt, output-schema, token-budget, and expiry contracts for sensitive model use. //! //! Route admission is a prerequisite, not disclosure authority. This contract additionally binds -//! one reviewed prompt contract, one reviewed output schema, and finite input/output token budgets -//! before a trusted broker/orchestrator may consider a model invocation. +//! one reviewed prompt contract, one reviewed output schema, finite input/output token budgets, and +//! one exclusive invocation-policy expiry before a trusted broker/orchestrator may consider a model +//! invocation. use originweave_core::Origin; use originweave_policy::{ @@ -58,26 +59,31 @@ fn request(input_tokens: u32, output_tokens: u32) -> ModelInvocationRequest { ) } -fn scope(maximum_input_tokens: u32, maximum_output_tokens: u32) -> ModelInvocationScope { +fn scope( + maximum_input_tokens: u32, + maximum_output_tokens: u32, + valid_until: u64, +) -> ModelInvocationScope { ModelInvocationScope::new( route_scope(), "case-resolution-prompt-v1", "customer-email-summary-v1", maximum_input_tokens, maximum_output_tokens, + valid_until, ) } #[test] -fn exact_route_prompt_schema_and_bounded_tokens_are_authorized() { +fn exact_route_prompt_schema_bounded_tokens_and_fresh_policy_are_authorized() { assert_eq!( - evaluate_model_invocation(&request(4_096, 1_024), &scope(8_192, 2_048)), + evaluate_model_invocation(&request(4_096, 1_024), &scope(8_192, 2_048, 1_000), 999), ModelInvocationDecision::Authorized ); } #[test] -fn route_denial_remains_distinct_from_invocation_policy_mismatch() { +fn route_denial_remains_distinct_from_invocation_policy_mismatch_or_expiry() { let request = ModelInvocationRequest::new( route_request("provider-other"), "case-resolution-prompt-v1", @@ -87,7 +93,7 @@ fn route_denial_remains_distinct_from_invocation_policy_mismatch() { ); assert_eq!( - evaluate_model_invocation(&request, &scope(8_192, 2_048)), + evaluate_model_invocation(&request, &scope(8_192, 2_048, 1), 1_000), ModelInvocationDecision::RouteDenied(ModelRouteDecision::RouteMismatch) ); } @@ -111,7 +117,7 @@ fn prompt_and_output_schema_contracts_are_exact() { for candidate in [wrong_prompt, wrong_schema] { assert_eq!( - evaluate_model_invocation(&candidate, &scope(8_192, 2_048)), + evaluate_model_invocation(&candidate, &scope(8_192, 2_048, 1_000), 999), ModelInvocationDecision::InvocationPolicyMismatch ); } @@ -126,14 +132,14 @@ fn token_budgets_must_be_nonzero_and_within_reviewed_maxima() { request(4_096, 2_049), ] { assert_eq!( - evaluate_model_invocation(&candidate, &scope(8_192, 2_048)), + evaluate_model_invocation(&candidate, &scope(8_192, 2_048, 1_000), 999), ModelInvocationDecision::InvocationPolicyMismatch ); } - for malformed_scope in [scope(0, 2_048), scope(8_192, 0)] { + for malformed_scope in [scope(0, 2_048, 1_000), scope(8_192, 0, 1_000)] { assert_eq!( - evaluate_model_invocation(&request(4_096, 1_024), &malformed_scope), + evaluate_model_invocation(&request(4_096, 1_024), &malformed_scope, 999), ModelInvocationDecision::InvocationPolicyMismatch ); } @@ -162,7 +168,7 @@ fn malformed_prompt_or_schema_policy_identifiers_fail_closed() { ]; for candidate in candidates { assert_eq!( - evaluate_model_invocation(&candidate, &scope(8_192, 2_048)), + evaluate_model_invocation(&candidate, &scope(8_192, 2_048, 1_000), 999), ModelInvocationDecision::InvocationPolicyMismatch ); } @@ -174,6 +180,7 @@ fn malformed_prompt_or_schema_policy_identifiers_fail_closed() { "customer-email-summary-v1", 8_192, 2_048, + 1_000, ), ModelInvocationScope::new( route_scope(), @@ -181,13 +188,48 @@ fn malformed_prompt_or_schema_policy_identifiers_fail_closed() { malformed, 8_192, 2_048, + 1_000, ), ]; for malformed_scope in scopes { assert_eq!( - evaluate_model_invocation(&request(4_096, 1_024), &malformed_scope), + evaluate_model_invocation(&request(4_096, 1_024), &malformed_scope, 999), ModelInvocationDecision::InvocationPolicyMismatch ); } } } + +#[test] +fn invocation_policy_expiry_is_exclusive_and_fail_closed() { + let policy = scope(8_192, 2_048, 1_000); + + assert_eq!( + evaluate_model_invocation(&request(4_096, 1_024), &policy, 999), + ModelInvocationDecision::Authorized + ); + assert_eq!( + evaluate_model_invocation(&request(4_096, 1_024), &policy, 1_000), + ModelInvocationDecision::InvocationExpired + ); + assert_eq!( + evaluate_model_invocation(&request(4_096, 1_024), &policy, u64::MAX), + ModelInvocationDecision::InvocationExpired + ); +} + +#[test] +fn zero_expiry_is_invalid_but_maximum_epoch_remains_representable() { + assert_eq!( + evaluate_model_invocation(&request(4_096, 1_024), &scope(8_192, 2_048, 0), 0), + ModelInvocationDecision::InvocationPolicyMismatch + ); + assert_eq!( + evaluate_model_invocation( + &request(4_096, 1_024), + &scope(8_192, 2_048, u64::MAX), + u64::MAX - 1, + ), + ModelInvocationDecision::Authorized + ); +} From 39ec1659541b10785fcd85a9531bdd8d823578e3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 19:08:21 +0900 Subject: [PATCH 2/3] feat(sensitive): expire model invocation authority --- crates/originweave-policy/src/model_route.rs | 40 ++++++++++++++------ 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/crates/originweave-policy/src/model_route.rs b/crates/originweave-policy/src/model_route.rs index 20423a04..e2a89cb3 100644 --- a/crates/originweave-policy/src/model_route.rs +++ b/crates/originweave-policy/src/model_route.rs @@ -4,7 +4,8 @@ //! or [`ModelInvocationDecision::Authorized`] result does not authorize disclosure of a protected //! value, authenticate a provider, prove the provider's physical region, invoke a model, validate //! model output, or choose a fallback. A trusted broker/orchestrator must independently authorize -//! the permitted value form and derive actual runtime identities from trusted configuration. +//! the permitted value form, derive actual runtime identities from trusted configuration, and +//! supply invocation time from the same authoritative time domain used to issue policy expiry. use crate::sensitive_data::{ DisclosureDecision, DisclosureScope, SensitiveDataAuthority, SensitiveDataRequest, @@ -152,12 +153,14 @@ impl ModelRouteScope { /// Result of composing exact route admission with one reviewed model-invocation policy. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ModelInvocationDecision { - /// Exact route, prompt/schema contracts, and token budgets are authorized. + /// Exact route, prompt/schema contracts, token budgets, and policy lifetime are authorized. Authorized, /// Route admission failed before invocation-specific policy could authorize the request. RouteDenied(ModelRouteDecision), - /// Prompt/schema metadata or token budgets are malformed or outside the reviewed scope. + /// Prompt/schema metadata, token budgets, or the reviewed expiry are malformed or out of scope. InvocationPolicyMismatch, + /// The otherwise valid invocation policy is no longer fresh at the caller-supplied trusted time. + InvocationExpired, } /// One proposed model invocation after a route has been selected. @@ -198,13 +201,16 @@ pub struct ModelInvocationScope { output_schema_id: String, maximum_input_tokens: u32, maximum_output_tokens: u32, + valid_until: u64, } impl ModelInvocationScope { - /// Build trusted invocation policy for one prompt/schema pair and finite token maxima. + /// Build trusted invocation policy for one prompt/schema pair, token maxima, and expiry. /// - /// Identifiers and token maxima are validated during evaluation so malformed trusted policy - /// state remains fail-closed instead of becoming authority because request and scope match. + /// Identifiers, token maxima, and the exclusive `valid_until` value are validated during + /// evaluation so malformed trusted policy remains fail-closed instead of becoming authority + /// because request and scope happen to match. The expiry is only meaningful when compared with + /// a trusted time from the same caller-owned authoritative time domain. #[must_use] pub fn new( route: ModelRouteScope, @@ -212,6 +218,7 @@ impl ModelInvocationScope { output_schema_id: &str, maximum_input_tokens: u32, maximum_output_tokens: u32, + valid_until: u64, ) -> Self { Self { route, @@ -219,6 +226,7 @@ impl ModelInvocationScope { output_schema_id: output_schema_id.to_owned(), maximum_input_tokens, maximum_output_tokens, + valid_until, } } } @@ -274,18 +282,23 @@ pub fn evaluate_model_route( } } -/// Evaluate reviewed prompt/schema and token limits after exact route admission. +/// Evaluate reviewed prompt/schema, token limits, and lifetime after exact route admission. /// /// Route admission remains a separate prerequisite and its failure is preserved in /// [`ModelInvocationDecision::RouteDenied`]. Invocation policy then requires bounded 1–128 byte /// ASCII prompt/schema identifiers, exact identifier matches, nonzero requested and trusted token -/// budgets, and request budgets no larger than the reviewed maxima. Authorization from this -/// function is metadata-only: it does not disclose a protected value, invoke a provider, validate -/// output, retain/export data, or select a fallback route. +/// budgets, request budgets no larger than the reviewed maxima, and a nonzero exclusive expiry. +/// After those static checks pass, `trusted_time >= valid_until` returns +/// [`ModelInvocationDecision::InvocationExpired`]. The caller must source `trusted_time` from the +/// same authoritative time domain used to issue `valid_until`; this pure policy function neither +/// reads a clock nor attests clock provenance. Authorization remains metadata-only: it does not +/// disclose a protected value, invoke a provider, validate output, retain/export data, or select a +/// fallback route. #[must_use] pub fn evaluate_model_invocation( request: &ModelInvocationRequest, scope: &ModelInvocationScope, + trusted_time: u64, ) -> ModelInvocationDecision { let route_decision = evaluate_model_route(&request.route, &scope.route); if route_decision != ModelRouteDecision::Authorized { @@ -302,10 +315,15 @@ pub fn evaluate_model_invocation( || request.output_tokens == 0 || scope.maximum_input_tokens == 0 || scope.maximum_output_tokens == 0 + || scope.valid_until == 0 || request.input_tokens > scope.maximum_input_tokens || request.output_tokens > scope.maximum_output_tokens { - ModelInvocationDecision::InvocationPolicyMismatch + return ModelInvocationDecision::InvocationPolicyMismatch; + } + + if trusted_time >= scope.valid_until { + ModelInvocationDecision::InvocationExpired } else { ModelInvocationDecision::Authorized } From 2ad7a2162b4842fe57f74f69f08b258f4f6a9c07 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 19:11:44 +0900 Subject: [PATCH 3/3] docs(changelog): record model invocation expiry policy --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b05db16..682551ad 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 +- Reviewed sensitive-model invocation authority that composes exact route admission with bounded prompt-contract and output-schema identifiers, nonzero requested and reviewed token budgets, and an exclusive caller-supplied trusted-time expiry; malformed policy fails closed as `InvocationPolicyMismatch`, an otherwise valid policy at or after `valid_until` returns `InvocationExpired`, and this metadata-only boundary does not disclose protected values, invoke a provider, or attest clock provenance. - 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.