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
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ 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-safe verified action-outcome evidence that binds action kind, canonical target origin, complete action-intent digest, bounded browser post-condition kind, monotonic dispatch and observation timestamps with a bounded delay, and exact provenance; clock provenance is not independently verified, and construction rejects unverified or rejected post-conditions and any observation timestamp that predates action dispatch while allowing equal coarse-clock ticks.
- Credential-safe verified action-outcome evidence that binds action kind, canonical target origin, complete action-intent digest, bounded browser post-condition kind, a same-clock monotonic dispatch-to-observation window with a caller-selected positive freshness budget, and exact provenance; construction rejects unverified or rejected post-conditions, pre-dispatch observations, stale observations beyond that budget, and generic `NodeStateChanged` evidence without the exact governed `ObservedNodeHandle`, while node-state construction additionally requires canonical provenance origin equality and the independently observed node to match the exact governed session/context/origin/document-epoch/node identity; equal coarse-clock ticks and canonical-equivalent source origins remain valid.
- Credential-safe structured-value evidence that binds a bounded field identifier and lowercase SHA-256 value digest to one exact OriginWeave node plus independently verified same-origin DOM/accessibility and network-response provenance, without carrying the raw extracted value.
- Immutable capture-manifest evidence that binds a bounded capture identifier and extraction-schema identifier to one structured-value proof plus canonical SHA-256 identities for its durable WARC record set and provenance graph, without carrying raw extracted values or artifact bytes.
- Versioned schema-bound extraction contracts with bounded identifiers and field counts, typed value/cardinality metadata, explicit duplicate-free reviewed source channels, fail-closed schema validation, and deterministic `Display`/`std::error::Error` contracts for public schema failures.
- 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.
Expand Down Expand Up @@ -103,4 +105,4 @@ All notable changes to OriginWeave are documented in this file. The format follo
- The hourly product agent has no Git metadata or repository authority. A separate post-verification publisher opens one PR and cannot approve or merge it.
- The unprivileged OpenCode user is restricted to loopback egress during model execution, preventing runner-wide allow-listed endpoints from becoming direct source-exfiltration channels.

[Unreleased]: https://github.com/ContextualWisdomLab/OriginWeave/compare/main...HEAD
[Unreleased]: https://github.com/ContextualWisdomLab/OriginWeave/compare/main...HEAD
237 changes: 236 additions & 1 deletion crates/originweave-evidence/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ pub use sensitive_handle_lifecycle::{

use std::collections::BTreeMap;

use originweave_core::Origin;
use originweave_core::{ObservedNodeHandle, Origin};

const REDACTED: &str = "[REDACTED]";

Expand All @@ -48,6 +48,10 @@ pub const MAX_METADATA_NAME_BYTES: usize = 256;
pub const MAX_METADATA_VALUE_BYTES: usize = 8_192;
/// Maximum source URL or source-locator size retained in provenance metadata.
pub const MAX_PROVENANCE_TEXT_BYTES: usize = 8_192;
/// Maximum byte length of one structured extracted-field identifier.
pub const MAX_STRUCTURED_FIELD_NAME_BYTES: usize = 128;
/// Maximum byte length of one capture or extraction-schema identifier.
pub const MAX_CAPTURE_IDENTIFIER_BYTES: usize = MAX_STRUCTURED_FIELD_NAME_BYTES;

/// An HTTP method recorded for network evidence.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
Expand Down Expand Up @@ -405,3 +409,234 @@ fn valid_sha256(source_hash: &str) -> bool {
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
}

/// A credential-safe proof bundle for one extracted structured value.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StructuredValueEvidence {
field_name: String,
value_hash: String,
source_node: ObservedNodeHandle,
node_provenance: ProvenanceRecord,
network_provenance: ProvenanceRecord,
}

impl StructuredValueEvidence {
/// Bind one structured field digest to exact node and network provenance.
pub fn new(
field_name: &str,
value_hash: &str,
source_node: ObservedNodeHandle,
node_provenance: ProvenanceRecord,
network_provenance: ProvenanceRecord,
) -> Result<Self, StructuredValueEvidenceError> {
if !valid_structured_field_name(field_name) {
return Err(StructuredValueEvidenceError::InvalidFieldName);
}
if !valid_sha256(value_hash) {
return Err(StructuredValueEvidenceError::InvalidValueHash);
}
if node_provenance.verification_result() != VerificationResult::Verified {
return Err(StructuredValueEvidenceError::NodeProvenanceNotVerified);
}
if !matches!(
node_provenance.source_kind(),
EvidenceSourceKind::DomTree | EvidenceSourceKind::AccessibilityTree
) {
return Err(StructuredValueEvidenceError::NodeProvenanceKindMismatch);
}
if network_provenance.verification_result() != VerificationResult::Verified {
return Err(StructuredValueEvidenceError::NetworkProvenanceNotVerified);
}
if network_provenance.source_kind() != EvidenceSourceKind::NetworkResponse {
return Err(StructuredValueEvidenceError::NetworkProvenanceKindMismatch);
}
if node_provenance.source_origin() != source_node.origin()
|| network_provenance.source_origin() != source_node.origin()
{
return Err(StructuredValueEvidenceError::SourceOriginMismatch);
}
Ok(Self {
field_name: field_name.to_owned(),
value_hash: value_hash.to_owned(),
source_node,
node_provenance,
network_provenance,
})
}

/// Return the bounded structured field identifier.
#[must_use]
pub fn field_name(&self) -> &str {
&self.field_name
}

/// Return the lowercase SHA-256 digest of the canonical extracted value bytes.
#[must_use]
pub fn value_hash(&self) -> &str {
&self.value_hash
}

/// Return the exact OriginWeave-owned source node.
#[must_use]
pub const fn source_node(&self) -> &ObservedNodeHandle {
&self.source_node
}

/// Return the independently verified DOM/accessibility provenance for the source node.
#[must_use]
pub const fn node_provenance(&self) -> &ProvenanceRecord {
&self.node_provenance
}

/// Return the independently verified network provenance associated with the value.
#[must_use]
pub const fn network_provenance(&self) -> &ProvenanceRecord {
&self.network_provenance
}
}

