From 291b55c8cbb5e2b45917426abe2037b5624b2dca Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 05:40:00 -0700 Subject: [PATCH 01/29] test(evidence): require WARC PROV JSON-LD bundle --- .../tests/warc_prov_jsonld.rs | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 crates/originweave-evidence/tests/warc_prov_jsonld.rs diff --git a/crates/originweave-evidence/tests/warc_prov_jsonld.rs b/crates/originweave-evidence/tests/warc_prov_jsonld.rs new file mode 100644 index 000000000..ed620de41 --- /dev/null +++ b/crates/originweave-evidence/tests/warc_prov_jsonld.rs @@ -0,0 +1,110 @@ +#![allow(clippy::expect_used)] + +use originweave_evidence::{ + EvidenceSourceKind, MAX_PROV_SOFTWARE_COMMIT_SHA_BYTES, ProvenanceRecord, VerificationResult, + WarcProvBundle, WarcProvBundleError, WarcResourceRecord, +}; + +const SOURCE_HASH: &str = "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; +const RECORD_ID: &str = "urn:uuid:123e4567-e89b-12d3-a456-426614174000"; +const DATE: &str = "2026-08-22T12:00:00Z"; +const SOFTWARE_COMMIT_SHA: &str = "0123456789abcdef0123456789abcdef01234567"; + +fn resource_record() -> WarcResourceRecord { + let provenance = ProvenanceRecord::new( + "https://example.com/item", + "body", + SOURCE_HASH, + EvidenceSourceKind::NetworkResponse, + VerificationResult::Verified, + ) + .expect("verified provenance"); + WarcResourceRecord::new( + RECORD_ID, + DATE, + "https://example.com/item", + "text/plain", + b"hello".to_vec(), + provenance, + ) + .expect("WARC resource record") +} + +fn assert_standard_error_contract() {} + +#[test] +fn warc_prov_bundle_exposes_stable_capture_identities_and_standard_errors() { + assert_standard_error_contract::(); + assert_eq!( + WarcProvBundleError::InvalidSoftwareCommitSha.to_string(), + "invalid OriginWeave software commit SHA" + ); + assert_eq!( + WarcProvBundleError::LimitExceeded.to_string(), + "WARC PROV bundle limit exceeded" + ); + + let bundle = WarcProvBundle::new(&resource_record(), SOFTWARE_COMMIT_SHA) + .expect("PROV bundle over a validated WARC record"); + assert_eq!(bundle.record_entity_id(), RECORD_ID); + assert_eq!( + bundle.source_entity_id(), + "urn:uuid:123e4567-e89b-12d3-a456-426614174000#source" + ); + assert_eq!( + bundle.capture_activity_id(), + "urn:uuid:123e4567-e89b-12d3-a456-426614174000#capture" + ); + assert_eq!( + bundle.software_agent_id(), + "https://github.com/ContextualWisdomLab/OriginWeave/commit/0123456789abcdef0123456789abcdef01234567" + ); + assert_eq!(bundle.software_commit_sha(), SOFTWARE_COMMIT_SHA); +} + +#[test] +fn warc_prov_bundle_emits_deterministic_prov_o_json_ld_without_raw_payload() { + let bundle = WarcProvBundle::new(&resource_record(), SOFTWARE_COMMIT_SHA) + .expect("PROV bundle over a validated WARC record"); + let json_ld = bundle.to_json_ld(); + + assert_eq!( + json_ld, + concat!( + "{\"@context\":{\"prov\":\"http://www.w3.org/ns/prov#\",\"xsd\":\"http://www.w3.org/2001/XMLSchema#\"},\"@graph\":[", + "{\"@id\":\"urn:uuid:123e4567-e89b-12d3-a456-426614174000#source\",\"@type\":\"prov:Entity\",\"prov:atLocation\":{\"@id\":\"https://example.com/item\"},\"prov:value\":\"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\"},", + "{\"@id\":\"urn:uuid:123e4567-e89b-12d3-a456-426614174000#capture\",\"@type\":\"prov:Activity\",\"prov:startedAtTime\":{\"@value\":\"2026-08-22T12:00:00Z\",\"@type\":\"xsd:dateTime\"},\"prov:used\":{\"@id\":\"urn:uuid:123e4567-e89b-12d3-a456-426614174000#source\"},\"prov:wasAssociatedWith\":{\"@id\":\"https://github.com/ContextualWisdomLab/OriginWeave/commit/0123456789abcdef0123456789abcdef01234567\"}},", + "{\"@id\":\"https://github.com/ContextualWisdomLab/OriginWeave/commit/0123456789abcdef0123456789abcdef01234567\",\"@type\":\"prov:SoftwareAgent\"},", + "{\"@id\":\"urn:uuid:123e4567-e89b-12d3-a456-426614174000\",\"@type\":\"prov:Entity\",\"prov:value\":\"sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824\",\"prov:wasDerivedFrom\":{\"@id\":\"urn:uuid:123e4567-e89b-12d3-a456-426614174000#source\"},\"prov:wasGeneratedBy\":{\"@id\":\"urn:uuid:123e4567-e89b-12d3-a456-426614174000#capture\"}}", + "]}" + ) + ); + assert!(!json_ld.contains("hello")); +} + +#[test] +fn warc_prov_bundle_rejects_noncanonical_or_oversized_software_revisions() { + let record = resource_record(); + for software_commit_sha in [ + "", + "0123456789abcdef0123456789abcdef0123456", + "0123456789abcdef0123456789abcdef0123456G", + "0123456789ABCDEF0123456789ABCDEF01234567", + "0123456789abcdef0123456789abcdef0123456 ", + ] { + assert_eq!( + WarcProvBundle::new(&record, software_commit_sha), + Err(WarcProvBundleError::InvalidSoftwareCommitSha), + "software_commit_sha={software_commit_sha:?}" + ); + } + + assert_eq!( + MAX_PROV_SOFTWARE_COMMIT_SHA_BYTES, + SOFTWARE_COMMIT_SHA.len() + ); + assert_eq!( + WarcProvBundle::new(&record, &"a".repeat(MAX_PROV_SOFTWARE_COMMIT_SHA_BYTES + 1)), + Err(WarcProvBundleError::LimitExceeded) + ); +} From e34b44a53c4671d5bfb38421034da242992d906b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 05:44:38 -0700 Subject: [PATCH 02/29] feat(evidence): add bounded WARC PROV bundle --- .../src/warc_prov_bundle.rs | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 crates/originweave-evidence/src/warc_prov_bundle.rs diff --git a/crates/originweave-evidence/src/warc_prov_bundle.rs b/crates/originweave-evidence/src/warc_prov_bundle.rs new file mode 100644 index 000000000..181852078 --- /dev/null +++ b/crates/originweave-evidence/src/warc_prov_bundle.rs @@ -0,0 +1,153 @@ +use std::fmt; + +use crate::WarcResourceRecord; + +const ORIGINWEAVE_COMMIT_URL_PREFIX: &str = + "https://github.com/ContextualWisdomLab/OriginWeave/commit/"; + +/// Exact byte length accepted for a canonical Git SHA-1 software revision. +pub const MAX_PROV_SOFTWARE_COMMIT_SHA_BYTES: usize = 40; + +/// A validation failure while constructing a deterministic WARC provenance bundle. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum WarcProvBundleError { + /// The software revision was not one canonical lower-case 40-byte Git SHA-1. + InvalidSoftwareCommitSha, + /// A bounded provenance field exceeded its allowed size. + LimitExceeded, +} + +impl fmt::Display for WarcProvBundleError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(match self { + Self::InvalidSoftwareCommitSha => "invalid OriginWeave software commit SHA", + Self::LimitExceeded => "WARC PROV bundle limit exceeded", + }) + } +} + +impl std::error::Error for WarcProvBundleError {} + +/// A deterministic PROV-O JSON-LD projection over one validated WARC resource record. +/// +/// The bundle contains identifiers, hashes, source location, capture time, and the exact +/// OriginWeave software revision. It deliberately does not retain or emit the WARC payload. +#[derive(Clone, PartialEq, Eq)] +pub struct WarcProvBundle { + record_entity_id: String, + source_entity_id: String, + capture_activity_id: String, + software_agent_id: String, + software_commit_sha: String, + source_url: String, + source_hash: String, + warc_date: String, + block_digest: String, +} + +impl fmt::Debug for WarcProvBundle { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("WarcProvBundle") + .field("record_entity_id", &self.record_entity_id) + .field("source_entity_id", &self.source_entity_id) + .field("capture_activity_id", &self.capture_activity_id) + .field("software_agent_id", &self.software_agent_id) + .finish_non_exhaustive() + } +} + +impl WarcProvBundle { + /// Construct a provenance bundle from one already-validated WARC resource record. + /// + /// `software_commit_sha` is an immutable canonical Git SHA-1 identifier. This constructor + /// does not contact GitHub and does not treat the identifier as authentication or authority. + pub fn new( + record: &WarcResourceRecord, + software_commit_sha: &str, + ) -> Result { + if software_commit_sha.len() > MAX_PROV_SOFTWARE_COMMIT_SHA_BYTES { + return Err(WarcProvBundleError::LimitExceeded); + } + if !valid_software_commit_sha(software_commit_sha) { + return Err(WarcProvBundleError::InvalidSoftwareCommitSha); + } + + let record_entity_id = record.record_id().to_owned(); + let source_entity_id = format!("{}#source", record.record_id()); + let capture_activity_id = format!("{}#capture", record.record_id()); + let software_agent_id = format!("{ORIGINWEAVE_COMMIT_URL_PREFIX}{software_commit_sha}"); + + Ok(Self { + record_entity_id, + source_entity_id, + capture_activity_id, + software_agent_id, + software_commit_sha: software_commit_sha.to_owned(), + source_url: record.provenance().source_url().to_owned(), + source_hash: record.provenance().source_hash().to_owned(), + warc_date: record.warc_date().to_owned(), + block_digest: record.block_digest().to_owned(), + }) + } + + /// Return the PROV entity identifier for the WARC record. + #[must_use] + pub fn record_entity_id(&self) -> &str { + &self.record_entity_id + } + + /// Return the PROV entity identifier for the independently verified source. + #[must_use] + pub fn source_entity_id(&self) -> &str { + &self.source_entity_id + } + + /// Return the PROV activity identifier for this capture. + #[must_use] + pub fn capture_activity_id(&self) -> &str { + &self.capture_activity_id + } + + /// Return the immutable OriginWeave commit URL used as the PROV software-agent identifier. + #[must_use] + pub fn software_agent_id(&self) -> &str { + &self.software_agent_id + } + + /// Return the canonical lower-case Git SHA-1 of the OriginWeave revision. + #[must_use] + pub fn software_commit_sha(&self) -> &str { + &self.software_commit_sha + } + + /// Serialize the bundle as deterministic compact W3C PROV-O JSON-LD. + /// + /// All interpolated values originate from the validated WARC record or the canonical + /// lower-case software commit identifier, so no raw payload bytes enter this document. + #[must_use] + pub fn to_json_ld(&self) -> String { + format!( + "{{\"@context\":{{\"prov\":\"http://www.w3.org/ns/prov#\",\"xsd\":\"http://www.w3.org/2001/XMLSchema#\"}},\"@graph\":[{{\"@id\":\"{}\",\"@type\":\"prov:Entity\",\"prov:atLocation\":{{\"@id\":\"{}\"}},\"prov:value\":\"{}\"}},{{\"@id\":\"{}\",\"@type\":\"prov:Activity\",\"prov:startedAtTime\":{{\"@value\":\"{}\",\"@type\":\"xsd:dateTime\"}},\"prov:used\":{{\"@id\":\"{}\"}},\"prov:wasAssociatedWith\":{{\"@id\":\"{}\"}}}},{{\"@id\":\"{}\",\"@type\":\"prov:SoftwareAgent\"}},{{\"@id\":\"{}\",\"@type\":\"prov:Entity\",\"prov:value\":\"{}\",\"prov:wasDerivedFrom\":{{\"@id\":\"{}\"}},\"prov:wasGeneratedBy\":{{\"@id\":\"{}\"}}}}]}}", + self.source_entity_id, + self.source_url, + self.source_hash, + self.capture_activity_id, + self.warc_date, + self.source_entity_id, + self.software_agent_id, + self.software_agent_id, + self.record_entity_id, + self.block_digest, + self.source_entity_id, + self.capture_activity_id, + ) + } +} + +fn valid_software_commit_sha(software_commit_sha: &str) -> bool { + software_commit_sha.len() == MAX_PROV_SOFTWARE_COMMIT_SHA_BYTES + && software_commit_sha + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) +} From fced5ba4af2211af9a6aed69887ea4f6220b463d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 05:46:15 -0700 Subject: [PATCH 03/29] feat(evidence): expose WARC PROV bundle --- crates/originweave-evidence/src/lib.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/originweave-evidence/src/lib.rs b/crates/originweave-evidence/src/lib.rs index 65289c876..d89c26d69 100644 --- a/crates/originweave-evidence/src/lib.rs +++ b/crates/originweave-evidence/src/lib.rs @@ -9,6 +9,7 @@ mod extraction_schema; mod sensitive_access; +mod warc_prov_bundle; mod warc_resource_record; pub use extraction_schema::{ @@ -21,6 +22,9 @@ pub use sensitive_access::{ SensitiveAccessEvidence, SensitiveAccessEvidenceInput, SensitiveAccessOutcome, SensitiveEvidenceError, }; +pub use warc_prov_bundle::{ + MAX_PROV_SOFTWARE_COMMIT_SHA_BYTES, WarcProvBundle, WarcProvBundleError, +}; pub use warc_resource_record::{ MAX_WARC_CONTENT_TYPE_BYTES, MAX_WARC_DATE_BYTES, MAX_WARC_PAYLOAD_BYTES, MAX_WARC_RECORD_ID_BYTES, WarcPayloadCompleteness, WarcResourceRecord, WarcResourceRecordError, From d1003d2f7b8ad652479863720ec74cd2fae6971b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 05:47:06 -0700 Subject: [PATCH 04/29] test(evidence): cover provenance debug redaction --- crates/originweave-evidence/tests/warc_prov_jsonld.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/originweave-evidence/tests/warc_prov_jsonld.rs b/crates/originweave-evidence/tests/warc_prov_jsonld.rs index ed620de41..6fa6afe6a 100644 --- a/crates/originweave-evidence/tests/warc_prov_jsonld.rs +++ b/crates/originweave-evidence/tests/warc_prov_jsonld.rs @@ -60,6 +60,12 @@ fn warc_prov_bundle_exposes_stable_capture_identities_and_standard_errors() { "https://github.com/ContextualWisdomLab/OriginWeave/commit/0123456789abcdef0123456789abcdef01234567" ); assert_eq!(bundle.software_commit_sha(), SOFTWARE_COMMIT_SHA); + + let debug = format!("{bundle:?}"); + assert!(debug.contains(RECORD_ID)); + assert!(!debug.contains("https://example.com/item")); + assert!(!debug.contains(SOURCE_HASH)); + assert!(!debug.contains("hello")); } #[test] From a59e28fb1bb483b241455bf5188c9d2210c2474f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 06:06:02 -0700 Subject: [PATCH 05/29] test(evidence): preserve WARC completeness in PROV --- .../tests/warc_prov_jsonld.rs | 62 ++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/crates/originweave-evidence/tests/warc_prov_jsonld.rs b/crates/originweave-evidence/tests/warc_prov_jsonld.rs index 6fa6afe6a..8df7b4d1e 100644 --- a/crates/originweave-evidence/tests/warc_prov_jsonld.rs +++ b/crates/originweave-evidence/tests/warc_prov_jsonld.rs @@ -2,13 +2,18 @@ use originweave_evidence::{ EvidenceSourceKind, MAX_PROV_SOFTWARE_COMMIT_SHA_BYTES, ProvenanceRecord, VerificationResult, - WarcProvBundle, WarcProvBundleError, WarcResourceRecord, + WarcPayloadCompleteness, WarcProvBundle, WarcProvBundleError, WarcResourceRecord, + WarcTruncationReason, }; const SOURCE_HASH: &str = "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; const RECORD_ID: &str = "urn:uuid:123e4567-e89b-12d3-a456-426614174000"; const DATE: &str = "2026-08-22T12:00:00Z"; const SOFTWARE_COMMIT_SHA: &str = "0123456789abcdef0123456789abcdef01234567"; +const PAYLOAD_COMPLETENESS_IRI: &str = + "tag:contextualwisdomlab.github.io,2026:OriginWeave/warcPayloadCompleteness"; +const TRUNCATION_REASON_IRI: &str = + "tag:contextualwisdomlab.github.io,2026:OriginWeave/warcTruncationReason"; fn resource_record() -> WarcResourceRecord { let provenance = ProvenanceRecord::new( @@ -30,6 +35,27 @@ fn resource_record() -> WarcResourceRecord { .expect("WARC resource record") } +fn resource_record_with_completeness(completeness: WarcPayloadCompleteness) -> WarcResourceRecord { + let provenance = ProvenanceRecord::new( + "https://example.com/item", + "body", + SOURCE_HASH, + EvidenceSourceKind::NetworkResponse, + VerificationResult::Verified, + ) + .expect("verified provenance"); + WarcResourceRecord::new_with_completeness( + RECORD_ID, + DATE, + "https://example.com/item", + "text/plain", + b"hello".to_vec(), + provenance, + completeness, + ) + .expect("WARC resource record") +} + fn assert_standard_error_contract() {} #[test] @@ -114,3 +140,37 @@ fn warc_prov_bundle_rejects_noncanonical_or_oversized_software_revisions() { Err(WarcProvBundleError::LimitExceeded) ); } + +#[test] +fn warc_prov_bundle_preserves_warc_payload_completeness_for_replay() { + let complete = WarcProvBundle::new( + &resource_record_with_completeness(WarcPayloadCompleteness::Complete), + SOFTWARE_COMMIT_SHA, + ) + .expect("complete PROV bundle"); + let complete_json = complete.to_json_ld(); + assert!(complete_json.contains(&format!( + "\"{PAYLOAD_COMPLETENESS_IRI}\":\"complete\"" + ))); + assert!(!complete_json.contains(TRUNCATION_REASON_IRI)); + + for (reason, token) in [ + (WarcTruncationReason::Length, "length"), + (WarcTruncationReason::Time, "time"), + (WarcTruncationReason::Disconnect, "disconnect"), + (WarcTruncationReason::Unspecified, "unspecified"), + ] { + let truncated = WarcProvBundle::new( + &resource_record_with_completeness(WarcPayloadCompleteness::Truncated(reason)), + SOFTWARE_COMMIT_SHA, + ) + .expect("truncated PROV bundle"); + let truncated_json = truncated.to_json_ld(); + assert!(truncated_json.contains(&format!( + "\"{PAYLOAD_COMPLETENESS_IRI}\":\"truncated\"" + ))); + assert!(truncated_json.contains(&format!( + "\"{TRUNCATION_REASON_IRI}\":\"{token}\"" + ))); + } +} \ No newline at end of file From fc4e9167b63198a38681d862c9523fe32baa85c6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 06:08:41 -0700 Subject: [PATCH 06/29] fix(evidence): retain WARC completeness in PROV --- .../src/warc_prov_bundle.rs | 41 +++++++++++++++++-- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/crates/originweave-evidence/src/warc_prov_bundle.rs b/crates/originweave-evidence/src/warc_prov_bundle.rs index 181852078..c7bff1373 100644 --- a/crates/originweave-evidence/src/warc_prov_bundle.rs +++ b/crates/originweave-evidence/src/warc_prov_bundle.rs @@ -1,9 +1,13 @@ use std::fmt; -use crate::WarcResourceRecord; +use crate::{WarcPayloadCompleteness, WarcResourceRecord, WarcTruncationReason}; const ORIGINWEAVE_COMMIT_URL_PREFIX: &str = "https://github.com/ContextualWisdomLab/OriginWeave/commit/"; +const WARC_PAYLOAD_COMPLETENESS_IRI: &str = + "tag:contextualwisdomlab.github.io,2026:OriginWeave/warcPayloadCompleteness"; +const WARC_TRUNCATION_REASON_IRI: &str = + "tag:contextualwisdomlab.github.io,2026:OriginWeave/warcTruncationReason"; /// Exact byte length accepted for a canonical Git SHA-1 software revision. pub const MAX_PROV_SOFTWARE_COMMIT_SHA_BYTES: usize = 40; @@ -30,8 +34,9 @@ impl std::error::Error for WarcProvBundleError {} /// A deterministic PROV-O JSON-LD projection over one validated WARC resource record. /// -/// The bundle contains identifiers, hashes, source location, capture time, and the exact -/// OriginWeave software revision. It deliberately does not retain or emit the WARC payload. +/// The bundle contains identifiers, hashes, source location, capture time, explicit WARC payload +/// completeness, and the exact OriginWeave software revision. It deliberately does not retain or +/// emit the WARC payload. #[derive(Clone, PartialEq, Eq)] pub struct WarcProvBundle { record_entity_id: String, @@ -43,6 +48,7 @@ pub struct WarcProvBundle { source_hash: String, warc_date: String, block_digest: String, + payload_completeness: WarcPayloadCompleteness, } impl fmt::Debug for WarcProvBundle { @@ -53,6 +59,7 @@ impl fmt::Debug for WarcProvBundle { .field("source_entity_id", &self.source_entity_id) .field("capture_activity_id", &self.capture_activity_id) .field("software_agent_id", &self.software_agent_id) + .field("payload_completeness", &self.payload_completeness) .finish_non_exhaustive() } } @@ -88,6 +95,7 @@ impl WarcProvBundle { source_hash: record.provenance().source_hash().to_owned(), warc_date: record.warc_date().to_owned(), block_digest: record.block_digest().to_owned(), + payload_completeness: record.completeness(), }) } @@ -125,10 +133,13 @@ impl WarcProvBundle { /// /// All interpolated values originate from the validated WARC record or the canonical /// lower-case software commit identifier, so no raw payload bytes enter this document. + /// WARC payload completeness is retained as OriginWeave-owned absolute-IRI attributes on the + /// generated record entity; truncated records also retain the exact WARC truncation token. #[must_use] pub fn to_json_ld(&self) -> String { + let completeness_attributes = warc_payload_completeness_attributes(self.payload_completeness); format!( - "{{\"@context\":{{\"prov\":\"http://www.w3.org/ns/prov#\",\"xsd\":\"http://www.w3.org/2001/XMLSchema#\"}},\"@graph\":[{{\"@id\":\"{}\",\"@type\":\"prov:Entity\",\"prov:atLocation\":{{\"@id\":\"{}\"}},\"prov:value\":\"{}\"}},{{\"@id\":\"{}\",\"@type\":\"prov:Activity\",\"prov:startedAtTime\":{{\"@value\":\"{}\",\"@type\":\"xsd:dateTime\"}},\"prov:used\":{{\"@id\":\"{}\"}},\"prov:wasAssociatedWith\":{{\"@id\":\"{}\"}}}},{{\"@id\":\"{}\",\"@type\":\"prov:SoftwareAgent\"}},{{\"@id\":\"{}\",\"@type\":\"prov:Entity\",\"prov:value\":\"{}\",\"prov:wasDerivedFrom\":{{\"@id\":\"{}\"}},\"prov:wasGeneratedBy\":{{\"@id\":\"{}\"}}}}]}}", + "{{\"@context\":{{\"prov\":\"http://www.w3.org/ns/prov#\",\"xsd\":\"http://www.w3.org/2001/XMLSchema#\"}},\"@graph\":[{{\"@id\":\"{}\",\"@type\":\"prov:Entity\",\"prov:atLocation\":{{\"@id\":\"{}\"}},\"prov:value\":\"{}\"}},{{\"@id\":\"{}\",\"@type\":\"prov:Activity\",\"prov:startedAtTime\":{{\"@value\":\"{}\",\"@type\":\"xsd:dateTime\"}},\"prov:used\":{{\"@id\":\"{}\"}},\"prov:wasAssociatedWith\":{{\"@id\":\"{}\"}}}},{{\"@id\":\"{}\",\"@type\":\"prov:SoftwareAgent\"}},{{\"@id\":\"{}\",\"@type\":\"prov:Entity\",\"prov:value\":\"{}\",{},\"prov:wasDerivedFrom\":{{\"@id\":\"{}\"}},\"prov:wasGeneratedBy\":{{\"@id\":\"{}\"}}}}]}}", self.source_entity_id, self.source_url, self.source_hash, @@ -139,12 +150,34 @@ impl WarcProvBundle { self.software_agent_id, self.record_entity_id, self.block_digest, + completeness_attributes, self.source_entity_id, self.capture_activity_id, ) } } +fn warc_payload_completeness_attributes(completeness: WarcPayloadCompleteness) -> String { + match completeness { + WarcPayloadCompleteness::Complete => { + format!("\"{WARC_PAYLOAD_COMPLETENESS_IRI}\":\"complete\"") + } + WarcPayloadCompleteness::Truncated(reason) => format!( + "\"{WARC_PAYLOAD_COMPLETENESS_IRI}\":\"truncated\",\"{WARC_TRUNCATION_REASON_IRI}\":\"{}\"", + warc_truncation_reason_token(reason) + ), + } +} + +const fn warc_truncation_reason_token(reason: WarcTruncationReason) -> &'static str { + match reason { + WarcTruncationReason::Length => "length", + WarcTruncationReason::Time => "time", + WarcTruncationReason::Disconnect => "disconnect", + WarcTruncationReason::Unspecified => "unspecified", + } +} + fn valid_software_commit_sha(software_commit_sha: &str) -> bool { software_commit_sha.len() == MAX_PROV_SOFTWARE_COMMIT_SHA_BYTES && software_commit_sha From 39b29e2620a22e1994f67d764dff2f170066f1a7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 06:09:14 -0700 Subject: [PATCH 07/29] test(evidence): pin completeness-aware PROV output --- crates/originweave-evidence/tests/warc_prov_jsonld.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/originweave-evidence/tests/warc_prov_jsonld.rs b/crates/originweave-evidence/tests/warc_prov_jsonld.rs index 8df7b4d1e..246cb770f 100644 --- a/crates/originweave-evidence/tests/warc_prov_jsonld.rs +++ b/crates/originweave-evidence/tests/warc_prov_jsonld.rs @@ -107,7 +107,7 @@ fn warc_prov_bundle_emits_deterministic_prov_o_json_ld_without_raw_payload() { "{\"@id\":\"urn:uuid:123e4567-e89b-12d3-a456-426614174000#source\",\"@type\":\"prov:Entity\",\"prov:atLocation\":{\"@id\":\"https://example.com/item\"},\"prov:value\":\"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\"},", "{\"@id\":\"urn:uuid:123e4567-e89b-12d3-a456-426614174000#capture\",\"@type\":\"prov:Activity\",\"prov:startedAtTime\":{\"@value\":\"2026-08-22T12:00:00Z\",\"@type\":\"xsd:dateTime\"},\"prov:used\":{\"@id\":\"urn:uuid:123e4567-e89b-12d3-a456-426614174000#source\"},\"prov:wasAssociatedWith\":{\"@id\":\"https://github.com/ContextualWisdomLab/OriginWeave/commit/0123456789abcdef0123456789abcdef01234567\"}},", "{\"@id\":\"https://github.com/ContextualWisdomLab/OriginWeave/commit/0123456789abcdef0123456789abcdef01234567\",\"@type\":\"prov:SoftwareAgent\"},", - "{\"@id\":\"urn:uuid:123e4567-e89b-12d3-a456-426614174000\",\"@type\":\"prov:Entity\",\"prov:value\":\"sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824\",\"prov:wasDerivedFrom\":{\"@id\":\"urn:uuid:123e4567-e89b-12d3-a456-426614174000#source\"},\"prov:wasGeneratedBy\":{\"@id\":\"urn:uuid:123e4567-e89b-12d3-a456-426614174000#capture\"}}", + "{\"@id\":\"urn:uuid:123e4567-e89b-12d3-a456-426614174000\",\"@type\":\"prov:Entity\",\"prov:value\":\"sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824\",\"tag:contextualwisdomlab.github.io,2026:OriginWeave/warcPayloadCompleteness\":\"complete\",\"prov:wasDerivedFrom\":{\"@id\":\"urn:uuid:123e4567-e89b-12d3-a456-426614174000#source\"},\"prov:wasGeneratedBy\":{\"@id\":\"urn:uuid:123e4567-e89b-12d3-a456-426614174000#capture\"}}", "]}" ) ); From 93b2497f258ef3f4b5b830c6adc0a374f12dfd10 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 06:13:28 -0700 Subject: [PATCH 08/29] style(evidence): apply canonical Rust formatting --- crates/originweave-evidence/src/warc_prov_bundle.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/originweave-evidence/src/warc_prov_bundle.rs b/crates/originweave-evidence/src/warc_prov_bundle.rs index c7bff1373..d8e3cfd7a 100644 --- a/crates/originweave-evidence/src/warc_prov_bundle.rs +++ b/crates/originweave-evidence/src/warc_prov_bundle.rs @@ -137,7 +137,8 @@ impl WarcProvBundle { /// generated record entity; truncated records also retain the exact WARC truncation token. #[must_use] pub fn to_json_ld(&self) -> String { - let completeness_attributes = warc_payload_completeness_attributes(self.payload_completeness); + let completeness_attributes = + warc_payload_completeness_attributes(self.payload_completeness); format!( "{{\"@context\":{{\"prov\":\"http://www.w3.org/ns/prov#\",\"xsd\":\"http://www.w3.org/2001/XMLSchema#\"}},\"@graph\":[{{\"@id\":\"{}\",\"@type\":\"prov:Entity\",\"prov:atLocation\":{{\"@id\":\"{}\"}},\"prov:value\":\"{}\"}},{{\"@id\":\"{}\",\"@type\":\"prov:Activity\",\"prov:startedAtTime\":{{\"@value\":\"{}\",\"@type\":\"xsd:dateTime\"}},\"prov:used\":{{\"@id\":\"{}\"}},\"prov:wasAssociatedWith\":{{\"@id\":\"{}\"}}}},{{\"@id\":\"{}\",\"@type\":\"prov:SoftwareAgent\"}},{{\"@id\":\"{}\",\"@type\":\"prov:Entity\",\"prov:value\":\"{}\",{},\"prov:wasDerivedFrom\":{{\"@id\":\"{}\"}},\"prov:wasGeneratedBy\":{{\"@id\":\"{}\"}}}}]}}", self.source_entity_id, From 008f5265ca1250c430aa679fdd6a520f74607646 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 06:14:09 -0700 Subject: [PATCH 09/29] style(evidence): format completeness regression --- .../originweave-evidence/tests/warc_prov_jsonld.rs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/crates/originweave-evidence/tests/warc_prov_jsonld.rs b/crates/originweave-evidence/tests/warc_prov_jsonld.rs index 246cb770f..6892fe06f 100644 --- a/crates/originweave-evidence/tests/warc_prov_jsonld.rs +++ b/crates/originweave-evidence/tests/warc_prov_jsonld.rs @@ -149,9 +149,7 @@ fn warc_prov_bundle_preserves_warc_payload_completeness_for_replay() { ) .expect("complete PROV bundle"); let complete_json = complete.to_json_ld(); - assert!(complete_json.contains(&format!( - "\"{PAYLOAD_COMPLETENESS_IRI}\":\"complete\"" - ))); + assert!(complete_json.contains(&format!("\"{PAYLOAD_COMPLETENESS_IRI}\":\"complete\""))); assert!(!complete_json.contains(TRUNCATION_REASON_IRI)); for (reason, token) in [ @@ -166,11 +164,7 @@ fn warc_prov_bundle_preserves_warc_payload_completeness_for_replay() { ) .expect("truncated PROV bundle"); let truncated_json = truncated.to_json_ld(); - assert!(truncated_json.contains(&format!( - "\"{PAYLOAD_COMPLETENESS_IRI}\":\"truncated\"" - ))); - assert!(truncated_json.contains(&format!( - "\"{TRUNCATION_REASON_IRI}\":\"{token}\"" - ))); + assert!(truncated_json.contains(&format!("\"{PAYLOAD_COMPLETENESS_IRI}\":\"truncated\""))); + assert!(truncated_json.contains(&format!("\"{TRUNCATION_REASON_IRI}\":\"{token}\""))); } -} \ No newline at end of file +} From c9130a24a7c00ad27fb3bd2d88bc49af2d9fd57e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 06:17:57 -0700 Subject: [PATCH 10/29] docs(evidence): record completeness-aware PROV bundle --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49b3342bc..0d905aa82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - 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. - 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. +- Deterministic W3C PROV-O JSON-LD projection over validated WARC resource records, binding exact source and WARC digests, capture time, and OriginWeave software revision while preserving complete-versus-truncated payload state and exact standard WARC truncation reason without embedding raw payload bytes. - 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. @@ -79,4 +80,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 \ No newline at end of file From e22ced727af5c51f71e1f5d0b6fa8a61a5c9ad71 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 06:19:28 -0700 Subject: [PATCH 11/29] docs(evidence): doctor PROV completeness semantics --- docs/doctoring.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/doctoring.md b/docs/doctoring.md index 45bc7e656..988fa8ad9 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -86,6 +86,8 @@ The versioned `ExtractionSchema` is an admission and interpretation contract for WARC 1.1 defines `WARC-Truncated` as explicit record-block completeness evidence with standard reason tokens `length`, `time`, `disconnect`, and `unspecified`; `Content-Length` still reports the bytes actually retained in the truncated block. OriginWeave therefore requires callers to represent completeness explicitly: complete records omit the field, truncated records preserve one typed standard reason, and an oversized retained block remains a limit error rather than being silently truncated. This contract describes evidence already retained by the caller; it does not authorize capture, persistence, or retention. +PROV-O's RDF model remains extensible with domain-specific properties, while JSON-LD 1.1 permits absolute IRIs as property keys. The active WARC-to-PROV adapter therefore carries `WarcPayloadCompleteness` onto the generated record Entity using OriginWeave-owned absolute `tag:` IRIs rather than overloading `prov:value` or inventing a W3C PROV property. Complete records emit only `warcPayloadCompleteness=complete`; truncated records emit `warcPayloadCompleteness=truncated` plus the exact standard WARC truncation reason. These attributes preserve replay evidence; they do not grant capture, persistence, retention, export, replay execution, browser, network, or model authority. + ### AI risk and prompt injection NIST AI 600-1 provides generative-AI lifecycle risk guidance. WASP demonstrates that web-navigation agents can follow low-effort indirect prompt injections. OriginWeave therefore separates trusted instructions, untrusted observations, and protected secrets at type and process boundaries rather than rely on prompting alone. @@ -172,6 +174,8 @@ Web Hypertext Application Technology Working Group. (2026). *URL standard*. http World Wide Web Consortium. (2013). *PROV-O: The PROV ontology*. https://www.w3.org/TR/prov-o/ +World Wide Web Consortium. (2020). *JSON-LD 1.1: A JSON-based serialization for linked data*. https://www.w3.org/TR/json-ld11/ + World Wide Web Consortium. (2026, June 1). *WebDriver BiDi* (W3C Working Draft). https://www.w3.org/TR/2026/WD-webdriver-bidi-20260601/ Xu, J., Sun, Q., Schwendeman, P., Nielsen, S., Cetin, E., & Tang, Y. (2025). *TRINITY: An evolved LLM coordinator* [Preprint]. arXiv. https://doi.org/10.48550/arXiv.2512.04695 From f9a1efb24a922e1a542046314af7e88d732e24c0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 07:06:53 -0700 Subject: [PATCH 12/29] test(evidence): prove PROV binds exact WARC record --- .../tests/warc_prov_jsonld.rs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/crates/originweave-evidence/tests/warc_prov_jsonld.rs b/crates/originweave-evidence/tests/warc_prov_jsonld.rs index 6892fe06f..b5886aa0f 100644 --- a/crates/originweave-evidence/tests/warc_prov_jsonld.rs +++ b/crates/originweave-evidence/tests/warc_prov_jsonld.rs @@ -168,3 +168,42 @@ fn warc_prov_bundle_preserves_warc_payload_completeness_for_replay() { assert!(truncated_json.contains(&format!("\"{TRUNCATION_REASON_IRI}\":\"{token}\""))); } } + +#[test] +fn warc_prov_bundle_distinguishes_distinct_warc_serializations() { + let record_with_content_type = |content_type: &str| { + let provenance = ProvenanceRecord::new( + "https://example.com/item", + "body", + SOURCE_HASH, + EvidenceSourceKind::NetworkResponse, + VerificationResult::Verified, + ) + .expect("verified provenance"); + WarcResourceRecord::new( + RECORD_ID, + DATE, + "https://example.com/item", + content_type, + b"hello".to_vec(), + provenance, + ) + .expect("WARC resource record") + }; + + let text_record = record_with_content_type("text/plain"); + let binary_record = record_with_content_type("application/octet-stream"); + assert_ne!(text_record.to_warc_bytes(), binary_record.to_warc_bytes()); + + let text_prov = WarcProvBundle::new(&text_record, SOFTWARE_COMMIT_SHA) + .expect("text PROV bundle") + .to_json_ld(); + let binary_prov = WarcProvBundle::new(&binary_record, SOFTWARE_COMMIT_SHA) + .expect("binary PROV bundle") + .to_json_ld(); + + assert_ne!( + text_prov, binary_prov, + "provenance must distinguish WARC records whose serialized headers differ" + ); +} From 18a30960160ec4ade81bae6dca5c262a0883987a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 07:09:38 -0700 Subject: [PATCH 13/29] fix(evidence): bind PROV to serialized WARC record --- .../src/warc_prov_bundle.rs | 33 +++++++++++++++---- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/crates/originweave-evidence/src/warc_prov_bundle.rs b/crates/originweave-evidence/src/warc_prov_bundle.rs index d8e3cfd7a..cfe543c59 100644 --- a/crates/originweave-evidence/src/warc_prov_bundle.rs +++ b/crates/originweave-evidence/src/warc_prov_bundle.rs @@ -1,9 +1,13 @@ use std::fmt; +use sha2::{Digest, Sha256}; + use crate::{WarcPayloadCompleteness, WarcResourceRecord, WarcTruncationReason}; const ORIGINWEAVE_COMMIT_URL_PREFIX: &str = "https://github.com/ContextualWisdomLab/OriginWeave/commit/"; +const WARC_RECORD_DIGEST_IRI: &str = + "tag:contextualwisdomlab.github.io,2026:OriginWeave/warcRecordDigest"; const WARC_PAYLOAD_COMPLETENESS_IRI: &str = "tag:contextualwisdomlab.github.io,2026:OriginWeave/warcPayloadCompleteness"; const WARC_TRUNCATION_REASON_IRI: &str = @@ -34,9 +38,9 @@ impl std::error::Error for WarcProvBundleError {} /// A deterministic PROV-O JSON-LD projection over one validated WARC resource record. /// -/// The bundle contains identifiers, hashes, source location, capture time, explicit WARC payload -/// completeness, and the exact OriginWeave software revision. It deliberately does not retain or -/// emit the WARC payload. +/// The bundle contains identifiers, source and record hashes, source location, capture time, +/// explicit WARC payload completeness, and the exact OriginWeave software revision. It +/// deliberately does not retain or emit the WARC payload. #[derive(Clone, PartialEq, Eq)] pub struct WarcProvBundle { record_entity_id: String, @@ -48,6 +52,7 @@ pub struct WarcProvBundle { source_hash: String, warc_date: String, block_digest: String, + warc_record_digest: String, payload_completeness: WarcPayloadCompleteness, } @@ -84,6 +89,7 @@ impl WarcProvBundle { let source_entity_id = format!("{}#source", record.record_id()); let capture_activity_id = format!("{}#capture", record.record_id()); let software_agent_id = format!("{ORIGINWEAVE_COMMIT_URL_PREFIX}{software_commit_sha}"); + let warc_record_digest = sha256_digest(&record.to_warc_bytes()); Ok(Self { record_entity_id, @@ -95,6 +101,7 @@ impl WarcProvBundle { source_hash: record.provenance().source_hash().to_owned(), warc_date: record.warc_date().to_owned(), block_digest: record.block_digest().to_owned(), + warc_record_digest, payload_completeness: record.completeness(), }) } @@ -132,15 +139,17 @@ impl WarcProvBundle { /// Serialize the bundle as deterministic compact W3C PROV-O JSON-LD. /// /// All interpolated values originate from the validated WARC record or the canonical - /// lower-case software commit identifier, so no raw payload bytes enter this document. - /// WARC payload completeness is retained as OriginWeave-owned absolute-IRI attributes on the - /// generated record entity; truncated records also retain the exact WARC truncation token. + /// lower-case software commit identifier, so no raw payload bytes enter this document. The + /// payload block digest binds the retained resource bytes while `warcRecordDigest` binds the + /// complete deterministic WARC serialization, including its headers. WARC payload completeness + /// is retained as an OriginWeave-owned absolute-IRI attribute; truncated records also retain + /// the exact WARC truncation token. #[must_use] pub fn to_json_ld(&self) -> String { let completeness_attributes = warc_payload_completeness_attributes(self.payload_completeness); format!( - "{{\"@context\":{{\"prov\":\"http://www.w3.org/ns/prov#\",\"xsd\":\"http://www.w3.org/2001/XMLSchema#\"}},\"@graph\":[{{\"@id\":\"{}\",\"@type\":\"prov:Entity\",\"prov:atLocation\":{{\"@id\":\"{}\"}},\"prov:value\":\"{}\"}},{{\"@id\":\"{}\",\"@type\":\"prov:Activity\",\"prov:startedAtTime\":{{\"@value\":\"{}\",\"@type\":\"xsd:dateTime\"}},\"prov:used\":{{\"@id\":\"{}\"}},\"prov:wasAssociatedWith\":{{\"@id\":\"{}\"}}}},{{\"@id\":\"{}\",\"@type\":\"prov:SoftwareAgent\"}},{{\"@id\":\"{}\",\"@type\":\"prov:Entity\",\"prov:value\":\"{}\",{},\"prov:wasDerivedFrom\":{{\"@id\":\"{}\"}},\"prov:wasGeneratedBy\":{{\"@id\":\"{}\"}}}}]}}", + "{{\"@context\":{{\"prov\":\"http://www.w3.org/ns/prov#\",\"xsd\":\"http://www.w3.org/2001/XMLSchema#\"}},\"@graph\":[{{\"@id\":\"{}\",\"@type\":\"prov:Entity\",\"prov:atLocation\":{{\"@id\":\"{}\"}},\"prov:value\":\"{}\"}},{{\"@id\":\"{}\",\"@type\":\"prov:Activity\",\"prov:startedAtTime\":{{\"@value\":\"{}\",\"@type\":\"xsd:dateTime\"}},\"prov:used\":{{\"@id\":\"{}\"}},\"prov:wasAssociatedWith\":{{\"@id\":\"{}\"}}}},{{\"@id\":\"{}\",\"@type\":\"prov:SoftwareAgent\"}},{{\"@id\":\"{}\",\"@type\":\"prov:Entity\",\"prov:value\":\"{}\",\"{WARC_RECORD_DIGEST_IRI}\":\"{}\",{},\"prov:wasDerivedFrom\":{{\"@id\":\"{}\"}},\"prov:wasGeneratedBy\":{{\"@id\":\"{}\"}}}}]}}", self.source_entity_id, self.source_url, self.source_hash, @@ -151,6 +160,7 @@ impl WarcProvBundle { self.software_agent_id, self.record_entity_id, self.block_digest, + self.warc_record_digest, completeness_attributes, self.source_entity_id, self.capture_activity_id, @@ -179,6 +189,15 @@ const fn warc_truncation_reason_token(reason: WarcTruncationReason) -> &'static } } +fn sha256_digest(bytes: &[u8]) -> String { + let digest = Sha256::digest(bytes); + let mut encoded = String::from("sha256:"); + for byte in digest { + encoded.push_str(&format!("{byte:02x}")); + } + encoded +} + fn valid_software_commit_sha(software_commit_sha: &str) -> bool { software_commit_sha.len() == MAX_PROV_SOFTWARE_COMMIT_SHA_BYTES && software_commit_sha From 72a91bb2beb9ff166996372f40c5078a4da2fd6a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 07:10:29 -0700 Subject: [PATCH 14/29] test(evidence): pin serialized WARC digest in PROV --- crates/originweave-evidence/tests/warc_prov_jsonld.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/originweave-evidence/tests/warc_prov_jsonld.rs b/crates/originweave-evidence/tests/warc_prov_jsonld.rs index b5886aa0f..06acfb67a 100644 --- a/crates/originweave-evidence/tests/warc_prov_jsonld.rs +++ b/crates/originweave-evidence/tests/warc_prov_jsonld.rs @@ -10,6 +10,8 @@ const SOURCE_HASH: &str = "sha256:0123456789abcdef0123456789abcdef0123456789abcd const RECORD_ID: &str = "urn:uuid:123e4567-e89b-12d3-a456-426614174000"; const DATE: &str = "2026-08-22T12:00:00Z"; const SOFTWARE_COMMIT_SHA: &str = "0123456789abcdef0123456789abcdef01234567"; +const WARC_RECORD_DIGEST_IRI: &str = + "tag:contextualwisdomlab.github.io,2026:OriginWeave/warcRecordDigest"; const PAYLOAD_COMPLETENESS_IRI: &str = "tag:contextualwisdomlab.github.io,2026:OriginWeave/warcPayloadCompleteness"; const TRUNCATION_REASON_IRI: &str = @@ -107,7 +109,7 @@ fn warc_prov_bundle_emits_deterministic_prov_o_json_ld_without_raw_payload() { "{\"@id\":\"urn:uuid:123e4567-e89b-12d3-a456-426614174000#source\",\"@type\":\"prov:Entity\",\"prov:atLocation\":{\"@id\":\"https://example.com/item\"},\"prov:value\":\"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\"},", "{\"@id\":\"urn:uuid:123e4567-e89b-12d3-a456-426614174000#capture\",\"@type\":\"prov:Activity\",\"prov:startedAtTime\":{\"@value\":\"2026-08-22T12:00:00Z\",\"@type\":\"xsd:dateTime\"},\"prov:used\":{\"@id\":\"urn:uuid:123e4567-e89b-12d3-a456-426614174000#source\"},\"prov:wasAssociatedWith\":{\"@id\":\"https://github.com/ContextualWisdomLab/OriginWeave/commit/0123456789abcdef0123456789abcdef01234567\"}},", "{\"@id\":\"https://github.com/ContextualWisdomLab/OriginWeave/commit/0123456789abcdef0123456789abcdef01234567\",\"@type\":\"prov:SoftwareAgent\"},", - "{\"@id\":\"urn:uuid:123e4567-e89b-12d3-a456-426614174000\",\"@type\":\"prov:Entity\",\"prov:value\":\"sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824\",\"tag:contextualwisdomlab.github.io,2026:OriginWeave/warcPayloadCompleteness\":\"complete\",\"prov:wasDerivedFrom\":{\"@id\":\"urn:uuid:123e4567-e89b-12d3-a456-426614174000#source\"},\"prov:wasGeneratedBy\":{\"@id\":\"urn:uuid:123e4567-e89b-12d3-a456-426614174000#capture\"}}", + "{\"@id\":\"urn:uuid:123e4567-e89b-12d3-a456-426614174000\",\"@type\":\"prov:Entity\",\"prov:value\":\"sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824\",\"tag:contextualwisdomlab.github.io,2026:OriginWeave/warcRecordDigest\":\"sha256:b6ea360a1ec548527ff5ed9c03966b05c8afd5c2b882bee259e362effb0fe0a8\",\"tag:contextualwisdomlab.github.io,2026:OriginWeave/warcPayloadCompleteness\":\"complete\",\"prov:wasDerivedFrom\":{\"@id\":\"urn:uuid:123e4567-e89b-12d3-a456-426614174000#source\"},\"prov:wasGeneratedBy\":{\"@id\":\"urn:uuid:123e4567-e89b-12d3-a456-426614174000#capture\"}}", "]}" ) ); @@ -206,4 +208,10 @@ fn warc_prov_bundle_distinguishes_distinct_warc_serializations() { text_prov, binary_prov, "provenance must distinguish WARC records whose serialized headers differ" ); + assert!(text_prov.contains(&format!( + "\"{WARC_RECORD_DIGEST_IRI}\":\"sha256:b6ea360a1ec548527ff5ed9c03966b05c8afd5c2b882bee259e362effb0fe0a8\"" + ))); + assert!(binary_prov.contains(&format!( + "\"{WARC_RECORD_DIGEST_IRI}\":\"sha256:9c59979535d4a1b3589c0fe2d17837c4ddb0e4cf911854d9aae362903ff83db9\"" + ))); } From d02ed44a408831f4a59e048e3a9ba9b86e0c189b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 15:04:10 -0700 Subject: [PATCH 15/29] test(evidence): require offline WARC PROV verification --- .../tests/warc_prov_offline_verification.rs | 184 ++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 crates/originweave-evidence/tests/warc_prov_offline_verification.rs diff --git a/crates/originweave-evidence/tests/warc_prov_offline_verification.rs b/crates/originweave-evidence/tests/warc_prov_offline_verification.rs new file mode 100644 index 000000000..5646b60a9 --- /dev/null +++ b/crates/originweave-evidence/tests/warc_prov_offline_verification.rs @@ -0,0 +1,184 @@ +#![allow(clippy::expect_used)] + +use originweave_evidence::{ + EvidenceSourceKind, ProvenanceRecord, VerificationResult, WarcPayloadCompleteness, + WarcProvBundle, WarcProvBundleVerificationError, WarcResourceRecord, WarcTruncationReason, +}; + +const SOURCE_HASH: &str = "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; +const OTHER_SOURCE_HASH: &str = + "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const RECORD_ID: &str = "urn:uuid:123e4567-e89b-12d3-a456-426614174000"; +const OTHER_RECORD_ID: &str = "urn:uuid:123e4567-e89b-12d3-a456-426614174001"; +const DATE: &str = "2026-08-22T12:00:00Z"; +const OTHER_DATE: &str = "2026-08-22T12:00:01Z"; +const SOURCE_URL: &str = "https://example.com/item"; +const OTHER_SOURCE_URL: &str = "https://example.com/other"; +const SOFTWARE_COMMIT_SHA: &str = "0123456789abcdef0123456789abcdef01234567"; + +fn record( + record_id: &str, + date: &str, + source_url: &str, + source_hash: &str, + content_type: &str, + payload: &[u8], + completeness: WarcPayloadCompleteness, +) -> WarcResourceRecord { + let provenance = ProvenanceRecord::new( + source_url, + "body", + source_hash, + EvidenceSourceKind::NetworkResponse, + VerificationResult::Verified, + ) + .expect("verified provenance"); + WarcResourceRecord::new_with_completeness( + record_id, + date, + source_url, + content_type, + payload.to_vec(), + provenance, + completeness, + ) + .expect("WARC resource record") +} + +fn baseline_record() -> WarcResourceRecord { + record( + RECORD_ID, + DATE, + SOURCE_URL, + SOURCE_HASH, + "text/plain", + b"hello", + WarcPayloadCompleteness::Complete, + ) +} + +fn assert_standard_error_contract() {} + +#[test] +fn warc_prov_bundle_offline_verification_accepts_only_the_exact_bound_record() { + assert_standard_error_contract::(); + assert_eq!( + WarcProvBundleVerificationError::RecordIdentityMismatch.to_string(), + "WARC PROV record identity does not match" + ); + assert_eq!( + WarcProvBundleVerificationError::SourceEvidenceMismatch.to_string(), + "WARC PROV source evidence does not match" + ); + assert_eq!( + WarcProvBundleVerificationError::CaptureTimeMismatch.to_string(), + "WARC PROV capture time does not match" + ); + assert_eq!( + WarcProvBundleVerificationError::PayloadDigestMismatch.to_string(), + "WARC PROV payload digest does not match" + ); + assert_eq!( + WarcProvBundleVerificationError::PayloadCompletenessMismatch.to_string(), + "WARC PROV payload completeness does not match" + ); + assert_eq!( + WarcProvBundleVerificationError::WarcRecordDigestMismatch.to_string(), + "WARC PROV serialized record digest does not match" + ); + + let exact = baseline_record(); + let bundle = WarcProvBundle::new(&exact, SOFTWARE_COMMIT_SHA).expect("PROV bundle"); + assert_eq!(bundle.verify_record(&exact), Ok(())); + + let mismatches = [ + ( + record( + OTHER_RECORD_ID, + DATE, + SOURCE_URL, + SOURCE_HASH, + "text/plain", + b"hello", + WarcPayloadCompleteness::Complete, + ), + WarcProvBundleVerificationError::RecordIdentityMismatch, + ), + ( + record( + RECORD_ID, + DATE, + OTHER_SOURCE_URL, + SOURCE_HASH, + "text/plain", + b"hello", + WarcPayloadCompleteness::Complete, + ), + WarcProvBundleVerificationError::SourceEvidenceMismatch, + ), + ( + record( + RECORD_ID, + DATE, + SOURCE_URL, + OTHER_SOURCE_HASH, + "text/plain", + b"hello", + WarcPayloadCompleteness::Complete, + ), + WarcProvBundleVerificationError::SourceEvidenceMismatch, + ), + ( + record( + RECORD_ID, + OTHER_DATE, + SOURCE_URL, + SOURCE_HASH, + "text/plain", + b"hello", + WarcPayloadCompleteness::Complete, + ), + WarcProvBundleVerificationError::CaptureTimeMismatch, + ), + ( + record( + RECORD_ID, + DATE, + SOURCE_URL, + SOURCE_HASH, + "text/plain", + b"world", + WarcPayloadCompleteness::Complete, + ), + WarcProvBundleVerificationError::PayloadDigestMismatch, + ), + ( + record( + RECORD_ID, + DATE, + SOURCE_URL, + SOURCE_HASH, + "text/plain", + b"hello", + WarcPayloadCompleteness::Truncated(WarcTruncationReason::Length), + ), + WarcProvBundleVerificationError::PayloadCompletenessMismatch, + ), + ( + record( + RECORD_ID, + DATE, + SOURCE_URL, + SOURCE_HASH, + "application/octet-stream", + b"hello", + WarcPayloadCompleteness::Complete, + ), + WarcProvBundleVerificationError::WarcRecordDigestMismatch, + ), + ]; + + for (candidate, expected) in mismatches { + assert_eq!(bundle.verify_record(&candidate), Err(expected)); + } +} From 2102cf3a1512509e4a1297b079c9bcfade8699f5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 15:05:40 -0700 Subject: [PATCH 16/29] feat(evidence): verify WARC PROV bindings offline --- .../src/warc_prov_bundle.rs | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/crates/originweave-evidence/src/warc_prov_bundle.rs b/crates/originweave-evidence/src/warc_prov_bundle.rs index cfe543c59..394670b1e 100644 --- a/crates/originweave-evidence/src/warc_prov_bundle.rs +++ b/crates/originweave-evidence/src/warc_prov_bundle.rs @@ -36,6 +36,38 @@ impl fmt::Display for WarcProvBundleError { impl std::error::Error for WarcProvBundleError {} +/// A deterministic offline verification failure between a PROV bundle and a WARC record. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum WarcProvBundleVerificationError { + /// The WARC record identifier differs from the identifier bound into the PROV bundle. + RecordIdentityMismatch, + /// The independently verified source URL or source digest differs from the PROV bundle. + SourceEvidenceMismatch, + /// The WARC capture timestamp differs from the timestamp bound into the PROV bundle. + CaptureTimeMismatch, + /// The retained WARC payload digest differs from the digest bound into the PROV bundle. + PayloadDigestMismatch, + /// The WARC complete-versus-truncated state differs from the state bound into the bundle. + PayloadCompletenessMismatch, + /// The digest of the deterministic WARC serialization differs from the bundle binding. + WarcRecordDigestMismatch, +} + +impl fmt::Display for WarcProvBundleVerificationError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(match self { + Self::RecordIdentityMismatch => "WARC PROV record identity does not match", + Self::SourceEvidenceMismatch => "WARC PROV source evidence does not match", + Self::CaptureTimeMismatch => "WARC PROV capture time does not match", + Self::PayloadDigestMismatch => "WARC PROV payload digest does not match", + Self::PayloadCompletenessMismatch => "WARC PROV payload completeness does not match", + Self::WarcRecordDigestMismatch => "WARC PROV serialized record digest does not match", + }) + } +} + +impl std::error::Error for WarcProvBundleVerificationError {} + /// A deterministic PROV-O JSON-LD projection over one validated WARC resource record. /// /// The bundle contains identifiers, source and record hashes, source location, capture time, @@ -136,6 +168,38 @@ impl WarcProvBundle { &self.software_commit_sha } + /// Verify offline that one validated WARC record is exactly the record bound by this bundle. + /// + /// Verification is deterministic and performs no network, DNS, browser, model, persistence, + /// or authority operation. A matching digest proves byte identity only; it does not + /// authenticate the actor that produced either value or establish factual correctness. + pub fn verify_record( + &self, + record: &WarcResourceRecord, + ) -> Result<(), WarcProvBundleVerificationError> { + if self.record_entity_id != record.record_id() { + return Err(WarcProvBundleVerificationError::RecordIdentityMismatch); + } + if self.source_url != record.provenance().source_url() + || self.source_hash != record.provenance().source_hash() + { + return Err(WarcProvBundleVerificationError::SourceEvidenceMismatch); + } + if self.warc_date != record.warc_date() { + return Err(WarcProvBundleVerificationError::CaptureTimeMismatch); + } + if self.block_digest != record.block_digest() { + return Err(WarcProvBundleVerificationError::PayloadDigestMismatch); + } + if self.payload_completeness != record.completeness() { + return Err(WarcProvBundleVerificationError::PayloadCompletenessMismatch); + } + if self.warc_record_digest != sha256_digest(&record.to_warc_bytes()) { + return Err(WarcProvBundleVerificationError::WarcRecordDigestMismatch); + } + Ok(()) + } + /// Serialize the bundle as deterministic compact W3C PROV-O JSON-LD. /// /// All interpolated values originate from the validated WARC record or the canonical From 8d158b7f9ca9d5670cb74d32f2ac2898af9c3e98 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 15:06:22 -0700 Subject: [PATCH 17/29] feat(evidence): export offline WARC PROV verification error --- crates/originweave-evidence/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/originweave-evidence/src/lib.rs b/crates/originweave-evidence/src/lib.rs index d89c26d69..313f629d4 100644 --- a/crates/originweave-evidence/src/lib.rs +++ b/crates/originweave-evidence/src/lib.rs @@ -24,6 +24,7 @@ pub use sensitive_access::{ }; pub use warc_prov_bundle::{ MAX_PROV_SOFTWARE_COMMIT_SHA_BYTES, WarcProvBundle, WarcProvBundleError, + WarcProvBundleVerificationError, }; pub use warc_resource_record::{ MAX_WARC_CONTENT_TYPE_BYTES, MAX_WARC_DATE_BYTES, MAX_WARC_PAYLOAD_BYTES, From 080d0020aeff90eeecf9438dac604ec6da050971 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 15:09:55 -0700 Subject: [PATCH 18/29] docs(changelog): record offline WARC PROV verification --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d905aa82..a91615822 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - 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. - 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. -- Deterministic W3C PROV-O JSON-LD projection over validated WARC resource records, binding exact source and WARC digests, capture time, and OriginWeave software revision while preserving complete-versus-truncated payload state and exact standard WARC truncation reason without embedding raw payload bytes. +- Deterministic W3C PROV-O JSON-LD projection over validated WARC resource records, binding exact source and WARC digests, capture time, and OriginWeave software revision while preserving complete-versus-truncated payload state and exact standard WARC truncation reason without embedding raw payload bytes, with deterministic offline exact-record verification that fails closed on identity, source-evidence, timestamp, payload-digest, completeness, or serialized-record drift. - 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. From 22dbd913fcf8c574b9333bb261e014acbd2624f7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 15:12:09 -0700 Subject: [PATCH 19/29] test(evidence): require exact WARC provenance binding --- .../tests/warc_prov_offline_verification.rs | 58 ++++++++++++++++++- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/crates/originweave-evidence/tests/warc_prov_offline_verification.rs b/crates/originweave-evidence/tests/warc_prov_offline_verification.rs index 5646b60a9..9e643cbb4 100644 --- a/crates/originweave-evidence/tests/warc_prov_offline_verification.rs +++ b/crates/originweave-evidence/tests/warc_prov_offline_verification.rs @@ -16,20 +16,22 @@ const SOURCE_URL: &str = "https://example.com/item"; const OTHER_SOURCE_URL: &str = "https://example.com/other"; const SOFTWARE_COMMIT_SHA: &str = "0123456789abcdef0123456789abcdef01234567"; -fn record( +fn record_with_provenance( record_id: &str, date: &str, source_url: &str, + source_locator: &str, source_hash: &str, + source_kind: EvidenceSourceKind, content_type: &str, payload: &[u8], completeness: WarcPayloadCompleteness, ) -> WarcResourceRecord { let provenance = ProvenanceRecord::new( source_url, - "body", + source_locator, source_hash, - EvidenceSourceKind::NetworkResponse, + source_kind, VerificationResult::Verified, ) .expect("verified provenance"); @@ -45,6 +47,28 @@ fn record( .expect("WARC resource record") } +fn record( + record_id: &str, + date: &str, + source_url: &str, + source_hash: &str, + content_type: &str, + payload: &[u8], + completeness: WarcPayloadCompleteness, +) -> WarcResourceRecord { + record_with_provenance( + record_id, + date, + source_url, + "body", + source_hash, + EvidenceSourceKind::NetworkResponse, + content_type, + payload, + completeness, + ) +} + fn baseline_record() -> WarcResourceRecord { record( RECORD_ID, @@ -128,6 +152,34 @@ fn warc_prov_bundle_offline_verification_accepts_only_the_exact_bound_record() { ), WarcProvBundleVerificationError::SourceEvidenceMismatch, ), + ( + record_with_provenance( + RECORD_ID, + DATE, + SOURCE_URL, + "different-body", + SOURCE_HASH, + EvidenceSourceKind::NetworkResponse, + "text/plain", + b"hello", + WarcPayloadCompleteness::Complete, + ), + WarcProvBundleVerificationError::SourceEvidenceMismatch, + ), + ( + record_with_provenance( + RECORD_ID, + DATE, + SOURCE_URL, + "body", + SOURCE_HASH, + EvidenceSourceKind::StructuredData, + "text/plain", + b"hello", + WarcPayloadCompleteness::Complete, + ), + WarcProvBundleVerificationError::SourceEvidenceMismatch, + ), ( record( RECORD_ID, From a6e61c1a4f97e2666585cc3ef94a89e3a2242aba Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 15:14:31 -0700 Subject: [PATCH 20/29] fix(evidence): bind offline PROV verification to exact source evidence --- .../src/warc_prov_bundle.rs | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/crates/originweave-evidence/src/warc_prov_bundle.rs b/crates/originweave-evidence/src/warc_prov_bundle.rs index 394670b1e..64a7abb5c 100644 --- a/crates/originweave-evidence/src/warc_prov_bundle.rs +++ b/crates/originweave-evidence/src/warc_prov_bundle.rs @@ -2,7 +2,9 @@ use std::fmt; use sha2::{Digest, Sha256}; -use crate::{WarcPayloadCompleteness, WarcResourceRecord, WarcTruncationReason}; +use crate::{ + ProvenanceRecord, WarcPayloadCompleteness, WarcResourceRecord, WarcTruncationReason, +}; const ORIGINWEAVE_COMMIT_URL_PREFIX: &str = "https://github.com/ContextualWisdomLab/OriginWeave/commit/"; @@ -41,7 +43,7 @@ impl std::error::Error for WarcProvBundleError {} pub enum WarcProvBundleVerificationError { /// The WARC record identifier differs from the identifier bound into the PROV bundle. RecordIdentityMismatch, - /// The independently verified source URL or source digest differs from the PROV bundle. + /// The independently verified source provenance differs from the provenance bound into the bundle. SourceEvidenceMismatch, /// The WARC capture timestamp differs from the timestamp bound into the PROV bundle. CaptureTimeMismatch, @@ -70,9 +72,9 @@ impl std::error::Error for WarcProvBundleVerificationError {} /// A deterministic PROV-O JSON-LD projection over one validated WARC resource record. /// -/// The bundle contains identifiers, source and record hashes, source location, capture time, -/// explicit WARC payload completeness, and the exact OriginWeave software revision. It -/// deliberately does not retain or emit the WARC payload. +/// The bundle contains identifiers, exact validated source provenance, record hashes, source +/// location, capture time, explicit WARC payload completeness, and the exact OriginWeave software +/// revision. It deliberately does not retain or emit the WARC payload. #[derive(Clone, PartialEq, Eq)] pub struct WarcProvBundle { record_entity_id: String, @@ -80,8 +82,7 @@ pub struct WarcProvBundle { capture_activity_id: String, software_agent_id: String, software_commit_sha: String, - source_url: String, - source_hash: String, + source_provenance: ProvenanceRecord, warc_date: String, block_digest: String, warc_record_digest: String, @@ -129,8 +130,7 @@ impl WarcProvBundle { capture_activity_id, software_agent_id, software_commit_sha: software_commit_sha.to_owned(), - source_url: record.provenance().source_url().to_owned(), - source_hash: record.provenance().source_hash().to_owned(), + source_provenance: record.provenance().clone(), warc_date: record.warc_date().to_owned(), block_digest: record.block_digest().to_owned(), warc_record_digest, @@ -170,9 +170,11 @@ impl WarcProvBundle { /// Verify offline that one validated WARC record is exactly the record bound by this bundle. /// - /// Verification is deterministic and performs no network, DNS, browser, model, persistence, - /// or authority operation. A matching digest proves byte identity only; it does not - /// authenticate the actor that produced either value or establish factual correctness. + /// Verification includes the complete validated [`ProvenanceRecord`] rather than only the + /// source URL and digest, so locator or evidence-channel drift cannot collapse into a match. + /// It is deterministic and performs no network, DNS, browser, model, persistence, or authority + /// operation. A matching digest proves byte identity only; it does not authenticate the actor + /// that produced either value or establish factual correctness. pub fn verify_record( &self, record: &WarcResourceRecord, @@ -180,9 +182,7 @@ impl WarcProvBundle { if self.record_entity_id != record.record_id() { return Err(WarcProvBundleVerificationError::RecordIdentityMismatch); } - if self.source_url != record.provenance().source_url() - || self.source_hash != record.provenance().source_hash() - { + if self.source_provenance != *record.provenance() { return Err(WarcProvBundleVerificationError::SourceEvidenceMismatch); } if self.warc_date != record.warc_date() { @@ -207,7 +207,8 @@ impl WarcProvBundle { /// payload block digest binds the retained resource bytes while `warcRecordDigest` binds the /// complete deterministic WARC serialization, including its headers. WARC payload completeness /// is retained as an OriginWeave-owned absolute-IRI attribute; truncated records also retain - /// the exact WARC truncation token. + /// the exact WARC truncation token. The JSON-LD projection exposes source URL and digest while + /// offline verification additionally preserves the exact validated source locator and channel. #[must_use] pub fn to_json_ld(&self) -> String { let completeness_attributes = @@ -215,8 +216,8 @@ impl WarcProvBundle { format!( "{{\"@context\":{{\"prov\":\"http://www.w3.org/ns/prov#\",\"xsd\":\"http://www.w3.org/2001/XMLSchema#\"}},\"@graph\":[{{\"@id\":\"{}\",\"@type\":\"prov:Entity\",\"prov:atLocation\":{{\"@id\":\"{}\"}},\"prov:value\":\"{}\"}},{{\"@id\":\"{}\",\"@type\":\"prov:Activity\",\"prov:startedAtTime\":{{\"@value\":\"{}\",\"@type\":\"xsd:dateTime\"}},\"prov:used\":{{\"@id\":\"{}\"}},\"prov:wasAssociatedWith\":{{\"@id\":\"{}\"}}}},{{\"@id\":\"{}\",\"@type\":\"prov:SoftwareAgent\"}},{{\"@id\":\"{}\",\"@type\":\"prov:Entity\",\"prov:value\":\"{}\",\"{WARC_RECORD_DIGEST_IRI}\":\"{}\",{},\"prov:wasDerivedFrom\":{{\"@id\":\"{}\"}},\"prov:wasGeneratedBy\":{{\"@id\":\"{}\"}}}}]}}", self.source_entity_id, - self.source_url, - self.source_hash, + self.source_provenance.source_url(), + self.source_provenance.source_hash(), self.capture_activity_id, self.warc_date, self.source_entity_id, From 3af43d927c5bfe732ca930fd575062c8ab4aaf7e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 15:15:42 -0700 Subject: [PATCH 21/29] style(evidence): apply canonical rustfmt --- crates/originweave-evidence/src/warc_prov_bundle.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/originweave-evidence/src/warc_prov_bundle.rs b/crates/originweave-evidence/src/warc_prov_bundle.rs index 64a7abb5c..fa7ae4023 100644 --- a/crates/originweave-evidence/src/warc_prov_bundle.rs +++ b/crates/originweave-evidence/src/warc_prov_bundle.rs @@ -2,9 +2,7 @@ use std::fmt; use sha2::{Digest, Sha256}; -use crate::{ - ProvenanceRecord, WarcPayloadCompleteness, WarcResourceRecord, WarcTruncationReason, -}; +use crate::{ProvenanceRecord, WarcPayloadCompleteness, WarcResourceRecord, WarcTruncationReason}; const ORIGINWEAVE_COMMIT_URL_PREFIX: &str = "https://github.com/ContextualWisdomLab/OriginWeave/commit/"; From 0f781cda4dd545ae38a2d0a0398bbd1b93c89c3e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 15:18:02 -0700 Subject: [PATCH 22/29] test(evidence): keep provenance regression clippy-clean --- .../tests/warc_prov_offline_verification.rs | 67 +++++++------------ 1 file changed, 25 insertions(+), 42 deletions(-) diff --git a/crates/originweave-evidence/tests/warc_prov_offline_verification.rs b/crates/originweave-evidence/tests/warc_prov_offline_verification.rs index 9e643cbb4..280201089 100644 --- a/crates/originweave-evidence/tests/warc_prov_offline_verification.rs +++ b/crates/originweave-evidence/tests/warc_prov_offline_verification.rs @@ -16,22 +16,20 @@ const SOURCE_URL: &str = "https://example.com/item"; const OTHER_SOURCE_URL: &str = "https://example.com/other"; const SOFTWARE_COMMIT_SHA: &str = "0123456789abcdef0123456789abcdef01234567"; -fn record_with_provenance( +fn record( record_id: &str, date: &str, source_url: &str, - source_locator: &str, source_hash: &str, - source_kind: EvidenceSourceKind, content_type: &str, payload: &[u8], completeness: WarcPayloadCompleteness, ) -> WarcResourceRecord { let provenance = ProvenanceRecord::new( source_url, - source_locator, + "body", source_hash, - source_kind, + EvidenceSourceKind::NetworkResponse, VerificationResult::Verified, ) .expect("verified provenance"); @@ -47,26 +45,28 @@ fn record_with_provenance( .expect("WARC resource record") } -fn record( - record_id: &str, - date: &str, - source_url: &str, - source_hash: &str, - content_type: &str, - payload: &[u8], - completeness: WarcPayloadCompleteness, +fn baseline_record_with_source_evidence( + source_locator: &str, + source_kind: EvidenceSourceKind, ) -> WarcResourceRecord { - record_with_provenance( - record_id, - date, - source_url, - "body", - source_hash, - EvidenceSourceKind::NetworkResponse, - content_type, - payload, - completeness, + let provenance = ProvenanceRecord::new( + SOURCE_URL, + source_locator, + SOURCE_HASH, + source_kind, + VerificationResult::Verified, + ) + .expect("verified provenance"); + WarcResourceRecord::new_with_completeness( + RECORD_ID, + DATE, + SOURCE_URL, + "text/plain", + b"hello".to_vec(), + provenance, + WarcPayloadCompleteness::Complete, ) + .expect("WARC resource record") } fn baseline_record() -> WarcResourceRecord { @@ -153,31 +153,14 @@ fn warc_prov_bundle_offline_verification_accepts_only_the_exact_bound_record() { WarcProvBundleVerificationError::SourceEvidenceMismatch, ), ( - record_with_provenance( - RECORD_ID, - DATE, - SOURCE_URL, + baseline_record_with_source_evidence( "different-body", - SOURCE_HASH, EvidenceSourceKind::NetworkResponse, - "text/plain", - b"hello", - WarcPayloadCompleteness::Complete, ), WarcProvBundleVerificationError::SourceEvidenceMismatch, ), ( - record_with_provenance( - RECORD_ID, - DATE, - SOURCE_URL, - "body", - SOURCE_HASH, - EvidenceSourceKind::StructuredData, - "text/plain", - b"hello", - WarcPayloadCompleteness::Complete, - ), + baseline_record_with_source_evidence("body", EvidenceSourceKind::StructuredData), WarcProvBundleVerificationError::SourceEvidenceMismatch, ), ( From e41bf2c867bf2af58e5eb872019e38d40031f3d4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 16:01:07 -0700 Subject: [PATCH 23/29] test(prov): reject null software revision identity --- crates/originweave-evidence/tests/warc_prov_jsonld.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/originweave-evidence/tests/warc_prov_jsonld.rs b/crates/originweave-evidence/tests/warc_prov_jsonld.rs index 06acfb67a..4dfd56ebf 100644 --- a/crates/originweave-evidence/tests/warc_prov_jsonld.rs +++ b/crates/originweave-evidence/tests/warc_prov_jsonld.rs @@ -121,6 +121,7 @@ fn warc_prov_bundle_rejects_noncanonical_or_oversized_software_revisions() { let record = resource_record(); for software_commit_sha in [ "", + "0000000000000000000000000000000000000000", "0123456789abcdef0123456789abcdef0123456", "0123456789abcdef0123456789abcdef0123456G", "0123456789ABCDEF0123456789ABCDEF01234567", From 133289e6ec2c09d0b407164176a166865bbec79c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 16:04:01 -0700 Subject: [PATCH 24/29] fix(prov): reject null Git software identity --- crates/originweave-evidence/src/warc_prov_bundle.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/originweave-evidence/src/warc_prov_bundle.rs b/crates/originweave-evidence/src/warc_prov_bundle.rs index fa7ae4023..e2ad32268 100644 --- a/crates/originweave-evidence/src/warc_prov_bundle.rs +++ b/crates/originweave-evidence/src/warc_prov_bundle.rs @@ -266,4 +266,5 @@ fn valid_software_commit_sha(software_commit_sha: &str) -> bool { && software_commit_sha .bytes() .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) + && software_commit_sha.bytes().any(|byte| byte != b'0') } From c11e7333557007373a8178b4ca5e742040f1c508 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 02:52:38 -0700 Subject: [PATCH 25/29] feat(evidence): expose PROV bundle on current stack --- crates/originweave-evidence/src/lib.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/originweave-evidence/src/lib.rs b/crates/originweave-evidence/src/lib.rs index d57dc524f..3b9f2e6c5 100644 --- a/crates/originweave-evidence/src/lib.rs +++ b/crates/originweave-evidence/src/lib.rs @@ -9,6 +9,7 @@ mod extraction_schema; mod sensitive_access; +mod warc_prov_bundle; mod warc_resource_record; pub use extraction_schema::{ @@ -21,6 +22,10 @@ pub use sensitive_access::{ SensitiveAccessEvidence, SensitiveAccessEvidenceInput, SensitiveAccessOutcome, SensitiveEvidenceError, }; +pub use warc_prov_bundle::{ + MAX_PROV_SOFTWARE_COMMIT_SHA_BYTES, WarcProvBundle, WarcProvBundleError, + WarcProvBundleVerificationError, +}; pub use warc_resource_record::{ MAX_WARC_CONTENT_TYPE_BYTES, MAX_WARC_DATE_BYTES, MAX_WARC_PAYLOAD_BYTES, MAX_WARC_RECORD_ID_BYTES, WarcPayloadCompleteness, WarcResourceRecord, WarcResourceRecordError, From 85eef87c8610565a5c4f25d142bd634f82dfd373 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 12:13:58 +0900 Subject: [PATCH 26/29] docs(evidence): document WARC PROV bundle boundary --- CHANGELOG.md | 3 ++- docs/adr/0106-provenance-evidence-model.md | 7 +++++-- tests/test_product_documentation_contract.py | 13 +++++++++++++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f747adeae..3e68f3452 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Authoritative product documentation graph spanning PRD, TRD, ADR lifecycle/index, product-wide UML, conceptual ERD, requirement/decision traceability, threat modeling, product-wide test strategy, operability, API/protocol, release/rollback, and current primary-source standards doctoring, with machine-checkable repository contracts that keep conversation-derived future work distinct from protected-main implementation claims. - Purpose-bound data-governance and privacy baseline that rejects both blanket masking and ambient raw-value propagation, defines field-scoped just-in-time disclosure, opaque-handle/trusted-broker boundaries, model/provider/region policy, retention/deletion/residency/break-glass controls, truthful CSAP/SOC 2 readiness language, and machine-checkable documentation contracts without inventing an OriginWeave-owned production database. - Proposed product-wide target-architecture ADRs for the Rust control plane, isolated execution modes, typed actions, semantic observation/stale-node authority, prompt-injection and secret separation, resource-governor priority, provenance evidence, browser/protocol adapters, crawler policy, and hourly automation operational closure; these remain Proposed rather than shipped claims until protected review and merge. +- Active PR #217 adds a bounded in-memory `WarcProvBundle` for deterministic W3C PROV-O JSON-LD projection and offline verification of exact WARC records; durable persistence, retention, and transport-specific export remain planned rather than shipped. ### Changed @@ -102,4 +103,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 \ No newline at end of file +[Unreleased]: https://github.com/ContextualWisdomLab/OriginWeave/compare/main...HEAD diff --git a/docs/adr/0106-provenance-evidence-model.md b/docs/adr/0106-provenance-evidence-model.md index d2494ad29..5bc6147a9 100644 --- a/docs/adr/0106-provenance-evidence-model.md +++ b/docs/adr/0106-provenance-evidence-model.md @@ -14,8 +14,11 @@ OriginWeave must not merely act; it must let a user, operator, auditor, or downs The extraction lane currently implements one bounded, verified in-memory WARC 1.1 `resource` record contract over already-authorized bytes. It binds the WARC target URI to independently verified provenance, computes a SHA-256 block digest, and -emits deterministic record bytes. This is active-PR evidence rather than a claim -of durable storage, tenant retention, request/response capture, or PROV export. +emits deterministic record bytes. The active-PR `WarcProvBundle` projection adds +deterministic W3C PROV-O JSON-LD and offline verification of the exact bound WARC +record without retaining the payload in the bundle. This is active-PR evidence; +durable persistence remains planned, as do tenant retention, request/response +capture, and transport-specific export adapters. ## Decision drivers diff --git a/tests/test_product_documentation_contract.py b/tests/test_product_documentation_contract.py index f192aaa4d..412aaeba4 100644 --- a/tests/test_product_documentation_contract.py +++ b/tests/test_product_documentation_contract.py @@ -77,6 +77,19 @@ def test_product_technical_gap_baseline_records_live_delivery_state(self) -> Non vpn_status, ) + def test_warc_prov_bundle_boundary_is_documented_as_in_memory_evidence(self) -> None: + """The public WARC/PROV projection must not be mistaken for durable persistence.""" + adr = (ROOT / "docs/adr/0106-provenance-evidence-model.md").read_text(encoding="utf-8") + changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8") + for text in (adr, changelog): + with self.subTest(document=text[:40]): + self.assertIn("WarcProvBundle", text) + self.assertIn("deterministic", text) + self.assertIn("JSON-LD", text) + self.assertIn("offline verification", text) + self.assertIn("in-memory", text) + self.assertIn("durable persistence remains planned", adr) + def test_root_architecture_links_the_authoritative_product_graph(self) -> None: """Architecture readers must be able to reach requirements, decisions, diagrams, and data.""" architecture = (ROOT / "ARCHITECTURE.md").read_text(encoding="utf-8") From 33e6c6e3ce9d9bb473bf6963e5f9dca570c861c7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 14:18:21 +0900 Subject: [PATCH 27/29] docs(evidence): trace WARC PROV bundle boundary --- CHANGELOG.md | 1 + docs/doctoring.md | 2 ++ tests/test_doctoring_reference_contract.py | 12 ++++++++++++ 3 files changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e68f3452..a95a489b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Purpose-bound data-governance and privacy baseline that rejects both blanket masking and ambient raw-value propagation, defines field-scoped just-in-time disclosure, opaque-handle/trusted-broker boundaries, model/provider/region policy, retention/deletion/residency/break-glass controls, truthful CSAP/SOC 2 readiness language, and machine-checkable documentation contracts without inventing an OriginWeave-owned production database. - Proposed product-wide target-architecture ADRs for the Rust control plane, isolated execution modes, typed actions, semantic observation/stale-node authority, prompt-injection and secret separation, resource-governor priority, provenance evidence, browser/protocol adapters, crawler policy, and hourly automation operational closure; these remain Proposed rather than shipped claims until protected review and merge. - Active PR #217 adds a bounded in-memory `WarcProvBundle` for deterministic W3C PROV-O JSON-LD projection and offline verification of exact WARC records; durable persistence, retention, and transport-specific export remain planned rather than shipped. +- Connected the active WARC/PROV bundle evidence to the primary standards doctoring record, preserving the in-memory-only and non-shipped durable-adapter boundary. ### Changed diff --git a/docs/doctoring.md b/docs/doctoring.md index ec51daaf3..6611b9247 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -90,6 +90,8 @@ RFC 9309 standardizes robots parsing, matching, error handling, and caching. It W3C PROV-O supplies interoperable Entity, Activity, Agent, derivation, attribution, and responsibility concepts. ISO 28500:2017, confirmed in 2023, defines WARC storage for protocol payloads, control information, metadata, transformations, duplicate detection, integrity, and segmentation. OriginWeave uses source hashes and locators in the safety kernel, then adds WARC and PROV adapters as separately testable modules. +Active PR #217 adds a bounded in-memory `WarcProvBundle` for deterministic W3C PROV-O JSON-LD projection and offline verification of exact WARC records. The bundle emits credential-free identifiers, source location, capture time, completeness, and digests without retaining the WARC payload. This is active-PR evidence only until merged; durable persistence, retention, and transport-specific export remain planned and are not implied by the in-memory projection. + The versioned `ExtractionSchema` is an admission and interpretation contract for typed extracted fields: each field is bounded, declares a value type, cardinality, normalization rule, and a canonical duplicate-free set of reviewed source-channel classes. That declaration does not create browser, network, model, secret, storage, retention, disclosure, or governance authority. PROV/WARC interoperability is therefore layered after the schema contract rather than inferred from it. RFC 3986 remains Internet Standard STD 66 for generic URI syntax. RFC 8820 is the current URI design-and-ownership Best Current Practice; it obsoletes RFC 7320 and updates RFC 3986 without replacing RFC 3986's path grammar. Section 3.3 of RFC 3986 defines each path segment as `*pchar`, where literal path characters are unreserved characters, sub-delimiters, `:`, or `@`; `/` separates segments and other reserved characters such as `[` and `]` are not literal `pchar`. OriginWeave's shared evidence-path validator therefore applies that literal ASCII `pchar` set plus validated percent-encoded octets and explicit slash separators to both `NetworkEvidence::capture` paths and provenance source-URL paths. Existing stricter evidence-safety rules continue to reject encoded separators, dot-segment ambiguity, controls, whitespace, query strings, fragments, backslashes, and credential-bearing authority. This fail-closed syntax tightening affects both evidence surfaces; it does not authorize the source origin, destination, network access, capture, disclosure, or retention. diff --git a/tests/test_doctoring_reference_contract.py b/tests/test_doctoring_reference_contract.py index bdeded44f..144f0c1e0 100644 --- a/tests/test_doctoring_reference_contract.py +++ b/tests/test_doctoring_reference_contract.py @@ -23,6 +23,18 @@ def test_rfc_5280_reference_uses_canonical_author_initials(self) -> None: ) self.assertIn(expected, text) + def test_warc_prov_decision_trace_distinguishes_in_memory_evidence(self) -> None: + """The standards record must bound the public WARC/PROV projection honestly.""" + text = DOCTORING.read_text(encoding="utf-8") + for expected in ( + "WarcProvBundle", + "deterministic W3C PROV-O JSON-LD projection", + "offline verification of exact WARC records", + "durable persistence, retention, and transport-specific export remain planned", + ): + with self.subTest(expected=expected): + self.assertIn(expected, text) + if __name__ == "__main__": unittest.main() From f1fcdd0002f4ebe241acdabcc143e52bb4ff84e6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 14:22:59 +0900 Subject: [PATCH 28/29] refactor(evidence): share WARC truncation tokens --- .../src/warc_prov_bundle.rs | 29 ++++++++++++------- .../src/warc_resource_record.rs | 3 +- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/crates/originweave-evidence/src/warc_prov_bundle.rs b/crates/originweave-evidence/src/warc_prov_bundle.rs index e2ad32268..247908d6c 100644 --- a/crates/originweave-evidence/src/warc_prov_bundle.rs +++ b/crates/originweave-evidence/src/warc_prov_bundle.rs @@ -2,7 +2,7 @@ use std::fmt; use sha2::{Digest, Sha256}; -use crate::{ProvenanceRecord, WarcPayloadCompleteness, WarcResourceRecord, WarcTruncationReason}; +use crate::{ProvenanceRecord, WarcPayloadCompleteness, WarcResourceRecord}; const ORIGINWEAVE_COMMIT_URL_PREFIX: &str = "https://github.com/ContextualWisdomLab/OriginWeave/commit/"; @@ -238,20 +238,11 @@ fn warc_payload_completeness_attributes(completeness: WarcPayloadCompleteness) - } WarcPayloadCompleteness::Truncated(reason) => format!( "\"{WARC_PAYLOAD_COMPLETENESS_IRI}\":\"truncated\",\"{WARC_TRUNCATION_REASON_IRI}\":\"{}\"", - warc_truncation_reason_token(reason) + reason.warc_token() ), } } -const fn warc_truncation_reason_token(reason: WarcTruncationReason) -> &'static str { - match reason { - WarcTruncationReason::Length => "length", - WarcTruncationReason::Time => "time", - WarcTruncationReason::Disconnect => "disconnect", - WarcTruncationReason::Unspecified => "unspecified", - } -} - fn sha256_digest(bytes: &[u8]) -> String { let digest = Sha256::digest(bytes); let mut encoded = String::from("sha256:"); @@ -268,3 +259,19 @@ fn valid_software_commit_sha(software_commit_sha: &str) -> bool { .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) && software_commit_sha.bytes().any(|byte| byte != b'0') } + +#[cfg(test)] +mod tests { + use crate::WarcTruncationReason; + + #[test] + fn truncation_tokens_use_the_shared_warc_mapping() { + assert_eq!(WarcTruncationReason::Length.warc_token(), "length"); + assert_eq!(WarcTruncationReason::Time.warc_token(), "time"); + assert_eq!(WarcTruncationReason::Disconnect.warc_token(), "disconnect"); + assert_eq!( + WarcTruncationReason::Unspecified.warc_token(), + "unspecified" + ); + } +} diff --git a/crates/originweave-evidence/src/warc_resource_record.rs b/crates/originweave-evidence/src/warc_resource_record.rs index c5ba2d003..069a06610 100644 --- a/crates/originweave-evidence/src/warc_resource_record.rs +++ b/crates/originweave-evidence/src/warc_resource_record.rs @@ -27,7 +27,8 @@ pub enum WarcTruncationReason { } impl WarcTruncationReason { - const fn warc_token(self) -> &'static str { + /// Return the canonical WARC 1.1 truncation token for this reason. + pub(crate) const fn warc_token(self) -> &'static str { match self { Self::Length => "length", Self::Time => "time", From 1d5d2939b594c54ce14aa831fa850d5d91e34db5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 14:45:59 +0900 Subject: [PATCH 29/29] fix: make WARC provenance digest binding authoritative --- CHANGELOG.md | 1 + .../src/warc_prov_bundle.rs | 8 +--- .../src/warc_resource_record.rs | 37 +++++++++++++++++++ .../tests/warc_prov_offline_verification.rs | 4 -- docs/doctoring.md | 2 +- 5 files changed, 41 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c30fd632c..755fa7002 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Proposed product-wide target-architecture ADRs for the Rust control plane, isolated execution modes, typed actions, semantic observation/stale-node authority, prompt-injection and secret separation, resource-governor priority, provenance evidence, browser/protocol adapters, crawler policy, and hourly automation operational closure; these remain Proposed rather than shipped claims until protected review and merge. - Active PR #217 adds a bounded in-memory `WarcProvBundle` for deterministic W3C PROV-O JSON-LD projection and offline verification of exact WARC records; durable persistence, retention, and transport-specific export remain planned rather than shipped. - Connected the active WARC/PROV bundle evidence to the primary standards doctoring record, preserving the in-memory-only and non-shipped durable-adapter boundary. +- Made WARC/PROV offline verification use the complete deterministic WARC digest as the authoritative block-digest binding, preserving fail-closed exact-record checks while keeping the public verifier surface minimal. ### Changed diff --git a/crates/originweave-evidence/src/warc_prov_bundle.rs b/crates/originweave-evidence/src/warc_prov_bundle.rs index 247908d6c..11788e771 100644 --- a/crates/originweave-evidence/src/warc_prov_bundle.rs +++ b/crates/originweave-evidence/src/warc_prov_bundle.rs @@ -45,8 +45,6 @@ pub enum WarcProvBundleVerificationError { SourceEvidenceMismatch, /// The WARC capture timestamp differs from the timestamp bound into the PROV bundle. CaptureTimeMismatch, - /// The retained WARC payload digest differs from the digest bound into the PROV bundle. - PayloadDigestMismatch, /// The WARC complete-versus-truncated state differs from the state bound into the bundle. PayloadCompletenessMismatch, /// The digest of the deterministic WARC serialization differs from the bundle binding. @@ -59,7 +57,6 @@ impl fmt::Display for WarcProvBundleVerificationError { Self::RecordIdentityMismatch => "WARC PROV record identity does not match", Self::SourceEvidenceMismatch => "WARC PROV source evidence does not match", Self::CaptureTimeMismatch => "WARC PROV capture time does not match", - Self::PayloadDigestMismatch => "WARC PROV payload digest does not match", Self::PayloadCompletenessMismatch => "WARC PROV payload completeness does not match", Self::WarcRecordDigestMismatch => "WARC PROV serialized record digest does not match", }) @@ -186,9 +183,8 @@ impl WarcProvBundle { if self.warc_date != record.warc_date() { return Err(WarcProvBundleVerificationError::CaptureTimeMismatch); } - if self.block_digest != record.block_digest() { - return Err(WarcProvBundleVerificationError::PayloadDigestMismatch); - } + // `warc_record_digest` covers the complete deterministic serialization, including the + // WARC-Block-Digest header, so a block-digest drift is reported by that binding below. if self.payload_completeness != record.completeness() { return Err(WarcProvBundleVerificationError::PayloadCompletenessMismatch); } diff --git a/crates/originweave-evidence/src/warc_resource_record.rs b/crates/originweave-evidence/src/warc_resource_record.rs index 027954756..4fdc7a475 100644 --- a/crates/originweave-evidence/src/warc_resource_record.rs +++ b/crates/originweave-evidence/src/warc_resource_record.rs @@ -523,3 +523,40 @@ fn sha256_digest(payload: &[u8]) -> String { } encoded } + +#[cfg(test)] +#[allow(clippy::expect_used)] +mod tests { + use super::*; + use crate::{WarcProvBundle, WarcProvBundleVerificationError}; + + #[test] + fn provenance_bundle_rejects_a_tampered_block_digest() { + let provenance = ProvenanceRecord::new( + "https://example.com/item", + "body", + "sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824", + crate::EvidenceSourceKind::NetworkResponse, + crate::VerificationResult::Verified, + ) + .expect("verified provenance"); + let mut record = WarcResourceRecord::new( + "urn:uuid:123e4567-e89b-12d3-a456-426614174000", + "2026-08-22T12:00:00Z", + "https://example.com/item", + "text/plain", + b"hello".to_vec(), + provenance, + ) + .expect("WARC resource record"); + let bundle = WarcProvBundle::new(&record, "0123456789abcdef0123456789abcdef01234567") + .expect("PROV bundle"); + + record.block_digest = "sha256:tampered".to_owned(); + + assert_eq!( + bundle.verify_record(&record), + Err(WarcProvBundleVerificationError::WarcRecordDigestMismatch) + ); + } +} diff --git a/crates/originweave-evidence/tests/warc_prov_offline_verification.rs b/crates/originweave-evidence/tests/warc_prov_offline_verification.rs index b1ddd109d..7222cd956 100644 --- a/crates/originweave-evidence/tests/warc_prov_offline_verification.rs +++ b/crates/originweave-evidence/tests/warc_prov_offline_verification.rs @@ -98,10 +98,6 @@ fn warc_prov_bundle_offline_verification_accepts_only_the_exact_bound_record() { WarcProvBundleVerificationError::CaptureTimeMismatch.to_string(), "WARC PROV capture time does not match" ); - assert_eq!( - WarcProvBundleVerificationError::PayloadDigestMismatch.to_string(), - "WARC PROV payload digest does not match" - ); assert_eq!( WarcProvBundleVerificationError::PayloadCompletenessMismatch.to_string(), "WARC PROV payload completeness does not match" diff --git a/docs/doctoring.md b/docs/doctoring.md index 265fc9658..8d8f1ac02 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -90,7 +90,7 @@ RFC 9309 standardizes robots parsing, matching, error handling, and caching. It W3C PROV-O supplies interoperable Entity, Activity, Agent, derivation, attribution, and responsibility concepts. ISO 28500:2017, confirmed in 2023, defines WARC storage for protocol payloads, control information, metadata, transformations, duplicate detection, integrity, and segmentation. OriginWeave uses source hashes and locators in the safety kernel, then adds WARC and PROV adapters as separately testable modules. -Active PR #217 adds a bounded in-memory `WarcProvBundle` for deterministic W3C PROV-O JSON-LD projection and offline verification of exact WARC records. The bundle emits credential-free identifiers, source location, capture time, completeness, and digests without retaining the WARC payload. This is active-PR evidence only until merged; durable persistence, retention, and transport-specific export remain planned and are not implied by the in-memory projection. +Active PR #217 adds a bounded in-memory `WarcProvBundle` for deterministic W3C PROV-O JSON-LD projection and offline verification of exact WARC records. The bundle emits credential-free identifiers, source location, capture time, completeness, and digests without retaining the WARC payload. Offline verification uses the complete deterministic WARC serialization digest as the authoritative record binding, so block-digest drift cannot bypass the exact-record check. This is active-PR evidence only until merged; durable persistence, retention, and transport-specific export remain planned and are not implied by the in-memory projection. The IIPC WARC 1.1 specification and its annotated guidance define the `resource` record fields used by the active capture slice: a UTC `WARC-Date`, an RFC 3986 target URI, an algorithm-prefixed block digest, `Content-Length`, and an explicit `WARC-Truncated` reason when a retained block is incomplete. Active PR #210 applies those provisions only to already-authorized bytes: record identifiers, dates, content types, target URIs, and payloads are bounded before serialization; target-URI validation reuses the provenance source-URL authority so bracketed hosts are accepted only when the shared origin parser accepts their IP-literal form. The serializer is deterministic and in-memory; persistence, retention, encryption, and third-party conformance remain unreleased adapters.