Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ All notable changes to OriginWeave are documented in this file. The format follo

### Added

- Route-bound sensitive-model fallback availability evidence that retains the exact reviewed primary `ModelRouteRequest` and fails closed with `PrimaryAvailabilityRouteMismatch` before lifetime or state can trigger fallback when evidence belongs to another provider/model/region/policy route; primary route authorization remains the first boundary, and this policy primitive does not attest route identity, provider health, or clock provenance.
- 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.
Expand Down
44 changes: 28 additions & 16 deletions crates/originweave-policy/src/model_fallback.rs
Original file line number Diff line number Diff line change
@@ -1,12 +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`]. 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.
//! itself passes [`crate::evaluate_model_route`]. Availability evidence is bound to the exact route it
//! describes, 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};

Expand All @@ -23,21 +23,27 @@ pub enum ModelRouteAvailability {

/// 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)]
/// The embedded route identifies the exact provider/model/region/retention/training/subprocessor/
/// export authority tuple observed by the trusted runtime boundary. `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 route identity, clock, or provider health.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModelRouteAvailabilityEvidence {
route: ModelRouteRequest,
state: ModelRouteAvailability,
valid_until: u64,
}

impl ModelRouteAvailabilityEvidence {
/// Build availability evidence with an exclusive validity horizon.
/// Build route-bound availability evidence with an exclusive validity horizon.
#[must_use]
pub fn new(state: ModelRouteAvailability, valid_until: u64) -> Self {
Self { state, valid_until }
pub fn new(route: ModelRouteRequest, state: ModelRouteAvailability, valid_until: u64) -> Self {
Self {
route,
state,
valid_until,
}
}
}

Expand Down Expand Up @@ -113,6 +119,8 @@ pub enum ModelFallbackDecision {
PrimaryAuthorized,
/// Primary route policy failed; availability and fallback are intentionally not considered.
PrimaryRouteDenied(ModelRouteDecision),
/// Availability evidence belongs to a different route than the authorized primary request.
PrimaryAvailabilityRouteMismatch,
/// 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.
Expand All @@ -133,8 +141,9 @@ pub enum ModelFallbackDecision {
///
/// `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
/// never become a fallback trigger. After primary authorization, availability evidence must describe
/// the exact same primary route before lifetime or state is considered. 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.
Expand All @@ -149,6 +158,9 @@ pub fn evaluate_model_fallback(
return ModelFallbackDecision::PrimaryRouteDenied(primary_decision);
}

if request.primary_availability.route != request.primary_route {
return ModelFallbackDecision::PrimaryAvailabilityRouteMismatch;
}
if request.primary_availability.valid_until == 0 {
return ModelFallbackDecision::PrimaryAvailabilityInvalid;
}
Expand Down
82 changes: 59 additions & 23 deletions crates/originweave-policy/tests/sensitive_model_fallback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,20 @@ 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)
fn availability(
route: &ModelRouteRequest,
state: ModelRouteAvailability,
valid_until: u64,
) -> ModelRouteAvailabilityEvidence {
ModelRouteAvailabilityEvidence::new(route.clone(), state, valid_until)
}

#[test]
fn available_exact_primary_route_is_used_with_fresh_evidence() {
let primary = primary_request();
let request = ModelFallbackRequest::new(
primary_request(),
availability(ModelRouteAvailability::Available, 101),
primary.clone(),
availability(&primary, ModelRouteAvailability::Available, 101),
);
let scope = ModelFallbackScope::new(primary_scope());

Expand All @@ -77,10 +82,16 @@ fn available_exact_primary_route_is_used_with_fresh_evidence() {
}

#[test]
fn primary_policy_mismatch_precedes_availability_freshness() {
fn primary_policy_mismatch_precedes_availability_route_binding() {
let unreviewed_primary = route_request("provider-unreviewed", "model-primary-v1", "kr-central");
let unrelated_evidence_route = primary_request();
let request = ModelFallbackRequest::new(
route_request("provider-unreviewed", "model-primary-v1", "kr-central"),
availability(ModelRouteAvailability::Unavailable, 100),
unreviewed_primary,
availability(
&unrelated_evidence_route,
ModelRouteAvailability::Unavailable,
100,
),
)
.with_fallback(fallback_request());
let scope = ModelFallbackScope::new(primary_scope()).with_fallback(fallback_scope());
Expand All @@ -91,11 +102,29 @@ fn primary_policy_mismatch_precedes_availability_freshness() {
);
}

#[test]
fn availability_evidence_for_another_route_fails_closed_before_freshness() {
let primary = primary_request();
let other_route = route_request("provider-primary", "model-primary-v2", "kr-central");
let request = ModelFallbackRequest::new(
primary,
availability(&other_route, ModelRouteAvailability::Unavailable, 100),
)
.with_fallback(fallback_request());
let scope = ModelFallbackScope::new(primary_scope()).with_fallback(fallback_scope());

assert_eq!(
evaluate_model_fallback(&request, &scope, 100),
ModelFallbackDecision::PrimaryAvailabilityRouteMismatch
);
}

#[test]
fn malformed_availability_lifetime_fails_closed() {
let primary = primary_request();
let request = ModelFallbackRequest::new(
primary_request(),
availability(ModelRouteAvailability::Available, 0),
primary.clone(),
availability(&primary, ModelRouteAvailability::Available, 0),
);
let scope = ModelFallbackScope::new(primary_scope());

Expand All @@ -107,9 +136,10 @@ fn malformed_availability_lifetime_fails_closed() {

#[test]
fn expired_primary_availability_fails_closed_at_exclusive_boundary() {
let primary = primary_request();
let request = ModelFallbackRequest::new(
primary_request(),
availability(ModelRouteAvailability::Available, 100),
primary.clone(),
availability(&primary, ModelRouteAvailability::Available, 100),
);
let scope = ModelFallbackScope::new(primary_scope());

Expand All @@ -121,9 +151,10 @@ fn expired_primary_availability_fails_closed_at_exclusive_boundary() {

#[test]
fn unknown_primary_availability_fails_closed() {
let primary = primary_request();
let request = ModelFallbackRequest::new(
primary_request(),
availability(ModelRouteAvailability::Unknown, 101),
primary.clone(),
availability(&primary, ModelRouteAvailability::Unknown, 101),
)
.with_fallback(fallback_request());
let scope = ModelFallbackScope::new(primary_scope()).with_fallback(fallback_scope());
Expand All @@ -136,9 +167,10 @@ fn unknown_primary_availability_fails_closed() {

#[test]
fn unavailable_primary_without_reviewed_fallback_fails_closed() {
let primary = primary_request();
let request = ModelFallbackRequest::new(
primary_request(),
availability(ModelRouteAvailability::Unavailable, 101),
primary.clone(),
availability(&primary, ModelRouteAvailability::Unavailable, 101),
);
let scope = ModelFallbackScope::new(primary_scope());

Expand All @@ -150,9 +182,10 @@ fn unavailable_primary_without_reviewed_fallback_fails_closed() {

#[test]
fn fallback_must_exist_on_both_request_and_trusted_scope() {
let request_primary = primary_request();
let request_only = ModelFallbackRequest::new(
primary_request(),
availability(ModelRouteAvailability::Unavailable, 101),
request_primary.clone(),
availability(&request_primary, ModelRouteAvailability::Unavailable, 101),
)
.with_fallback(fallback_request());
let no_fallback_scope = ModelFallbackScope::new(primary_scope());
Expand All @@ -161,9 +194,10 @@ fn fallback_must_exist_on_both_request_and_trusted_scope() {
ModelFallbackDecision::FallbackPolicyMismatch
);

let scope_primary = primary_request();
let no_fallback_request = ModelFallbackRequest::new(
primary_request(),
availability(ModelRouteAvailability::Unavailable, 101),
scope_primary.clone(),
availability(&scope_primary, ModelRouteAvailability::Unavailable, 101),
);
let scope_only = ModelFallbackScope::new(primary_scope()).with_fallback(fallback_scope());
assert_eq!(
Expand All @@ -174,9 +208,10 @@ fn fallback_must_exist_on_both_request_and_trusted_scope() {

#[test]
fn unavailable_primary_can_use_only_an_exact_reviewed_fallback() {
let primary = primary_request();
let request = ModelFallbackRequest::new(
primary_request(),
availability(ModelRouteAvailability::Unavailable, 101),
primary.clone(),
availability(&primary, ModelRouteAvailability::Unavailable, 101),
)
.with_fallback(fallback_request());
let scope = ModelFallbackScope::new(primary_scope()).with_fallback(fallback_scope());
Expand All @@ -189,9 +224,10 @@ fn unavailable_primary_can_use_only_an_exact_reviewed_fallback() {

#[test]
fn mismatched_reviewed_fallback_is_denied() {
let primary = primary_request();
let request = ModelFallbackRequest::new(
primary_request(),
availability(ModelRouteAvailability::Unavailable, 101),
primary.clone(),
availability(&primary, ModelRouteAvailability::Unavailable, 101),
)
.with_fallback(route_request(
"provider-fallback",
Expand Down
Loading