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 @@ -22,6 +22,7 @@ All notable changes to OriginWeave are documented in this file. The format follo
- Real loopback rustls integration covering trusted DNS SAN, Common-Name fallback rejection, wrong-name and untrusted-root rejection, fixed-time expiry and not-yet-valid failures, exact IPv4 and IPv6 SANs, TLS 1.2/TLS 1.3, required and optional ALPN, and transport-origin binding.
- Cumulative interactive-first RAM, VRAM, batch, local-model, admission, pause, and compositor-pressure mitigation plans, including active-consumer reduction at exact hard limits.
- Universally value-redacted network evidence with explicit path, metadata, and provenance bounds; ambiguous path rejection; validated source URLs; lowercase SHA-256 identifiers; and verification state.
- Credential-free sensitive-model disclosure evidence linking a sensitive-data request and policy decision to the exact reviewed provider, model, processing-region, retention, training, subprocessor, and export-policy identifiers without carrying protected values, prompts, outputs, or provider credentials.
- Rust 1.97.1 build contract, strict Clippy and rustdoc gates, and exact production function, line, region, and branch coverage enforcement.
- Hourly bounded OpenCode product-development workflow using `NVIDIA_NIM_API_KEY`, an unprivileged disposable workspace, loopback-only model broker, independently verified patches, and publication through a dedicated `OPENCODE_PR_TOKEN` that cannot review or merge.
- Architecture, agent, security, contribution, research, database naming, roadmap, quality-gate, and TLS service-identity ADR documentation.
Expand Down
4 changes: 4 additions & 0 deletions crates/originweave-evidence/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

mod sensitive_access;
mod sensitive_handle_lifecycle;
mod sensitive_model_disclosure;

pub use sensitive_access::{
MAX_SENSITIVE_FIELD_COUNT, MAX_SENSITIVE_IDENTIFIER_BYTES, SensitiveAccessClass,
Expand All @@ -18,6 +19,9 @@ pub use sensitive_access::{
pub use sensitive_handle_lifecycle::{
SensitiveHandleLifecycleEvidence, SensitiveHandleLifecycleEvidenceInput,
};
pub use sensitive_model_disclosure::{
SensitiveModelDisclosureEvidence, SensitiveModelDisclosureEvidenceInput,
};

use std::collections::BTreeMap;

Expand Down
139 changes: 139 additions & 0 deletions crates/originweave-evidence/src/sensitive_model_disclosure.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
//! Credential-free audit metadata for policy-approved model disclosure.
//!
//! This value object records only the reviewed route and policy identifiers
//! associated with a sensitive-data decision. It intentionally carries no
//! protected value, model prompt or output, provider credential, or execution
//! authority.

use crate::sensitive_access::{SensitiveEvidenceError, valid_identifier};

/// Unvalidated metadata describing one sensitive-data model disclosure route.
///
/// The input is restricted to correlation and policy identifiers. It cannot
/// carry protected field values, prompts, model outputs, or provider secrets.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SensitiveModelDisclosureEvidenceInput {
/// Correlation identifier for the sensitive-data access request.
pub request_id: String,
/// Identifier for the policy decision governing the disclosure.
pub decision_id: String,
/// Reviewed provider identifier selected for the model route.
pub provider_id: String,
/// Reviewed model identifier selected for the model route.
pub model_id: String,
/// Reviewed processing-region identifier for the model route.
pub region_id: String,
/// Reviewed retention-policy identifier for the model route.
pub retention_policy_id: String,
/// Reviewed provider training-policy identifier for the model route.
pub training_policy_id: String,
/// Reviewed subprocessor-policy identifier for the model route.
pub subprocessor_policy_id: String,
/// Reviewed export-policy identifier for the model route.
pub export_policy_id: String,
}

/// Immutable credential-free evidence for one approved model disclosure route.
///
/// Construction validates every identifier with the same bounded evidence
/// rules as other sensitive-access records. The object records policy metadata
/// only and does not authorize or execute a model disclosure.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SensitiveModelDisclosureEvidence {
request_id: String,
decision_id: String,
provider_id: String,
model_id: String,
region_id: String,
retention_policy_id: String,
training_policy_id: String,
subprocessor_policy_id: String,
export_policy_id: String,
}

impl TryFrom<SensitiveModelDisclosureEvidenceInput> for SensitiveModelDisclosureEvidence {
type Error = SensitiveEvidenceError;

fn try_from(input: SensitiveModelDisclosureEvidenceInput) -> Result<Self, Self::Error> {
if !valid_identifier(&input.request_id)
|| !valid_identifier(&input.decision_id)
|| !valid_identifier(&input.provider_id)
|| !valid_identifier(&input.model_id)
|| !valid_identifier(&input.region_id)
|| !valid_identifier(&input.retention_policy_id)
|| !valid_identifier(&input.training_policy_id)
|| !valid_identifier(&input.subprocessor_policy_id)
|| !valid_identifier(&input.export_policy_id)
{
return Err(SensitiveEvidenceError::InvalidIdentifier);
}

Ok(Self {
request_id: input.request_id,
decision_id: input.decision_id,
provider_id: input.provider_id,
model_id: input.model_id,
region_id: input.region_id,
retention_policy_id: input.retention_policy_id,
training_policy_id: input.training_policy_id,
subprocessor_policy_id: input.subprocessor_policy_id,
export_policy_id: input.export_policy_id,
})
}
}

