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

- 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.
- 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.
Expand Down
5 changes: 5 additions & 0 deletions crates/originweave-policy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@
#![forbid(unsafe_code)]
#![deny(missing_docs)]

mod model_output;
mod model_route;
mod sensitive_data;

pub use model_output::{
ModelOutputDecision, ModelOutputRequest, ModelOutputScope, ModelOutputValidation,
evaluate_model_output,
};
pub use model_route::{
ModelInvocationDecision, ModelInvocationRequest, ModelInvocationScope, ModelRouteDecision,
ModelRouteRequest, ModelRouteScope, evaluate_model_invocation, evaluate_model_route,
Expand Down
112 changes: 112 additions & 0 deletions crates/originweave-policy/src/model_output.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
//! Separate admission for validated model output and reviewed retention policy.
//!
//! This module evaluates output-policy metadata only. Authorization never implies that OriginWeave
//! inspected model-output bytes, executed schema validation, persisted output, enforced retention,
//! or disclosed a protected value. A trusted validator and retention owner must supply the facts
//! consumed by this deterministic policy boundary.

const MAX_OUTPUT_POLICY_IDENTIFIER_BYTES: usize = 128;

/// Trusted validation result for one model output.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModelOutputValidation {
/// A trusted validator accepted the output under the reviewed schema contract.
Validated,
/// A trusted validator rejected the output.
Rejected,
}

/// Result of evaluating one model output against a separate reviewed output policy.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModelOutputDecision {
/// The output policy matches exactly and trusted validation accepted the output.
Authorized,
/// Output-schema or retention-policy metadata is malformed or does not match exactly.
OutputPolicyMismatch,
/// The reviewed output policy matched, but trusted validation rejected the output.
ValidationRejected,
}

/// One proposed model output after model invocation has completed.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModelOutputRequest {
output_schema_id: String,
output_retention_policy_id: String,
validation: ModelOutputValidation,
}

impl ModelOutputRequest {
/// Build output metadata without authorizing validation, persistence, retention, or disclosure.
#[must_use]
pub fn new(
output_schema_id: &str,
output_retention_policy_id: &str,
validation: ModelOutputValidation,
) -> Self {
Self {
output_schema_id: output_schema_id.to_owned(),
output_retention_policy_id: output_retention_policy_id.to_owned(),
validation,
}
}
}

/// Reviewed output-schema and retention-policy scope for one model-output boundary.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModelOutputScope {
output_schema_id: String,
output_retention_policy_id: String,
}

impl ModelOutputScope {
/// Build trusted output policy metadata.
///
/// Identifiers are validated during evaluation so malformed trusted policy cannot become
/// authority merely because request and scope happen to match.
#[must_use]
pub fn new(output_schema_id: &str, output_retention_policy_id: &str) -> Self {
Self {
output_schema_id: output_schema_id.to_owned(),
output_retention_policy_id: output_retention_policy_id.to_owned(),
}
}
}

