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

- 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.
Expand Down
2 changes: 1 addition & 1 deletion crates/originweave-policy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
63 changes: 50 additions & 13 deletions crates/originweave-policy/src/model_fallback.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand All @@ -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<ModelRouteRequest>,
}

Expand All @@ -35,7 +57,7 @@ impl ModelFallbackRequest {
#[must_use]
pub fn new(
primary_route: ModelRouteRequest,
primary_availability: ModelRouteAvailability,
primary_availability: ModelRouteAvailabilityEvidence,
) -> Self {
Self {
primary_route,
Expand Down Expand Up @@ -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.
Expand All @@ -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 => {
Expand Down
111 changes: 81 additions & 30 deletions crates/originweave-policy/tests/sensitive_model_fallback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -58,99 +58,150 @@ 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)
);
}
Loading