impl SensitiveModelDisclosureEvidence {
/// Return the originating sensitive-data access request identifier.
#[must_use]
pub fn request_id(&self) -> &str {
&self.request_id
}

/// Return the sensitive-data policy decision identifier.
#[must_use]
pub fn decision_id(&self) -> &str {
&self.decision_id
}

/// Return the reviewed provider identifier.
#[must_use]
pub fn provider_id(&self) -> &str {
&self.provider_id
}

/// Return the reviewed model identifier.
#[must_use]
pub fn model_id(&self) -> &str {
&self.model_id
}

/// Return the reviewed processing-region identifier.
#[must_use]
pub fn region_id(&self) -> &str {
&self.region_id
}

/// Return the reviewed retention-policy identifier.
#[must_use]
pub fn retention_policy_id(&self) -> &str {
&self.retention_policy_id
}

/// Return the reviewed provider training-policy identifier.
#[must_use]
pub fn training_policy_id(&self) -> &str {
&self.training_policy_id
}

/// Return the reviewed subprocessor-policy identifier.
#[must_use]
pub fn subprocessor_policy_id(&self) -> &str {
&self.subprocessor_policy_id
}

/// Return the reviewed export-policy identifier.
#[must_use]
pub fn export_policy_id(&self) -> &str {
&self.export_policy_id
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
use originweave_evidence::{
MAX_SENSITIVE_IDENTIFIER_BYTES, SensitiveEvidenceError, SensitiveModelDisclosureEvidence,
SensitiveModelDisclosureEvidenceInput,
};

fn valid_input() -> SensitiveModelDisclosureEvidenceInput {
SensitiveModelDisclosureEvidenceInput {
request_id: "request-42".to_owned(),
decision_id: "decision-42".to_owned(),
provider_id: "provider-private".to_owned(),
model_id: "model-reviewed-v1".to_owned(),
region_id: "kr-central".to_owned(),
retention_policy_id: "ephemeral-retention".to_owned(),
training_policy_id: "no-training".to_owned(),
subprocessor_policy_id: "subprocessors-reviewed-v1".to_owned(),
export_policy_id: "no-export".to_owned(),
}
}

#[test]
fn records_exact_model_route_policy_without_protected_values() -> Result<(), SensitiveEvidenceError>
{
let evidence = SensitiveModelDisclosureEvidence::try_from(valid_input())?;

assert_eq!(evidence.request_id(), "request-42");
assert_eq!(evidence.decision_id(), "decision-42");
assert_eq!(evidence.provider_id(), "provider-private");
assert_eq!(evidence.model_id(), "model-reviewed-v1");
assert_eq!(evidence.region_id(), "kr-central");
assert_eq!(evidence.retention_policy_id(), "ephemeral-retention");
assert_eq!(evidence.training_policy_id(), "no-training");
assert_eq!(
evidence.subprocessor_policy_id(),
"subprocessors-reviewed-v1"
);
assert_eq!(evidence.export_policy_id(), "no-export");

let debug = format!("{evidence:?}");
assert!(!debug.contains("protected-value-must-never-enter-evidence"));
assert!(!debug.contains("provider-credential-must-never-enter-evidence"));
Ok(())
}

#[test]
fn rejects_invalid_model_route_evidence_identifiers() {
for field in 0..9 {
let mut input = valid_input();
let invalid = if field == 8 {
"a".repeat(MAX_SENSITIVE_IDENTIFIER_BYTES + 1)
} else {
"bad/value".to_owned()
};
match field {
0 => input.request_id = invalid,
1 => input.decision_id = invalid,
2 => input.provider_id = invalid,
3 => input.model_id = invalid,
4 => input.region_id = invalid,
5 => input.retention_policy_id = invalid,
6 => input.training_policy_id = invalid,
7 => input.subprocessor_policy_id = invalid,
8 => input.export_policy_id = invalid,
_ => unreachable!(),
}
assert_eq!(
SensitiveModelDisclosureEvidence::try_from(input),
Err(SensitiveEvidenceError::InvalidIdentifier)
);
}
}

#[test]
fn identifiers_require_meaningful_ascii_policy_tokens() {
for invalid in [String::new(), "---".to_owned(), "with space".to_owned()] {
let mut input = valid_input();
input.provider_id = invalid;
assert_eq!(
SensitiveModelDisclosureEvidence::try_from(input),
Err(SensitiveEvidenceError::InvalidIdentifier)
);
}
}
Loading