From 3a1df5e3f2127f01d414a4d178869f24d35d460d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 20:16:51 +0900 Subject: [PATCH 1/6] test(sensitive): define model output policy boundary --- .../tests/sensitive_model_output.rs | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 crates/originweave-policy/tests/sensitive_model_output.rs diff --git a/crates/originweave-policy/tests/sensitive_model_output.rs b/crates/originweave-policy/tests/sensitive_model_output.rs new file mode 100644 index 00000000..894dae77 --- /dev/null +++ b/crates/originweave-policy/tests/sensitive_model_output.rs @@ -0,0 +1,108 @@ +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_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 + ); +} From 32af45f2525f17fa1dbae50f59e4641999a50695 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 20:19:47 +0900 Subject: [PATCH 2/6] style(sensitive): format model output contract --- .../tests/sensitive_model_output.rs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/crates/originweave-policy/tests/sensitive_model_output.rs b/crates/originweave-policy/tests/sensitive_model_output.rs index 894dae77..a1aac296 100644 --- a/crates/originweave-policy/tests/sensitive_model_output.rs +++ b/crates/originweave-policy/tests/sensitive_model_output.rs @@ -7,11 +7,7 @@ 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, - ) + ModelOutputRequest::new(OUTPUT_SCHEMA_ID, OUTPUT_RETENTION_POLICY_ID, validation) } fn scope() -> ModelOutputScope { @@ -85,10 +81,7 @@ fn malformed_output_policy_identifiers_fail_closed_on_request_or_scope() { ]; for malformed in malformed_scopes { assert_eq!( - evaluate_model_output( - &request(ModelOutputValidation::Validated), - &malformed, - ), + evaluate_model_output(&request(ModelOutputValidation::Validated), &malformed,), ModelOutputDecision::OutputPolicyMismatch ); } From c7b3d3b0d36a9f1fc8e73f769b01b479144ce550 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 20:32:47 +0900 Subject: [PATCH 3/6] feat(sensitive): add separate model output policy boundary --- crates/originweave-policy/src/model_output.rs | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 crates/originweave-policy/src/model_output.rs diff --git a/crates/originweave-policy/src/model_output.rs b/crates/originweave-policy/src/model_output.rs new file mode 100644 index 00000000..d3c962fc --- /dev/null +++ b/crates/originweave-policy/src/model_output.rs @@ -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, + } +} From 99165350c13c9920c052e845815ee4e32af3ecf1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 20:34:38 +0900 Subject: [PATCH 4/6] feat(sensitive): export model output policy boundary --- crates/originweave-policy/src/lib.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/originweave-policy/src/lib.rs b/crates/originweave-policy/src/lib.rs index d0a54b70..25f1a6bf 100644 --- a/crates/originweave-policy/src/lib.rs +++ b/crates/originweave-policy/src/lib.rs @@ -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, From 902ecc5d69f637774b39c6628d041c08a263c52d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 20:40:48 +0900 Subject: [PATCH 5/6] test(sensitive): cover punctuation-only output policy IDs --- crates/originweave-policy/tests/sensitive_model_output.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/originweave-policy/tests/sensitive_model_output.rs b/crates/originweave-policy/tests/sensitive_model_output.rs index a1aac296..37ba03b1 100644 --- a/crates/originweave-policy/tests/sensitive_model_output.rs +++ b/crates/originweave-policy/tests/sensitive_model_output.rs @@ -61,6 +61,11 @@ fn malformed_output_policy_identifiers_fail_closed_on_request_or_scope() { OUTPUT_RETENTION_POLICY_ID, ModelOutputValidation::Validated, ), + ModelOutputRequest::new( + "---", + OUTPUT_RETENTION_POLICY_ID, + ModelOutputValidation::Validated, + ), ModelOutputRequest::new( OUTPUT_SCHEMA_ID, "retention/policy", From 62f69cadbe0b4011fec67f9e482b04c4cacf181b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 20:44:19 +0900 Subject: [PATCH 6/6] docs(changelog): record model output policy boundary --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 682551ad..19e1d846 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 +- 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.