/// A fail-closed reason why structured extraction evidence could not be constructed.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StructuredValueEvidenceError {
/// The structured field identifier was empty, oversized, or contained unsupported bytes.
InvalidFieldName,
/// The value digest was not a canonical lowercase SHA-256 identifier.
InvalidValueHash,
/// The node provenance did not carry independent verified status.
NodeProvenanceNotVerified,
/// The node provenance was not a DOM or accessibility observation.
NodeProvenanceKindMismatch,
/// The network provenance did not carry independent verified status.
NetworkProvenanceNotVerified,
/// The network provenance was not a structured network-response observation.
NetworkProvenanceKindMismatch,
/// Node or network provenance belonged to a different canonical origin than the source node.
SourceOriginMismatch,
}

impl std::fmt::Display for StructuredValueEvidenceError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(match self {
Self::InvalidFieldName => "invalid structured field name",
Self::InvalidValueHash => "invalid structured value hash",
Self::NodeProvenanceNotVerified => "node provenance is not verified",
Self::NodeProvenanceKindMismatch => {
"node provenance is not DOM or accessibility evidence"
}
Self::NetworkProvenanceNotVerified => "network provenance is not verified",
Self::NetworkProvenanceKindMismatch => {
"network provenance is not network-response evidence"
}
Self::SourceOriginMismatch => "provenance origin does not match source node origin",
})
}
}

impl std::error::Error for StructuredValueEvidenceError {}

fn valid_structured_field_name(field_name: &str) -> bool {
if field_name.is_empty() || field_name.len() > MAX_STRUCTURED_FIELD_NAME_BYTES {
return false;
}
field_name
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
&& field_name.bytes().any(|byte| byte.is_ascii_alphanumeric())
}

/// Immutable identity binding for one structured extraction and its durable evidence artifacts.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CaptureManifest {
capture_id: String,
extraction_schema_id: String,
structured_value: StructuredValueEvidence,
warc_record_set_hash: String,
provenance_graph_hash: String,
}

impl CaptureManifest {
/// Bind one structured extraction to bounded capture/schema identifiers and durable artifact digests.
pub fn new(
capture_id: &str,
extraction_schema_id: &str,
structured_value: StructuredValueEvidence,
warc_record_set_hash: &str,
provenance_graph_hash: &str,
) -> Result<Self, CaptureManifestError> {
if !valid_structured_field_name(capture_id) {
return Err(CaptureManifestError::InvalidCaptureId);
}
if !valid_structured_field_name(extraction_schema_id) {
return Err(CaptureManifestError::InvalidExtractionSchemaId);
}
if !valid_sha256(warc_record_set_hash) {
return Err(CaptureManifestError::InvalidWarcRecordSetHash);
}
if !valid_sha256(provenance_graph_hash) {
return Err(CaptureManifestError::InvalidProvenanceGraphHash);
}
Ok(Self {
capture_id: capture_id.to_owned(),
extraction_schema_id: extraction_schema_id.to_owned(),
structured_value,
warc_record_set_hash: warc_record_set_hash.to_owned(),
provenance_graph_hash: provenance_graph_hash.to_owned(),
})
}

/// Return the bounded capture identifier.
#[must_use]
pub fn capture_id(&self) -> &str {
&self.capture_id
}

/// Return the bounded extraction-schema identifier.
#[must_use]
pub fn extraction_schema_id(&self) -> &str {
&self.extraction_schema_id
}

/// Return the structured value evidence bound into this capture.
#[must_use]
pub const fn structured_value(&self) -> &StructuredValueEvidence {
&self.structured_value
}

/// Return the lowercase SHA-256 digest of the durable WARC record set.
#[must_use]
pub fn warc_record_set_hash(&self) -> &str {
&self.warc_record_set_hash
}

/// Return the lowercase SHA-256 digest of the durable provenance graph.
#[must_use]
pub fn provenance_graph_hash(&self) -> &str {
&self.provenance_graph_hash
}
}

/// A fail-closed reason why a capture manifest could not be constructed.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CaptureManifestError {
/// The capture identifier was empty, oversized, or contained unsupported bytes.
InvalidCaptureId,
/// The extraction-schema identifier was empty, oversized, or contained unsupported bytes.
InvalidExtractionSchemaId,
/// The WARC record-set digest was not a canonical lowercase SHA-256 identifier.
InvalidWarcRecordSetHash,
/// The provenance-graph digest was not a canonical lowercase SHA-256 identifier.
InvalidProvenanceGraphHash,
}

impl std::fmt::Display for CaptureManifestError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(match self {
Self::InvalidCaptureId => "invalid capture identifier",
Self::InvalidExtractionSchemaId => "invalid extraction schema identifier",
Self::InvalidWarcRecordSetHash => "invalid WARC record-set hash",
Self::InvalidProvenanceGraphHash => "invalid provenance graph hash",
})
}
}

impl std::error::Error for CaptureManifestError {}
Loading
Loading