From 34d49aea1c77491c63a22f2670d10f39a162d24d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 22:27:07 +0900 Subject: [PATCH 1/6] test(sensitive): require fresh fallback availability evidence --- .../tests/sensitive_model_fallback.rs | 114 +++++++++++++----- 1 file changed, 84 insertions(+), 30 deletions(-) diff --git a/crates/originweave-policy/tests/sensitive_model_fallback.rs b/crates/originweave-policy/tests/sensitive_model_fallback.rs index 5afada7..31d9100 100644 --- a/crates/originweave-policy/tests/sensitive_model_fallback.rs +++ b/crates/originweave-policy/tests/sensitive_model_fallback.rs @@ -3,8 +3,8 @@ use originweave_core::Origin; use originweave_policy::{ DataClassification, ModelFallbackDecision, ModelFallbackRequest, ModelFallbackScope, - ModelRouteAvailability, ModelRouteDecision, ModelRouteRequest, ModelRouteScope, - SensitiveDataAuthority, evaluate_model_fallback, + ModelRouteAvailability, ModelRouteAvailabilityEvidence, ModelRouteDecision, ModelRouteRequest, + ModelRouteScope, SensitiveDataAuthority, evaluate_model_fallback, }; fn authority() -> SensitiveDataAuthority { @@ -58,99 +58,153 @@ fn fallback_scope() -> ModelRouteScope { route_scope("provider-fallback", "model-fallback-v1", "kr-central") } +fn availability( + state: ModelRouteAvailability, + valid_until: u64, +) -> ModelRouteAvailabilityEvidence { + ModelRouteAvailabilityEvidence::new(state, valid_until) +} + #[test] -fn available_exact_primary_route_is_used_without_fallback() { - let request = ModelFallbackRequest::new(primary_request(), ModelRouteAvailability::Available); +fn available_exact_primary_route_is_used_with_fresh_evidence() { + let request = ModelFallbackRequest::new( + primary_request(), + availability(ModelRouteAvailability::Available, 101), + ); let scope = ModelFallbackScope::new(primary_scope()); assert_eq!( - evaluate_model_fallback(&request, &scope), + evaluate_model_fallback(&request, &scope, 100), ModelFallbackDecision::PrimaryAuthorized ); } #[test] -fn primary_policy_mismatch_never_falls_back() { +fn primary_policy_mismatch_precedes_availability_freshness() { let request = ModelFallbackRequest::new( route_request("provider-unreviewed", "model-primary-v1", "kr-central"), - ModelRouteAvailability::Unavailable, + availability(ModelRouteAvailability::Unavailable, 100), ) .with_fallback(fallback_request()); let scope = ModelFallbackScope::new(primary_scope()).with_fallback(fallback_scope()); assert_eq!( - evaluate_model_fallback(&request, &scope), + evaluate_model_fallback(&request, &scope, 100), ModelFallbackDecision::PrimaryRouteDenied(ModelRouteDecision::RouteMismatch) ); } +#[test] +fn malformed_availability_lifetime_fails_closed() { + let request = ModelFallbackRequest::new( + primary_request(), + availability(ModelRouteAvailability::Available, 0), + ); + let scope = ModelFallbackScope::new(primary_scope()); + + assert_eq!( + evaluate_model_fallback(&request, &scope, 0), + ModelFallbackDecision::PrimaryAvailabilityInvalid + ); +} + +#[test] +fn expired_primary_availability_fails_closed_at_exclusive_boundary() { + let request = ModelFallbackRequest::new( + primary_request(), + availability(ModelRouteAvailability::Available, 100), + ); + let scope = ModelFallbackScope::new(primary_scope()); + + assert_eq!( + evaluate_model_fallback(&request, &scope, 100), + ModelFallbackDecision::PrimaryAvailabilityExpired + ); +} + #[test] fn unknown_primary_availability_fails_closed() { - let request = ModelFallbackRequest::new(primary_request(), ModelRouteAvailability::Unknown) - .with_fallback(fallback_request()); + let request = ModelFallbackRequest::new( + primary_request(), + availability(ModelRouteAvailability::Unknown, 101), + ) + .with_fallback(fallback_request()); let scope = ModelFallbackScope::new(primary_scope()).with_fallback(fallback_scope()); assert_eq!( - evaluate_model_fallback(&request, &scope), + evaluate_model_fallback(&request, &scope, 100), ModelFallbackDecision::PrimaryAvailabilityUnknown ); } #[test] fn unavailable_primary_without_reviewed_fallback_fails_closed() { - let request = ModelFallbackRequest::new(primary_request(), ModelRouteAvailability::Unavailable); + let request = ModelFallbackRequest::new( + primary_request(), + availability(ModelRouteAvailability::Unavailable, 101), + ); let scope = ModelFallbackScope::new(primary_scope()); assert_eq!( - evaluate_model_fallback(&request, &scope), + evaluate_model_fallback(&request, &scope, 100), ModelFallbackDecision::PrimaryUnavailableNoReviewedFallback ); } #[test] fn fallback_must_exist_on_both_request_and_trusted_scope() { - let request_only = - ModelFallbackRequest::new(primary_request(), ModelRouteAvailability::Unavailable) - .with_fallback(fallback_request()); + let request_only = ModelFallbackRequest::new( + primary_request(), + availability(ModelRouteAvailability::Unavailable, 101), + ) + .with_fallback(fallback_request()); let no_fallback_scope = ModelFallbackScope::new(primary_scope()); assert_eq!( - evaluate_model_fallback(&request_only, &no_fallback_scope), + evaluate_model_fallback(&request_only, &no_fallback_scope, 100), ModelFallbackDecision::FallbackPolicyMismatch ); - let no_fallback_request = - ModelFallbackRequest::new(primary_request(), ModelRouteAvailability::Unavailable); + let no_fallback_request = ModelFallbackRequest::new( + primary_request(), + availability(ModelRouteAvailability::Unavailable, 101), + ); let scope_only = ModelFallbackScope::new(primary_scope()).with_fallback(fallback_scope()); assert_eq!( - evaluate_model_fallback(&no_fallback_request, &scope_only), + evaluate_model_fallback(&no_fallback_request, &scope_only, 100), ModelFallbackDecision::FallbackPolicyMismatch ); } #[test] fn unavailable_primary_can_use_only_an_exact_reviewed_fallback() { - let request = ModelFallbackRequest::new(primary_request(), ModelRouteAvailability::Unavailable) - .with_fallback(fallback_request()); + let request = ModelFallbackRequest::new( + primary_request(), + availability(ModelRouteAvailability::Unavailable, 101), + ) + .with_fallback(fallback_request()); let scope = ModelFallbackScope::new(primary_scope()).with_fallback(fallback_scope()); assert_eq!( - evaluate_model_fallback(&request, &scope), + evaluate_model_fallback(&request, &scope, 100), ModelFallbackDecision::ReviewedFallbackAuthorized ); } #[test] fn mismatched_reviewed_fallback_is_denied() { - let request = ModelFallbackRequest::new(primary_request(), ModelRouteAvailability::Unavailable) - .with_fallback(route_request( - "provider-fallback", - "model-unreviewed-v2", - "kr-central", - )); + let request = ModelFallbackRequest::new( + primary_request(), + availability(ModelRouteAvailability::Unavailable, 101), + ) + .with_fallback(route_request( + "provider-fallback", + "model-unreviewed-v2", + "kr-central", + )); let scope = ModelFallbackScope::new(primary_scope()).with_fallback(fallback_scope()); assert_eq!( - evaluate_model_fallback(&request, &scope), + evaluate_model_fallback(&request, &scope, 100), ModelFallbackDecision::FallbackRouteDenied(ModelRouteDecision::RouteMismatch) ); } From 4ac8a00a2674e6b66fcea9a794c3475d874dec41 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 23:08:13 +0900 Subject: [PATCH 2/6] test(sensitive): format fallback freshness RED contract --- crates/originweave-policy/tests/sensitive_model_fallback.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/originweave-policy/tests/sensitive_model_fallback.rs b/crates/originweave-policy/tests/sensitive_model_fallback.rs index 31d9100..4716a58 100644 --- a/crates/originweave-policy/tests/sensitive_model_fallback.rs +++ b/crates/originweave-policy/tests/sensitive_model_fallback.rs @@ -58,10 +58,7 @@ fn fallback_scope() -> ModelRouteScope { route_scope("provider-fallback", "model-fallback-v1", "kr-central") } -fn availability( - state: ModelRouteAvailability, - valid_until: u64, -) -> ModelRouteAvailabilityEvidence { +fn availability(state: ModelRouteAvailability, valid_until: u64) -> ModelRouteAvailabilityEvidence { ModelRouteAvailabilityEvidence::new(state, valid_until) } From f25de0ed1d44d25319265129c7f8353791936c42 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 23:11:54 +0900 Subject: [PATCH 3/6] feat(sensitive): bound fallback availability lifetime --- .../originweave-policy/src/model_fallback.rs | 63 +++++++++++++++---- 1 file changed, 50 insertions(+), 13 deletions(-) diff --git a/crates/originweave-policy/src/model_fallback.rs b/crates/originweave-policy/src/model_fallback.rs index 975830e..af4c5dd 100644 --- a/crates/originweave-policy/src/model_fallback.rs +++ b/crates/originweave-policy/src/model_fallback.rs @@ -1,10 +1,12 @@ //! Fail-closed fallback selection layered on exact sensitive-model route authority. //! //! This module consumes caller-supplied provider availability evidence only after the primary route -//! itself passes [`crate::evaluate_model_route`]. It performs no provider health check, retry, -//! network I/O, protected-value disclosure, model invocation, or execution of the selected route. -//! A trusted broker/orchestrator must derive availability from an authoritative runtime boundary and -//! may execute only the exact route authorized by this deterministic policy. +//! itself passes [`crate::evaluate_model_route`]. Availability evidence carries an exclusive validity +//! horizon and is evaluated against trusted time supplied by the broker/orchestrator. This module +//! performs no provider health check, clock attestation, retry, network I/O, protected-value +//! disclosure, model invocation, or execution of the selected route. A trusted broker/orchestrator +//! must derive availability from an authoritative runtime boundary and may execute only the exact +//! route authorized by this deterministic policy. use crate::{ModelRouteDecision, ModelRouteRequest, ModelRouteScope, evaluate_model_route}; @@ -15,15 +17,35 @@ pub enum ModelRouteAvailability { Available, /// The trusted runtime boundary reports that the exact primary route is unavailable. Unavailable, - /// Availability is missing, stale, contradictory, or otherwise not trustworthy enough to use. + /// Availability is missing, contradictory, or otherwise not trustworthy enough to use. Unknown, } +/// Availability evidence for one exact primary route with an exclusive validity horizon. +/// +/// `valid_until` belongs to the same trusted time domain supplied later to +/// [`evaluate_model_fallback`]. A zero horizon is intentionally invalid, and evidence is expired when +/// evaluation time is greater than or equal to the horizon. Constructing this value does not attest +/// the clock or prove provider health. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ModelRouteAvailabilityEvidence { + state: ModelRouteAvailability, + valid_until: u64, +} + +impl ModelRouteAvailabilityEvidence { + /// Build availability evidence with an exclusive validity horizon. + #[must_use] + pub fn new(state: ModelRouteAvailability, valid_until: u64) -> Self { + Self { state, valid_until } + } +} + /// One proposed primary route and optional fallback after trusted availability observation. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ModelFallbackRequest { primary_route: ModelRouteRequest, - primary_availability: ModelRouteAvailability, + primary_availability: ModelRouteAvailabilityEvidence, fallback_route: Option, } @@ -35,7 +57,7 @@ impl ModelFallbackRequest { #[must_use] pub fn new( primary_route: ModelRouteRequest, - primary_availability: ModelRouteAvailability, + primary_availability: ModelRouteAvailabilityEvidence, ) -> Self { Self { primary_route, @@ -87,10 +109,14 @@ impl ModelFallbackScope { /// Result of composing exact primary route authority, trusted availability, and reviewed fallback. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ModelFallbackDecision { - /// The exact primary route is policy-authorized and reported available. + /// The exact primary route is policy-authorized and reported available by fresh evidence. PrimaryAuthorized, /// Primary route policy failed; availability and fallback are intentionally not considered. PrimaryRouteDenied(ModelRouteDecision), + /// The primary route is authorized but availability evidence has an invalid lifetime. + PrimaryAvailabilityInvalid, + /// The primary route is authorized but its availability evidence is no longer fresh. + PrimaryAvailabilityExpired, /// The primary route is authorized but its runtime availability cannot be trusted. PrimaryAvailabilityUnknown, /// The primary route is unavailable and policy contains no reviewed fallback route. @@ -105,21 +131,32 @@ pub enum ModelFallbackDecision { /// Evaluate fail-closed sensitive-model fallback selection without executing a model route. /// -/// Primary route policy is always evaluated first. A malformed or mismatched primary route therefore -/// cannot be converted into a fallback trigger. Unknown availability also fails closed. Only explicit -/// `Unavailable` evidence permits fallback consideration, and only when request and trusted scope both -/// carry a fallback that independently passes the existing exact route evaluator. +/// `trusted_time` must come from the same authoritative time domain as the availability horizon. +/// Primary route policy is always evaluated first, so malformed or mismatched primary authority can +/// never become a fallback trigger. After primary authorization, a zero horizon is invalid and an +/// exclusive horizon at or before `trusted_time` is expired. Unknown fresh availability also fails +/// closed. Only fresh explicit `Unavailable` evidence permits fallback consideration, and only when +/// request and trusted scope both carry a fallback that independently passes the existing exact route +/// evaluator. #[must_use] pub fn evaluate_model_fallback( request: &ModelFallbackRequest, scope: &ModelFallbackScope, + trusted_time: u64, ) -> ModelFallbackDecision { let primary_decision = evaluate_model_route(&request.primary_route, &scope.primary_route); if primary_decision != ModelRouteDecision::Authorized { return ModelFallbackDecision::PrimaryRouteDenied(primary_decision); } - match request.primary_availability { + if request.primary_availability.valid_until == 0 { + return ModelFallbackDecision::PrimaryAvailabilityInvalid; + } + if trusted_time >= request.primary_availability.valid_until { + return ModelFallbackDecision::PrimaryAvailabilityExpired; + } + + match request.primary_availability.state { ModelRouteAvailability::Available => ModelFallbackDecision::PrimaryAuthorized, ModelRouteAvailability::Unknown => ModelFallbackDecision::PrimaryAvailabilityUnknown, ModelRouteAvailability::Unavailable => { From 746786bc8e19bc1c48b9a59534f1540b46327aee Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 23:12:52 +0900 Subject: [PATCH 4/6] feat(sensitive): export fallback availability evidence --- crates/originweave-policy/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/originweave-policy/src/lib.rs b/crates/originweave-policy/src/lib.rs index 12f89f2..8753f43 100644 --- a/crates/originweave-policy/src/lib.rs +++ b/crates/originweave-policy/src/lib.rs @@ -14,7 +14,7 @@ mod sensitive_data; pub use model_fallback::{ ModelFallbackDecision, ModelFallbackRequest, ModelFallbackScope, ModelRouteAvailability, - evaluate_model_fallback, + ModelRouteAvailabilityEvidence, evaluate_model_fallback, }; pub use model_output::{ ModelOutputDecision, ModelOutputRequest, ModelOutputScope, ModelOutputValidation, From 1437462dd5704b8cbb365702629ece2139ca24c0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 23:18:18 +0900 Subject: [PATCH 5/6] docs(changelog): record fallback availability freshness --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57cc355..6219499 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 +- Freshness-bound sensitive-model fallback availability evidence with an exclusive caller-supplied validity horizon and trusted evaluation time; exact primary-route authorization remains the first boundary, zero or expired availability lifetimes fail closed, unknown fresh availability remains denied, and only fresh explicit unavailability can enter the separately reviewed fallback path without claiming provider-health or clock attestation. - Fail-closed sensitive-model fallback selection that authorizes the exact primary route before considering trusted availability, rejects unknown availability and one-sided or unreviewed fallback policy, and permits only an exact separately reviewed fallback route through the existing model-route authority; this deterministic boundary does not probe provider health, retry, invoke a model, disclose protected values, or execute the selected route. - Separate sensitive-model output admission that requires an exact reviewed output-schema identifier, exact retention-policy identifier, and trusted validation result before authorization; malformed or mismatched policy fails closed, validation rejection remains distinct, and this metadata-only boundary does not inspect model-output bytes, persist output, enforce retention, authorize invocation, or disclose protected values. - 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. @@ -68,7 +69,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - TLS accepts only an already verified direct stream, never a hostname or new socket, and requires the TLS origin to match the transport-authority origin exactly. - DNS TLS identity requires an applicable subjectAltName and never falls back to Common Name; literal IPv4 and IPv6 origins require exact IP subjectAltName entries. - TLS uses an explicit immutable trust-root bundle and fixed verification time, and permits only TLS 1.2 and TLS 1.3. -- TLS resumption, 0-RTT, secret extraction, key logging, client certificates, certificate compression, and dangerous custom verifier hooks are disabled in the first slice. +- TLS resumption, 0-RTT, secret extraction, key logging, client certificates, certificate compression, and dangerous custom verification are disabled in the first slice. - The operating-system peer is rechecked before, during, and after the deadline-bound TLS handshake. - ALPN selection is restricted to the caller's bounded allow-list, while absence is either explicitly recorded or rejected by policy. - Revocation is reported as not configured; the product makes no OCSP or CRL validation claim without supplied revocation evidence. From b1273d7bc34fffee262be4bd2da24c24520d11db Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 23:20:09 +0900 Subject: [PATCH 6/6] docs(changelog): remove unrelated wording drift --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6219499..f4c002d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,7 +69,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - TLS accepts only an already verified direct stream, never a hostname or new socket, and requires the TLS origin to match the transport-authority origin exactly. - DNS TLS identity requires an applicable subjectAltName and never falls back to Common Name; literal IPv4 and IPv6 origins require exact IP subjectAltName entries. - TLS uses an explicit immutable trust-root bundle and fixed verification time, and permits only TLS 1.2 and TLS 1.3. -- TLS resumption, 0-RTT, secret extraction, key logging, client certificates, certificate compression, and dangerous custom verification are disabled in the first slice. +- TLS resumption, 0-RTT, secret extraction, key logging, client certificates, certificate compression, and dangerous custom verifier hooks are disabled in the first slice. - The operating-system peer is rechecked before, during, and after the deadline-bound TLS handshake. - ALPN selection is restricted to the caller's bounded allow-list, while absence is either explicitly recorded or rejected by policy. - Revocation is reported as not configured; the product makes no OCSP or CRL validation claim without supplied revocation evidence.