fn output_policy_identifier_is_valid(identifier: &str) -> bool {
!identifier.is_empty()
&& identifier.len() <= MAX_OUTPUT_POLICY_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 one trusted validation result under an exact separate output policy.
///
/// Policy metadata is evaluated before the validation result so a rejected output carrying a
/// different schema or retention policy remains a policy mismatch rather than collapsing distinct
/// governance failures. Identifiers are bounded 1–128 byte ASCII policy tokens containing at least
/// one alphanumeric character and otherwise only `.`, `_`, `:`, or `-`.
///
/// Authorization is metadata-only. It does not inspect output bytes, perform schema validation,
/// persist output, enforce retention, authorize model invocation, or disclose protected values.
#[must_use]
pub fn evaluate_model_output(
request: &ModelOutputRequest,
scope: &ModelOutputScope,
) -> ModelOutputDecision {
if !output_policy_identifier_is_valid(&request.output_schema_id)
|| !output_policy_identifier_is_valid(&request.output_retention_policy_id)
|| !output_policy_identifier_is_valid(&scope.output_schema_id)
|| !output_policy_identifier_is_valid(&scope.output_retention_policy_id)
|| request.output_schema_id != scope.output_schema_id
|| request.output_retention_policy_id != scope.output_retention_policy_id
{
return ModelOutputDecision::OutputPolicyMismatch;
}

match request.validation {
ModelOutputValidation::Validated => ModelOutputDecision::Authorized,
ModelOutputValidation::Rejected => ModelOutputDecision::ValidationRejected,
}
}
106 changes: 106 additions & 0 deletions crates/originweave-policy/tests/sensitive_model_output.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
use originweave_policy::{
ModelOutputDecision, ModelOutputRequest, ModelOutputScope, ModelOutputValidation,
evaluate_model_output,
};

const OUTPUT_SCHEMA_ID: &str = "customer-summary.v1";
const OUTPUT_RETENTION_POLICY_ID: &str = "task-local-retention";

fn request(validation: ModelOutputValidation) -> ModelOutputRequest {
ModelOutputRequest::new(OUTPUT_SCHEMA_ID, OUTPUT_RETENTION_POLICY_ID, validation)
}

fn scope() -> ModelOutputScope {
ModelOutputScope::new(OUTPUT_SCHEMA_ID, OUTPUT_RETENTION_POLICY_ID)
}

#[test]
fn validated_output_requires_an_exact_separate_output_policy() {
assert_eq!(
evaluate_model_output(&request(ModelOutputValidation::Validated), &scope()),
ModelOutputDecision::Authorized
);
}

#[test]
fn rejected_output_validation_cannot_be_authorized() {
assert_eq!(
evaluate_model_output(&request(ModelOutputValidation::Rejected), &scope()),
ModelOutputDecision::ValidationRejected
);
}

#[test]
fn output_schema_and_retention_policy_must_match_exactly() {
let wrong_schema = ModelOutputRequest::new(
"different-schema.v1",
OUTPUT_RETENTION_POLICY_ID,
ModelOutputValidation::Validated,
);
assert_eq!(
evaluate_model_output(&wrong_schema, &scope()),
ModelOutputDecision::OutputPolicyMismatch
);

let wrong_retention = ModelOutputRequest::new(
OUTPUT_SCHEMA_ID,
"different-retention",
ModelOutputValidation::Validated,
);
assert_eq!(
evaluate_model_output(&wrong_retention, &scope()),
ModelOutputDecision::OutputPolicyMismatch
);
}

#[test]
fn malformed_output_policy_identifiers_fail_closed_on_request_or_scope() {
let malformed_requests = [
ModelOutputRequest::new(
"",
OUTPUT_RETENTION_POLICY_ID,
ModelOutputValidation::Validated,
),
ModelOutputRequest::new(
"---",
OUTPUT_RETENTION_POLICY_ID,
ModelOutputValidation::Validated,
),
ModelOutputRequest::new(
OUTPUT_SCHEMA_ID,
"retention/policy",
ModelOutputValidation::Validated,
),
];
for malformed in malformed_requests {
assert_eq!(
evaluate_model_output(&malformed, &scope()),
ModelOutputDecision::OutputPolicyMismatch
);
}

let oversized_schema = "a".repeat(129);
let malformed_scopes = [
ModelOutputScope::new(&oversized_schema, OUTPUT_RETENTION_POLICY_ID),
ModelOutputScope::new(OUTPUT_SCHEMA_ID, "retention\npolicy"),
];
for malformed in malformed_scopes {
assert_eq!(
evaluate_model_output(&request(ModelOutputValidation::Validated), &malformed,),
ModelOutputDecision::OutputPolicyMismatch
);
}
}

#[test]
fn policy_mismatch_precedes_validation_result() {
let mismatched = ModelOutputRequest::new(
"different-schema.v1",
OUTPUT_RETENTION_POLICY_ID,
ModelOutputValidation::Rejected,
);
assert_eq!(
evaluate_model_output(&mismatched, &scope()),
ModelOutputDecision::OutputPolicyMismatch
);
}
Loading