From f1b65d89855f4ae11a650d940eb9f47ea6b9f97a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 09:10:28 +0900 Subject: [PATCH 01/62] feat(evidence): add bounded WARC resource records --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + Cargo.lock | 1 + crates/originweave-evidence/Cargo.toml | 1 + crates/originweave-evidence/src/lib.rs | 5 + .../src/warc_resource_record.rs | 222 ++++++++++++++++++ .../tests/warc_resource_record.rs | 160 +++++++++++++ docs/adr/0106-provenance-evidence-model.md | 8 + docs/traceability/README.md | 4 +- 9 files changed, 401 insertions(+), 3 deletions(-) create mode 100644 crates/originweave-evidence/src/warc_resource_record.rs create mode 100644 crates/originweave-evidence/tests/warc_resource_record.rs diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 9b23ef9f0..d6ebfee02 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -134,7 +134,7 @@ Owns validated task budgets and deterministic cumulative mitigation plans. Platf ### `originweave-evidence` -Owns universally value-redacted network evidence and source-bound provenance records. Generic network records retain only bounded method, canonical origin, unambiguous bounded path, and bounded field names. Body capture, typed metadata values, WARC serialization, object storage, retention, encryption, and legal policy remain future bounded modules. +Owns universally value-redacted network evidence and source-bound provenance records. Generic network records retain only bounded method, canonical origin, unambiguous bounded path, and bounded field names. The active extraction lane adds a verified, bounded in-memory WARC 1.1 `resource` record contract over already-authorized bytes; object storage, retention, encryption, legal policy, request/response capture, and PROV export remain future bounded modules. ## 6. Planned modules diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a9a59e77..90dd8e777 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Changed +- Added a bounded immutable WARC 1.1 resource-record contract on the active extraction lane, binding deterministic bytes and SHA-256 block digests to verified provenance without claiming durable persistence or PROV export. - Separated logical origin authority from resolved network destination authority; an origin grant no longer implies permission to connect to every resolver result. - Separated resolved-address authorization from direct transport evidence; an approved IP now becomes a usable stream only after the operating system reports the exact requested IP and port. - Separated exact TCP peer proof from authenticated TLS service identity; an observed peer becomes an authenticated HTTPS stream only after explicit-root, fixed-time, SAN-bound WebPKI verification over that same stream. diff --git a/Cargo.lock b/Cargo.lock index e2ada3c4e..805549630 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -279,6 +279,7 @@ name = "originweave-evidence" version = "0.1.0" dependencies = [ "originweave-core", + "sha2", ] [[package]] diff --git a/crates/originweave-evidence/Cargo.toml b/crates/originweave-evidence/Cargo.toml index a69386c38..35c21a7fb 100644 --- a/crates/originweave-evidence/Cargo.toml +++ b/crates/originweave-evidence/Cargo.toml @@ -12,6 +12,7 @@ publish = false [dependencies] originweave-core = { path = "../originweave-core" } +sha2 = "=0.10.9" [lints] workspace = true diff --git a/crates/originweave-evidence/src/lib.rs b/crates/originweave-evidence/src/lib.rs index 05ae7c3f1..2daf7a129 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_resource_record; pub use extraction_schema::{ ExtractionCardinality, ExtractionField, ExtractionNormalizationRule, ExtractionSchema, @@ -20,6 +21,10 @@ pub use sensitive_access::{ SensitiveAccessEvidence, SensitiveAccessEvidenceInput, SensitiveAccessOutcome, SensitiveEvidenceError, }; +pub use warc_resource_record::{ + MAX_WARC_CONTENT_TYPE_BYTES, MAX_WARC_DATE_BYTES, MAX_WARC_PAYLOAD_BYTES, + MAX_WARC_RECORD_ID_BYTES, WarcResourceRecord, WarcResourceRecordError, +}; use std::collections::BTreeMap; diff --git a/crates/originweave-evidence/src/warc_resource_record.rs b/crates/originweave-evidence/src/warc_resource_record.rs new file mode 100644 index 000000000..5b4d03c55 --- /dev/null +++ b/crates/originweave-evidence/src/warc_resource_record.rs @@ -0,0 +1,222 @@ +use sha2::{Digest, Sha256}; + +use crate::{ProvenanceRecord, VerificationResult}; + +/// Maximum encoded size of the UUID-based WARC record identifier. +pub const MAX_WARC_RECORD_ID_BYTES: usize = 45; +/// Maximum encoded size accepted for a UTC WARC date. +pub const MAX_WARC_DATE_BYTES: usize = 30; +/// Maximum encoded size retained for a WARC content type. +pub const MAX_WARC_CONTENT_TYPE_BYTES: usize = 256; +/// Maximum resource payload retained by one immutable WARC record. +pub const MAX_WARC_PAYLOAD_BYTES: usize = 1_048_576; + +/// A validation failure while constructing an immutable WARC resource record. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum WarcResourceRecordError { + /// The record identifier was not a bounded UUID URN. + InvalidRecordId, + /// The date was not a bounded UTC RFC 3339 timestamp. + InvalidDate, + /// The content type was empty or contained unsafe whitespace/control input. + InvalidContentType, + /// A record field or payload exceeded its retention limit. + LimitExceeded, + /// The WARC target URI differed from its provenance source URL. + TargetUriMismatch, + /// The source provenance was not independently verified. + UnverifiedProvenance, +} + +/// An immutable, bounded WARC `resource` record over already-authorized bytes. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WarcResourceRecord { + record_id: String, + warc_date: String, + target_uri: String, + content_type: String, + payload: Vec, + block_digest: String, + provenance: ProvenanceRecord, +} + +impl WarcResourceRecord { + /// Validate and construct one resource record without contacting a live origin. + pub fn new( + record_id: &str, + warc_date: &str, + target_uri: &str, + content_type: &str, + payload: Vec, + provenance: ProvenanceRecord, + ) -> Result { + if !valid_record_id(record_id) { + return Err(WarcResourceRecordError::InvalidRecordId); + } + if !valid_utc_date(warc_date) { + return Err(WarcResourceRecordError::InvalidDate); + } + if !valid_content_type(content_type) { + return Err(if content_type.len() > MAX_WARC_CONTENT_TYPE_BYTES { + WarcResourceRecordError::LimitExceeded + } else { + WarcResourceRecordError::InvalidContentType + }); + } + if target_uri != provenance.source_url() { + return Err(WarcResourceRecordError::TargetUriMismatch); + } + if provenance.verification_result() != VerificationResult::Verified { + return Err(WarcResourceRecordError::UnverifiedProvenance); + } + if payload.len() > MAX_WARC_PAYLOAD_BYTES { + return Err(WarcResourceRecordError::LimitExceeded); + } + + Ok(Self { + record_id: record_id.to_owned(), + warc_date: warc_date.to_owned(), + target_uri: target_uri.to_owned(), + content_type: content_type.to_owned(), + block_digest: sha256_digest(&payload), + payload, + provenance, + }) + } + + /// Return the UUID URN used as the WARC record identity. + #[must_use] + pub fn record_id(&self) -> &str { + &self.record_id + } + + /// Return the normalized UTC capture timestamp. + #[must_use] + pub fn warc_date(&self) -> &str { + &self.warc_date + } + + /// Return the provenance-bound target URI. + #[must_use] + pub fn target_uri(&self) -> &str { + &self.target_uri + } + + /// Return the payload media type retained in the WARC record. + #[must_use] + pub fn content_type(&self) -> &str { + &self.content_type + } + + /// Return the immutable resource bytes. + #[must_use] + pub fn payload(&self) -> &[u8] { + &self.payload + } + + /// Return the lowercase SHA-256 block digest. + #[must_use] + pub fn block_digest(&self) -> &str { + &self.block_digest + } + + /// Return the independently verified provenance bound to this record. + #[must_use] + pub const fn provenance(&self) -> &ProvenanceRecord { + &self.provenance + } + + /// Serialize this bounded resource record as deterministic WARC 1.1 bytes. + #[must_use] + pub fn to_warc_bytes(&self) -> Vec { + let header = format!( + "WARC/1.1\r\nWARC-Type: resource\r\nWARC-Record-ID: <{}>\r\nWARC-Date: {}\r\nWARC-Target-URI: {}\r\nContent-Type: {}\r\nWARC-Block-Digest: {}\r\nContent-Length: {}\r\n\r\n", + self.record_id, + self.warc_date, + self.target_uri, + self.content_type, + self.block_digest, + self.payload.len() + ); + let mut bytes = header.into_bytes(); + bytes.extend_from_slice(&self.payload); + bytes.extend_from_slice(b"\r\n\r\n"); + bytes + } +} + +fn valid_record_id(record_id: &str) -> bool { + let bytes = record_id.as_bytes(); + if bytes.len() != MAX_WARC_RECORD_ID_BYTES || !record_id.starts_with("urn:uuid:") { + return false; + } + for (index, byte) in bytes[9..].iter().copied().enumerate() { + if matches!(index, 8 | 13 | 18 | 23) { + if byte != b'-' { + return false; + } + } else if !byte.is_ascii_hexdigit() { + return false; + } + } + true +} + +fn valid_utc_date(date: &str) -> bool { + let bytes = date.as_bytes(); + if !(20..=MAX_WARC_DATE_BYTES).contains(&bytes.len()) + || bytes.get(4) != Some(&b'-') + || bytes.get(7) != Some(&b'-') + || bytes.get(10) != Some(&b'T') + || bytes.get(13) != Some(&b':') + || bytes.get(16) != Some(&b':') + { + return false; + } + let has_fraction = bytes[19] == b'.'; + if has_fraction { + if bytes.last() != Some(&b'Z') || !(22..=MAX_WARC_DATE_BYTES).contains(&bytes.len()) { + return false; + } + let fraction = &bytes[20..bytes.len() - 1]; + if fraction.iter().any(|byte| !byte.is_ascii_digit()) { + return false; + } + } else if bytes.len() != 20 || bytes[19] != b'Z' { + return false; + } + if !bytes[..19] + .iter() + .enumerate() + .all(|(index, byte)| matches!(index, 4 | 7 | 10 | 13 | 16) || byte.is_ascii_digit()) + { + return false; + } + let month = two_digits(bytes[5], bytes[6]); + let day = two_digits(bytes[8], bytes[9]); + let hour = two_digits(bytes[11], bytes[12]); + let minute = two_digits(bytes[14], bytes[15]); + let second = two_digits(bytes[17], bytes[18]); + (1..=12).contains(&month) && (1..=31).contains(&day) && hour < 24 && minute < 60 && second <= 60 +} + +fn two_digits(high: u8, low: u8) -> u8 { + (high - b'0') * 10 + (low - b'0') +} + +fn valid_content_type(content_type: &str) -> bool { + !content_type.is_empty() + && content_type.len() <= MAX_WARC_CONTENT_TYPE_BYTES + && !content_type + .chars() + .any(|character| character.is_control() || character.is_whitespace()) +} + +fn sha256_digest(payload: &[u8]) -> String { + let digest = Sha256::digest(payload); + let mut encoded = String::from("sha256:"); + for byte in digest { + encoded.push_str(&format!("{byte:02x}")); + } + encoded +} diff --git a/crates/originweave-evidence/tests/warc_resource_record.rs b/crates/originweave-evidence/tests/warc_resource_record.rs new file mode 100644 index 000000000..d35484d17 --- /dev/null +++ b/crates/originweave-evidence/tests/warc_resource_record.rs @@ -0,0 +1,160 @@ +#![allow(clippy::expect_used)] + +use originweave_evidence::{ + EvidenceSourceKind, MAX_WARC_PAYLOAD_BYTES, ProvenanceRecord, VerificationResult, + WarcResourceRecord, WarcResourceRecordError, +}; + +const SOURCE_HASH: &str = "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; +const RECORD_ID: &str = "urn:uuid:123e4567-e89b-12d3-a456-426614174000"; +const DATE: &str = "2026-08-21T00:00:00Z"; + +fn provenance(source_url: &str, verification: VerificationResult) -> ProvenanceRecord { + ProvenanceRecord::new( + source_url, + "body", + SOURCE_HASH, + EvidenceSourceKind::NetworkResponse, + verification, + ) + .expect("provenance") +} + +#[test] +fn resource_record_binds_verified_provenance_and_emits_deterministic_warc_bytes() { + let record = WarcResourceRecord::new( + RECORD_ID, + DATE, + "https://example.com/item", + "text/plain", + b"hello".to_vec(), + provenance("https://example.com/item", VerificationResult::Verified), + ) + .expect("resource record"); + + assert_eq!(record.record_id(), RECORD_ID); + assert_eq!(record.warc_date(), DATE); + assert_eq!(record.target_uri(), "https://example.com/item"); + assert_eq!(record.content_type(), "text/plain"); + assert_eq!(record.payload(), b"hello"); + assert_eq!( + record.block_digest(), + "sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824" + ); + assert_eq!(record.provenance().source_url(), record.target_uri()); + assert!(record.provenance().verification_result() == VerificationResult::Verified); + assert_eq!( + record.to_warc_bytes(), + b"WARC/1.1\r\nWARC-Type: resource\r\nWARC-Record-ID: \r\nWARC-Date: 2026-08-21T00:00:00Z\r\nWARC-Target-URI: https://example.com/item\r\nContent-Type: text/plain\r\nWARC-Block-Digest: sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824\r\nContent-Length: 5\r\n\r\nhello\r\n\r\n" + ); + WarcResourceRecord::new( + RECORD_ID, + "2026-08-21T00:00:00.123Z", + "https://example.com/item", + "text/plain", + Vec::new(), + provenance("https://example.com/item", VerificationResult::Verified), + ) + .expect("fractional UTC date"); +} + +#[test] +fn resource_record_rejects_invalid_identifiers_dates_content_and_limits() { + let valid = |record_id, date, content_type, payload| { + WarcResourceRecord::new( + record_id, + date, + "https://example.com/item", + content_type, + payload, + provenance("https://example.com/item", VerificationResult::Verified), + ) + }; + + for record_id in [ + "", + "http://example.com/record", + "urn:uuid:123e4567-e89b-12d3-a456-42661417400", + "urn:uuid:123e4567_e89b-12d3-a456-426614174000", + "urn:uuid:123e4567-e89b-12d3-a456-42661417400z", + ] { + assert_eq!( + valid(record_id, DATE, "text/plain", Vec::new()), + Err(WarcResourceRecordError::InvalidRecordId), + "record_id={record_id:?}" + ); + } + + for date in [ + "", + "2026-08-21 00:00:00Z", + "2026-13-21T00:00:00Z", + "2026-08-32T00:00:00Z", + "2026-08-21T24:00:00Z", + "2026-0x-21T00:00:00Z", + "2026-08-21T00:00:00+00:00", + "2026-08-21T00:00:00.123", + "2026-08-21T00:00:00.XZ", + ] { + assert_eq!( + valid(RECORD_ID, date, "text/plain", Vec::new()), + Err(WarcResourceRecordError::InvalidDate), + "date={date:?}" + ); + } + + for content_type in ["", "text plain", "text\nplain"] { + assert_eq!( + valid(RECORD_ID, DATE, content_type, Vec::new()), + Err(WarcResourceRecordError::InvalidContentType), + "content_type={content_type:?}" + ); + } + + assert_eq!( + valid( + RECORD_ID, + DATE, + "text/plain", + vec![b'x'; MAX_WARC_PAYLOAD_BYTES + 1], + ), + Err(WarcResourceRecordError::LimitExceeded) + ); + assert_eq!( + valid( + RECORD_ID, + DATE, + &"x".repeat(originweave_evidence::MAX_WARC_CONTENT_TYPE_BYTES + 1), + Vec::new(), + ), + Err(WarcResourceRecordError::LimitExceeded) + ); +} + +#[test] +fn resource_record_rejects_provenance_drift_and_unverified_sources() { + assert_eq!( + WarcResourceRecord::new( + RECORD_ID, + DATE, + "https://example.com/other", + "text/plain", + Vec::new(), + provenance("https://example.com/item", VerificationResult::Verified), + ), + Err(WarcResourceRecordError::TargetUriMismatch) + ); + for verification in [VerificationResult::Unverified, VerificationResult::Rejected] { + assert_eq!( + WarcResourceRecord::new( + RECORD_ID, + DATE, + "https://example.com/item", + "text/plain", + Vec::new(), + provenance("https://example.com/item", verification), + ), + Err(WarcResourceRecordError::UnverifiedProvenance) + ); + } +} diff --git a/docs/adr/0106-provenance-evidence-model.md b/docs/adr/0106-provenance-evidence-model.md index 0e2741f37..303edac0d 100644 --- a/docs/adr/0106-provenance-evidence-model.md +++ b/docs/adr/0106-provenance-evidence-model.md @@ -9,6 +9,14 @@ OriginWeave must not merely act; it must let a user, operator, auditor, or downstream system establish what was observed, authorized, executed, and verified. Browser logs alone are not sufficient because they collapse observation, policy, action, network identity, approvals, and post-conditions. At the same time, evidence can itself contain sensitive or attacker-controlled content. The product needs a durable model compatible with web-archive and provenance concepts without claiming that every conceptual record is already persisted. +## Active implementation boundary + +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. + ## Decision drivers - `Browse. Act. Prove.` requires evidence as a first-class product output. diff --git a/docs/traceability/README.md b/docs/traceability/README.md index e30b9eda1..29eeb03c7 100644 --- a/docs/traceability/README.md +++ b/docs/traceability/README.md @@ -71,7 +71,7 @@ ADR lifecycle is separate and remains `Proposed`, `Accepted`, `Superseded`, `Dep | WebDriver BiDi / CDP / WebMCP / MCP are adapters, not internal authority | ACCEPTED_ARCHITECTURE | PRD Section 9.8; TRD Section 12 | Protocol adapter implementation remains Planned/active under issue #28; active PR #40 may not be called shipped | | Manifest V3 compatibility is preserved upstream where practical | PARTIAL | ADR 0001; issue #27; Proposed ADR 0013 | Protected main has pinned real-Chromium compatibility evidence for service worker/content script/storage/DNR/tabs/windows/scripting/commands/side panel/bookmarks/history/restart/repeatability; active PR #43 adds real bounded downloads evidence; full issue #27 matrix remains incomplete | | Extension permission does not imply OriginWeave Agent capability | PARTIAL | protected-main extension authority kernel; Proposed ADR 0013 | Core extension-to-Agent authority isolation exists on protected main; complete managed-extension/native-messaging/enterprise release policy remains incomplete | -| WARC/PROV-oriented durable evidence adapters | PLANNED | ADR 0003; PRD-EVD-005 | Source/provenance kernel foundation exists; persistence/export adapters remain Planned | +| WARC/PROV-oriented durable evidence adapters | PARTIAL | ADR 0003; PRD-EVD-005 | The active extraction lane adds a verified, bounded in-memory WARC 1.1 `resource` record contract; durable persistence/export adapters remain Planned | | Origin Map visualizes value/action provenance | PLANNED | PRD-EVD-004; this traceability record | No shipped UI claim | | Browser / Runtime / Observe / Capture / Governor / Policy / Evidence / Protocol / SDK product surfaces | PARTIAL | PRD Section 6 | Some foundations exist under crates; named commercial surfaces are not all shipped artifacts | | Constrained GPU phase scheduling for browser rendering vs local inference | PARTIAL | PRD-RES-005; TRD Section 10 | Deterministic resource plan exists; real GPU scheduler/telemetry remains Planned | @@ -99,7 +99,7 @@ ADR lifecycle is separate and remains `Proposed`, `Accepted`, `Superseded`, `Dep | Purpose-bound sensitive-data policy/evidence | `originweave-policy` + evidence foundations; active lifecycle/reservation work #45/#46 | ADR 0007; issue #10 | PARTIAL | | Trusted sensitive-data broker/storage/lifecycle | future bounded service/crate | issue #10; PRD/TRD/data governance | PLANNED | | BiDi/CDP/WebMCP/MCP | future/versioned adapter crates; registry prerequisite active in #40 | protocol compatibility tests required | PLANNED | -| WARC/PROV persistence | persistence/export adapters | doctoring + future conformance tests | PLANNED | +| WARC/PROV persistence | persistence/export adapters | doctoring + future conformance tests | PARTIAL - active extraction lane has a bounded in-memory WARC `resource` contract; durable persistence and PROV serialization remain Planned | ## 5. Requirement-to-ADR trace From 7607c250435898f69d6cbec54e46e9caadc3f87f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 09:32:02 +0900 Subject: [PATCH 02/62] test(evidence): cover WARC validation branches --- .../originweave-evidence/src/warc_resource_record.rs | 2 +- .../originweave-evidence/tests/warc_resource_record.rs | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/originweave-evidence/src/warc_resource_record.rs b/crates/originweave-evidence/src/warc_resource_record.rs index 5b4d03c55..bf5799c67 100644 --- a/crates/originweave-evidence/src/warc_resource_record.rs +++ b/crates/originweave-evidence/src/warc_resource_record.rs @@ -175,7 +175,7 @@ fn valid_utc_date(date: &str) -> bool { } let has_fraction = bytes[19] == b'.'; if has_fraction { - if bytes.last() != Some(&b'Z') || !(22..=MAX_WARC_DATE_BYTES).contains(&bytes.len()) { + if bytes.last() != Some(&b'Z') || bytes.len() < 22 { return false; } let fraction = &bytes[20..bytes.len() - 1]; diff --git a/crates/originweave-evidence/tests/warc_resource_record.rs b/crates/originweave-evidence/tests/warc_resource_record.rs index d35484d17..a9261ab17 100644 --- a/crates/originweave-evidence/tests/warc_resource_record.rs +++ b/crates/originweave-evidence/tests/warc_resource_record.rs @@ -77,6 +77,7 @@ fn resource_record_rejects_invalid_identifiers_dates_content_and_limits() { "urn:uuid:123e4567-e89b-12d3-a456-42661417400", "urn:uuid:123e4567_e89b-12d3-a456-426614174000", "urn:uuid:123e4567-e89b-12d3-a456-42661417400z", + "xrn:uuid:123e4567-e89b-12d3-a456-426614174000", ] { assert_eq!( valid(record_id, DATE, "text/plain", Vec::new()), @@ -95,6 +96,15 @@ fn resource_record_rejects_invalid_identifiers_dates_content_and_limits() { "2026-08-21T00:00:00+00:00", "2026-08-21T00:00:00.123", "2026-08-21T00:00:00.XZ", + "2026-08-21T00:00:00.Z", + "2026x08-21T00:00:00Z", + "2026-08x21T00:00:00Z", + "2026-08-21T00x00:00Z", + "2026-08-21T00:00x00Z", + "2026-08-21T00:00:00.12345678901234567890Z", + "2026-08-21T00:00:00X", + "2026-08-21T00:61:00Z", + "2026-08-21T00:00:61Z", ] { assert_eq!( valid(RECORD_ID, date, "text/plain", Vec::new()), From 3c6b3bae988a2bef9462806e88c0ce160c7e9405 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 19:10:59 -0700 Subject: [PATCH 03/62] test(evidence): reject impossible WARC dates --- .../tests/warc_resource_record.rs | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/crates/originweave-evidence/tests/warc_resource_record.rs b/crates/originweave-evidence/tests/warc_resource_record.rs index a9261ab17..0222f4a7e 100644 --- a/crates/originweave-evidence/tests/warc_resource_record.rs +++ b/crates/originweave-evidence/tests/warc_resource_record.rs @@ -47,15 +47,21 @@ fn resource_record_binds_verified_provenance_and_emits_deterministic_warc_bytes( record.to_warc_bytes(), b"WARC/1.1\r\nWARC-Type: resource\r\nWARC-Record-ID: \r\nWARC-Date: 2026-08-21T00:00:00Z\r\nWARC-Target-URI: https://example.com/item\r\nContent-Type: text/plain\r\nWARC-Block-Digest: sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824\r\nContent-Length: 5\r\n\r\nhello\r\n\r\n" ); - WarcResourceRecord::new( - RECORD_ID, + for date in [ "2026-08-21T00:00:00.123Z", - "https://example.com/item", - "text/plain", - Vec::new(), - provenance("https://example.com/item", VerificationResult::Verified), - ) - .expect("fractional UTC date"); + "2024-02-29T00:00:00Z", + "2000-02-29T00:00:00Z", + ] { + WarcResourceRecord::new( + RECORD_ID, + date, + "https://example.com/item", + "text/plain", + Vec::new(), + provenance("https://example.com/item", VerificationResult::Verified), + ) + .expect("valid UTC date"); + } } #[test] @@ -91,6 +97,10 @@ fn resource_record_rejects_invalid_identifiers_dates_content_and_limits() { "2026-08-21 00:00:00Z", "2026-13-21T00:00:00Z", "2026-08-32T00:00:00Z", + "2026-02-29T00:00:00Z", + "2024-02-30T00:00:00Z", + "2026-04-31T00:00:00Z", + "1900-02-29T00:00:00Z", "2026-08-21T24:00:00Z", "2026-0x-21T00:00:00Z", "2026-08-21T00:00:00+00:00", From 999979a511c3a890ba93a1a09da8810858877940 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 19:12:55 -0700 Subject: [PATCH 04/62] fix(evidence): validate WARC calendar dates --- .../src/warc_resource_record.rs | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/crates/originweave-evidence/src/warc_resource_record.rs b/crates/originweave-evidence/src/warc_resource_record.rs index bf5799c67..16533eb3c 100644 --- a/crates/originweave-evidence/src/warc_resource_record.rs +++ b/crates/originweave-evidence/src/warc_resource_record.rs @@ -192,18 +192,42 @@ fn valid_utc_date(date: &str) -> bool { { return false; } + let year = four_digits(bytes[0], bytes[1], bytes[2], bytes[3]); let month = two_digits(bytes[5], bytes[6]); let day = two_digits(bytes[8], bytes[9]); let hour = two_digits(bytes[11], bytes[12]); let minute = two_digits(bytes[14], bytes[15]); let second = two_digits(bytes[17], bytes[18]); - (1..=12).contains(&month) && (1..=31).contains(&day) && hour < 24 && minute < 60 && second <= 60 + valid_calendar_date(year, month, day) && hour < 24 && minute < 60 && second <= 60 +} + +fn four_digits(first: u8, second: u8, third: u8, fourth: u8) -> u16 { + u16::from(first - b'0') * 1000 + + u16::from(second - b'0') * 100 + + u16::from(third - b'0') * 10 + + u16::from(fourth - b'0') } fn two_digits(high: u8, low: u8) -> u8 { (high - b'0') * 10 + (low - b'0') } +fn valid_calendar_date(year: u16, month: u8, day: u8) -> bool { + if !(1..=12).contains(&month) { + return false; + } + let days_in_month = if month == 2 { + if is_leap_year(year) { 29 } else { 28 } + } else { + 30 + ((month + month / 8) % 2) + }; + (1..=days_in_month).contains(&day) +} + +fn is_leap_year(year: u16) -> bool { + year.is_multiple_of(400) || (year.is_multiple_of(4) && !year.is_multiple_of(100)) +} + fn valid_content_type(content_type: &str) -> bool { !content_type.is_empty() && content_type.len() <= MAX_WARC_CONTENT_TYPE_BYTES From a55dd93ca5c2ef10aa1816e6a684a8e500c0a3e5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 20:24:38 -0700 Subject: [PATCH 05/62] test(warc): reject impossible leap-second placement --- crates/originweave-evidence/tests/warc_resource_record.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/originweave-evidence/tests/warc_resource_record.rs b/crates/originweave-evidence/tests/warc_resource_record.rs index 0222f4a7e..410888aa3 100644 --- a/crates/originweave-evidence/tests/warc_resource_record.rs +++ b/crates/originweave-evidence/tests/warc_resource_record.rs @@ -115,6 +115,8 @@ fn resource_record_rejects_invalid_identifiers_dates_content_and_limits() { "2026-08-21T00:00:00X", "2026-08-21T00:61:00Z", "2026-08-21T00:00:61Z", + "2026-08-21T12:34:60Z", + "2026-06-30T23:58:60Z", ] { assert_eq!( valid(RECORD_ID, date, "text/plain", Vec::new()), From 9fd8c05c5f3b5a2e789cdbeb43b19083cb3ac220 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 21:02:28 -0700 Subject: [PATCH 06/62] fix(warc): reject unverifiable leap-second timestamps --- crates/originweave-evidence/src/warc_resource_record.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/originweave-evidence/src/warc_resource_record.rs b/crates/originweave-evidence/src/warc_resource_record.rs index 16533eb3c..a8786009b 100644 --- a/crates/originweave-evidence/src/warc_resource_record.rs +++ b/crates/originweave-evidence/src/warc_resource_record.rs @@ -198,7 +198,7 @@ fn valid_utc_date(date: &str) -> bool { let hour = two_digits(bytes[11], bytes[12]); let minute = two_digits(bytes[14], bytes[15]); let second = two_digits(bytes[17], bytes[18]); - valid_calendar_date(year, month, day) && hour < 24 && minute < 60 && second <= 60 + valid_calendar_date(year, month, day) && hour < 24 && minute < 60 && second < 60 } fn four_digits(first: u8, second: u8, third: u8, fourth: u8) -> u16 { From 68fa94b8e105ce996bf3b77bf23344c8bd6e91d3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 21:05:19 -0700 Subject: [PATCH 07/62] docs(adr): carry extraction binding into WARC stack --- docs/adr/0106-provenance-evidence-model.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/adr/0106-provenance-evidence-model.md b/docs/adr/0106-provenance-evidence-model.md index 303edac0d..f8de171a9 100644 --- a/docs/adr/0106-provenance-evidence-model.md +++ b/docs/adr/0106-provenance-evidence-model.md @@ -41,29 +41,47 @@ OriginWeave maintains provenance-native evidence with stable identifiers for ses WARC and PROV are interoperability/export contracts, not substitutes for OriginWeave's internal authorization or evidence schema. A WARC record can contain untrusted or sensitive payload bytes and therefore inherits capture, retention, encryption, and export policy. A PROV entity/activity/agent relation records derivation or responsibility; it cannot manufacture authentication, authorization, durable completion, or tenant ownership not established by the producing system. +### Versioned extraction-schema binding + +A versioned `ExtractionSchema` is the binding contract for typed extraction before any capture persistence or export format is allowed to claim semantic authority. Each schema version contains an ordered, non-empty set of unique `ExtractionField` definitions. Schema-version and field identifiers are bounded to 128 encoded bytes, begin with a lowercase ASCII letter, and thereafter admit only lowercase ASCII letters, digits, `_`, or `-`. One schema admits at most 256 fields. + +Every extraction field binds its stable identifier to a value type, cardinality, required/optional status, deterministic normalization rule, and a non-empty duplicate-free set of reviewed source-channel classes. `Verbatim` is the compatibility default used by the existing constructor. `TrimTextWhitespace` is admitted only for text fields and `Rfc3339Utc` only for timestamp fields; type-incompatible normalization fails closed. A `ModelInterpretation` source channel is classification metadata only and does not grant model execution, approval, disclosure, browser, network, secret, or storage authority. + +At this value-object boundary, the version identifier is immutable schema identity; there is deliberately no registry that silently treats two different field contracts as compatible merely because their version strings compare or sort in a particular way. Callers changing a field identifier, value type, cardinality, required status, normalization rule, or admitted source-channel set must use a distinct reviewed schema version and perform any migration/compatibility decision at an explicit higher layer. The current schema object does not itself read browser data, materialize extracted values, persist artifacts, execute models, or change governance policy. Those capabilities require separately authorized runtime boundaries and are not implied by schema construction. + ## Consequences Capture becomes a designed product surface rather than incidental logging. Storage and retention need budgets. Consumers can distinguish a model claim from source evidence and an action request from verified completion. Export adapters can target WARC, provenance graphs, audit streams, or buyer-specific schemas. +A schema consumer can also determine the exact field/type/cardinality/normalization/source contract it reviewed rather than relying on free-form extraction instructions. Schema evolution is explicit instead of being inferred from mutable field definitions; runtime compatibility, migrations, durable storage, and extracted-value validation remain separate implementation work until those boundaries are delivered. + ## Failure and degraded behavior If mandatory evidence cannot be recorded durably enough for a governed state-changing action, the action fails before execution or reports an explicit unverifiable failure; it is never marked proved. Read-only operations may degrade to reduced evidence only when the API contract declares that mode. Corrupt or incomplete evidence is quarantined rather than silently accepted. +Invalid or oversized extraction identifiers, empty or duplicate field sets, missing or duplicate source channels, and type-incompatible normalization rules fail during schema construction. A caller must not reinterpret such a failure as an empty/default-success schema or silently substitute another source channel. + ## Security / privacy / governance impact Evidence is tenant-scoped, selectively disclosed, encrypted as appropriate, retention-bounded, and auditable. Credential-bearing headers, cookies, secret values, and sensitive form data are excluded or transformed according to explicit schema policy. Integrity metadata and immutable artifact identities support tamper detection without claiming external certification. `docs/DATA_GOVERNANCE.md` defines the disclosure/retention boundary for protected content and derived artifacts. +The extraction-schema contract does not modify governance authority. It describes admissible typed fields and reviewed evidence-channel classes only. In particular, declaring `NetworkResponse` or `ModelInterpretation` does not authorize network access, model execution, protected-data disclosure, approvals, retention, or export; those remain governed by their existing owning boundaries. + ## Tests and acceptance evidence Require provenance-link tests, credential-leak tests, integrity/corruption tests, crash-recovery tests, WARC/export conformance where implemented, PROV relation/schema tests where implemented, retention/deletion tests, tenant-isolation tests, and end-to-end checks that state-changing actions link request, policy, approval, execution, and post-condition as separate records. Export tests must prove that disabled or unauthorized source bodies never appear merely because metadata provenance is exportable. +The extraction-schema boundary additionally requires tests for the identifier grammar and limits, field-count bound, duplicate identifiers, source-channel presence and uniqueness, every reviewed value/cardinality/source-channel variant, deterministic normalization selection, incompatible normalization rejection, and the backward-compatible `Verbatim` constructor default. + ## Migration and rollback Introduce stable evidence identifiers and schema versions before changing export formats. Migrations preserve old evidence semantics or explicitly mark unavailable fields. Rollback may revert an exporter but cannot collapse mandatory action and policy evidence into opaque logs. +Extraction contract changes that alter field identity or semantics require a new reviewed schema version rather than mutating the meaning of an existing version. Rolling back a consumer may stop accepting a newer version, but it must not reinterpret that newer contract as an older one or silently discard required fields. + ## Open follow-ups -Finalize canonical evidence schemas, content-retention defaults, signing/attestation strategy, cross-system export identifiers, and buyer-controlled disclosure policies. +Finalize canonical evidence schemas, content-retention defaults, signing/attestation strategy, cross-system export identifiers, and buyer-controlled disclosure policies. Add the runtime that validates concrete extracted values against an `ExtractionSchema`, plus explicit migration/compatibility policy when durable schema registration is introduced. ## Supersession / reversal conditions From 58ba97a8613cb4fe58d5371dead20e9d5ccb13be Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 21:27:55 -0700 Subject: [PATCH 08/62] test(evidence): reject malformed WARC media types --- .../tests/warc_resource_record.rs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/crates/originweave-evidence/tests/warc_resource_record.rs b/crates/originweave-evidence/tests/warc_resource_record.rs index 410888aa3..a78fbce34 100644 --- a/crates/originweave-evidence/tests/warc_resource_record.rs +++ b/crates/originweave-evidence/tests/warc_resource_record.rs @@ -153,6 +153,47 @@ fn resource_record_rejects_invalid_identifiers_dates_content_and_limits() { ); } +#[test] +fn resource_record_accepts_valid_mime_parameters_and_rejects_malformed_media_types() { + let build = |content_type| { + WarcResourceRecord::new( + RECORD_ID, + DATE, + "https://example.com/item", + content_type, + Vec::new(), + provenance("https://example.com/item", VerificationResult::Verified), + ) + }; + + for content_type in [ + "text/plain; charset=utf-8", + "application/http; msgtype=response", + "multipart/form-data; boundary=example-boundary", + ] { + let record = build(content_type).expect("valid WARC MIME media type"); + assert_eq!(record.content_type(), content_type); + } + + for content_type in [ + "plain", + "/plain", + "text/", + "text//plain", + "text/plain;", + "text/plain; charset", + "text/plain; =utf-8", + "text/plain; charset=", + "text/(plain)", + ] { + assert_eq!( + build(content_type), + Err(WarcResourceRecordError::InvalidContentType), + "content_type={content_type:?}" + ); + } +} + #[test] fn resource_record_rejects_provenance_drift_and_unverified_sources() { assert_eq!( From fc0a059679ce401cce272c5f10cd7e96657360bb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 21:30:01 -0700 Subject: [PATCH 09/62] fix(evidence): validate WARC MIME media types --- .../src/warc_resource_record.rs | 110 +++++++++++++++++- 1 file changed, 104 insertions(+), 6 deletions(-) diff --git a/crates/originweave-evidence/src/warc_resource_record.rs b/crates/originweave-evidence/src/warc_resource_record.rs index a8786009b..0499c45c5 100644 --- a/crates/originweave-evidence/src/warc_resource_record.rs +++ b/crates/originweave-evidence/src/warc_resource_record.rs @@ -18,7 +18,7 @@ pub enum WarcResourceRecordError { InvalidRecordId, /// The date was not a bounded UTC RFC 3339 timestamp. InvalidDate, - /// The content type was empty or contained unsafe whitespace/control input. + /// The content type was not a bounded, syntactically valid MIME media type. InvalidContentType, /// A record field or payload exceeded its retention limit. LimitExceeded, @@ -229,11 +229,109 @@ fn is_leap_year(year: u16) -> bool { } fn valid_content_type(content_type: &str) -> bool { - !content_type.is_empty() - && content_type.len() <= MAX_WARC_CONTENT_TYPE_BYTES - && !content_type - .chars() - .any(|character| character.is_control() || character.is_whitespace()) + if content_type.is_empty() || content_type.len() > MAX_WARC_CONTENT_TYPE_BYTES { + return false; + } + + let mut parts = content_type.split(';'); + let Some(essence) = parts.next() else { + return false; + }; + let essence = trim_ows_end(essence); + let Some((media_type, media_subtype)) = essence.split_once('/') else { + return false; + }; + if !valid_mime_token(media_type) || !valid_mime_token(media_subtype) { + return false; + } + + parts.all(valid_mime_parameter) +} + +fn valid_mime_parameter(parameter: &str) -> bool { + let parameter = trim_ows(parameter); + let Some((attribute, value)) = parameter.split_once('=') else { + return false; + }; + valid_mime_token(attribute) && valid_mime_parameter_value(value) +} + +fn valid_mime_parameter_value(value: &str) -> bool { + if valid_mime_token(value) { + return true; + } + + let bytes = value.as_bytes(); + if bytes.len() < 2 || bytes.first() != Some(&b'"') || bytes.last() != Some(&b'"') { + return false; + } + + let mut escaped = false; + for byte in bytes[1..bytes.len() - 1].iter().copied() { + if escaped { + if !valid_quoted_pair_byte(byte) { + return false; + } + escaped = false; + } else if byte == b'\\' { + escaped = true; + } else if !valid_quoted_text_byte(byte) { + return false; + } + } + !escaped +} + +const fn valid_mime_token(value: &str) -> bool { + let bytes = value.as_bytes(); + if bytes.is_empty() { + return false; + } + let mut index = 0; + while index < bytes.len() { + if !is_mime_token_byte(bytes[index]) { + return false; + } + index += 1; + } + true +} + +const fn is_mime_token_byte(byte: u8) -> bool { + byte.is_ascii_alphanumeric() + || matches!( + byte, + b'!' | b'#' + | b'$' + | b'%' + | b'&' + | b'\'' + | b'*' + | b'+' + | b'-' + | b'.' + | b'^' + | b'_' + | b'`' + | b'|' + | b'~' + ) +} + +const fn valid_quoted_text_byte(byte: u8) -> bool { + byte == b'\t' || byte == b' ' || byte == b'!' || (byte >= 0x23 && byte <= 0x5b) || (byte >= 0x5d && byte <= 0x7e) +} + +const fn valid_quoted_pair_byte(byte: u8) -> bool { + byte == b'\t' || byte == b' ' || (byte >= 0x21 && byte <= 0x7e) +} + +fn trim_ows(value: &str) -> &str { + value.trim_matches(|character| matches!(character, ' ' | '\t')) +} + +fn trim_ows_end(value: &str) -> &str { + value.trim_end_matches(|character| matches!(character, ' ' | '\t')) } fn sha256_digest(payload: &[u8]) -> String { From 7c0d6e343095ed890bfe4ab0b3b7cd592598f866 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 21:32:46 -0700 Subject: [PATCH 10/62] style(evidence): apply rustfmt to MIME validator --- crates/originweave-evidence/src/warc_resource_record.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/originweave-evidence/src/warc_resource_record.rs b/crates/originweave-evidence/src/warc_resource_record.rs index 0499c45c5..5171a7fad 100644 --- a/crates/originweave-evidence/src/warc_resource_record.rs +++ b/crates/originweave-evidence/src/warc_resource_record.rs @@ -319,7 +319,11 @@ const fn is_mime_token_byte(byte: u8) -> bool { } const fn valid_quoted_text_byte(byte: u8) -> bool { - byte == b'\t' || byte == b' ' || byte == b'!' || (byte >= 0x23 && byte <= 0x5b) || (byte >= 0x5d && byte <= 0x7e) + byte == b'\t' + || byte == b' ' + || byte == b'!' + || (byte >= 0x23 && byte <= 0x5b) + || (byte >= 0x5d && byte <= 0x7e) } const fn valid_quoted_pair_byte(byte: u8) -> bool { From a93de9ef2ee23afc980a82977bb5eef366382f95 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 21:35:04 -0700 Subject: [PATCH 11/62] fix(evidence): satisfy strict MIME validator lint --- crates/originweave-evidence/src/warc_resource_record.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/originweave-evidence/src/warc_resource_record.rs b/crates/originweave-evidence/src/warc_resource_record.rs index 5171a7fad..51c255906 100644 --- a/crates/originweave-evidence/src/warc_resource_record.rs +++ b/crates/originweave-evidence/src/warc_resource_record.rs @@ -331,11 +331,11 @@ const fn valid_quoted_pair_byte(byte: u8) -> bool { } fn trim_ows(value: &str) -> &str { - value.trim_matches(|character| matches!(character, ' ' | '\t')) + value.trim_matches([' ', '\t']) } fn trim_ows_end(value: &str) -> &str { - value.trim_end_matches(|character| matches!(character, ' ' | '\t')) + value.trim_end_matches([' ', '\t']) } fn sha256_digest(payload: &[u8]) -> String { From ba11c697bf14740a171b49992707951c71dffb2e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 21:35:33 -0700 Subject: [PATCH 12/62] test(evidence): preserve quoted MIME parameter delimiters --- crates/originweave-evidence/tests/warc_resource_record.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/originweave-evidence/tests/warc_resource_record.rs b/crates/originweave-evidence/tests/warc_resource_record.rs index a78fbce34..3af10798e 100644 --- a/crates/originweave-evidence/tests/warc_resource_record.rs +++ b/crates/originweave-evidence/tests/warc_resource_record.rs @@ -170,6 +170,7 @@ fn resource_record_accepts_valid_mime_parameters_and_rejects_malformed_media_typ "text/plain; charset=utf-8", "application/http; msgtype=response", "multipart/form-data; boundary=example-boundary", + "text/plain; note=\"a;b\"", ] { let record = build(content_type).expect("valid WARC MIME media type"); assert_eq!(record.content_type(), content_type); From 9a73dd7bb04f087e46eacc27f1f75943c5bae560 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 21:38:41 -0700 Subject: [PATCH 13/62] fix(evidence): preserve quoted MIME parameter syntax --- .../src/warc_resource_record.rs | 39 ++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/crates/originweave-evidence/src/warc_resource_record.rs b/crates/originweave-evidence/src/warc_resource_record.rs index 51c255906..f5438b937 100644 --- a/crates/originweave-evidence/src/warc_resource_record.rs +++ b/crates/originweave-evidence/src/warc_resource_record.rs @@ -233,10 +233,11 @@ fn valid_content_type(content_type: &str) -> bool { return false; } - let mut parts = content_type.split(';'); - let Some(essence) = parts.next() else { - return false; - }; + let (essence, mut parameters) = content_type + .split_once(';') + .map_or((content_type, None), |(essence, parameters)| { + (essence, Some(parameters)) + }); let essence = trim_ows_end(essence); let Some((media_type, media_subtype)) = essence.split_once('/') else { return false; @@ -245,7 +246,35 @@ fn valid_content_type(content_type: &str) -> bool { return false; } - parts.all(valid_mime_parameter) + while let Some(parameter_text) = parameters { + let (parameter, remaining) = split_mime_parameter(parameter_text); + if !valid_mime_parameter(parameter) { + return false; + } + parameters = remaining; + } + true +} + +fn split_mime_parameter(value: &str) -> (&str, Option<&str>) { + let mut quoted = false; + let mut escaped = false; + for (index, byte) in value.bytes().enumerate() { + if quoted { + if escaped { + escaped = false; + } else if byte == b'\\' { + escaped = true; + } else if byte == b'"' { + quoted = false; + } + } else if byte == b'"' { + quoted = true; + } else if byte == b';' { + return (&value[..index], Some(&value[index + 1..])); + } + } + (value, None) } fn valid_mime_parameter(parameter: &str) -> bool { From 020e24197dc22da45bc82336f7bc03c233e478c7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 21:43:39 -0700 Subject: [PATCH 14/62] test(evidence): cover quoted MIME parser branches --- .../tests/warc_resource_record.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/crates/originweave-evidence/tests/warc_resource_record.rs b/crates/originweave-evidence/tests/warc_resource_record.rs index 3af10798e..753468ba3 100644 --- a/crates/originweave-evidence/tests/warc_resource_record.rs +++ b/crates/originweave-evidence/tests/warc_resource_record.rs @@ -171,6 +171,12 @@ fn resource_record_accepts_valid_mime_parameters_and_rejects_malformed_media_typ "application/http; msgtype=response", "multipart/form-data; boundary=example-boundary", "text/plain; note=\"a;b\"", + "text/plain; note=\"a;b\"; charset=utf-8", + "text/plain; note=\"a\\;b\"", + "text/plain; note=\"\t !\"", + "text/plain; note=\"\\\t\"", + "text/plain; note=\"\\ \"", + "text/plain; note=\"\\!\"", ] { let record = build(content_type).expect("valid WARC MIME media type"); assert_eq!(record.content_type(), content_type); @@ -186,6 +192,11 @@ fn resource_record_accepts_valid_mime_parameters_and_rejects_malformed_media_typ "text/plain; =utf-8", "text/plain; charset=", "text/(plain)", + "text/plain; note=(x", + "text/plain; note=\"unterminated", + "text/plain; note=\"a\"b\"", + "text/plain; note=\"a\nb\"", + "text/plain; note=\"\\\u{7f}\"", ] { assert_eq!( build(content_type), From a6d765b69782ed9d63f0fb30e1d5eb90193e1608 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 21:47:15 -0700 Subject: [PATCH 15/62] test(evidence): cover rejected quoted-pair controls --- crates/originweave-evidence/tests/warc_resource_record.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/originweave-evidence/tests/warc_resource_record.rs b/crates/originweave-evidence/tests/warc_resource_record.rs index 753468ba3..0a7cc7b2a 100644 --- a/crates/originweave-evidence/tests/warc_resource_record.rs +++ b/crates/originweave-evidence/tests/warc_resource_record.rs @@ -196,6 +196,7 @@ fn resource_record_accepts_valid_mime_parameters_and_rejects_malformed_media_typ "text/plain; note=\"unterminated", "text/plain; note=\"a\"b\"", "text/plain; note=\"a\nb\"", + "text/plain; note=\"\\\nb\"", "text/plain; note=\"\\\u{7f}\"", ] { assert_eq!( From 61c48771ef67843debe7e57ed2b7cdcd3f2a0dc2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 00:32:18 -0700 Subject: [PATCH 16/62] test(evidence): reject ambiguous WARC target URI formatting --- .../tests/warc_target_uri_presentation.rs | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 crates/originweave-evidence/tests/warc_target_uri_presentation.rs diff --git a/crates/originweave-evidence/tests/warc_target_uri_presentation.rs b/crates/originweave-evidence/tests/warc_target_uri_presentation.rs new file mode 100644 index 000000000..1bace9f5b --- /dev/null +++ b/crates/originweave-evidence/tests/warc_target_uri_presentation.rs @@ -0,0 +1,62 @@ +#![allow(clippy::expect_used)] + +use originweave_evidence::{ + EvidenceSourceKind, ProvenanceRecord, VerificationResult, WarcResourceRecord, + WarcResourceRecordError, +}; + +const SOURCE_HASH: &str = + "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; +const RECORD_ID: &str = "urn:uuid:123e4567-e89b-12d3-a456-426614174000"; +const DATE: &str = "2026-08-21T00:00:00Z"; + +fn provenance(source_url: &str) -> ProvenanceRecord { + ProvenanceRecord::new( + source_url, + "body", + SOURCE_HASH, + EvidenceSourceKind::NetworkResponse, + VerificationResult::Verified, + ) + .expect("provenance") +} + +#[test] +fn warc_target_uri_rejects_invisible_formatting_characters_before_serialization() { + for formatting_character in [ + '\u{00ad}', '\u{061c}', '\u{200b}', '\u{200e}', '\u{202e}', '\u{2066}', '\u{2060}', + '\u{feff}', + ] { + let target_uri = format!("https://example.com/item{formatting_character}shadow"); + let source_provenance = provenance(&target_uri); + + assert_eq!( + WarcResourceRecord::new( + RECORD_ID, + DATE, + &target_uri, + "text/plain", + Vec::new(), + source_provenance, + ), + Err(WarcResourceRecordError::InvalidTargetUri), + "target_uri={target_uri:?}" + ); + } +} + +#[test] +fn warc_target_uri_preserves_printable_unicode_path_text() { + let target_uri = "https://example.com/상품/상세"; + let record = WarcResourceRecord::new( + RECORD_ID, + DATE, + target_uri, + "text/plain", + Vec::new(), + provenance(target_uri), + ) + .expect("printable Unicode target URI"); + + assert_eq!(record.target_uri(), target_uri); +} From adb5ac8e45a56bd41ff8ca942051704dd28ca661 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 00:35:05 -0700 Subject: [PATCH 17/62] fix(evidence): reject ambiguous WARC target URI formatting --- .../src/warc_resource_record.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/crates/originweave-evidence/src/warc_resource_record.rs b/crates/originweave-evidence/src/warc_resource_record.rs index f5438b937..ea44ec17a 100644 --- a/crates/originweave-evidence/src/warc_resource_record.rs +++ b/crates/originweave-evidence/src/warc_resource_record.rs @@ -22,6 +22,8 @@ pub enum WarcResourceRecordError { InvalidContentType, /// A record field or payload exceeded its retention limit. LimitExceeded, + /// The WARC target URI contained a disallowed control or invisible formatting character. + InvalidTargetUri, /// The WARC target URI differed from its provenance source URL. TargetUriMismatch, /// The source provenance was not independently verified. @@ -63,6 +65,9 @@ impl WarcResourceRecord { WarcResourceRecordError::InvalidContentType }); } + if !valid_target_uri_presentation(target_uri) { + return Err(WarcResourceRecordError::InvalidTargetUri); + } if target_uri != provenance.source_url() { return Err(WarcResourceRecordError::TargetUriMismatch); } @@ -228,6 +233,20 @@ fn is_leap_year(year: u16) -> bool { year.is_multiple_of(400) || (year.is_multiple_of(4) && !year.is_multiple_of(100)) } +fn valid_target_uri_presentation(target_uri: &str) -> bool { + !target_uri.chars().any(disallowed_target_uri_character) +} + +fn disallowed_target_uri_character(character: char) -> bool { + let code_point = character as u32; + character.is_control() + || character.is_whitespace() + || matches!( + code_point, + 0x00ad | 0x061c | 0x200b..=0x200f | 0x2028..=0x202e | 0x2060..=0x206f | 0xfeff + ) +} + fn valid_content_type(content_type: &str) -> bool { if content_type.is_empty() || content_type.len() > MAX_WARC_CONTENT_TYPE_BYTES { return false; From ad1192b864485d9de45a9e81ed791365d2f34aed Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 00:37:31 -0700 Subject: [PATCH 18/62] test(evidence): cover WARC target presentation branches --- .../tests/warc_target_uri_presentation.rs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/crates/originweave-evidence/tests/warc_target_uri_presentation.rs b/crates/originweave-evidence/tests/warc_target_uri_presentation.rs index 1bace9f5b..7d773121c 100644 --- a/crates/originweave-evidence/tests/warc_target_uri_presentation.rs +++ b/crates/originweave-evidence/tests/warc_target_uri_presentation.rs @@ -45,6 +45,28 @@ fn warc_target_uri_rejects_invisible_formatting_characters_before_serialization( } } +#[test] +fn warc_target_uri_rejects_control_and_whitespace_before_provenance_comparison() { + let source_provenance = provenance("https://example.com/item"); + for target_uri in [ + "https://example.com/item\rshadow", + "https://example.com/item shadow", + ] { + assert_eq!( + WarcResourceRecord::new( + RECORD_ID, + DATE, + target_uri, + "text/plain", + Vec::new(), + source_provenance.clone(), + ), + Err(WarcResourceRecordError::InvalidTargetUri), + "target_uri={target_uri:?}" + ); + } +} + #[test] fn warc_target_uri_preserves_printable_unicode_path_text() { let target_uri = "https://example.com/상품/상세"; From 4d34db95aa4d8698edf33dac29612e8ca7eca26b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 00:39:28 -0700 Subject: [PATCH 19/62] docs(evidence): record WARC target URI presentation hardening --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90dd8e777..3ec10bbab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Security +- WARC target URI admission rejects control, whitespace, bidirectional-control, zero-width, soft-hyphen, Arabic-letter-mark, word-joiner/isolate, and BOM formatting characters before line-oriented WARC header serialization, while preserving ordinary printable Unicode path text. - Raw page content cannot become a trusted instruction. - Raw secrets are rejected and secret-capable actions require an opaque broker handle. - Crawler mode is read-only, must pair with the public-crawl purpose, and fails closed without an applicable robots-policy decision. From b6fb3a5501d7fb465afa290c0a3138b70380cdd4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 00:42:27 -0700 Subject: [PATCH 20/62] style(evidence): apply canonical rustfmt --- .../originweave-evidence/tests/warc_target_uri_presentation.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/originweave-evidence/tests/warc_target_uri_presentation.rs b/crates/originweave-evidence/tests/warc_target_uri_presentation.rs index 7d773121c..e67559f1b 100644 --- a/crates/originweave-evidence/tests/warc_target_uri_presentation.rs +++ b/crates/originweave-evidence/tests/warc_target_uri_presentation.rs @@ -5,8 +5,7 @@ use originweave_evidence::{ WarcResourceRecordError, }; -const SOURCE_HASH: &str = - "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; +const SOURCE_HASH: &str = "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; const RECORD_ID: &str = "urn:uuid:123e4567-e89b-12d3-a456-426614174000"; const DATE: &str = "2026-08-21T00:00:00Z"; From 577346ec3ecdac90bee85887eb660e8ae0089b6a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 03:09:29 -0700 Subject: [PATCH 21/62] test(evidence): require credential-safe WARC debug output --- .../tests/warc_debug_redaction.rs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 crates/originweave-evidence/tests/warc_debug_redaction.rs diff --git a/crates/originweave-evidence/tests/warc_debug_redaction.rs b/crates/originweave-evidence/tests/warc_debug_redaction.rs new file mode 100644 index 000000000..ca9a4a1ac --- /dev/null +++ b/crates/originweave-evidence/tests/warc_debug_redaction.rs @@ -0,0 +1,29 @@ +use originweave_evidence::{ + EvidenceSourceKind, ProvenanceRecord, VerificationResult, WarcResourceRecord, +}; + +#[test] +fn warc_record_debug_does_not_disclose_payload_or_provenance_locator() { + let provenance = ProvenanceRecord::new( + "https://example.com/resource", + "private-selector-marker", + "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + EvidenceSourceKind::NetworkResponse, + VerificationResult::Verified, + ) + .expect("verified provenance"); + let record = WarcResourceRecord::new( + "urn:uuid:123e4567-e89b-12d3-a456-426614174000", + "2026-08-21T09:00:00Z", + "https://example.com/resource", + "application/octet-stream", + vec![254, 237, 250, 206], + provenance, + ) + .expect("WARC resource record"); + + let debug = format!("{record:?}"); + assert!(debug.contains("payload_byte_count")); + assert!(!debug.contains("254, 237, 250, 206")); + assert!(!debug.contains("private-selector-marker")); +} From faa2e2d0b6cc900db69be98cdfbe8d90eaf68578 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 03:14:40 -0700 Subject: [PATCH 22/62] fix(evidence): redact sensitive WARC debug fields --- .../src/warc_resource_record.rs | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/crates/originweave-evidence/src/warc_resource_record.rs b/crates/originweave-evidence/src/warc_resource_record.rs index ea44ec17a..495379774 100644 --- a/crates/originweave-evidence/src/warc_resource_record.rs +++ b/crates/originweave-evidence/src/warc_resource_record.rs @@ -1,3 +1,5 @@ +use std::fmt; + use sha2::{Digest, Sha256}; use crate::{ProvenanceRecord, VerificationResult}; @@ -31,7 +33,7 @@ pub enum WarcResourceRecordError { } /// An immutable, bounded WARC `resource` record over already-authorized bytes. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Clone, PartialEq, Eq)] pub struct WarcResourceRecord { record_id: String, warc_date: String, @@ -42,6 +44,23 @@ pub struct WarcResourceRecord { provenance: ProvenanceRecord, } +impl fmt::Debug for WarcResourceRecord { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("WarcResourceRecord") + .field("record_id", &self.record_id) + .field("warc_date", &self.warc_date) + .field("content_type", &self.content_type) + .field("payload_byte_count", &self.payload.len()) + .field("block_digest", &self.block_digest) + .field( + "provenance_verification_result", + &self.provenance.verification_result(), + ) + .finish() + } +} + impl WarcResourceRecord { /// Validate and construct one resource record without contacting a live origin. pub fn new( @@ -393,4 +412,4 @@ fn sha256_digest(payload: &[u8]) -> String { encoded.push_str(&format!("{byte:02x}")); } encoded -} +} \ No newline at end of file From d17f9c02780a0056eb4d9e5990ee0a3c8ba2ce38 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 03:18:01 -0700 Subject: [PATCH 23/62] style(evidence): apply canonical Rust formatting --- crates/originweave-evidence/src/warc_resource_record.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/originweave-evidence/src/warc_resource_record.rs b/crates/originweave-evidence/src/warc_resource_record.rs index 495379774..d4ce3d641 100644 --- a/crates/originweave-evidence/src/warc_resource_record.rs +++ b/crates/originweave-evidence/src/warc_resource_record.rs @@ -412,4 +412,4 @@ fn sha256_digest(payload: &[u8]) -> String { encoded.push_str(&format!("{byte:02x}")); } encoded -} \ No newline at end of file +} From f84e6baa3f29be9bca9951124a278fc8b1ed0be4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 03:21:03 -0700 Subject: [PATCH 24/62] test(evidence): align debug regression with strict clippy --- crates/originweave-evidence/tests/warc_debug_redaction.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/originweave-evidence/tests/warc_debug_redaction.rs b/crates/originweave-evidence/tests/warc_debug_redaction.rs index ca9a4a1ac..b51d8d604 100644 --- a/crates/originweave-evidence/tests/warc_debug_redaction.rs +++ b/crates/originweave-evidence/tests/warc_debug_redaction.rs @@ -1,3 +1,5 @@ +#![allow(clippy::expect_used)] + use originweave_evidence::{ EvidenceSourceKind, ProvenanceRecord, VerificationResult, WarcResourceRecord, }; From d16e648b22c2ab36b3efd9348f80b23f22e03e8e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 06:31:57 -0700 Subject: [PATCH 25/62] test(evidence): classify oversized WARC fields as limits --- .../tests/warc_field_limit_errors.rs | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 crates/originweave-evidence/tests/warc_field_limit_errors.rs diff --git a/crates/originweave-evidence/tests/warc_field_limit_errors.rs b/crates/originweave-evidence/tests/warc_field_limit_errors.rs new file mode 100644 index 000000000..2752b9ced --- /dev/null +++ b/crates/originweave-evidence/tests/warc_field_limit_errors.rs @@ -0,0 +1,52 @@ +#![allow(clippy::expect_used)] + +use originweave_evidence::{ + EvidenceSourceKind, ProvenanceRecord, VerificationResult, WarcResourceRecord, + WarcResourceRecordError, +}; + +const SOURCE_HASH: &str = + "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; +const RECORD_ID: &str = "urn:uuid:123e4567-e89b-12d3-a456-426614174000"; +const DATE: &str = "2026-08-21T00:00:00Z"; +const TARGET_URI: &str = "https://example.com/item"; + +fn provenance() -> ProvenanceRecord { + ProvenanceRecord::new( + TARGET_URI, + "body", + SOURCE_HASH, + EvidenceSourceKind::NetworkResponse, + VerificationResult::Verified, + ) + .expect("valid provenance") +} + +#[test] +fn oversized_record_fields_report_the_bounded_limit_error() { + let oversized_record_id = format!("{RECORD_ID}x"); + assert_eq!( + WarcResourceRecord::new( + &oversized_record_id, + DATE, + TARGET_URI, + "text/plain", + Vec::new(), + provenance(), + ), + Err(WarcResourceRecordError::LimitExceeded), + ); + + let oversized_date = "2026-08-21T00:00:00.1234567890Z"; + assert_eq!( + WarcResourceRecord::new( + RECORD_ID, + oversized_date, + TARGET_URI, + "text/plain", + Vec::new(), + provenance(), + ), + Err(WarcResourceRecordError::LimitExceeded), + ); +} From 894dbecd705c94556badbd9dd94062f6b7e099cf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 06:33:21 -0700 Subject: [PATCH 26/62] test(evidence): apply canonical formatting --- crates/originweave-evidence/tests/warc_field_limit_errors.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/originweave-evidence/tests/warc_field_limit_errors.rs b/crates/originweave-evidence/tests/warc_field_limit_errors.rs index 2752b9ced..2a07adb81 100644 --- a/crates/originweave-evidence/tests/warc_field_limit_errors.rs +++ b/crates/originweave-evidence/tests/warc_field_limit_errors.rs @@ -5,8 +5,7 @@ use originweave_evidence::{ WarcResourceRecordError, }; -const SOURCE_HASH: &str = - "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; +const SOURCE_HASH: &str = "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; const RECORD_ID: &str = "urn:uuid:123e4567-e89b-12d3-a456-426614174000"; const DATE: &str = "2026-08-21T00:00:00Z"; const TARGET_URI: &str = "https://example.com/item"; From c9af0ca3e2704a1633770b8bcf9590e89ddddd21 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 06:36:49 -0700 Subject: [PATCH 27/62] fix(evidence): preserve WARC field limit errors --- crates/originweave-evidence/src/warc_resource_record.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/originweave-evidence/src/warc_resource_record.rs b/crates/originweave-evidence/src/warc_resource_record.rs index d4ce3d641..302110570 100644 --- a/crates/originweave-evidence/src/warc_resource_record.rs +++ b/crates/originweave-evidence/src/warc_resource_record.rs @@ -71,6 +71,12 @@ impl WarcResourceRecord { payload: Vec, provenance: ProvenanceRecord, ) -> Result { + if record_id.len() > MAX_WARC_RECORD_ID_BYTES { + return Err(WarcResourceRecordError::LimitExceeded); + } + if warc_date.len() > MAX_WARC_DATE_BYTES { + return Err(WarcResourceRecordError::LimitExceeded); + } if !valid_record_id(record_id) { return Err(WarcResourceRecordError::InvalidRecordId); } From a38f9e56528c66e8b60a5e9af7d6437b5b3395e1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 06:39:50 -0700 Subject: [PATCH 28/62] test(evidence): align WARC limit error contract --- crates/originweave-evidence/tests/warc_resource_record.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/originweave-evidence/tests/warc_resource_record.rs b/crates/originweave-evidence/tests/warc_resource_record.rs index 0a7cc7b2a..137cc6d53 100644 --- a/crates/originweave-evidence/tests/warc_resource_record.rs +++ b/crates/originweave-evidence/tests/warc_resource_record.rs @@ -111,7 +111,6 @@ fn resource_record_rejects_invalid_identifiers_dates_content_and_limits() { "2026-08x21T00:00:00Z", "2026-08-21T00x00:00Z", "2026-08-21T00:00x00Z", - "2026-08-21T00:00:00.12345678901234567890Z", "2026-08-21T00:00:00X", "2026-08-21T00:61:00Z", "2026-08-21T00:00:61Z", From 84642d93160f86282fabcc2297f24b14f97b13a6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 07:48:54 -0700 Subject: [PATCH 29/62] test(evidence): require RFC 3986 WARC target URIs --- .../tests/warc_target_uri_presentation.rs | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/crates/originweave-evidence/tests/warc_target_uri_presentation.rs b/crates/originweave-evidence/tests/warc_target_uri_presentation.rs index e67559f1b..70ba868ab 100644 --- a/crates/originweave-evidence/tests/warc_target_uri_presentation.rs +++ b/crates/originweave-evidence/tests/warc_target_uri_presentation.rs @@ -67,8 +67,25 @@ fn warc_target_uri_rejects_control_and_whitespace_before_provenance_comparison() } #[test] -fn warc_target_uri_preserves_printable_unicode_path_text() { +fn warc_target_uri_rejects_raw_unicode_because_warc_uses_rfc3986_uri_syntax() { let target_uri = "https://example.com/상품/상세"; + + assert_eq!( + WarcResourceRecord::new( + RECORD_ID, + DATE, + target_uri, + "text/plain", + Vec::new(), + provenance(target_uri), + ), + Err(WarcResourceRecordError::InvalidTargetUri), + ); +} + +#[test] +fn warc_target_uri_accepts_percent_encoded_utf8_path_octets() { + let target_uri = "https://example.com/%EC%83%81%ED%92%88/%EC%83%81%EC%84%B8"; let record = WarcResourceRecord::new( RECORD_ID, DATE, @@ -77,7 +94,7 @@ fn warc_target_uri_preserves_printable_unicode_path_text() { Vec::new(), provenance(target_uri), ) - .expect("printable Unicode target URI"); + .expect("RFC 3986 percent-encoded target URI"); assert_eq!(record.target_uri(), target_uri); } From f4cd9df989dd327de5b951c6cc09d0629d187869 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 07:50:03 -0700 Subject: [PATCH 30/62] test(evidence): cover RFC 3986 target URI syntax --- .../tests/warc_target_uri_presentation.rs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/crates/originweave-evidence/tests/warc_target_uri_presentation.rs b/crates/originweave-evidence/tests/warc_target_uri_presentation.rs index 70ba868ab..ed637b7ae 100644 --- a/crates/originweave-evidence/tests/warc_target_uri_presentation.rs +++ b/crates/originweave-evidence/tests/warc_target_uri_presentation.rs @@ -83,6 +83,26 @@ fn warc_target_uri_rejects_raw_unicode_because_warc_uses_rfc3986_uri_syntax() { ); } +#[test] +fn warc_target_uri_rejects_ascii_characters_outside_rfc3986_uri_syntax() { + for invalid_character in ['<', '>', '"', '{', '}', '|', '^', '`'] { + let target_uri = format!("https://example.com/item{invalid_character}shadow"); + + assert_eq!( + WarcResourceRecord::new( + RECORD_ID, + DATE, + &target_uri, + "text/plain", + Vec::new(), + provenance(&target_uri), + ), + Err(WarcResourceRecordError::InvalidTargetUri), + "target_uri={target_uri:?}" + ); + } +} + #[test] fn warc_target_uri_accepts_percent_encoded_utf8_path_octets() { let target_uri = "https://example.com/%EC%83%81%ED%92%88/%EC%83%81%EC%84%B8"; From 05df0b71f87c0bbf403fe22481e9b56158939f9f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 07:52:55 -0700 Subject: [PATCH 31/62] fix(evidence): enforce RFC 3986 WARC target syntax --- .../src/warc_resource_record.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/crates/originweave-evidence/src/warc_resource_record.rs b/crates/originweave-evidence/src/warc_resource_record.rs index 302110570..9f3448c96 100644 --- a/crates/originweave-evidence/src/warc_resource_record.rs +++ b/crates/originweave-evidence/src/warc_resource_record.rs @@ -24,7 +24,7 @@ pub enum WarcResourceRecordError { InvalidContentType, /// A record field or payload exceeded its retention limit. LimitExceeded, - /// The WARC target URI contained a disallowed control or invisible formatting character. + /// The WARC target URI contained octets outside RFC 3986 URI syntax. InvalidTargetUri, /// The WARC target URI differed from its provenance source URL. TargetUriMismatch, @@ -259,16 +259,17 @@ fn is_leap_year(year: u16) -> bool { } fn valid_target_uri_presentation(target_uri: &str) -> bool { - !target_uri.chars().any(disallowed_target_uri_character) + target_uri.bytes().all(is_rfc3986_uri_byte) } -fn disallowed_target_uri_character(character: char) -> bool { - let code_point = character as u32; - character.is_control() - || character.is_whitespace() +const fn is_rfc3986_uri_byte(byte: u8) -> bool { + byte.is_ascii_alphanumeric() || matches!( - code_point, - 0x00ad | 0x061c | 0x200b..=0x200f | 0x2028..=0x202e | 0x2060..=0x206f | 0xfeff + byte, + b'-' | b'.' | b'_' | b'~' + | b':' | b'/' | b'?' | b'#' | b'[' | b']' | b'@' + | b'!' | b'$' | b'&' | b'\'' | b'(' | b')' | b'*' | b'+' | b',' | b';' | b'=' + | b'%' ) } From c00f1686dfe039bf3b48682312d39cff2e423337 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 08:32:21 -0700 Subject: [PATCH 32/62] chore(evidence): apply canonical RFC3986 formatting --- .../src/warc_resource_record.rs | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/crates/originweave-evidence/src/warc_resource_record.rs b/crates/originweave-evidence/src/warc_resource_record.rs index 9f3448c96..6d3adc4fe 100644 --- a/crates/originweave-evidence/src/warc_resource_record.rs +++ b/crates/originweave-evidence/src/warc_resource_record.rs @@ -266,9 +266,27 @@ const fn is_rfc3986_uri_byte(byte: u8) -> bool { byte.is_ascii_alphanumeric() || matches!( byte, - b'-' | b'.' | b'_' | b'~' - | b':' | b'/' | b'?' | b'#' | b'[' | b']' | b'@' - | b'!' | b'$' | b'&' | b'\'' | b'(' | b')' | b'*' | b'+' | b',' | b';' | b'=' + b'-' | b'.' + | b'_' + | b'~' + | b':' + | b'/' + | b'?' + | b'#' + | b'[' + | b']' + | b'@' + | b'!' + | b'$' + | b'&' + | b'\'' + | b'(' + | b')' + | b'*' + | b'+' + | b',' + | b';' + | b'=' | b'%' ) } From 1609b9191c9ade942bd184a69bb5f6444f8eef63 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 09:33:04 -0700 Subject: [PATCH 33/62] test(evidence): require standard WARC error contract --- .../tests/warc_resource_record.rs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/crates/originweave-evidence/tests/warc_resource_record.rs b/crates/originweave-evidence/tests/warc_resource_record.rs index 137cc6d53..bc6172f05 100644 --- a/crates/originweave-evidence/tests/warc_resource_record.rs +++ b/crates/originweave-evidence/tests/warc_resource_record.rs @@ -20,6 +20,43 @@ fn provenance(source_url: &str, verification: VerificationResult) -> ProvenanceR .expect("provenance") } +fn assert_standard_error_contract() {} + +#[test] +fn warc_resource_record_error_implements_standard_error_contract() { + assert_standard_error_contract::(); + + for (error, message) in [ + ( + WarcResourceRecordError::InvalidRecordId, + "invalid WARC record identifier", + ), + (WarcResourceRecordError::InvalidDate, "invalid WARC date"), + ( + WarcResourceRecordError::InvalidContentType, + "invalid WARC content type", + ), + ( + WarcResourceRecordError::LimitExceeded, + "WARC resource record limit exceeded", + ), + ( + WarcResourceRecordError::InvalidTargetUri, + "invalid WARC target URI", + ), + ( + WarcResourceRecordError::TargetUriMismatch, + "WARC target URI does not match provenance", + ), + ( + WarcResourceRecordError::UnverifiedProvenance, + "WARC provenance is not independently verified", + ), + ] { + assert_eq!(error.to_string(), message); + } +} + #[test] fn resource_record_binds_verified_provenance_and_emits_deterministic_warc_bytes() { let record = WarcResourceRecord::new( From 6bfcadca2ccb4e3d61573e7bc2680ec62f34ae8a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 09:36:23 -0700 Subject: [PATCH 34/62] fix(evidence): expose standard WARC error contract --- .../src/warc_resource_record.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/crates/originweave-evidence/src/warc_resource_record.rs b/crates/originweave-evidence/src/warc_resource_record.rs index 6d3adc4fe..031afa50f 100644 --- a/crates/originweave-evidence/src/warc_resource_record.rs +++ b/crates/originweave-evidence/src/warc_resource_record.rs @@ -32,6 +32,22 @@ pub enum WarcResourceRecordError { UnverifiedProvenance, } +impl fmt::Display for WarcResourceRecordError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(match self { + Self::InvalidRecordId => "invalid WARC record identifier", + Self::InvalidDate => "invalid WARC date", + Self::InvalidContentType => "invalid WARC content type", + Self::LimitExceeded => "WARC resource record limit exceeded", + Self::InvalidTargetUri => "invalid WARC target URI", + Self::TargetUriMismatch => "WARC target URI does not match provenance", + Self::UnverifiedProvenance => "WARC provenance is not independently verified", + }) + } +} + +impl std::error::Error for WarcResourceRecordError {} + /// An immutable, bounded WARC `resource` record over already-authorized bytes. #[derive(Clone, PartialEq, Eq)] pub struct WarcResourceRecord { From 920dd198bf6e2be3e31658b9cdae8bea84724acf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 09:38:31 -0700 Subject: [PATCH 35/62] docs: record WARC error integration --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ec10bbab..7b53d2ebe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,7 +34,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Changed -- Added a bounded immutable WARC 1.1 resource-record contract on the active extraction lane, binding deterministic bytes and SHA-256 block digests to verified provenance without claiming durable persistence or PROV export. +- Added a bounded immutable WARC 1.1 resource-record contract on the active extraction lane, binding deterministic bytes and SHA-256 block digests to verified provenance and exposing its typed construction failures through deterministic `Display` and `std::error::Error` contracts without claiming durable persistence or PROV export. - Separated logical origin authority from resolved network destination authority; an origin grant no longer implies permission to connect to every resolver result. - Separated resolved-address authorization from direct transport evidence; an approved IP now becomes a usable stream only after the operating system reports the exact requested IP and port. - Separated exact TCP peer proof from authenticated TLS service identity; an observed peer becomes an authenticated HTTPS stream only after explicit-root, fixed-time, SAN-bound WebPKI verification over that same stream. From ff1d27a0ae779bd27bcc5ea90a2b58dd76c19bb0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 15:16:33 -0700 Subject: [PATCH 36/62] test(evidence): retain extraction error contract in WARC stack --- .../tests/extraction_schema_error_contract.rs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 crates/originweave-evidence/tests/extraction_schema_error_contract.rs diff --git a/crates/originweave-evidence/tests/extraction_schema_error_contract.rs b/crates/originweave-evidence/tests/extraction_schema_error_contract.rs new file mode 100644 index 000000000..40cc6d79b --- /dev/null +++ b/crates/originweave-evidence/tests/extraction_schema_error_contract.rs @@ -0,0 +1,44 @@ +use std::error::Error as _; + +use originweave_evidence::ExtractionSchemaError; + +fn assert_standard_error_contract() {} + +#[test] +fn extraction_schema_errors_implement_standard_error_contract() { + assert_standard_error_contract::(); + + for (error, message) in [ + ( + ExtractionSchemaError::InvalidIdentifier, + "invalid extraction schema identifier", + ), + ( + ExtractionSchemaError::LimitExceeded, + "extraction schema limit exceeded", + ), + ( + ExtractionSchemaError::MissingSourceChannel, + "extraction field requires at least one source channel", + ), + ( + ExtractionSchemaError::DuplicateSourceChannel, + "extraction field contains a duplicate source channel", + ), + ( + ExtractionSchemaError::InvalidNormalizationRule, + "extraction normalization rule is incompatible with the field value type", + ), + ( + ExtractionSchemaError::MissingField, + "extraction schema requires at least one field", + ), + ( + ExtractionSchemaError::DuplicateField, + "extraction schema contains a duplicate field identifier", + ), + ] { + assert_eq!(error.to_string(), message); + assert!(error.source().is_none()); + } +} From f5084a68def46573d2f2c50e110845cdc6174104 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 15:18:50 -0700 Subject: [PATCH 37/62] fix(evidence): converge WARC stack on schema error contract --- .../src/extraction_schema.rs | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/crates/originweave-evidence/src/extraction_schema.rs b/crates/originweave-evidence/src/extraction_schema.rs index d8f978e74..abffc4679 100644 --- a/crates/originweave-evidence/src/extraction_schema.rs +++ b/crates/originweave-evidence/src/extraction_schema.rs @@ -5,7 +5,7 @@ //! disclose protected values, persist artifacts, execute models, or grant any //! browser, network, secret, approval, or storage authority. -use std::collections::BTreeSet; +use std::{collections::BTreeSet, fmt}; /// Maximum encoded byte length for an extraction schema or field identifier. pub const MAX_EXTRACTION_IDENTIFIER_BYTES: usize = 128; @@ -83,6 +83,24 @@ pub enum ExtractionSchemaError { DuplicateField, } +impl fmt::Display for ExtractionSchemaError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(match self { + Self::InvalidIdentifier => "invalid extraction schema identifier", + Self::LimitExceeded => "extraction schema limit exceeded", + Self::MissingSourceChannel => "extraction field requires at least one source channel", + Self::DuplicateSourceChannel => "extraction field contains a duplicate source channel", + Self::InvalidNormalizationRule => { + "extraction normalization rule is incompatible with the field value type" + } + Self::MissingField => "extraction schema requires at least one field", + Self::DuplicateField => "extraction schema contains a duplicate field identifier", + }) + } +} + +impl std::error::Error for ExtractionSchemaError {} + /// One typed field declared by a versioned extraction schema. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ExtractionField { From 67dad69869280c4a910140d23c0d6810de8db99d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 15:22:28 -0700 Subject: [PATCH 38/62] docs(changelog): converge WARC stack schema error contract --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b53d2ebe..06b6592e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Real loopback rustls integration covering trusted DNS SAN, Common-Name fallback rejection, wrong-name and untrusted-root rejection, fixed-time expiry and not-yet-valid failures, exact IPv4 and IPv6 SANs, TLS 1.2/TLS 1.3, required and optional ALPN, and transport-origin binding. - Cumulative interactive-first RAM, VRAM, batch, local-model, admission, pause, and compositor-pressure mitigation plans, including active-consumer reduction at exact hard limits. - Universally value-redacted network evidence with explicit path, metadata, and provenance bounds; ambiguous path rejection; validated source URLs; lowercase SHA-256 identifiers; and verification state. -- Versioned schema-bound extraction contracts with bounded identifiers and field counts, typed value/cardinality metadata, explicit duplicate-free reviewed source channels, and fail-closed schema validation. +- Versioned schema-bound extraction contracts with bounded identifiers and field counts, typed value/cardinality metadata, explicit duplicate-free reviewed source channels, fail-closed schema validation, and deterministic `Display`/`std::error::Error` contracts for public schema failures. - Rust 1.97.1 build contract, strict Clippy and rustdoc gates, and exact production function, line, region, and branch coverage enforcement. - Hourly bounded OpenCode product-development workflow using `NVIDIA_NIM_API_KEY`, an unprivileged disposable workspace, loopback-only model broker, independently verified patches, and publication through a dedicated `OPENCODE_PR_TOKEN` that cannot review or merge. - Architecture, agent, security, contribution, research, database naming, roadmap, quality-gate, and TLS service-identity ADR documentation. From e4aeacb63e561a821fb08acd649f9c619922f274 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 17:10:32 -0700 Subject: [PATCH 39/62] test(evidence): preserve parent source-channel set identity --- .../tests/extraction_source_channel_set.rs | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 crates/originweave-evidence/tests/extraction_source_channel_set.rs diff --git a/crates/originweave-evidence/tests/extraction_source_channel_set.rs b/crates/originweave-evidence/tests/extraction_source_channel_set.rs new file mode 100644 index 000000000..1f5070e8a --- /dev/null +++ b/crates/originweave-evidence/tests/extraction_source_channel_set.rs @@ -0,0 +1,40 @@ +#![allow(clippy::expect_used)] + +use originweave_evidence::{ + ExtractionCardinality, ExtractionField, ExtractionSourceChannel, ExtractionValueType, +}; + +#[test] +fn equivalent_source_channel_sets_have_canonical_identity() { + let semantic_then_network = ExtractionField::new( + "product_name", + ExtractionValueType::Text, + ExtractionCardinality::One, + true, + &[ + ExtractionSourceChannel::SemanticNode, + ExtractionSourceChannel::NetworkResponse, + ], + ) + .expect("reviewed source set must be valid"); + let network_then_semantic = ExtractionField::new( + "product_name", + ExtractionValueType::Text, + ExtractionCardinality::One, + true, + &[ + ExtractionSourceChannel::NetworkResponse, + ExtractionSourceChannel::SemanticNode, + ], + ) + .expect("equivalent reviewed source set must be valid"); + + assert_eq!(semantic_then_network, network_then_semantic); + assert_eq!( + network_then_semantic.source_channels(), + &[ + ExtractionSourceChannel::SemanticNode, + ExtractionSourceChannel::NetworkResponse, + ] + ); +} From 57db9905c5dd09da934ad35031a102e4cc7466c8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 17:12:11 -0700 Subject: [PATCH 40/62] fix(evidence): preserve parent extraction source-set identity --- crates/originweave-evidence/src/extraction_schema.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/originweave-evidence/src/extraction_schema.rs b/crates/originweave-evidence/src/extraction_schema.rs index abffc4679..ca2ba4881 100644 --- a/crates/originweave-evidence/src/extraction_schema.rs +++ b/crates/originweave-evidence/src/extraction_schema.rs @@ -86,7 +86,7 @@ pub enum ExtractionSchemaError { impl fmt::Display for ExtractionSchemaError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str(match self { - Self::InvalidIdentifier => "invalid extraction schema identifier", + Self::InvalidIdentifier => "invalid extraction schema or field identifier", Self::LimitExceeded => "extraction schema limit exceeded", Self::MissingSourceChannel => "extraction field requires at least one source channel", Self::DuplicateSourceChannel => "extraction field contains a duplicate source channel", @@ -169,7 +169,7 @@ impl ExtractionField { cardinality, required, normalization_rule, - source_channels: source_channels.to_vec(), + source_channels: seen_channels.into_iter().collect(), }) } From 20a516e83d99b471589974bc8150c2a0f8989fbb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 17:12:52 -0700 Subject: [PATCH 41/62] test(evidence): align extraction error contract with parent --- .../tests/extraction_schema_error_contract.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/originweave-evidence/tests/extraction_schema_error_contract.rs b/crates/originweave-evidence/tests/extraction_schema_error_contract.rs index 40cc6d79b..1ba248a1d 100644 --- a/crates/originweave-evidence/tests/extraction_schema_error_contract.rs +++ b/crates/originweave-evidence/tests/extraction_schema_error_contract.rs @@ -11,7 +11,7 @@ fn extraction_schema_errors_implement_standard_error_contract() { for (error, message) in [ ( ExtractionSchemaError::InvalidIdentifier, - "invalid extraction schema identifier", + "invalid extraction schema or field identifier", ), ( ExtractionSchemaError::LimitExceeded, From e0a091797913f9b0c08b44f33b034482d6ccd3a0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 17:17:19 -0700 Subject: [PATCH 42/62] test(evidence): require explicit WARC truncation state --- .../tests/warc_truncation_state.rs | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 crates/originweave-evidence/tests/warc_truncation_state.rs diff --git a/crates/originweave-evidence/tests/warc_truncation_state.rs b/crates/originweave-evidence/tests/warc_truncation_state.rs new file mode 100644 index 000000000..9e20e9cda --- /dev/null +++ b/crates/originweave-evidence/tests/warc_truncation_state.rs @@ -0,0 +1,66 @@ +#![allow(clippy::expect_used)] + +use originweave_evidence::{ + EvidenceSourceKind, ProvenanceRecord, VerificationResult, WarcPayloadCompleteness, + WarcResourceRecord, WarcTruncationReason, +}; + +const RECORD_ID: &str = "urn:uuid:01234567-89ab-cdef-0123-456789abcdef"; +const DATE: &str = "2026-08-22T00:00:00Z"; +const SOURCE_URL: &str = "https://example.com/resource"; +const SOURCE_HASH: &str = + "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + +fn verified_provenance() -> ProvenanceRecord { + ProvenanceRecord::new( + SOURCE_URL, + "body", + SOURCE_HASH, + EvidenceSourceKind::NetworkResponse, + VerificationResult::Verified, + ) + .expect("reviewed provenance fixture must be valid") +} + +#[test] +fn warc_resource_records_preserve_explicit_completeness_and_truncation_reason() { + let complete = WarcResourceRecord::new_with_completeness( + RECORD_ID, + DATE, + SOURCE_URL, + "text/plain", + b"hello".to_vec(), + verified_provenance(), + WarcPayloadCompleteness::Complete, + ) + .expect("complete capture must be admitted"); + assert_eq!(complete.completeness(), WarcPayloadCompleteness::Complete); + let complete_bytes = String::from_utf8(complete.to_warc_bytes()) + .expect("text fixture must serialize as UTF-8 WARC bytes"); + assert!(!complete_bytes.contains("WARC-Truncated:")); + + for (reason, token) in [ + (WarcTruncationReason::Length, "length"), + (WarcTruncationReason::Time, "time"), + (WarcTruncationReason::Disconnect, "disconnect"), + (WarcTruncationReason::Unspecified, "unspecified"), + ] { + let completeness = WarcPayloadCompleteness::Truncated(reason); + let truncated = WarcResourceRecord::new_with_completeness( + RECORD_ID, + DATE, + SOURCE_URL, + "text/plain", + b"hello".to_vec(), + verified_provenance(), + completeness, + ) + .expect("typed truncated capture must be admitted"); + + assert_eq!(truncated.completeness(), completeness); + let truncated_bytes = String::from_utf8(truncated.to_warc_bytes()) + .expect("text fixture must serialize as UTF-8 WARC bytes"); + assert!(truncated_bytes.contains(&format!("WARC-Truncated: {token}\r\n"))); + assert!(truncated_bytes.contains("Content-Length: 5\r\n")); + } +} From 634c4b7d34c9f56a93450758c2fcc1ddd3ffc648 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 18:34:30 -0700 Subject: [PATCH 43/62] style(evidence): apply canonical rustfmt to WARC truncation regression --- crates/originweave-evidence/tests/warc_truncation_state.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/originweave-evidence/tests/warc_truncation_state.rs b/crates/originweave-evidence/tests/warc_truncation_state.rs index 9e20e9cda..e09d01ae0 100644 --- a/crates/originweave-evidence/tests/warc_truncation_state.rs +++ b/crates/originweave-evidence/tests/warc_truncation_state.rs @@ -8,8 +8,7 @@ use originweave_evidence::{ const RECORD_ID: &str = "urn:uuid:01234567-89ab-cdef-0123-456789abcdef"; const DATE: &str = "2026-08-22T00:00:00Z"; const SOURCE_URL: &str = "https://example.com/resource"; -const SOURCE_HASH: &str = - "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; +const SOURCE_HASH: &str = "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; fn verified_provenance() -> ProvenanceRecord { ProvenanceRecord::new( From 9d193df3ebd386c7aaddfd176b98717045ccbefb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 18:35:20 -0700 Subject: [PATCH 44/62] feat(evidence): preserve explicit WARC truncation state --- .../src/warc_resource_record.rs | 78 ++++++++++++++++++- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/crates/originweave-evidence/src/warc_resource_record.rs b/crates/originweave-evidence/src/warc_resource_record.rs index 031afa50f..fe93f5e4b 100644 --- a/crates/originweave-evidence/src/warc_resource_record.rs +++ b/crates/originweave-evidence/src/warc_resource_record.rs @@ -13,6 +13,39 @@ pub const MAX_WARC_CONTENT_TYPE_BYTES: usize = 256; /// Maximum resource payload retained by one immutable WARC record. pub const MAX_WARC_PAYLOAD_BYTES: usize = 1_048_576; +/// Standard WARC 1.1 reason for a deliberately or unexpectedly truncated record block. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum WarcTruncationReason { + /// Capture stopped because the configured maximum length was reached. + Length, + /// Capture stopped because the configured maximum capture time was reached. + Time, + /// Capture stopped because the network connection disconnected. + Disconnect, + /// Capture stopped for another or unknown reason. + Unspecified, +} + +impl WarcTruncationReason { + const fn warc_token(self) -> &'static str { + match self { + Self::Length => "length", + Self::Time => "time", + Self::Disconnect => "disconnect", + Self::Unspecified => "unspecified", + } + } +} + +/// Whether the retained WARC record block is complete or explicitly truncated. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum WarcPayloadCompleteness { + /// The retained record block is complete for the caller-authorized capture. + Complete, + /// The retained record block is partial for the stated WARC 1.1 truncation reason. + Truncated(WarcTruncationReason), +} + /// A validation failure while constructing an immutable WARC resource record. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum WarcResourceRecordError { @@ -58,6 +91,7 @@ pub struct WarcResourceRecord { payload: Vec, block_digest: String, provenance: ProvenanceRecord, + completeness: WarcPayloadCompleteness, } impl fmt::Debug for WarcResourceRecord { @@ -69,6 +103,7 @@ impl fmt::Debug for WarcResourceRecord { .field("content_type", &self.content_type) .field("payload_byte_count", &self.payload.len()) .field("block_digest", &self.block_digest) + .field("completeness", &self.completeness) .field( "provenance_verification_result", &self.provenance.verification_result(), @@ -78,7 +113,7 @@ impl fmt::Debug for WarcResourceRecord { } impl WarcResourceRecord { - /// Validate and construct one resource record without contacting a live origin. + /// Validate and construct one complete resource record without contacting a live origin. pub fn new( record_id: &str, warc_date: &str, @@ -86,6 +121,31 @@ impl WarcResourceRecord { content_type: &str, payload: Vec, provenance: ProvenanceRecord, + ) -> Result { + Self::new_with_completeness( + record_id, + warc_date, + target_uri, + content_type, + payload, + provenance, + WarcPayloadCompleteness::Complete, + ) + } + + /// Validate and construct one resource record with explicit capture completeness. + /// + /// Truncated records retain the caller-provided partial block and emit the standard WARC 1.1 + /// `WARC-Truncated` reason. This constructor never truncates an oversized payload implicitly; + /// the retained block must still satisfy [`MAX_WARC_PAYLOAD_BYTES`]. + pub fn new_with_completeness( + record_id: &str, + warc_date: &str, + target_uri: &str, + content_type: &str, + payload: Vec, + provenance: ProvenanceRecord, + completeness: WarcPayloadCompleteness, ) -> Result { if record_id.len() > MAX_WARC_RECORD_ID_BYTES { return Err(WarcResourceRecordError::LimitExceeded); @@ -127,6 +187,7 @@ impl WarcResourceRecord { block_digest: sha256_digest(&payload), payload, provenance, + completeness, }) } @@ -172,14 +233,27 @@ impl WarcResourceRecord { &self.provenance } + /// Return whether the retained block is complete or explicitly truncated. + #[must_use] + pub const fn completeness(&self) -> WarcPayloadCompleteness { + self.completeness + } + /// Serialize this bounded resource record as deterministic WARC 1.1 bytes. #[must_use] pub fn to_warc_bytes(&self) -> Vec { + let truncated_header = match self.completeness { + WarcPayloadCompleteness::Complete => String::new(), + WarcPayloadCompleteness::Truncated(reason) => { + format!("WARC-Truncated: {}\r\n", reason.warc_token()) + } + }; let header = format!( - "WARC/1.1\r\nWARC-Type: resource\r\nWARC-Record-ID: <{}>\r\nWARC-Date: {}\r\nWARC-Target-URI: {}\r\nContent-Type: {}\r\nWARC-Block-Digest: {}\r\nContent-Length: {}\r\n\r\n", + "WARC/1.1\r\nWARC-Type: resource\r\nWARC-Record-ID: <{}>\r\nWARC-Date: {}\r\nWARC-Target-URI: {}\r\n{}Content-Type: {}\r\nWARC-Block-Digest: {}\r\nContent-Length: {}\r\n\r\n", self.record_id, self.warc_date, self.target_uri, + truncated_header, self.content_type, self.block_digest, self.payload.len() From ce3d054db3dd1de0c12c8f34eceaf37b0fac0177 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 18:35:55 -0700 Subject: [PATCH 45/62] feat(evidence): export typed WARC truncation contract --- crates/originweave-evidence/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/originweave-evidence/src/lib.rs b/crates/originweave-evidence/src/lib.rs index 2daf7a129..0024a6394 100644 --- a/crates/originweave-evidence/src/lib.rs +++ b/crates/originweave-evidence/src/lib.rs @@ -23,7 +23,8 @@ pub use sensitive_access::{ }; pub use warc_resource_record::{ MAX_WARC_CONTENT_TYPE_BYTES, MAX_WARC_DATE_BYTES, MAX_WARC_PAYLOAD_BYTES, - MAX_WARC_RECORD_ID_BYTES, WarcResourceRecord, WarcResourceRecordError, + MAX_WARC_RECORD_ID_BYTES, WarcPayloadCompleteness, WarcResourceRecord, + WarcResourceRecordError, WarcTruncationReason, }; use std::collections::BTreeMap; From b22a2628a8aec051a66cb81308568711cc52d9b1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 18:37:22 -0700 Subject: [PATCH 46/62] style(evidence): apply canonical rustfmt to truncation exports --- crates/originweave-evidence/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/originweave-evidence/src/lib.rs b/crates/originweave-evidence/src/lib.rs index 0024a6394..65289c876 100644 --- a/crates/originweave-evidence/src/lib.rs +++ b/crates/originweave-evidence/src/lib.rs @@ -23,8 +23,8 @@ pub use sensitive_access::{ }; 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, WarcTruncationReason, + MAX_WARC_RECORD_ID_BYTES, WarcPayloadCompleteness, WarcResourceRecord, WarcResourceRecordError, + WarcTruncationReason, }; use std::collections::BTreeMap; From e2fdb93b673a6fccfe6164dc9608d373bfaf2364 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 18:41:28 -0700 Subject: [PATCH 47/62] docs(evidence): record WARC truncation semantics truthfully --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06b6592e8..49b3342bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Changed +- Added explicit WARC 1.1 payload completeness to resource records: complete records omit `WARC-Truncated`, while truncated records preserve the standard `length`, `time`, `disconnect`, or `unspecified` reason and keep `Content-Length` equal to the retained block size; oversized retained blocks still fail closed rather than being truncated implicitly. - Added a bounded immutable WARC 1.1 resource-record contract on the active extraction lane, binding deterministic bytes and SHA-256 block digests to verified provenance and exposing its typed construction failures through deterministic `Display` and `std::error::Error` contracts without claiming durable persistence or PROV export. - Separated logical origin authority from resolved network destination authority; an origin grant no longer implies permission to connect to every resolver result. - Separated resolved-address authorization from direct transport evidence; an approved IP now becomes a usable stream only after the operating system reports the exact requested IP and port. @@ -48,7 +49,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Security -- WARC target URI admission rejects control, whitespace, bidirectional-control, zero-width, soft-hyphen, Arabic-letter-mark, word-joiner/isolate, and BOM formatting characters before line-oriented WARC header serialization, while preserving ordinary printable Unicode path text. +- WARC target URI admission accepts only RFC 3986 ASCII URI presentation; raw Unicode, controls, whitespace, and other non-URI bytes are rejected before line-oriented WARC header serialization, while percent-encoded UTF-8 octets remain admissible when the URI otherwise satisfies the same bounded provenance contract. - Raw page content cannot become a trusted instruction. - Raw secrets are rejected and secret-capable actions require an opaque broker handle. - Crawler mode is read-only, must pair with the public-crawl purpose, and fails closed without an applicable robots-policy decision. From 9d19c7886f56528499040c665a0b94d20168af2f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 18:43:33 -0700 Subject: [PATCH 48/62] docs(evidence): record WARC truncation standard decision --- docs/doctoring.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/doctoring.md b/docs/doctoring.md index f0133bb5d..edde97f41 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -82,6 +82,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. +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. + ### 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. @@ -134,6 +136,8 @@ Internet Assigned Numbers Authority. (2025, October 9). *IPv6 special-purpose ad Internet Assigned Numbers Authority. (2025, October 10). *IPv6 global unicast address space*. https://www.iana.org/assignments/ipv6-unicast-address-assignments/ipv6-unicast-address-assignments.xhtml +International Internet Preservation Consortium. (n.d.). *The WARC format 1.1*. Retrieved August 22, 2026, from https://iipc.github.io/warc-specifications/specifications/warc-format/warc-1.1/ + International Organization for Standardization. (2017). *Information and documentation—WARC file format* (ISO Standard No. 28500:2017). https://www.iso.org/standard/68004.html Koster, M., Illyes, G., Zeller, H., & Sassman, L. (2022). *Robots Exclusion Protocol* (RFC 9309). Internet Engineering Task Force. https://doi.org/10.17487/RFC9309 From 57175081b5b42c53311e3ee45d5c26631010604b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 18:51:18 -0700 Subject: [PATCH 49/62] docs(evidence): integrate ExtractionSchema authority basis --- docs/doctoring.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/doctoring.md b/docs/doctoring.md index edde97f41..45bc7e656 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -82,6 +82,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. +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. + 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. ### AI risk and prompt injection From bb2332bfbbc531e6be3c39e1f43f1db262023e52 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 19:30:27 -0700 Subject: [PATCH 50/62] test(evidence): reject malformed WARC percent encoding --- .../tests/warc_target_uri_presentation.rs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/crates/originweave-evidence/tests/warc_target_uri_presentation.rs b/crates/originweave-evidence/tests/warc_target_uri_presentation.rs index ed637b7ae..e13a413d5 100644 --- a/crates/originweave-evidence/tests/warc_target_uri_presentation.rs +++ b/crates/originweave-evidence/tests/warc_target_uri_presentation.rs @@ -103,6 +103,29 @@ fn warc_target_uri_rejects_ascii_characters_outside_rfc3986_uri_syntax() { } } +#[test] +fn warc_target_uri_rejects_malformed_percent_encoding() { + for target_uri in [ + "https://example.com/%", + "https://example.com/%2", + "https://example.com/%GG", + "https://example.com/%0G", + ] { + assert_eq!( + WarcResourceRecord::new( + RECORD_ID, + DATE, + target_uri, + "text/plain", + Vec::new(), + provenance(target_uri), + ), + Err(WarcResourceRecordError::InvalidTargetUri), + "target_uri={target_uri:?}" + ); + } +} + #[test] fn warc_target_uri_accepts_percent_encoded_utf8_path_octets() { let target_uri = "https://example.com/%EC%83%81%ED%92%88/%EC%83%81%EC%84%B8"; From a865207ce699917d8bb4c0d2ccc512818bf35509 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 19:31:48 -0700 Subject: [PATCH 51/62] test(evidence): reach WARC percent validation boundary --- .../originweave-evidence/tests/warc_target_uri_presentation.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/originweave-evidence/tests/warc_target_uri_presentation.rs b/crates/originweave-evidence/tests/warc_target_uri_presentation.rs index e13a413d5..ec197480f 100644 --- a/crates/originweave-evidence/tests/warc_target_uri_presentation.rs +++ b/crates/originweave-evidence/tests/warc_target_uri_presentation.rs @@ -105,6 +105,7 @@ fn warc_target_uri_rejects_ascii_characters_outside_rfc3986_uri_syntax() { #[test] fn warc_target_uri_rejects_malformed_percent_encoding() { + let source_provenance = provenance("https://example.com/valid"); for target_uri in [ "https://example.com/%", "https://example.com/%2", @@ -118,7 +119,7 @@ fn warc_target_uri_rejects_malformed_percent_encoding() { target_uri, "text/plain", Vec::new(), - provenance(target_uri), + source_provenance.clone(), ), Err(WarcResourceRecordError::InvalidTargetUri), "target_uri={target_uri:?}" From af1c29ce5e18ebe5f85178b53de3148493b66a8d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 20:01:55 -0700 Subject: [PATCH 52/62] fix(evidence): validate WARC percent encoding --- .../src/warc_resource_record.rs | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/crates/originweave-evidence/src/warc_resource_record.rs b/crates/originweave-evidence/src/warc_resource_record.rs index fe93f5e4b..5064979f7 100644 --- a/crates/originweave-evidence/src/warc_resource_record.rs +++ b/crates/originweave-evidence/src/warc_resource_record.rs @@ -349,7 +349,26 @@ fn is_leap_year(year: u16) -> bool { } fn valid_target_uri_presentation(target_uri: &str) -> bool { - target_uri.bytes().all(is_rfc3986_uri_byte) + let bytes = target_uri.as_bytes(); + let mut index = 0_usize; + while index < bytes.len() { + let byte = bytes[index]; + if !is_rfc3986_uri_byte(byte) { + return false; + } + if byte == b'%' { + if index + 2 >= bytes.len() + || !bytes[index + 1].is_ascii_hexdigit() + || !bytes[index + 2].is_ascii_hexdigit() + { + return false; + } + index += 3; + } else { + index += 1; + } + } + true } const fn is_rfc3986_uri_byte(byte: u8) -> bool { From 0ac205231d69f78648c9d8f69113f8d5e2ea096b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 00:36:05 -0700 Subject: [PATCH 53/62] test(evidence): reject RFC 3986 gen-delims in WARC paths --- .../tests/warc_target_uri_presentation.rs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/crates/originweave-evidence/tests/warc_target_uri_presentation.rs b/crates/originweave-evidence/tests/warc_target_uri_presentation.rs index ec197480f..85d5bc6b0 100644 --- a/crates/originweave-evidence/tests/warc_target_uri_presentation.rs +++ b/crates/originweave-evidence/tests/warc_target_uri_presentation.rs @@ -103,6 +103,27 @@ fn warc_target_uri_rejects_ascii_characters_outside_rfc3986_uri_syntax() { } } +#[test] +fn warc_target_uri_rejects_general_delimiters_in_path_segments() { + for target_uri in [ + "https://example.com/[segment]", + "https://example.com/item]shadow", + ] { + assert_eq!( + WarcResourceRecord::new( + RECORD_ID, + DATE, + target_uri, + "text/plain", + Vec::new(), + provenance(target_uri), + ), + Err(WarcResourceRecordError::InvalidTargetUri), + "target_uri={target_uri:?}" + ); + } +} + #[test] fn warc_target_uri_rejects_malformed_percent_encoding() { let source_provenance = provenance("https://example.com/valid"); From c3fd33d98dc7abe6cf8b3387e2b9edc3fe7463be Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 00:41:14 -0700 Subject: [PATCH 54/62] test(evidence): preserve bracketed IPv6 WARC authority --- .../tests/warc_target_uri_presentation.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/crates/originweave-evidence/tests/warc_target_uri_presentation.rs b/crates/originweave-evidence/tests/warc_target_uri_presentation.rs index 85d5bc6b0..b31452482 100644 --- a/crates/originweave-evidence/tests/warc_target_uri_presentation.rs +++ b/crates/originweave-evidence/tests/warc_target_uri_presentation.rs @@ -124,6 +124,22 @@ fn warc_target_uri_rejects_general_delimiters_in_path_segments() { } } +#[test] +fn warc_target_uri_preserves_brackets_in_ipv6_authority() { + let target_uri = "https://[::1]:8443/path"; + let record = WarcResourceRecord::new( + RECORD_ID, + DATE, + target_uri, + "text/plain", + Vec::new(), + provenance(target_uri), + ) + .expect("RFC 3986 bracketed IPv6 authority"); + + assert_eq!(record.target_uri(), target_uri); +} + #[test] fn warc_target_uri_rejects_malformed_percent_encoding() { let source_provenance = provenance("https://example.com/valid"); From e8a73fd3c75439417f5d181c31d82a7b4a9a26f6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 00:42:11 -0700 Subject: [PATCH 55/62] fix(evidence): enforce WARC path delimiter syntax --- crates/originweave-evidence/src/warc_resource_record.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/originweave-evidence/src/warc_resource_record.rs b/crates/originweave-evidence/src/warc_resource_record.rs index 5064979f7..e621f75df 100644 --- a/crates/originweave-evidence/src/warc_resource_record.rs +++ b/crates/originweave-evidence/src/warc_resource_record.rs @@ -351,11 +351,15 @@ fn is_leap_year(year: u16) -> bool { fn valid_target_uri_presentation(target_uri: &str) -> bool { let bytes = target_uri.as_bytes(); let mut index = 0_usize; + let mut slash_count = 0_usize; while index < bytes.len() { let byte = bytes[index]; if !is_rfc3986_uri_byte(byte) { return false; } + if matches!(byte, b'[' | b']') && slash_count > 2 { + return false; + } if byte == b'%' { if index + 2 >= bytes.len() || !bytes[index + 1].is_ascii_hexdigit() @@ -365,6 +369,9 @@ fn valid_target_uri_presentation(target_uri: &str) -> bool { } index += 3; } else { + if byte == b'/' { + slash_count += 1; + } index += 1; } } From f4dba5e1e4878bfbbbca82b0cda56961694bf9ae Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 19:28:38 -0700 Subject: [PATCH 56/62] test(evidence): reject nil WARC record identity --- crates/originweave-evidence/tests/warc_resource_record.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/originweave-evidence/tests/warc_resource_record.rs b/crates/originweave-evidence/tests/warc_resource_record.rs index bc6172f05..d360169f5 100644 --- a/crates/originweave-evidence/tests/warc_resource_record.rs +++ b/crates/originweave-evidence/tests/warc_resource_record.rs @@ -117,6 +117,7 @@ fn resource_record_rejects_invalid_identifiers_dates_content_and_limits() { for record_id in [ "", "http://example.com/record", + "urn:uuid:00000000-0000-0000-0000-000000000000", "urn:uuid:123e4567-e89b-12d3-a456-42661417400", "urn:uuid:123e4567_e89b-12d3-a456-426614174000", "urn:uuid:123e4567-e89b-12d3-a456-42661417400z", From 65dbad3cb58fcd3ca42b4794652acbdf85f5ef8c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 19:31:51 -0700 Subject: [PATCH 57/62] fix(evidence): reject nil WARC record identifiers --- crates/originweave-evidence/src/warc_resource_record.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/originweave-evidence/src/warc_resource_record.rs b/crates/originweave-evidence/src/warc_resource_record.rs index e621f75df..01f04515c 100644 --- a/crates/originweave-evidence/src/warc_resource_record.rs +++ b/crates/originweave-evidence/src/warc_resource_record.rs @@ -49,7 +49,7 @@ pub enum WarcPayloadCompleteness { /// A validation failure while constructing an immutable WARC resource record. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum WarcResourceRecordError { - /// The record identifier was not a bounded UUID URN. + /// The record identifier was not a bounded non-nil UUID URN. InvalidRecordId, /// The date was not a bounded UTC RFC 3339 timestamp. InvalidDate, @@ -270,6 +270,7 @@ fn valid_record_id(record_id: &str) -> bool { if bytes.len() != MAX_WARC_RECORD_ID_BYTES || !record_id.starts_with("urn:uuid:") { return false; } + let mut has_nonzero_hex = false; for (index, byte) in bytes[9..].iter().copied().enumerate() { if matches!(index, 8 | 13 | 18 | 23) { if byte != b'-' { @@ -277,9 +278,11 @@ fn valid_record_id(record_id: &str) -> bool { } } else if !byte.is_ascii_hexdigit() { return false; + } else if byte != b'0' { + has_nonzero_hex = true; } } - true + has_nonzero_hex } fn valid_utc_date(date: &str) -> bool { @@ -553,4 +556,4 @@ fn sha256_digest(payload: &[u8]) -> String { encoded.push_str(&format!("{byte:02x}")); } encoded -} +} \ No newline at end of file From 597f3a653f2169364e82cb29fbf150af4bfe2e8c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 19:33:19 -0700 Subject: [PATCH 58/62] style(evidence): preserve canonical Rust newline --- crates/originweave-evidence/src/warc_resource_record.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/originweave-evidence/src/warc_resource_record.rs b/crates/originweave-evidence/src/warc_resource_record.rs index 01f04515c..c5ba2d003 100644 --- a/crates/originweave-evidence/src/warc_resource_record.rs +++ b/crates/originweave-evidence/src/warc_resource_record.rs @@ -556,4 +556,4 @@ fn sha256_digest(payload: &[u8]) -> String { encoded.push_str(&format!("{byte:02x}")); } encoded -} \ No newline at end of file +} From ddb840b3f051634f1a5d97fac7d327b4d472e124 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 02:34:52 -0700 Subject: [PATCH 59/62] fix(evidence): restore current prerequisite tree before WARC integration --- .github/dependabot.yml | 7 + .../workflows/apply-rust-nightly-refresh.yml | 57 +++ .github/workflows/ci.yml | 6 +- .../workflows/hourly-product-development.yml | 4 +- ARCHITECTURE.md | 3 +- CHANGELOG.md | 19 +- Cargo.lock | 1 - README.md | 6 +- crates/originweave-core/Cargo.toml | 3 + crates/originweave-core/src/mcp.rs | 248 ++++++++++++ crates/originweave-core/src/root.rs | 15 + .../tests/mcp_authority_route.rs | 362 ++++++++++++++++++ crates/originweave-evidence/Cargo.toml | 1 - crates/originweave-evidence/src/lib.rs | 35 +- crates/originweave-evidence/tests/evidence.rs | 4 + crates/originweave-policy/src/lib.rs | 20 + .../tests/mcp_route_binding.rs | 96 +++++ docs/README.md | 1 + docs/adr/0106-provenance-evidence-model.md | 8 - .../0107-browser-protocol-adapter-strategy.md | 18 +- docs/doctoring.md | 14 +- docs/doctoring/rust-toolchain-freshness.md | 44 +++ docs/product-technical-gap-baseline.md | 325 ++++++++++++++++ docs/traceability/README.md | 4 +- docs/traceability/mcp-authority-route.md | 53 +++ tests/test_doctoring_reference_contract.py | 28 ++ ...cumentation_active_pr_evidence_contract.py | 27 ++ tests/test_product_completion_gap_contract.py | 111 ++++++ tests/test_product_documentation_contract.py | 49 +++ tests/test_rust_toolchain_contract.py | 53 +++ 30 files changed, 1586 insertions(+), 36 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/apply-rust-nightly-refresh.yml create mode 100644 crates/originweave-core/src/mcp.rs create mode 100644 crates/originweave-core/src/root.rs create mode 100644 crates/originweave-core/tests/mcp_authority_route.rs create mode 100644 crates/originweave-policy/tests/mcp_route_binding.rs create mode 100644 docs/doctoring/rust-toolchain-freshness.md create mode 100644 docs/product-technical-gap-baseline.md create mode 100644 docs/traceability/mcp-authority-route.md create mode 100644 tests/test_doctoring_reference_contract.py create mode 100644 tests/test_product_completion_gap_contract.py create mode 100644 tests/test_rust_toolchain_contract.py diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..d331df5fd --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: + - package-ecosystem: "rust-toolchain" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 1 diff --git a/.github/workflows/apply-rust-nightly-refresh.yml b/.github/workflows/apply-rust-nightly-refresh.yml new file mode 100644 index 000000000..7f3186b39 --- /dev/null +++ b/.github/workflows/apply-rust-nightly-refresh.yml @@ -0,0 +1,57 @@ +name: Materialize Rust nightly refresh once + +on: + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + materialize-owned-branch: + if: >- + github.repository == 'ContextualWisdomLab/OriginWeave' && + github.event.pull_request.head.repo.full_name == github.repository && + github.event.pull_request.head.ref == 'agent/rust-toolchain-refresh-2026-08-19' && + github.event.pull_request.user.login == 'seonghobae' + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false + - name: Materialize only the reviewed nightly snapshot + run: | + set -euo pipefail + python3 - <<'PY' + from pathlib import Path + + source_path = Path('.github/workflows/hourly-product-development.yml') + source = source_path.read_text(encoding='utf-8') + old = 'nightly-2026-08-01' + new = 'nightly-2026-08-18' + old_count = source.count(old) + new_count = source.count(new) + if old_count == 2 and new_count == 0: + refreshed_source = source.replace(old, new) + elif old_count == 0 and new_count == 2: + refreshed_source = source + else: + raise SystemExit( + f'expected exactly two selectors in one state, found old={old_count}, new={new_count}' + ) + output = Path('nightly-refresh-artifact/hourly-product-development.yml') + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(refreshed_source, encoding='utf-8') + refreshed = output.read_text(encoding='utf-8') + if old in refreshed or refreshed.count(new) < 2: + raise SystemExit('nightly refresh artifact failed its replacement contract') + PY + - name: Upload exact refreshed workflow + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: hourly-rust-nightly-${{ github.event.pull_request.head.sha }} + path: nightly-refresh-artifact/hourly-product-development.yml + if-no-files-found: error + retention-days: 1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f804f7496..95c2fa1d7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,13 +73,13 @@ jobs: persist-credentials: false - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 with: - toolchain: nightly-2026-08-01 + toolchain: nightly-2026-08-18 components: llvm-tools-preview - name: Install pinned cargo-llvm-cov run: cargo +1.97.1 install cargo-llvm-cov --version 0.8.6 --locked - name: Measure production functions, lines, regions, and branches run: >- - cargo +nightly-2026-08-01 llvm-cov + cargo +nightly-2026-08-18 llvm-cov --locked --workspace --all-features @@ -88,7 +88,7 @@ jobs: --output-path coverage.json - name: Record uncovered production lines run: >- - cargo +nightly-2026-08-01 llvm-cov report + cargo +nightly-2026-08-18 llvm-cov report --branch --text --show-missing-lines diff --git a/.github/workflows/hourly-product-development.yml b/.github/workflows/hourly-product-development.yml index 672754c69..396af4a95 100644 --- a/.github/workflows/hourly-product-development.yml +++ b/.github/workflows/hourly-product-development.yml @@ -154,7 +154,7 @@ jobs: run: | set -euo pipefail rustup toolchain install 1.97.1 --profile minimal --component clippy,rustfmt - rustup toolchain install nightly-2026-08-01 --profile minimal --component llvm-tools-preview + rustup toolchain install nightly-2026-08-18 --profile minimal --component llvm-tools-preview cargo +1.97.1 install cargo-llvm-cov --version 0.8.6 --locked archive="${RUNNER_TEMP}/opencode-linux-x64.tar.gz" curl -fsSL -o "$archive" \ @@ -894,7 +894,7 @@ jobs: cargo +1.97.1 test --locked --workspace --all-targets cargo +1.97.1 clippy --locked --workspace --all-targets -- -D warnings RUSTDOCFLAGS='-D warnings' cargo +1.97.1 doc --locked --workspace --no-deps - cargo +nightly-2026-08-01 llvm-cov \ + cargo +nightly-2026-08-18 llvm-cov \ --locked --workspace --all-features --branch --json --summary-only \ --output-path "${RUNNER_TEMP}/coverage.json" python3 scripts/ci/verify_coverage.py "${RUNNER_TEMP}/coverage.json" diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index d6ebfee02..fe287389b 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -12,6 +12,7 @@ This file is the canonical product-wide topology and bounded-context view. It is - [Requirement, decision, standards, and implementation traceability](docs/traceability/README.md) - [Research and standards doctoring](docs/doctoring.md) - [Product roadmap](docs/product-roadmap.md) +- [Live product and technical gap baseline](docs/product-technical-gap-baseline.md) Protected-main code and executable tests define current implementation truth; deployed build/release artifacts, migrations, and configuration are additional operational evidence when they exist. Accepted ADRs define design authority, not proof that planned behavior has shipped. The PRD/TRD/diagrams may also contain `Planned`, `Proposed`, or `Open` product direction; those labels must remain explicit until corresponding implementation and review evidence reaches protected `main`. @@ -134,7 +135,7 @@ Owns validated task budgets and deterministic cumulative mitigation plans. Platf ### `originweave-evidence` -Owns universally value-redacted network evidence and source-bound provenance records. Generic network records retain only bounded method, canonical origin, unambiguous bounded path, and bounded field names. The active extraction lane adds a verified, bounded in-memory WARC 1.1 `resource` record contract over already-authorized bytes; object storage, retention, encryption, legal policy, request/response capture, and PROV export remain future bounded modules. +Owns universally value-redacted network evidence and source-bound provenance records. Generic network records retain only bounded method, canonical origin, unambiguous bounded path, and bounded field names. Body capture, typed metadata values, WARC serialization, object storage, retention, encryption, and legal policy remain future bounded modules. ## 6. Planned modules diff --git a/CHANGELOG.md b/CHANGELOG.md index 49b3342bc..8412f3ddc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,17 +6,21 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Added +- Refreshed the product and technical gap baseline with the 2026-08-24 live inventory: 158 open pull requests (44 ready, 114 draft), refreshed exact base/head evidence for the #208–#222 release, enterprise-approval, BAP, and WARC/PROV chains, the governance issue additions #212 and #215, and a required-check provider-failure record for the fail-closed Strix re-dispatches on #208/#218/#220. +- Added a dated product and technical gap baseline that separates protected-main implementation truth, active pull-request evidence, live review/check blockers, and the next buyer-visible Phase 1 acceptance work. +- Refreshed the product and technical gap baseline with the current open-PR inventory and exact base/head evidence for the newest Chromium, BAP, extraction, WARC, and idempotency slices. - Bound explicit extension-to-Agent grants to exclusive trusted-time expiry in addition to extension identity, session, browsing context, and canonical origin, so a same-origin grant cannot be reused at or after the deadline. - Bound explicit extension-to-Agent grants to the exact canonical origin in addition to extension identity, session, and browsing context, so a same-session navigation or port change cannot reuse the grant. - Rust workspace for independently reusable core, policy, destination, network, TLS, resource, and evidence modules. - Canonical HTTPS and loopback-origin boundary with case-normalized schemes and hosts, default-port normalization, IPv4/IPv6 handling, browser-special numeric-host rejection, and explicit malformed-input errors. - Typed browser actions, capabilities, risk classes, execution modes, robots decisions, secret-delivery contracts, immutable canonical action-intent digests, and intent-bound approval scopes. +- Active PR #168 adds deterministic MCP `2026-07-28` stateless tool-routing foundations with bounded names, a single reviewed tool-to-action registry shared by routing and discovery metadata, and fail-closed policy binding that grants no ambient authority. This is active-PR evidence only; the complete MCP adapter, transport serialization, discovery response handling, OAuth, browser I/O, and persistence remain planned until separately integrated on protected `main`. - Deterministic fail-closed policy evaluation for untrusted instructions, origin grants, crawler restrictions, execution-mode and purpose consistency, approvals, and brokered secrets. - Fail-closed resolved-destination policy with IPv4/IPv6 special-purpose and reviewed cloud-platform endpoint classification, IPv4-mapped canonicalization, explicit class grants, non-empty origin-bound DNS snapshots capped at 256 resolver addresses, concrete connection pinning, DNS-set expansion detection, and per-hop redirect reauthorization. - Direct-only `originweave-network` TCP boundary with explicit canonical `SocketAddr` authority, zero IPv6 flow and scope metadata unless separately modeled, a non-cloneable single-use plan, a 30-second per-attempt timeout ceiling, at most four attempts, exact `peer_addr` verification before stream exposure, and no hostname re-resolution or ambient proxy inheritance. - Authenticated `originweave-tls` service-identity boundary that consumes an existing verified TCP stream, requires exact TLS-origin and transport-origin equality, derives RFC 9525 DNS or literal-IP reference identity only from the canonical HTTPS origin, validates WebPKI with explicit roots and fixed time, permits only TLS 1.2 and TLS 1.3, and never reconnects or resolves. - Bounded TLS policy for total handshake time, ALPN identifiers, trust-root count and bytes, and server-presented certificate count and bytes, with explicit optional-versus-required ALPN behavior and `NotConfigured` revocation evidence. -- Credential-free TLS evidence containing canonical origin, requested and observed peer, DNS/IP reference identity, TLS version, cipher-suite identifier, selected ALPN or explicit absence, leaf certificate and SPKI hashes, server-presented certificate hashes and bounds, trust-bundle identity and hash, validity interval, fixed verification time, revocation configuration, and measured handshake duration. +- Credential-free TLS evidence containing canonical origin, TCP peers, reference identity, TLS version, cipher-suite identifier, selected ALPN or explicit absence, leaf certificate and SPKI hashes, server-presented certificate hashes and bounds, trust-bundle identity and hash, validity interval, fixed verification time, revocation configuration, and measured handshake duration. - Credential-free connection and redirect evidence containing canonical addresses, destination classes, target digests, hop numbers, and approved-address counts. - Credential-free verified TCP evidence containing the logical origin, requested socket, observed peer, destination class, successful attempt number, and per-attempt timeout. - Standard `Display` and `std::error::Error` contracts for destination, redirect, digest, direct-network, and TLS failures, including preserved destination-policy, rustls, and operating-system sources where applicable. @@ -34,8 +38,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Changed -- Added explicit WARC 1.1 payload completeness to resource records: complete records omit `WARC-Truncated`, while truncated records preserve the standard `length`, `time`, `disconnect`, or `unspecified` reason and keep `Content-Length` equal to the retained block size; oversized retained blocks still fail closed rather than being truncated implicitly. -- Added a bounded immutable WARC 1.1 resource-record contract on the active extraction lane, binding deterministic bytes and SHA-256 block digests to verified provenance and exposing its typed construction failures through deterministic `Display` and `std::error::Error` contracts without claiming durable persistence or PROV export. +- Aligned the hourly product-development branch-coverage toolchain and its one-shot materializer with the reviewed `nightly-2026-08-18` pin, and corrected the official Dependabot Rust-toolchain reference. - Separated logical origin authority from resolved network destination authority; an origin grant no longer implies permission to connect to every resolver result. - Separated resolved-address authorization from direct transport evidence; an approved IP now becomes a usable stream only after the operating system reports the exact requested IP and port. - Separated exact TCP peer proof from authenticated TLS service identity; an observed peer becomes an authenticated HTTPS stream only after explicit-root, fixed-time, SAN-bound WebPKI verification over that same stream. @@ -44,12 +47,19 @@ All notable changes to OriginWeave are documented in this file. The format follo - Updated the first Chromium slice to distinguish implemented origin, destination, direct TCP, and TLS identity kernels from the remaining trusted DNS adapter, proxy/PAC, HTTP budget, MIME, download, and Chromium integration required before safe navigation can be claimed. - Separated hourly product PR publication authority from the organization review and merge system, and added live default-branch and release-blocker rechecks immediately before publication. - Made the agent-development contract work-conserving: completing one bounded slice, RCA, review request, check, merge, or documentation change is an intermediate state; maintenance must return to the live queue, treat waits as item-local, and perform a mandatory exit sweep before terminating while executable OriginWeave work remains. +- Hardened the dated baseline evidence collector with fail-fast isolated artifacts, paginated branch and collaborator rules, and post-collection exact-head revalidation. +- Flattened every paginated workflow-run page in the baseline merge verdict so exact-head evidence cannot silently discard later runs. +- Hardened the baseline evidence procedure with exact-head legacy status and workflow-run capture, counted approval binding, required-workflow recording, merge verdict artifacts, and bounded moving-head retries. - Moved autonomous-agent Cargo targets and Python bytecode caches outside the proposed source tree and prefetched locked Cargo dependencies for offline verification. - Updated research doctoring to pin Chromium canonicalizer evidence to an immutable revision, add RFC 9293, RFC 5280, RFC 8446, RFC 9525, rustls 0.23.42, and Rust `TcpStream` evidence, distinguish the April 2026 Fugu beta from the June 2026 release, and treat vendor benchmark claims as first-party evidence rather than independent validation. +- Tightened the product-baseline contract so the BiDi opening path and VPN/profile evidence retain their explicit not-shipped status within their own documentation sections. +- Refreshed the product and technical gap baseline against the 2026-08-21 live inventory: 150 open pull requests, 110 drafts, and the new hardened-runner/MV3 evidence gap issue #206. +- Tightened the baseline completion-gap contract so superseded inventory counts (including the 2026-08-21 150/40/110 snapshot) can no longer pass as current evidence. +- Refreshed the baseline's merge-authority statement to the live ruleset: two approving reviews are required, while the collaborator inventory still contains only the solo maintainer. +- Corrected the baseline evidence collector to flatten every paginated input, apply current reviewer and last-push approval semantics, and discard verdicts when either the PR head or base moves. ### Security -- WARC target URI admission accepts only RFC 3986 ASCII URI presentation; raw Unicode, controls, whitespace, and other non-URI bytes are rejected before line-oriented WARC header serialization, while percent-encoded UTF-8 octets remain admissible when the URI otherwise satisfies the same bounded provenance contract. - Raw page content cannot become a trusted instruction. - Raw secrets are rejected and secret-capable actions require an opaque broker handle. - Crawler mode is read-only, must pair with the public-crawl purpose, and fails closed without an applicable robots-policy decision. @@ -75,6 +85,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Revocation is reported as not configured; the product makes no OCSP or CRL validation claim without supplied revocation evidence. - Every generic network header and query value is redacted before evidence leaves the trusted boundary, including conventionally benign field names containing attacker-controlled bytes. - Evidence capture enforces count and byte bounds and rejects credential-bearing source URLs, query strings, fragments, controls, whitespace, malformed percent escapes, encoded separators, dot segments, and backslash paths. +- Network-evidence paths and provenance source URL paths accept only RFC 3986 literal `pchar` syntax plus validated percent-encoded octets and slash separators, preventing raw general delimiters such as `[` and `]` or other invalid URI-presentation bytes from entering either evidence surface. - Hard RAM and VRAM pressure pauses the active agent and rejects new admission; hard VRAM pressure also offloads a resident local model. - 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. diff --git a/Cargo.lock b/Cargo.lock index 805549630..e2ada3c4e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -279,7 +279,6 @@ name = "originweave-evidence" version = "0.1.0" dependencies = [ "originweave-core", - "sha2", ] [[package]] diff --git a/README.md b/README.md index 17085c05d..a956ff60b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ OriginWeave is a Chromium-compatible, Rust-first control plane for governed AI agents on the web. It is designed to let an agent observe, extract, and act without turning untrusted page content into authority, exposing secrets to a model, connecting to an unapproved network destination, accepting an unauthenticated web service, or losing the evidence required to explain what happened. -> Project status: pre-alpha. The current repository contains independently reusable safety, resolved-destination, direct TCP peer-binding, and authenticated TLS service-identity kernels. Chromium, WebDriver BiDi, CDP, MCP, HTTP, proxy, WARC, and persistent provenance adapters are planned but not yet shipped. +> Project status: pre-alpha. The current protected repository contains independently reusable safety, resolved-destination, direct TCP peer-binding, and authenticated TLS service-identity kernels. Chromium, WebDriver BiDi, CDP, complete MCP, HTTP, proxy, WARC, and persistent provenance adapters are planned but not yet shipped. Active PR #168 implements only a bounded MCP `2026-07-28` stateless tool-routing and typed-action/policy foundation; it remains non-shipped active-PR evidence and does not make the complete MCP adapter available. ## Why OriginWeave @@ -40,6 +40,8 @@ The repository is organized as independently consumable Rust crates: - `originweave-resource`: task-level RAM, VRAM, thread, and frame-time budgets with cumulative mitigation plans. - `originweave-evidence`: universally value-redacted network evidence and source-bound provenance records. +Active PR #168 additionally carries a non-shipped `originweave-core` MCP routing registry and `originweave-policy` binding for the MCP `2026-07-28` `tools/call` boundary. That foundation validates and maps an explicit tool name to an existing typed action; it does not implement transport parsing, `tools/list`, OAuth, browser control, secret materialization, persistence, or ambient authority. + See [ARCHITECTURE.md](ARCHITECTURE.md) and the [architecture decision records](docs/adr/) for binding design decisions. ## Safety model @@ -97,7 +99,7 @@ isolated Chromium session → redacted provenance bundle ``` -Subsequent work connects the live Chromium network service, adds explicit proxy and download policy, WARC/PROV persistence, MCP and Browser Agent Protocol adapters, extension compatibility testing, GPU/RAM telemetry, prompt-injection benchmarks, and an accessible approval interface. See [docs/product-roadmap.md](docs/product-roadmap.md). +Subsequent work connects the live Chromium network service, adds explicit proxy and download policy, WARC/PROV persistence, completes the MCP and Browser Agent Protocol adapters beyond the active routing foundation, expands extension compatibility testing, adds GPU/RAM telemetry and prompt-injection benchmarks, and builds an accessible approval interface. See [docs/product-roadmap.md](docs/product-roadmap.md). ## Hourly product-development loop diff --git a/crates/originweave-core/Cargo.toml b/crates/originweave-core/Cargo.toml index 35c83b19b..517e41217 100644 --- a/crates/originweave-core/Cargo.toml +++ b/crates/originweave-core/Cargo.toml @@ -10,6 +10,9 @@ repository.workspace = true homepage.workspace = true publish = false +[lib] +path = "src/root.rs" + [dependencies] [lints] diff --git a/crates/originweave-core/src/mcp.rs b/crates/originweave-core/src/mcp.rs new file mode 100644 index 000000000..b026d5e61 --- /dev/null +++ b/crates/originweave-core/src/mcp.rs @@ -0,0 +1,248 @@ +//! Fail-closed MCP routing integrity for the external adapter boundary. +//! +//! This module validates only the stateless MCP protocol/method/tool routing +//! envelope and derives an existing [`ActionKind`]. It is deliberately not an +//! authorization decision: callers must independently enforce OriginWeave +//! capability, risk, approval, origin, secret-broker, and evidence policies. +//! No MCP arguments, outputs, credentials, or arbitrary model-visible values +//! are retained by this boundary. + +use std::fmt; + +use crate::{ActionKind, Capability, RiskClass}; + +/// MCP protocol generation accepted by this stateless adapter boundary. +pub const MCP_PROTOCOL_VERSION: &str = "2026-07-28"; + +/// The only MCP method that can enter the typed action-routing boundary. +pub const MCP_TOOLS_CALL_METHOD: &str = "tools/call"; + +/// Maximum accepted MCP method-name length in bytes. +pub const MAX_MCP_METHOD_NAME_BYTES: usize = 64; + +/// Maximum accepted MCP tool-name length in bytes. +pub const MAX_MCP_TOOL_NAME_BYTES: usize = 128; + +/// One deterministic MCP tool descriptor derived from OriginWeave's reviewed action registry. +/// +/// The descriptor is discovery metadata only. It does not grant capabilities, origin access, +/// approval, secret access, or any other authority. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct McpToolCatalogEntry { + tool_name: &'static str, + action_kind: ActionKind, +} + +impl McpToolCatalogEntry { + /// Return the canonical MCP tool name exposed by this registry entry. + #[must_use] + pub const fn tool_name(&self) -> &'static str { + self.tool_name + } + + /// Return the typed OriginWeave action represented by this registry entry. + #[must_use] + pub const fn action_kind(&self) -> ActionKind { + self.action_kind + } + + /// Return the capability required by the represented action. + #[must_use] + pub const fn required_capability(&self) -> Capability { + self.action_kind.required_capability() + } + + /// Return the risk class assigned to the represented action. + #[must_use] + pub const fn risk_class(&self) -> RiskClass { + self.action_kind.risk_class() + } +} + +/// The complete explicit MCP tool-to-action registry accepted by this boundary. +/// +/// Order is deterministic so adapters can derive stable discovery output from this single +/// reviewed registry rather than maintaining a second mapping that could drift from routing. +const MCP_TOOL_CATALOG: &[McpToolCatalogEntry] = &[ + McpToolCatalogEntry { + tool_name: "originweave.observe", + action_kind: ActionKind::Observe, + }, + McpToolCatalogEntry { + tool_name: "originweave.extract", + action_kind: ActionKind::Extract, + }, + McpToolCatalogEntry { + tool_name: "originweave.navigate", + action_kind: ActionKind::Navigate, + }, + McpToolCatalogEntry { + tool_name: "originweave.download", + action_kind: ActionKind::Download, + }, + McpToolCatalogEntry { + tool_name: "originweave.draft", + action_kind: ActionKind::Draft, + }, + McpToolCatalogEntry { + tool_name: "originweave.submit", + action_kind: ActionKind::Submit, + }, + McpToolCatalogEntry { + tool_name: "originweave.upload", + action_kind: ActionKind::Upload, + }, + McpToolCatalogEntry { + tool_name: "originweave.fill_secret", + action_kind: ActionKind::FillSecret, + }, + McpToolCatalogEntry { + tool_name: "originweave.purchase", + action_kind: ActionKind::Purchase, + }, + McpToolCatalogEntry { + tool_name: "originweave.delete", + action_kind: ActionKind::Delete, + }, + McpToolCatalogEntry { + tool_name: "originweave.manage_permission", + action_kind: ActionKind::ManagePermission, + }, +]; + +/// Return the deterministic reviewed MCP tool catalog. +/// +/// Adapters may use this slice to derive discovery responses. Serialization, pagination, cache +/// policy, transport I/O, and authorization remain outside this stateless registry boundary. +#[must_use] +pub const fn supported_mcp_tools() -> &'static [McpToolCatalogEntry] { + MCP_TOOL_CATALOG +} + +/// A deterministic failure while validating untrusted MCP routing metadata. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum McpToolBoundaryError { + /// The request names an MCP protocol generation this boundary does not support. + UnsupportedProtocolVersion, + /// MCP routing metadata disagrees with the method or tool name in the body. + HeaderBodyMismatch, + /// The request method violates the bounded ASCII MCP routing syntax. + InvalidMethod, + /// The request method is not the supported `tools/call` operation. + UnsupportedMethod, + /// The tool name violates the bounded ASCII MCP routing syntax. + InvalidToolName, + /// The tool name has no explicit mapping to an OriginWeave typed action. + UnknownTool, +} + +impl fmt::Display for McpToolBoundaryError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UnsupportedProtocolVersion => { + formatter.write_str("unsupported MCP protocol version") + } + Self::HeaderBodyMismatch => { + formatter.write_str("MCP routing headers do not match the request body") + } + Self::InvalidMethod => { + formatter.write_str("MCP method violates the bounded ASCII routing syntax") + } + Self::UnsupportedMethod => formatter + .write_str("only MCP tools/call requests can enter the typed action boundary"), + Self::InvalidToolName => { + formatter.write_str("MCP tool name violates the bounded ASCII routing syntax") + } + Self::UnknownTool => { + formatter.write_str("MCP tool is not mapped to an OriginWeave typed action") + } + } + } +} + +impl std::error::Error for McpToolBoundaryError {} + +/// An MCP tool call whose routing envelope has been validated and mapped. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ValidatedMcpToolCall { + tool_name: &'static str, + action_kind: ActionKind, +} + +impl ValidatedMcpToolCall { + /// Validate one stateless MCP tool-call routing envelope. + /// + /// Routing integrity is intentionally narrower than authorization. A + /// successful value proves only that the untrusted protocol version, + /// routing metadata, body method, and body tool name agree with one + /// explicitly supported mapping. Each untrusted method and tool name is + /// shape-validated before cross-field comparison so malformed or oversized + /// metadata cannot bypass the bounded routing syntax through mismatch handling. + pub fn new( + protocol_version: &str, + routing_method: &str, + routing_tool_name: &str, + body_method: &str, + body_tool_name: &str, + ) -> Result { + if protocol_version != MCP_PROTOCOL_VERSION { + return Err(McpToolBoundaryError::UnsupportedProtocolVersion); + } + if !valid_method(routing_method) || !valid_method(body_method) { + return Err(McpToolBoundaryError::InvalidMethod); + } + if !valid_tool_name(routing_tool_name) || !valid_tool_name(body_tool_name) { + return Err(McpToolBoundaryError::InvalidToolName); + } + if routing_method != body_method || routing_tool_name != body_tool_name { + return Err(McpToolBoundaryError::HeaderBodyMismatch); + } + if routing_method != MCP_TOOLS_CALL_METHOD { + return Err(McpToolBoundaryError::UnsupportedMethod); + } + + let (tool_name, action_kind) = map_tool(routing_tool_name)?; + Ok(Self { + tool_name, + action_kind, + }) + } + + /// Return the canonical static tool name selected by the explicit mapping. + #[must_use] + pub const fn tool_name(&self) -> &'static str { + self.tool_name + } + + /// Return the existing OriginWeave typed action selected by this tool. + #[must_use] + pub const fn action_kind(&self) -> ActionKind { + self.action_kind + } +} + +fn valid_method(method: &str) -> bool { + if method.is_empty() || method.len() > MAX_MCP_METHOD_NAME_BYTES { + return false; + } + method + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.' | b'/')) +} + +fn valid_tool_name(tool_name: &str) -> bool { + if tool_name.is_empty() || tool_name.len() > MAX_MCP_TOOL_NAME_BYTES { + return false; + } + tool_name + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.')) +} + +fn map_tool(tool_name: &str) -> Result<(&'static str, ActionKind), McpToolBoundaryError> { + MCP_TOOL_CATALOG + .iter() + .find(|entry| entry.tool_name == tool_name) + .map(|entry| (entry.tool_name, entry.action_kind)) + .ok_or(McpToolBoundaryError::UnknownTool) +} diff --git a/crates/originweave-core/src/root.rs b/crates/originweave-core/src/root.rs new file mode 100644 index 000000000..7acced460 --- /dev/null +++ b/crates/originweave-core/src/root.rs @@ -0,0 +1,15 @@ +//! Shared security and governance contracts for OriginWeave. +//! +//! The historical core contracts remain source-compatible while adapter-specific +//! boundaries can live in focused modules without changing their authority model. + +#![forbid(unsafe_code)] +#![deny(missing_docs)] + +#[path = "lib.rs"] +mod contracts; + +pub use contracts::*; + +/// Stateless MCP routing validation that maps only explicit tools to typed actions. +pub mod mcp; diff --git a/crates/originweave-core/tests/mcp_authority_route.rs b/crates/originweave-core/tests/mcp_authority_route.rs new file mode 100644 index 000000000..80357ec63 --- /dev/null +++ b/crates/originweave-core/tests/mcp_authority_route.rs @@ -0,0 +1,362 @@ +use std::error::Error; + +use originweave_core::mcp::{ + MAX_MCP_METHOD_NAME_BYTES, MAX_MCP_TOOL_NAME_BYTES, MCP_PROTOCOL_VERSION, + MCP_TOOLS_CALL_METHOD, McpToolBoundaryError, ValidatedMcpToolCall, supported_mcp_tools, +}; +use originweave_core::{ActionKind, Capability, RiskClass}; + +fn validate(tool_name: &str) -> Result { + ValidatedMcpToolCall::new( + MCP_PROTOCOL_VERSION, + MCP_TOOLS_CALL_METHOD, + tool_name, + MCP_TOOLS_CALL_METHOD, + tool_name, + ) +} + +#[test] +fn supported_mcp_tools_map_to_exact_originweave_actions() -> Result<(), Box> { + let cases = [ + ( + "originweave.observe", + ActionKind::Observe, + Capability::Observe, + RiskClass::R0, + ), + ( + "originweave.extract", + ActionKind::Extract, + Capability::Extract, + RiskClass::R0, + ), + ( + "originweave.navigate", + ActionKind::Navigate, + Capability::Navigate, + RiskClass::R1, + ), + ( + "originweave.download", + ActionKind::Download, + Capability::Download, + RiskClass::R1, + ), + ( + "originweave.draft", + ActionKind::Draft, + Capability::Draft, + RiskClass::R2, + ), + ( + "originweave.submit", + ActionKind::Submit, + Capability::Submit, + RiskClass::R3, + ), + ( + "originweave.upload", + ActionKind::Upload, + Capability::Upload, + RiskClass::R3, + ), + ( + "originweave.fill_secret", + ActionKind::FillSecret, + Capability::FillSecret, + RiskClass::R3, + ), + ( + "originweave.purchase", + ActionKind::Purchase, + Capability::Purchase, + RiskClass::R4, + ), + ( + "originweave.delete", + ActionKind::Delete, + Capability::Delete, + RiskClass::R4, + ), + ( + "originweave.manage_permission", + ActionKind::ManagePermission, + Capability::ManagePermission, + RiskClass::R4, + ), + ]; + + for (tool_name, expected_action, expected_capability, expected_risk) in cases { + let call = validate(tool_name)?; + assert_eq!(call.tool_name(), tool_name); + assert_eq!(call.action_kind(), expected_action); + assert_eq!( + call.action_kind().required_capability(), + expected_capability + ); + assert_eq!(call.action_kind().risk_class(), expected_risk); + } + Ok(()) +} + +#[test] +fn mcp_tool_catalog_is_deterministic_complete_and_action_unambiguous() -> Result<(), Box> +{ + let expected = [ + ("originweave.observe", ActionKind::Observe), + ("originweave.extract", ActionKind::Extract), + ("originweave.navigate", ActionKind::Navigate), + ("originweave.download", ActionKind::Download), + ("originweave.draft", ActionKind::Draft), + ("originweave.submit", ActionKind::Submit), + ("originweave.upload", ActionKind::Upload), + ("originweave.fill_secret", ActionKind::FillSecret), + ("originweave.purchase", ActionKind::Purchase), + ("originweave.delete", ActionKind::Delete), + ( + "originweave.manage_permission", + ActionKind::ManagePermission, + ), + ]; + let catalog = supported_mcp_tools(); + + assert_eq!(catalog.len(), expected.len()); + for (entry, (expected_name, expected_action)) in catalog.iter().zip(expected) { + assert_eq!(entry.tool_name(), expected_name); + assert_eq!(entry.action_kind(), expected_action); + assert_eq!( + entry.required_capability(), + expected_action.required_capability() + ); + assert_eq!(entry.risk_class(), expected_action.risk_class()); + + let call = validate(entry.tool_name())?; + assert_eq!(call.action_kind(), entry.action_kind()); + } + + for (index, entry) in catalog.iter().enumerate() { + for other in &catalog[index + 1..] { + assert_ne!(entry.tool_name(), other.tool_name()); + assert_ne!(entry.action_kind(), other.action_kind()); + } + } + assert!( + catalog + .iter() + .all(|entry| entry.action_kind() != ActionKind::LegalConsent) + ); + Ok(()) +} + +#[test] +fn mcp_route_rejects_protocol_header_body_and_method_drift() { + assert_eq!( + ValidatedMcpToolCall::new( + "2025-11-25", + MCP_TOOLS_CALL_METHOD, + "originweave.observe", + MCP_TOOLS_CALL_METHOD, + "originweave.observe", + ), + Err(McpToolBoundaryError::UnsupportedProtocolVersion) + ); + assert_eq!( + ValidatedMcpToolCall::new( + MCP_PROTOCOL_VERSION, + MCP_TOOLS_CALL_METHOD, + "originweave.observe", + "tools/list", + "originweave.observe", + ), + Err(McpToolBoundaryError::HeaderBodyMismatch) + ); + assert_eq!( + ValidatedMcpToolCall::new( + MCP_PROTOCOL_VERSION, + MCP_TOOLS_CALL_METHOD, + "originweave.observe", + MCP_TOOLS_CALL_METHOD, + "originweave.extract", + ), + Err(McpToolBoundaryError::HeaderBodyMismatch) + ); + assert_eq!( + ValidatedMcpToolCall::new( + MCP_PROTOCOL_VERSION, + "resources/read", + "originweave.observe", + "resources/read", + "originweave.observe", + ), + Err(McpToolBoundaryError::UnsupportedMethod) + ); +} + +#[test] +fn mcp_route_bounds_each_untrusted_method_before_cross_field_comparison() { + let at_limit = "x".repeat(MAX_MCP_METHOD_NAME_BYTES); + let oversized_routing = "r".repeat(MAX_MCP_METHOD_NAME_BYTES + 1); + let oversized_body = "b".repeat(MAX_MCP_METHOD_NAME_BYTES + 1); + + assert_eq!( + ValidatedMcpToolCall::new( + MCP_PROTOCOL_VERSION, + "", + "originweave.observe", + MCP_TOOLS_CALL_METHOD, + "originweave.observe", + ), + Err(McpToolBoundaryError::InvalidMethod) + ); + assert_eq!( + ValidatedMcpToolCall::new( + MCP_PROTOCOL_VERSION, + MCP_TOOLS_CALL_METHOD, + "originweave.observe", + "", + "originweave.observe", + ), + Err(McpToolBoundaryError::InvalidMethod) + ); + assert_eq!( + ValidatedMcpToolCall::new( + MCP_PROTOCOL_VERSION, + &oversized_routing, + "originweave.observe", + MCP_TOOLS_CALL_METHOD, + "originweave.observe", + ), + Err(McpToolBoundaryError::InvalidMethod) + ); + assert_eq!( + ValidatedMcpToolCall::new( + MCP_PROTOCOL_VERSION, + MCP_TOOLS_CALL_METHOD, + "originweave.observe", + &oversized_body, + "originweave.observe", + ), + Err(McpToolBoundaryError::InvalidMethod) + ); + assert_eq!( + ValidatedMcpToolCall::new( + MCP_PROTOCOL_VERSION, + "tools call", + "originweave.observe", + "tools call", + "originweave.observe", + ), + Err(McpToolBoundaryError::InvalidMethod) + ); + assert_eq!( + ValidatedMcpToolCall::new( + MCP_PROTOCOL_VERSION, + &at_limit, + "originweave.observe", + &at_limit, + "originweave.observe", + ), + Err(McpToolBoundaryError::UnsupportedMethod) + ); +} + +#[test] +fn mcp_route_rejects_unbounded_malformed_and_unmapped_tool_names() { + let at_limit = "x".repeat(MAX_MCP_TOOL_NAME_BYTES); + let oversized = "x".repeat(MAX_MCP_TOOL_NAME_BYTES + 1); + for tool_name in [ + "", + "originweave legal", + "originweave/observe", + "originweave.관찰", + &oversized, + ] { + assert_eq!( + validate(tool_name), + Err(McpToolBoundaryError::InvalidToolName) + ); + } + + assert_eq!(validate(&at_limit), Err(McpToolBoundaryError::UnknownTool)); + assert_eq!( + validate("originweave.legal_consent"), + Err(McpToolBoundaryError::UnknownTool) + ); + assert_eq!( + validate("third_party.arbitrary_javascript"), + Err(McpToolBoundaryError::UnknownTool) + ); +} + +#[test] +fn mcp_route_validates_each_untrusted_tool_name_before_cross_field_comparison() { + let oversized_routing = "r".repeat(MAX_MCP_TOOL_NAME_BYTES + 1); + let oversized_body = "b".repeat(MAX_MCP_TOOL_NAME_BYTES + 1); + + assert_eq!( + ValidatedMcpToolCall::new( + MCP_PROTOCOL_VERSION, + MCP_TOOLS_CALL_METHOD, + &oversized_routing, + MCP_TOOLS_CALL_METHOD, + "originweave.observe", + ), + Err(McpToolBoundaryError::InvalidToolName) + ); + assert_eq!( + ValidatedMcpToolCall::new( + MCP_PROTOCOL_VERSION, + MCP_TOOLS_CALL_METHOD, + "originweave.observe", + MCP_TOOLS_CALL_METHOD, + &oversized_body, + ), + Err(McpToolBoundaryError::InvalidToolName) + ); + assert_eq!( + ValidatedMcpToolCall::new( + MCP_PROTOCOL_VERSION, + MCP_TOOLS_CALL_METHOD, + "originweave/observe", + MCP_TOOLS_CALL_METHOD, + "originweave.observe", + ), + Err(McpToolBoundaryError::InvalidToolName) + ); +} + +#[test] +fn mcp_boundary_errors_are_deterministic_and_do_not_echo_untrusted_values() { + let cases = [ + ( + McpToolBoundaryError::UnsupportedProtocolVersion, + "unsupported MCP protocol version", + ), + ( + McpToolBoundaryError::HeaderBodyMismatch, + "MCP routing headers do not match the request body", + ), + ( + McpToolBoundaryError::UnsupportedMethod, + "only MCP tools/call requests can enter the typed action boundary", + ), + ( + McpToolBoundaryError::InvalidMethod, + "MCP method violates the bounded ASCII routing syntax", + ), + ( + McpToolBoundaryError::InvalidToolName, + "MCP tool name violates the bounded ASCII routing syntax", + ), + ( + McpToolBoundaryError::UnknownTool, + "MCP tool is not mapped to an OriginWeave typed action", + ), + ]; + + for (error, expected_message) in cases { + assert_eq!(error.to_string(), expected_message); + assert!(error.source().is_none()); + } +} diff --git a/crates/originweave-evidence/Cargo.toml b/crates/originweave-evidence/Cargo.toml index 35c21a7fb..a69386c38 100644 --- a/crates/originweave-evidence/Cargo.toml +++ b/crates/originweave-evidence/Cargo.toml @@ -12,7 +12,6 @@ publish = false [dependencies] originweave-core = { path = "../originweave-core" } -sha2 = "=0.10.9" [lints] workspace = true diff --git a/crates/originweave-evidence/src/lib.rs b/crates/originweave-evidence/src/lib.rs index 65289c876..8474b3618 100644 --- a/crates/originweave-evidence/src/lib.rs +++ b/crates/originweave-evidence/src/lib.rs @@ -9,7 +9,6 @@ mod extraction_schema; mod sensitive_access; -mod warc_resource_record; pub use extraction_schema::{ ExtractionCardinality, ExtractionField, ExtractionNormalizationRule, ExtractionSchema, @@ -21,11 +20,6 @@ pub use sensitive_access::{ SensitiveAccessEvidence, SensitiveAccessEvidenceInput, SensitiveAccessOutcome, SensitiveEvidenceError, }; -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, - WarcTruncationReason, -}; use std::collections::BTreeMap; @@ -236,6 +230,9 @@ fn validate_path(path: &str) -> Result<(), EvidenceError> { index += 3; continue; } + if !is_rfc3986_pchar(byte) { + return Err(EvidenceError::InvalidPath); + } segment.push(byte); index += 1; } @@ -245,6 +242,32 @@ fn validate_path(path: &str) -> Result<(), EvidenceError> { Ok(()) } +const fn is_rfc3986_pchar(byte: u8) -> bool { + matches!( + byte, + b'A'..=b'Z' + | b'a'..=b'z' + | b'0'..=b'9' + | b'-' + | b'.' + | b'_' + | b'~' + | b'!' + | b'$' + | b'&' + | b'\'' + | b'(' + | b')' + | b'*' + | b'+' + | b',' + | b';' + | b'=' + | b':' + | b'@' + ) +} + const fn hexadecimal_value(byte: u8) -> Option { match byte { b'0'..=b'9' => Some(byte - b'0'), diff --git a/crates/originweave-evidence/tests/evidence.rs b/crates/originweave-evidence/tests/evidence.rs index 2912180f1..48d49cbc4 100644 --- a/crates/originweave-evidence/tests/evidence.rs +++ b/crates/originweave-evidence/tests/evidence.rs @@ -80,6 +80,9 @@ fn network_evidence_rejects_non_path_inputs() { "/bad\npath", "/bad path", "/windows\\path", + "/[segment]", + "/raw|pipe", + "/raw-한글", ] { assert_eq!( NetworkEvidence::capture( @@ -128,6 +131,7 @@ fn provenance_rejects_credential_bearing_or_ambiguous_source_urls() { "https://example.com/bad\\path", "https://example.com/\n", "https://example.com/a/%2f/b", + "https://example.com/[segment]", ] { assert_eq!( ProvenanceRecord::new( diff --git a/crates/originweave-policy/src/lib.rs b/crates/originweave-policy/src/lib.rs index 243ae8ce7..dbfb3c16d 100644 --- a/crates/originweave-policy/src/lib.rs +++ b/crates/originweave-policy/src/lib.rs @@ -15,6 +15,7 @@ pub use sensitive_data::{ evaluate_handle_use, }; +use originweave_core::mcp::ValidatedMcpToolCall; use originweave_core::{ ActionRequest, ApprovalEvidence, ApprovalScope, Capability, ExecutionPurpose, InstructionSource, PolicyContext, RiskClass, RobotsDecision, SecretDelivery, SessionMode, @@ -40,6 +41,8 @@ pub enum DenialReason { ModePurposeMismatch, /// Page or document content attempted to become a trusted instruction. UntrustedInstructionSource, + /// The validated MCP route resolved to a different action than the policy request. + McpActionMismatch, /// The session lacks the exact capability required by the action. MissingCapability(Capability), /// The target origin is outside the session's read grant. @@ -66,6 +69,23 @@ pub enum DenialReason { ApprovalScopeMismatch, } +/// Evaluate a policy request only when it matches an already validated MCP route. +/// +/// Matching routing metadata grants no authority. Once route and request action agree, the request +/// still passes through the existing action policy unchanged. +#[must_use] +pub fn evaluate_mcp( + call: &ValidatedMcpToolCall, + request: &ActionRequest, + context: &PolicyContext, +) -> Decision { + if call.action_kind() != request.action() { + return Decision::Deny(DenialReason::McpActionMismatch); + } + + evaluate(request, context) +} + /// Evaluate a typed browser action against one explicit policy context. #[must_use] pub fn evaluate(request: &ActionRequest, context: &PolicyContext) -> Decision { diff --git a/crates/originweave-policy/tests/mcp_route_binding.rs b/crates/originweave-policy/tests/mcp_route_binding.rs new file mode 100644 index 000000000..8e9661af6 --- /dev/null +++ b/crates/originweave-policy/tests/mcp_route_binding.rs @@ -0,0 +1,96 @@ +#![allow(clippy::expect_used)] + +use std::collections::BTreeSet; + +use originweave_core::mcp::{MCP_PROTOCOL_VERSION, MCP_TOOLS_CALL_METHOD, ValidatedMcpToolCall}; +use originweave_core::{ + ActionIntentDigest, ActionKind, ActionRequest, ApprovalEvidence, Capability, ExecutionPurpose, + InstructionSource, Origin, PolicyContext, RobotsDecision, SecretDelivery, SessionMode, +}; +use originweave_policy::{Decision, DenialReason, evaluate_mcp}; + +const VALID_INTENT: &str = + "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + +fn origin() -> Origin { + Origin::parse("https://mcp.example").expect("valid test origin") +} + +fn intent() -> ActionIntentDigest { + ActionIntentDigest::parse(VALID_INTENT).expect("valid intent digest") +} + +fn validated_call(tool_name: &str) -> ValidatedMcpToolCall { + ValidatedMcpToolCall::new( + MCP_PROTOCOL_VERSION, + MCP_TOOLS_CALL_METHOD, + tool_name, + MCP_TOOLS_CALL_METHOD, + tool_name, + ) + .expect("known test MCP tool") +} + +fn request(action: ActionKind) -> ActionRequest { + let site = origin(); + ActionRequest::new( + action, + site.clone(), + site, + InstructionSource::User, + SecretDelivery::None, + intent(), + ) +} + +fn context(capabilities: BTreeSet) -> PolicyContext { + let site = origin(); + PolicyContext::new( + SessionMode::AgentTask, + ExecutionPurpose::UserDelegatedTask, + capabilities, + BTreeSet::from([site.clone()]), + BTreeSet::from([site]), + RobotsDecision::Allowed, + ApprovalEvidence::None, + ) +} + +#[test] +fn matching_mcp_route_enters_the_existing_policy_boundary() { + let call = validated_call("originweave.observe"); + let decision = evaluate_mcp( + &call, + &request(ActionKind::Observe), + &context(BTreeSet::from([Capability::Observe])), + ); + + assert_eq!(decision, Decision::Allow); +} + +#[test] +fn mismatched_mcp_route_cannot_be_reinterpreted_as_another_action() { + let call = validated_call("originweave.observe"); + let decision = evaluate_mcp( + &call, + &request(ActionKind::Navigate), + &context(BTreeSet::from([Capability::Navigate])), + ); + + assert_eq!(decision, Decision::Deny(DenialReason::McpActionMismatch)); +} + +#[test] +fn matching_mcp_route_does_not_bypass_existing_policy_denials() { + let call = validated_call("originweave.navigate"); + let decision = evaluate_mcp( + &call, + &request(ActionKind::Navigate), + &context(BTreeSet::from([Capability::Observe])), + ); + + assert_eq!( + decision, + Decision::Deny(DenialReason::MissingCapability(Capability::Navigate)) + ); +} diff --git a/docs/README.md b/docs/README.md index 03b573c54..775dd0de6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -22,6 +22,7 @@ - [OriginWeave API and protocol contract](API_CONTRACT.md) - [Release and rollback contract](RELEASE_AND_ROLLBACK.md) - [Product roadmap](product-roadmap.md) +- [Product and technical gap baseline](product-technical-gap-baseline.md) - [Research and standards](doctoring.md) - [Browser and Agent protocol standards evidence](doctoring/browser-agent-protocols.md) - [Current product-baseline standards addendum](doctoring/product-documentation-baseline.md) diff --git a/docs/adr/0106-provenance-evidence-model.md b/docs/adr/0106-provenance-evidence-model.md index d2494ad29..09cb0d7ca 100644 --- a/docs/adr/0106-provenance-evidence-model.md +++ b/docs/adr/0106-provenance-evidence-model.md @@ -9,14 +9,6 @@ OriginWeave must not merely act; it must let a user, operator, auditor, or downstream system establish what was observed, authorized, executed, and verified. Browser logs alone are not sufficient because they collapse observation, policy, action, network identity, approvals, and post-conditions. At the same time, evidence can itself contain sensitive or attacker-controlled content. The product needs a durable model compatible with web-archive and provenance concepts without claiming that every conceptual record is already persisted. -## Active implementation boundary - -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. - ## Decision drivers - `Browse. Act. Prove.` requires evidence as a first-class product output. diff --git a/docs/adr/0107-browser-protocol-adapter-strategy.md b/docs/adr/0107-browser-protocol-adapter-strategy.md index 8923616be..e3c0bf657 100644 --- a/docs/adr/0107-browser-protocol-adapter-strategy.md +++ b/docs/adr/0107-browser-protocol-adapter-strategy.md @@ -34,6 +34,14 @@ OriginWeave exposes its own versioned protocol for session, observation, query, MCP version negotiation is independent of the OriginWeave Protocol version. As of this review, MCP `2026-07-28` is the current released protocol generation; a future MCP change does not silently alter OriginWeave task, approval, secret, tenant, or browser semantics. MCP tool/resource content remains untrusted input and any server-to-client/user interaction capability is mediated by the same OriginWeave policy/approval boundaries as other adapter traffic. +### Current implementation boundary + +The complete MCP adapter remains **Planned**. Active PR #168 is narrower **IMPLEMENTED_ON_ACTIVE_PR** evidence inside the Rust control plane: it validates the `2026-07-28` stateless `tools/call` routing envelope presented to this boundary, bounds and syntax-checks both untrusted method fields and both untrusted tool-name fields before cross-field correlation, derives one of the existing typed `ActionKind` values from a deterministic reviewed registry, exposes discovery metadata from that same registry, and requires the resulting action to pass the ordinary OriginWeave policy evaluator. The method boundary accepts only nonempty ASCII method names up to 64 bytes using the reviewed routing alphabet, while the tool-name boundary accepts only nonempty ASCII names up to 128 bytes using its narrower reviewed alphabet. The catalog and validated route grant no capability, approval, origin, secret, browser, persistence, or evidence authority by themselves. + +PR #168 does not implement Streamable HTTP transport parsing, complete request `_meta` validation, `tools/list` serialization/caching/pagination, OAuth, browser I/O, WebMCP/BiDi/CDP translation, secret delivery, persistence, or a complete OriginWeave Protocol adapter. Those remain separate adapter/runtime work. Protected `main` therefore must continue to describe MCP as planned until this active-PR evidence is integrated, and even after integration only the merged bounded routing foundation may be called implemented; the full adapter remains planned until its remaining acceptance boundaries ship. + +The version boundary is explicit: the routing foundation accepts only MCP `2026-07-28`; it does not infer compatibility with later protocol generations. OriginWeave Protocol versioning remains independent and cannot be changed by MCP metadata. + ## Consequences OriginWeave carries adapter maintenance and version negotiation but gains a durable customer API. Multiple browser/control transports can coexist. New upstream capabilities do not silently change risk or action semantics. Compatibility matrices become release artifacts. @@ -44,19 +52,21 @@ Adapter negotiation failure disables only affected capabilities. Unsupported or ## Security / privacy / governance impact -Protocol validation occurs before messages influence policy. Tool/page-provided strings remain untrusted. Secret handles never become raw secret protocol payloads; only the separately authorized trusted broker-to-browser delivery path may materialize the value, and that value does not pass through MCP, WebMCP, BiDi observation, or model-visible CDP output. Adapter version/provenance is recorded for audit and incident reconstruction. +Protocol validation occurs before messages influence policy. Tool/page-provided strings remain untrusted. Method and tool routing metadata is shape-bounded before correlation, preventing malformed or oversized untrusted routing strings from being reinterpreted through mismatch handling. Secret handles never become raw secret protocol payloads; only the separately authorized trusted broker-to-browser delivery path may materialize the value, and that value does not pass through MCP, WebMCP, BiDi observation, or model-visible CDP output. Adapter version/provenance is recorded for audit and incident reconstruction. ## Tests and acceptance evidence Require version-negotiation tests, schema/property tests, malformed-message tests, BiDi/CDP semantic parity tests for shared capabilities, WebMCP prompt-injection tests, MCP authority-separation and version-change tests, browser-version compatibility matrices, and end-to-end proof that unsupported capabilities fail without side effects. +For active PR #168 specifically, acceptance additionally requires deterministic method and tool-name bounds/syntax, exact header/body method and tool-name correlation only after both sides are bounded, explicit invalid-method/invalid-tool-name/unknown-tool rejection, one unambiguous tool-to-action registry, independent capability/risk expectations, route/action mismatch denial before ordinary policy evaluation, exact 100% owned-production coverage, and unchanged-head CI/security/review evidence. These checks do not substitute for complete transport or adapter conformance. + ## Migration and rollback Adapters are independently versioned and can be canaried. Clients migrate through OriginWeave Protocol compatibility rules, not upstream protocol rewrites. Rollback pins a previously supported adapter/browser/protocol pair and records that pair in provenance. ## Open follow-ups -Define internal protocol versioning rules, adapter capability descriptors, minimum supported BiDi level, CDP pin policy, and MCP/WebMCP schema isolation. +Define internal protocol versioning rules, adapter capability descriptors, minimum supported BiDi level, CDP pin policy, complete MCP Streamable HTTP/request-metadata validation, MCP discovery/serialization/cache behavior, and MCP/WebMCP schema isolation. ## Supersession / reversal conditions @@ -68,10 +78,12 @@ Chrome DevTools Protocol. (2026). *Chrome DevTools Protocol — latest (tip-of-t Chrome DevTools Protocol. (2026). *WebMCP domain*. Chromium. Retrieved August 9, 2026, from https://chromedevtools.github.io/devtools-protocol/tot/WebMCP/ +Model Context Protocol. (2026, July 28). *Specification: 2026-07-28*. https://modelcontextprotocol.io/specification/2026-07-28 + Parra, D. S., & Delimarsky, D. (2026, July 28). *The 2026-07-28 specification*. Model Context Protocol Blog. https://blog.modelcontextprotocol.io/posts/2026-07-28/ World Wide Web Consortium. (2026, June 29). *WebDriver BiDi* [Working Draft]. https://www.w3.org/TR/2026/WD-webdriver-bidi-20260629/ ## Related documents -See `docs/API_CONTRACT.md`, `docs/TRD.md`, `docs/doctoring/product-documentation-baseline.md`, and `docs/DATA_GOVERNANCE.md`. +See `docs/API_CONTRACT.md`, `docs/TRD.md`, `docs/doctoring.md`, `docs/doctoring/product-documentation-baseline.md`, `docs/traceability/README.md`, and `docs/DATA_GOVERNANCE.md`. diff --git a/docs/doctoring.md b/docs/doctoring.md index 45bc7e656..fcd9dd4f0 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -8,6 +8,8 @@ This document records external evidence that changes OriginWeave architecture, t The 1 June 2026 WebDriver BiDi Working Draft defines a bidirectional remote-control protocol, events, commands, and user contexts. Because it remains a W3C Working Draft, OriginWeave places BiDi behind a versioned adapter and Web Platform Tests-derived contract tests rather than make it the internal authority model. +The final Model Context Protocol `2026-07-28` specification defines the currently reviewed MCP generation. Its stateless request model carries protocol metadata per request and standard Streamable HTTP routing metadata for MCP operations; its Tools surface defines bounded, case-sensitive tool names and requires clients to treat tool annotations as untrusted unless supplied by a trusted server. OriginWeave therefore keeps MCP outside the product authority model. Active PR #168 implements only a bounded Rust `tools/call` routing/action-policy foundation for that exact generation; the complete transport, request-metadata, discovery, OAuth, browser, secret, and persistence adapter remains planned and cannot be inferred from the core routing primitive. + ### Browser origin equivalence The WHATWG URL host parser and Chromium canonicalizer classify shortened decimal, integer, hexadecimal, legacy octal-looking, and mixed-component numeric hosts as IPv4 or broken IPv4 candidates rather than ordinary DNS names. Chromium's regression suite includes values such as `192`, `0xC0a80001`, `030052000001`, and mixed hexadecimal components. A non-final empty `0x` component can participate in Chromium's multi-part IPv4 truncation behavior, but a final `0x` label does not produce an IPv4 number because stripping its prefix leaves no digits; it remains a domain label. Chromium also warns that broken IP-like hosts must not be connected because another resolver could accept them. OriginWeave therefore admits only canonical dotted-decimal IPv4 into its policy origin type, rejects browser-special numeric spellings before DNS validation, and preserves final non-numeric DNS labels such as `0x`. @@ -84,7 +86,7 @@ W3C PROV-O supplies interoperable Entity, Activity, Agent, derivation, attributi 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. -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. +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. ### AI risk and prompt injection @@ -110,6 +112,8 @@ Autio, C., Schwartz, R., Dunietz, J., Jain, S., Stanley, M., Tabassi, E., Hall, Barth, A. (2011). *The web origin concept* (RFC 6454). Internet Engineering Task Force. https://doi.org/10.17487/RFC6454 +Berners-Lee, T., Fielding, R., & Masinter, L. (2005). *Uniform resource identifier (URI): Generic syntax* (RFC 3986; STD 66). Internet Engineering Task Force. https://doi.org/10.17487/RFC3986 + Bonica, R., Cotton, M., Haberman, B., & Vegoda, L. (2017). *Updates to the special-purpose IP address registries* (RFC 8190). Internet Engineering Task Force. https://doi.org/10.17487/RFC8190 Chromium Authors. (n.d.). *Proxy support in Chrome* [Source documentation]. Chromium. https://chromium.googlesource.com/chromium/src/+/a3e71ebfa307d8760eb68b777e2998a869940092/net/docs/proxy.md @@ -138,8 +142,6 @@ Internet Assigned Numbers Authority. (2025, October 9). *IPv6 special-purpose ad Internet Assigned Numbers Authority. (2025, October 10). *IPv6 global unicast address space*. https://www.iana.org/assignments/ipv6-unicast-address-assignments/ipv6-unicast-address-assignments.xhtml -International Internet Preservation Consortium. (n.d.). *The WARC format 1.1*. Retrieved August 22, 2026, from https://iipc.github.io/warc-specifications/specifications/warc-format/warc-1.1/ - International Organization for Standardization. (2017). *Information and documentation—WARC file format* (ISO Standard No. 28500:2017). https://www.iso.org/standard/68004.html Koster, M., Illyes, G., Zeller, H., & Sassman, L. (2022). *Robots Exclusion Protocol* (RFC 9309). Internet Engineering Task Force. https://doi.org/10.17487/RFC9309 @@ -148,8 +150,14 @@ Lodderstedt, T., Bradley, J., Labunets, A., & Fett, D. (2025). *OAuth 2.0 securi Microsoft. (2025, July 25). *Azure IP address 168.63.129.16 overview*. Microsoft Learn. https://learn.microsoft.com/azure/virtual-network/what-is-ip-address-168-63-129-16 +Model Context Protocol. (2026, July 28). *Specification: 2026-07-28*. https://modelcontextprotocol.io/specification/2026-07-28 + Nielsen, S., Cetin, E., Schwendeman, P., Sun, Q., Xu, J., & Tang, Y. (2025). *Learning to orchestrate agents in natural language with the Conductor* [Preprint]. arXiv. https://doi.org/10.48550/arXiv.2512.04388 +Nottingham, M. (2014). *URI design and ownership* (RFC 7320). Internet Engineering Task Force. https://doi.org/10.17487/RFC7320 + +Nottingham, M. (2020). *URI design and ownership* (RFC 8820; BCP 190). Internet Engineering Task Force. https://doi.org/10.17487/RFC8820 + Rescorla, E. (2026). *The Transport Layer Security (TLS) protocol version 1.3* (RFC 9846). Internet Engineering Task Force. https://doi.org/10.17487/RFC9846 Rustls Project Developers. (2026). *rustls 0.23.42* [Computer software]. https://docs.rs/rustls/0.23.42/rustls/ diff --git a/docs/doctoring/rust-toolchain-freshness.md b/docs/doctoring/rust-toolchain-freshness.md new file mode 100644 index 000000000..a00e7fb08 --- /dev/null +++ b/docs/doctoring/rust-toolchain-freshness.md @@ -0,0 +1,44 @@ +# Rust toolchain freshness and reproducibility + +## Decision + +OriginWeave keeps Rust `1.97.1` as the exact stable compiler baseline. As of +2026-08-19 this is the current stable point release, so the generic compiler +suggestion to upgrade does not justify replacing it with a floating `stable` +channel. + +Production line, region, and function coverage remains on the stable compiler. +Branch coverage uses the independently date-pinned `nightly-2026-08-18` +toolchain because upstream `cargo-llvm-cov` still identifies Rust branch +coverage as unstable and nightly-only. Every branch-coverage command must use +the same date pin, and exact-head CI must prove that `llvm-tools-preview`, the +pinned `cargo-llvm-cov` release, the workspace, and the coverage verifier remain +compatible before merge. + +The root `rust-toolchain.toml` is tracked through GitHub Dependabot's +`rust-toolchain` ecosystem. Toolchain changes therefore arrive as reviewable +pull requests rather than silently changing underneath local or CI builds. +Date-pinned branch-coverage nightly updates remain explicit infrastructure +changes and must preserve the repository contract test. + +## Failure interpretation + +The historical OriginWeave coverage failure at PR #192 predecessor head +`ccb7d31dfe7654bab800d463c2391cc1a19c7d74` was not proof that the compiler was +too old. The compiler emitted the generic note while rejecting a non-stable +const conversion in test code. The current PR #192 head moved that conversion +out of a constant and passed the complete native CI workflow. Toolchain +freshness and source compatibility are therefore maintained as separate +controls. + +## References + +GitHub. (2025, August 19). *Dependabot now supports Rust toolchain updates*. +GitHub Changelog. +https://github.blog/changelog/2025-08-19-dependabot-now-supports-rust-toolchain-updates/ + +Rust Project Developers. (2026, July 16). *Announcing Rust 1.97.1*. Rust Blog. +https://blog.rust-lang.org/2026/07/16/Rust-1.97.1/ + +Taiki Endo and contributors. (2026). *cargo-llvm-cov* (Version 0.8.6) +[Computer software]. GitHub. https://github.com/taiki-e/cargo-llvm-cov diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md new file mode 100644 index 000000000..234e6ae5c --- /dev/null +++ b/docs/product-technical-gap-baseline.md @@ -0,0 +1,325 @@ +# Product and Technical Gap Baseline + +This is a dated delivery baseline, not a substitute for the PRD, TRD, roadmap, architecture decisions, or live GitHub state. It keeps buyer-visible gaps, current issues, active pull-request evidence, and commercial completion tracks in one discoverable place. Protected `main` is the implementation boundary: code in an open pull request is not shipped behavior. + +## Observed snapshot: 2026-08-24 + +### Protected-main truth + +- Protected `main` remained at `0841d2ab3d8b5e60a03c0a8e818cf438e2716829` when this snapshot was refreshed. +- Phase 0 is documented as complete as a reusable safety-kernel foundation: typed policy contracts, destination classification, direct TCP peer verification, TLS service identity, evidence bounds, resource mitigation, document-node authority, and protected-main tests. +- Phase 1 is **in progress**, not shipped. The first real Chromium vertical slice still needs the active WebDriver BiDi transport stack to reach protected `main`, then compose isolated Chromium launch, session/context identity, semantic observation, typed action authorization, native browser input, post-condition proof, evidence, cancellation, crash recovery, and profile/process teardown. +- HTTP/1.1 bounds, downloads/MIME, proxy/PAC consumption, full browser-network integration, the sensitive-data broker runtime, durable WARC/PROV capture, persistent task/API surfaces, signed cross-platform distribution, enterprise administration, and release-grade buyer acceptance remain open. +- Active pull requests remain evidence, not shipped behavior. Successful checks on a feature or stacked branch do not prove that protected `main` contains the capability or that a child can merge before its prerequisite. + +### Open pull requests + +The live repository contained **158 open pull requests: 44 non-draft and 114 draft** when this snapshot re-paginated the complete open inventory. The volume and stack depth are themselves a product-delivery risk: review, exact-head checks, dependency order, and integration truth can drift faster than a buyer-visible vertical slice reaches protected `main`. + +Representative active workstreams at this snapshot were: + +| Workstream | Representative active PR evidence | Delivery boundary | +|---|---|---| +| Product baseline | #196 | Ready/non-draft documentation PR; all exact-head checks passed and review threads resolved, blocked only by the reviewer-provisioning gap below | +| Enterprise approval authority | #220 | Ready/non-draft bounded maker-checker approval lifecycle on the exact `ApprovalScope`; Strix re-scan was re-dispatched after a provider-unavailability failure | +| Release artifact identity | #218 and #219 | Ready/non-draft fail-closed benchmark release decision and canonical release manifest binding; #218's Strix re-scan was re-dispatched after provider unavailability | +| Evidence path conformance | #216 | Ready/non-draft RFC 3986 evidence-path syntax enforcement | +| Schema-bound extraction and BAP lifecycle | #209 and #208 | Ready/non-draft schema-bound extraction contract and resumable task-lifecycle kernel; #208's Strix re-scan was re-dispatched after provider unavailability | +| WebDriver BiDi transport | #188 through #205 | Draft stack exercising framed `locateNodes` exchange over a bounded WebSocket opening path; still no authenticated browser-process provenance, semantic task execution, or protected-main shipment | +| MCP adapter | #168 and #170 | Typed MCP routing and conservative `tools/list` metadata are active-PR foundations; complete authenticated transport, durable task lifecycle, cancellation/resume, and browser execution remain open under #200 | +| Controlled Chromium and recovery | #65, #70-#73, #100, #105, #142-#153 and descendants | Real pinned-browser fixture, semantic location, resource, crash, and teardown evidence exists on active stacks; evidence does not transfer across heads or prerequisites | +| Durable WARC/PROV evidence | #210, #217 | Bounded WARC resource records and PROV JSON-LD binding are draft active-PR foundations; durable ownership, replay, retention/deletion, and browser side-effect reconciliation remain open | +| Manifest V3 and native messaging | #27 and its active extension/native-host stack, including #154 and #169 | Compatibility and Agent-authority isolation remain incomplete until exact release artifacts and platform matrices are proven | +| Sensitive-data and model route policy | #10 and its active policy stacks | Deterministic policy values exist, but trusted broker execution, retention/deletion, runtime isolation, and auditable product workflows remain open | +| VPN/profile intent | #149 | Bounded WireGuard/IKEv2 profile authority is active-PR evidence; it does not create a tunnel, route, DNS state, authenticated gateway, or connectivity proof | + +Draft PR #205 is the current top WebDriver BiDi locate-nodes slice; its opening-path prerequisites #195 and #198 remain draft evidence and cannot be treated as shipped behavior. + +#### Current exact-head active PR evidence + +The following newest product slices were re-fetched from GitHub for this snapshot. Their exact base/head pairs are recorded so later checks, reviews, and restacks cannot be confused with predecessor evidence: + +| PR | State | Exact base head | Exact head | +|---|---|---|---| +| #73 | Draft | `da99395b09b419845b4a1222a0725482e9231466` | `7861d88d21ed0f0adaeb467957e809826f835071` | +| #208 | Draft | `0841d2ab3d8b5e60a03c0a8e818cf438e2716829` | `c3b6e1a475dce333f6115e5113cae9c07974835f` | +| #209 | Draft | `0841d2ab3d8b5e60a03c0a8e818cf438e2716829` | `69bc738bd45a1b61a4673b122dc3eec8814baa22` | +| #210 | Draft | `69bc738bd45a1b61a4673b122dc3eec8814baa22` | `999979a511c3a890ba93a1a09da8810858877940` | +| #211 | Draft | `c3b6e1a475dce333f6115e5113cae9c07974835f` | `f6e3a3adcfb9cc7a60ef1d79e2aeee27ba54c084` | + +These rows are delivery evidence only. #73's latest Strix remediation is locally verified but its required policy workflows remain queued; #208–#211 are stacked product-gap foundations with no protected-main promotion. None has counted independent approval in the current collaborator inventory. + +#### Refreshed exact-head active PR evidence: 2026-08-24 + +The following newest slices were re-fetched from GitHub for this snapshot. Heads have moved since the 2026-08-21 rows above; those predecessor rows are retained as regression anchors and must never be promoted to current-head evidence: + +| PR | State | Exact base head | Exact head | +|---|---|---|---| +| #222 | Draft | `56fcfa56525e4f2e980e0ee05b6776d621bcddc5` | `1e2ce3d4071a1a75ee891bdcd71c506b3b50d4bc` | +| #221 | Draft | `8145d40f1b028a8f4dc7e7da47ac89bb9e5bb2c7` | `6f339df1e5b3ddb265f4ddd7b262d4de1e0b5e1f` | +| #220 | Ready | `0841d2ab3d8b5e60a03c0a8e818cf438e2716829` | `ed4cab16cf88c76ce1c145a22d0a274ef2d57263` | +| #219 | Ready | `0841d2ab3d8b5e60a03c0a8e818cf438e2716829` | `8145d40f1b028a8f4dc7e7da47ac89bb9e5bb2c7` | +| #218 | Ready | `0841d2ab3d8b5e60a03c0a8e818cf438e2716829` | `49e98fba6974219b3bb0336c822b12667f1e1c03` | +| #217 | Draft | `529d11a3571f6b1834b9baa49ef67eb08f043978` | `56fcfa56525e4f2e980e0ee05b6776d621bcddc5` | +| #216 | Ready | `0841d2ab3d8b5e60a03c0a8e818cf438e2716829` | `75130851a0f7ce528a7a36382eb026ac7942a0aa` | +| #214 | Draft | `40d642d5470a7753b8211907c190367f742f2f12` | `f79999681866ecf0e5fe17d895170f3f6cae7361` | +| #211 | Draft | `85cc477688246900697f4cfb91c0c8f1f692934a` | `40d642d5470a7753b8211907c190367f742f2f12` | +| #210 | Draft | `c38b9665774d6b3754e572bed527737b5e179833` | `529d11a3571f6b1834b9baa49ef67eb08f043978` | +| #209 | Ready | `0841d2ab3d8b5e60a03c0a8e818cf438e2716829` | `c38b9665774d6b3754e572bed527737b5e179833` | +| #208 | Ready | `0841d2ab3d8b5e60a03c0a8e818cf438e2716829` | `85cc477688246900697f4cfb91c0c8f1f692934a` | + +The stack topology shows #209 → #210 → #217 → #222 (WARC/PROV chain), #208 → #211 → #214 (BAP chain), #218 → #221 → #220 (release/enterprise chain) at this snapshot. Every row above remains active-PR evidence; none is protected-main behavior. + +### Required-check provider failure record + +On 2026-08-23 the required Strix security scan failed closed on exact heads of #220 (`ed4cab16…`), #218 (`49e98fba…`), and #208 (`85cc4776…`) because its LLM provider/backend was unavailable (rate limit, token cap, connection, warm-up, or model-behavior failure); no vulnerability report artifact was produced, so the workflow correctly refused to convert an incomplete scan into passing security evidence. Failed jobs were re-dispatched on the unchanged exact heads on 2026-08-24. This is a provider-infrastructure failure record, not a weakening of the fail-closed gate or a substitute for a completed authoritative scan. + +#### #195/#198 WebDriver BiDi opening path status + +Phase 1 is **in progress**, not shipped. #195 and #198 provide bounded WebSocket opening-path evidence on active branches; framed BiDi commands, authenticated browser-process provenance, semantic task execution, and protected-main integration remain open. + +#### #149 VPN/profile intent status + +It remains draft evidence and cannot be treated as shipped behavior. #149 describes bounded WireGuard/IKEv2 profile authority, but it does not create a tunnel, route, DNS state, authenticated gateway, or connectivity proof. + +The current queue must be processed in dependency order. A green child branch cannot substitute for current checks and review on its prerequisite, synthetic merge, or eventual protected-main commit. PRs that only duplicate, supersede, or preserve stale branch topology should be closed with explicit replacement evidence rather than retained indefinitely. + +### Review and merge authority + +The active `CWL Central required workflows` ruleset requires two approving reviews, approval after the last push, resolved review threads, and configured required workflows. The current collaborator inventory contains only `seonghobae` with administration and push permissions, creating a **reviewer-provisioning gap** for counted non-author approval. + +This gap does not authorize self-approval, administrative bypass, stale-head merge, or weaker checks. Exact current-head checks, security gates, complete coverage, rustdoc/Clippy, thread resolution, and branch protection remain mandatory. Before any merge decision, re-fetch the exact ruleset, collaborators, PR head/base, reviews, unresolved threads, and required checks; do not assume this dated observation remains current. + +### Open issues and operational signals + +| Issue | Current gap or signal | +|---|---| +| #28 | First real Chromium Agent Task vertical slice; highest immediate Phase 1 buyer-visible gap | +| #27 | Complete Manifest V3 compatibility and extension-authority isolation matrix | +| #9 | Bounded HTTP/1.1 semantics over the authenticated TLS stream | +| #10 | Purpose-bound operational PII disclosure and trusted broker/storage lifecycle | +| #123 | Fleet incident: disable orphaned TLS, HTTP, and one-shot workflow identities | +| #187 | Manual-authority review of the coverage-diagnostics workflow delta | +| #212 | Governance: remove or independently authorize the PR #43 MV3 workflow mutation | +| #215 | Governance: restore an enforceable protected-main policy that does not create a routine admin bypass | +| #199 | Schema-bound extraction with durable WARC/PROV replay, retention, deletion, and offline verification | +| #200 | Stable BAP/MCP runtime API with authenticated, idempotent, cancellable, resumable task lifecycle | +| #201 | Signed cross-platform Chromium distribution, installer/updater, patch SLA, rollback, SBOM, and provenance | +| #202 | Enterprise control and experience plane: operator UI, Keyverse-compatible identity, tenancy, approval, audit, SLO, Figma, and Storybook | +| #203 | Release-grade web-agent benchmark and commercial acceptance gate bound to exact signed artifacts | + +Issue #206 (harden-runner custom detection initialization failure) was closed after its remediation landed on protected `main` between snapshots. + +The five newly separated product-completion tracks are **durable WARC/PROV replay**, **stable BAP/MCP runtime API**, **signed cross-platform Chromium distribution**, **enterprise control and experience plane**, and the **commercial acceptance gate**. They are separate issues because each has a distinct authority, data, release, and buyer-acceptance boundary. + +The hourly product-development loop is operational infrastructure, not proof that a browser product, issue, pull request, or release meets buyer acceptance. + +## Buyer-visible and technical gap matrix + +| Priority | Buyer-visible outcome | Protected-main status | Completion issue and acceptance evidence | +|---|---|---|---| +| P0 | A bounded task observes a real Chromium page, performs one typed action, verifies the post-condition, and emits provenance | **Open / Phase 1** | #28; repeated real Chromium E2E with isolated context, exact session/node authority, typed dispatch, post-condition, crash cleanup, and protected-main checks | +| P0 | Navigation consumes approved origin, resolution, route, TCP peer, TLS identity, bounded HTTP, redirect, MIME, and download policy | **Partial foundation** | #9 plus #28; real browser-network adapter proves the governed path is consumed end to end | +| P1 | Existing Chromium extensions remain compatible while Agent authority stays separate | **Partial active-PR evidence** | #27; exact supported-build/platform compatibility matrix, managed allow-list, native-host isolation, repeatability, and release binding | +| P1 | Authorized work can use necessary PII without ambient exposure | **Policy foundation; runtime open** | #10; opaque broker, exact field/purpose/destination/model policy, atomic use/revocation, retention/deletion, and value-free telemetry | +| P1 | Every released structured field is traceable to replayable source evidence | **Foundations only** | #199; durable WARC/PROV replay, integrity, retention, deletion, offline verification, extraction precision/recall, and 100% provenance completeness | +| P1 | External Agents integrate through a stable, authenticated product contract | **Partial active-PR MCP primitives** | #200; BAP 1.0, MCP 2026-07-28 adapter, idempotency, task cancellation/resume, checkpoint/reconciliation, and SDK conformance | +| P1 | Buyers can install, update, verify, and roll back a supported product | **Not shipped** | #201; signed Windows/macOS/Linux/headless artifacts, Chromium revision manifest, updater security, patch SLA, SBOM, SLSA provenance, and recovery | +| P1 | Enterprise teams can provision, approve, audit, operate, and recover the service | **Not shipped** | #202; Keyverse-compatible OIDC/SCIM, tenant isolation, policy/approval/evidence UI, SLO/incident controls, data residency, CSAP/SOC 2 evidence mapping, WCAG 2.2, Figma File ID, and Storybook | +| P0 | A release has reproducible proof of usefulness, safety, evidence completeness, and recovery | **No product-wide release gate** | #203; deterministic, compatibility, adversarial, recovery, and enterprise suites with statistical reporting and an exact-artifact commercial acceptance gate | +| P0 | Valid changes reach protected `main` without authority improvisation or unbounded stack growth | **Blocked / high integration debt** | Shrink the 158-PR queue in dependency order, provision legitimate review authority, require exact-current evidence, and close duplicates/superseded branches | + +## Commercial completion definition + +OriginWeave is not complete merely because every low-level primitive exists in some open branch. A release candidate is commercially complete only when all of the following are true for the declared support profile: + +1. #9, #10, #27, and #28 are integrated on protected `main` as a complete browser/network/action/evidence chain. +2. #199 provides replayable, retention-governed evidence for every released structured result. +3. #200 exposes a stable authenticated runtime API and task lifecycle without raw Chromium authority leakage. +4. #201 produces signed, updateable, rollback-capable release artifacts bound to Chromium, SBOM, and provenance. +5. #202 supplies tenant-safe enterprise administration, approvals, audit, SLOs, incident recovery, accessible Figma/Storybook-backed UX, and control evidence. +6. #203 accepts the exact signed artifacts through a reproducible benchmark; missing or inconclusive evidence cannot be promoted to success. +7. Production function, line, region, and branch coverage and public API documentation remain exactly complete for OriginWeave-owned code. +8. CHANGELOG, version, supported-platform matrix, security policy, runbooks, licensing, release notes, upgrade/rollback guidance, and procurement evidence match the exact release. +9. No required check, browser/platform lane, security case, benchmark case, or independent review is skipped, stale, inherited, or represented by status-only evidence. +10. The open PR queue is reduced to bounded active work rather than being the only place where the product exists. + +## Next executable queue + +1. Re-fetch all 158 open PRs and compute the dependency graph, exact heads/bases, reviews, unresolved threads, current required checks, duplicate/supersession relationships, and branch ancestry. Re-dispatch required checks that failed closed on provider infrastructure instead of code defects. +2. Integrate merge-ready root PRs first; restack and independently revalidate only the immediate children. Close obsolete alternatives instead of carrying parallel truth. +3. Finish the #9/#28 browser-network and Chromium vertical slice, including the #195/#198 WebSocket opening path and the remaining framed BiDi command/response, semantic observation, policy, action, post-condition, and recovery boundaries. +4. Finish #27 and #10 as separate security tracks; neither should be hidden inside the first browser PR. +5. Implement #199, then #200, so durable evidence and stable task authority precede broad enterprise integrations. +6. Implement #201 before making release/support claims; exact CI browser evidence must be bound to the actual signed artifact. +7. Design #202 in Figma, record the Figma File ID in the ADR, implement reusable design tokens and Storybook components, then add identity/tenant/approval/audit/operations integration. +8. Make #203 the final release gate across the exact signed distribution, not a source branch or model narrative. +9. Only after the commercial acceptance gate passes, increment the version, finalize CHANGELOG/release notes, publish signed artifacts, and verify upgrade/rollback from the prior supported release. + +## Evidence commands + +The volatile counts above are reproducible by paginating the complete open-PR inventory, flattening every page, and then inspecting each PR's exact head, checks, reviews, and review threads: + +```bash +set -euo pipefail +EVIDENCE_DIR="$(mktemp -d /tmp/originweave-evidence.XXXXXX)" +printf 'Evidence directory: %s\n' "$EVIDENCE_DIR" >&2 + +gh api --paginate --slurp 'repos/ContextualWisdomLab/OriginWeave/pulls?state=open&per_page=100' \ + > "$EVIDENCE_DIR/open-pr-pages.json" +jq '[.[][]]' "$EVIDENCE_DIR/open-pr-pages.json" \ + > "$EVIDENCE_DIR/open-prs.json" +jq '{ + open_pull_requests: length, + non_draft: (map(select(.draft == false)) | length), + draft: (map(select(.draft == true)) | length) +}' "$EVIDENCE_DIR/open-prs.json" + +gh api 'repos/ContextualWisdomLab/OriginWeave/branches/main' \ + > "$EVIDENCE_DIR/main-branch.json" +gh api --paginate --slurp \ + 'repos/ContextualWisdomLab/OriginWeave/rules/branches/main?per_page=100' \ + > "$EVIDENCE_DIR/main-branch-rule-pages.json" +jq '[.[][]]' "$EVIDENCE_DIR/main-branch-rule-pages.json" \ + > "$EVIDENCE_DIR/main-branch-rules.json" +gh api --paginate --slurp \ + 'repos/ContextualWisdomLab/OriginWeave/collaborators?affiliation=all&per_page=100' \ + > "$EVIDENCE_DIR/collaborator-pages.json" +jq '[.[][]]' "$EVIDENCE_DIR/collaborator-pages.json" \ + > "$EVIDENCE_DIR/collaborators.json" + +jq -r '.[].number' "$EVIDENCE_DIR/open-prs.json" | while read -r PR; do + STABLE_HEAD=false + for ATTEMPT in 1 2 3; do + VERDICT_PATH="$EVIDENCE_DIR/pr-${PR}-merge-verdict.json" + VERDICT_TMP="$EVIDENCE_DIR/pr-${PR}-merge-verdict.json.tmp" + rm -f "$VERDICT_PATH" "$VERDICT_TMP" "$EVIDENCE_DIR/pr-${PR}-rechecked.json" + PR_JSON="$EVIDENCE_DIR/pr-${PR}.json" + gh api "repos/ContextualWisdomLab/OriginWeave/pulls/$PR" > "$PR_JSON" + HEAD_SHA=$(jq -r '.head.sha' "$PR_JSON") + BASE_SHA=$(jq -r '.base.sha' "$PR_JSON") + + gh api --paginate --slurp \ + "repos/ContextualWisdomLab/OriginWeave/commits/$HEAD_SHA/check-runs?per_page=100" \ + > "$EVIDENCE_DIR/pr-${PR}-check-runs.json" + gh api --paginate --slurp \ + "repos/ContextualWisdomLab/OriginWeave/commits/$HEAD_SHA/statuses?per_page=100" \ + > "$EVIDENCE_DIR/pr-${PR}-statuses.json" + gh api --paginate --slurp \ + "repos/ContextualWisdomLab/OriginWeave/pulls/$PR/reviews?per_page=100" \ + > "$EVIDENCE_DIR/pr-${PR}-reviews.json" + gh api --paginate --slurp \ + "repos/ContextualWisdomLab/OriginWeave/actions/runs?head_sha=$HEAD_SHA&per_page=100" \ + > "$EVIDENCE_DIR/pr-${PR}-workflow-runs.json" + gh api graphql --paginate --slurp \ + -F owner=ContextualWisdomLab \ + -F name=OriginWeave \ + -F number="$PR" \ + -f query=' +query($owner: String!, $name: String!, $number: Int!, $endCursor: String) { + repository(owner: $owner, name: $name) { + pullRequest(number: $number) { + reviewThreads(first: 100, after: $endCursor) { + nodes { id isResolved isOutdated } + pageInfo { hasNextPage endCursor } + } + } + } +}' > "$EVIDENCE_DIR/pr-${PR}-review-threads.json" + + jq -n \ + --arg head "$HEAD_SHA" \ + --slurpfile pr "$PR_JSON" \ + --slurpfile checks "$EVIDENCE_DIR/pr-${PR}-check-runs.json" \ + --slurpfile statuses "$EVIDENCE_DIR/pr-${PR}-statuses.json" \ + --slurpfile reviews "$EVIDENCE_DIR/pr-${PR}-reviews.json" \ + --slurpfile workflow_runs "$EVIDENCE_DIR/pr-${PR}-workflow-runs.json" \ + --slurpfile rules "$EVIDENCE_DIR/main-branch-rules.json" \ + --slurpfile collaborators "$EVIDENCE_DIR/collaborators.json" \ + --slurpfile threads "$EVIDENCE_DIR/pr-${PR}-review-threads.json" \ + --arg base "$BASE_SHA" \ + '( + [ + $rules[][]? + | select(.type == "pull_request") + | .parameters + ] | first // {} + ) as $pull_request_parameters + | ( + [ + $reviews[][][]? + | {reviewer: .user.login, state, submitted_at, commit_id} + | select(.submitted_at != null) + | select(.reviewer != $pr[0].user.login) + | select(.reviewer as $reviewer | + any($collaborators[][]?; + .login == $reviewer and + (.permissions.push == true or + .permissions.maintain == true or + .permissions.admin == true))) + ] + | group_by(.reviewer) + | map(sort_by(.submitted_at) | last) + | map(select(.state == "APPROVED" and .commit_id == $head)) + ) as $current_approvals + | ($pull_request_parameters.required_approving_review_count // 0) as $required_review_count + | ($pull_request_parameters.require_last_push_approval // false) as $require_last_push_approval + | { + head_sha: $head, + base_sha: $base, + required_status_checks: { + check_runs: [$checks[][].check_runs[]?], + legacy_statuses: [$statuses[][][]?] + }, + workflow_runs: [$workflow_runs[][].workflow_runs[]?], + counted_approvals: ($current_approvals | length), + required_approving_review_count: $required_review_count, + require_last_push_approval: $require_last_push_approval, + last_push_approval_authority: ( + if $require_last_push_approval == true + then "github_rule_evaluation_required" + else "not_required" + end + ), + approval_gate_satisfied: ( + if $pull_request_parameters.require_last_push_approval == true then false + else (($current_approvals | length) >= $required_review_count) + end + ), + required_workflows: [ + $rules[][]? + | select(.type == "workflows") + | .parameters.workflows[] + ], + unresolved_threads: [ + $threads[][].data.repository.pullRequest.reviewThreads.nodes[]? + | select(.isResolved == false and .isOutdated == false) + ] + }' > "$VERDICT_TMP" + + RECHECKED_PR_JSON="$EVIDENCE_DIR/pr-${PR}-rechecked.json" + RECHECKED_HEAD_SHA=$(gh api "repos/ContextualWisdomLab/OriginWeave/pulls/$PR" \ + | tee "$RECHECKED_PR_JSON" \ + | jq -r '.head.sha') + RECHECKED_BASE_SHA=$(jq -r '.base.sha' "$RECHECKED_PR_JSON") + if [[ "$RECHECKED_HEAD_SHA" == "$HEAD_SHA" && "$RECHECKED_BASE_SHA" == "$BASE_SHA" ]]; then + mv "$VERDICT_TMP" "$VERDICT_PATH" + mv "$RECHECKED_PR_JSON" "$PR_JSON" + STABLE_HEAD=true + break + fi + rm -f "$VERDICT_TMP" "$RECHECKED_PR_JSON" + printf 'Discarding moving head/base evidence for PR #%s (head %s -> %s, base %s -> %s) and retrying.\n' \ + "$PR" "$HEAD_SHA" "$RECHECKED_HEAD_SHA" "$BASE_SHA" "$RECHECKED_BASE_SHA" >&2 + done + if [[ "$STABLE_HEAD" != true ]]; then + rm -f "$EVIDENCE_DIR"/pr-${PR}-*.json + printf 'Unable to collect stable exact-head/base evidence for PR #%s after 3 attempts.\n' "$PR" >&2 + exit 1 + fi +done +``` + +The branch-scoped rules response determines the active rules affecting `main`; each PR's exact `HEAD_SHA` then determines which check runs, legacy statuses, workflow runs, reviews, and unresolved threads are current. The saved merge verdict binds counted approvals to the latest review per eligible collaborator, excludes the PR author, and requires `APPROVED` on the exact head. It deliberately does **not** infer GitHub's actual last-push actor from commit author or committer metadata: when `require_last_push_approval` is active, this portable evidence procedure records `github_rule_evaluation_required` and keeps `approval_gate_satisfied` false until GitHub's authoritative rule evaluation is consulted. The saved PR JSON also preserves the exact base reference and branch ancestry input for the dependency graph. Evidence is retained only when both `RECHECKED_HEAD_SHA` and `RECHECKED_BASE_SHA` match the collected values; a moving head or base discards the temporary verdict, and three failed attempts leave no unstable merge verdict. + +For standards and binding architecture, use [`doctoring.md`](doctoring.md), [`doctoring/browser-agent-protocols.md`](doctoring/browser-agent-protocols.md), [`PRD.md`](PRD.md), [`TRD.md`](TRD.md), [`product-roadmap.md`](product-roadmap.md), and linked ADR/UML/ERD/traceability records. Issues #199-#203 contain their own APA 7th standards and research traceability. This baseline intentionally records delivery state and never promotes planned adapters or active pull-request code to implemented behavior. \ No newline at end of file diff --git a/docs/traceability/README.md b/docs/traceability/README.md index 29eeb03c7..e30b9eda1 100644 --- a/docs/traceability/README.md +++ b/docs/traceability/README.md @@ -71,7 +71,7 @@ ADR lifecycle is separate and remains `Proposed`, `Accepted`, `Superseded`, `Dep | WebDriver BiDi / CDP / WebMCP / MCP are adapters, not internal authority | ACCEPTED_ARCHITECTURE | PRD Section 9.8; TRD Section 12 | Protocol adapter implementation remains Planned/active under issue #28; active PR #40 may not be called shipped | | Manifest V3 compatibility is preserved upstream where practical | PARTIAL | ADR 0001; issue #27; Proposed ADR 0013 | Protected main has pinned real-Chromium compatibility evidence for service worker/content script/storage/DNR/tabs/windows/scripting/commands/side panel/bookmarks/history/restart/repeatability; active PR #43 adds real bounded downloads evidence; full issue #27 matrix remains incomplete | | Extension permission does not imply OriginWeave Agent capability | PARTIAL | protected-main extension authority kernel; Proposed ADR 0013 | Core extension-to-Agent authority isolation exists on protected main; complete managed-extension/native-messaging/enterprise release policy remains incomplete | -| WARC/PROV-oriented durable evidence adapters | PARTIAL | ADR 0003; PRD-EVD-005 | The active extraction lane adds a verified, bounded in-memory WARC 1.1 `resource` record contract; durable persistence/export adapters remain Planned | +| WARC/PROV-oriented durable evidence adapters | PLANNED | ADR 0003; PRD-EVD-005 | Source/provenance kernel foundation exists; persistence/export adapters remain Planned | | Origin Map visualizes value/action provenance | PLANNED | PRD-EVD-004; this traceability record | No shipped UI claim | | Browser / Runtime / Observe / Capture / Governor / Policy / Evidence / Protocol / SDK product surfaces | PARTIAL | PRD Section 6 | Some foundations exist under crates; named commercial surfaces are not all shipped artifacts | | Constrained GPU phase scheduling for browser rendering vs local inference | PARTIAL | PRD-RES-005; TRD Section 10 | Deterministic resource plan exists; real GPU scheduler/telemetry remains Planned | @@ -99,7 +99,7 @@ ADR lifecycle is separate and remains `Proposed`, `Accepted`, `Superseded`, `Dep | Purpose-bound sensitive-data policy/evidence | `originweave-policy` + evidence foundations; active lifecycle/reservation work #45/#46 | ADR 0007; issue #10 | PARTIAL | | Trusted sensitive-data broker/storage/lifecycle | future bounded service/crate | issue #10; PRD/TRD/data governance | PLANNED | | BiDi/CDP/WebMCP/MCP | future/versioned adapter crates; registry prerequisite active in #40 | protocol compatibility tests required | PLANNED | -| WARC/PROV persistence | persistence/export adapters | doctoring + future conformance tests | PARTIAL - active extraction lane has a bounded in-memory WARC `resource` contract; durable persistence and PROV serialization remain Planned | +| WARC/PROV persistence | persistence/export adapters | doctoring + future conformance tests | PLANNED | ## 5. Requirement-to-ADR trace diff --git a/docs/traceability/mcp-authority-route.md b/docs/traceability/mcp-authority-route.md new file mode 100644 index 000000000..ddbd5927c --- /dev/null +++ b/docs/traceability/mcp-authority-route.md @@ -0,0 +1,53 @@ +# MCP 2026-07-28 authority-route traceability + +- **Capability maturity:** `IMPLEMENTED_ON_ACTIVE_PR` +- **Owning work:** PR #168 `feat(mcp): bind stateless tool routing to typed actions` +- **Protected-main status:** non-shipped active-PR evidence +- **Complete MCP adapter status:** `PLANNED` +- **Governing decision:** ADR 0107 + +## Scope + +PR #168 implements a bounded Rust control-plane foundation for MCP `2026-07-28` `tools/call` routing. It validates the represented stateless routing envelope, bounds and syntax-validates both attacker-controlled method fields and both attacker-controlled tool-name fields before correlation, maps only an explicit reviewed `originweave.*` catalog to existing typed `ActionKind` values, derives discovery metadata from the same catalog, and rejects route/action mismatch before ordinary deterministic policy evaluation. Methods are nonempty reviewed-ASCII routing tokens of at most 64 bytes; tool names are nonempty reviewed-ASCII identifiers of at most 128 bytes. Invalid method metadata is rejected distinctly from a bounded but unsupported MCP method. + +A successful `ValidatedMcpToolCall` proves routing integrity only. It grants no capability, origin, approval, secret, browser, tenant, persistence, network, or evidence authority. `originweave_policy::evaluate_mcp` still delegates to the ordinary policy evaluator after the route/action match. + +## Product-status reconciliation + +`docs/PRD.md` PRD-INT-004 and `docs/TRD.md` Section 12 intentionally remain **Planned** at the complete-adapter level. That status is not contradicted by this active PR: the PR implements only a reusable routing/action-policy foundation below the product adapter. `README.md` and `CHANGELOG.md` therefore distinguish the active foundation from shipped protected-main capability, and ADR 0107 records the same version and authority boundary. + +The following remain outside PR #168 and must not be inferred from it: + +- Streamable HTTP transport parsing and header materialization; +- complete request `_meta` validation, including per-request client capabilities; +- `tools/list` serialization, pagination, cache semantics, and subscription handling; +- OAuth and authenticated MCP deployment policy; +- browser-control I/O or BiDi/CDP/WebMCP translation; +- secret materialization or broker transport; +- persistence, durable audit storage, or WARC/PROV export; and +- an OriginWeave Protocol version transition. + +## Version boundary + +The active routing foundation accepts only protocol generation `2026-07-28`. MCP versioning is independent of the OriginWeave Protocol. A later MCP revision does not silently change OriginWeave action, risk, capability, approval, secret, origin, tenant, browser, or evidence semantics. + +The reviewed primary source is: + +Model Context Protocol. (2026, July 28). *Specification: 2026-07-28*. https://modelcontextprotocol.io/specification/2026-07-28 + +The canonical bibliography remains `docs/doctoring.md`. + +## Executable evidence + +Current PR #168 production/test surfaces include: + +- `crates/originweave-core/src/mcp.rs` — bounded deterministic catalog plus method/tool routing validation in the `ValidatedMcpToolCall` primitive; +- `crates/originweave-core/tests/mcp_authority_route.rs` — mapping, exact method/tool bounds, empty/oversized/malformed inputs, version/method/header-body correlation, and error-contract evidence; +- `crates/originweave-policy/src/lib.rs` — `evaluate_mcp` route/action guard before normal policy evaluation; and +- `crates/originweave-policy/tests/mcp_route_binding.rs` — confused-deputy and policy-preservation evidence. + +Exact current-head CI/security/review evidence must be regenerated after every branch mutation. Predecessor-head success is historical only. + +## Promotion rule + +This dossier may change to `IMPLEMENTED_ON_PROTECTED_MAIN` for the bounded routing foundation only after PR #168 reaches protected `main` under live governance and exact-head acceptance. That promotion still does **not** promote the complete MCP adapter from `PLANNED`; each remaining transport/runtime boundary requires its own integrated evidence. diff --git a/tests/test_doctoring_reference_contract.py b/tests/test_doctoring_reference_contract.py new file mode 100644 index 000000000..bdeded44f --- /dev/null +++ b/tests/test_doctoring_reference_contract.py @@ -0,0 +1,28 @@ +"""Regression contracts for standards references that bind OriginWeave design claims.""" + +from __future__ import annotations + +import pathlib +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +DOCTORING = ROOT / "docs" / "doctoring.md" + + +class DoctoringReferenceContractTests(unittest.TestCase): + """Keep cited primary-standard authorship aligned with the canonical source.""" + + def test_rfc_5280_reference_uses_canonical_author_initials(self) -> None: + """RFC 5280 must credit Sharon Boeyen as S. Boeyen, matching RFC Editor metadata.""" + text = DOCTORING.read_text(encoding="utf-8") + expected = ( + "Cooper, D., Santesson, S., Farrell, S., Boeyen, S., Housley, R., & Polk, W. " + "(2008). *Internet X.509 public key infrastructure certificate and certificate " + "revocation list (CRL) profile* (RFC 5280). Internet Engineering Task Force. " + "https://doi.org/10.17487/RFC5280" + ) + self.assertIn(expected, text) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_documentation_active_pr_evidence_contract.py b/tests/test_documentation_active_pr_evidence_contract.py index d2a067e50..34e8a0238 100644 --- a/tests/test_documentation_active_pr_evidence_contract.py +++ b/tests/test_documentation_active_pr_evidence_contract.py @@ -8,6 +8,8 @@ DOCS = ROOT / "docs" FITNESS = DOCS / "DOCUMENTATION_FITNESS.md" MATURITY = DOCS / "evidence" / "2026-08-10-active-pr-maturity.md" +BASELINE = DOCS / "product-technical-gap-baseline.md" +CHANGELOG = ROOT / "CHANGELOG.md" def active_pr_row(text: str, pr_number: int) -> str: @@ -28,6 +30,31 @@ class ActivePullRequestDocumentationContractTests(unittest.TestCase): def setUpClass(cls) -> None: cls.fitness = FITNESS.read_text(encoding="utf-8") cls.maturity = MATURITY.read_text(encoding="utf-8") + cls.baseline = BASELINE.read_text(encoding="utf-8") + cls.changelog = CHANGELOG.read_text(encoding="utf-8") + + def test_latest_live_pr_snapshot_is_recorded_in_the_product_baseline(self) -> None: + """The baseline must preserve exact heads for the newest active product slices.""" + for marker in ( + "Current exact-head active PR evidence", + "| #73 | Draft | `da99395b09b419845b4a1222a0725482e9231466` | `7861d88d21ed0f0adaeb467957e809826f835071` |", + "| #208 | Draft | `0841d2ab3d8b5e60a03c0a8e818cf438e2716829` | `c3b6e1a475dce333f6115e5113cae9c07974835f` |", + "| #209 | Draft | `0841d2ab3d8b5e60a03c0a8e818cf438e2716829` | `69bc738bd45a1b61a4673b122dc3eec8814baa22` |", + "| #210 | Draft | `69bc738bd45a1b61a4673b122dc3eec8814baa22` | `999979a511c3a890ba93a1a09da8810858877940` |", + "| #211 | Draft | `c3b6e1a475dce333f6115e5113cae9c07974835f` | `f6e3a3adcfb9cc7a60ef1d79e2aeee27ba54c084` |", + ): + with self.subTest(marker=marker): + self.assertIn(marker, self.baseline) + + def test_baseline_refresh_changelog_matches_the_live_snapshot(self) -> None: + """The changelog must classify and state the same baseline refresh.""" + refresh = "Refreshed the product and technical gap baseline with the current open-PR inventory" + added = self.changelog.split("### Added", 1)[1].split("### Changed", 1)[0] + changed = self.changelog.split("### Changed", 1)[1].split("### Security", 1)[0] + self.assertIn(refresh, added) + self.assertNotIn(refresh, changed) + self.assertIn("150 open pull requests, 110 drafts", self.changelog) + self.assertNotIn("150 open pull requests, 112 drafts", self.changelog) def test_dependency_stacks_are_explicit_and_non_shipped(self) -> None: """Current browser, network, sensitive and compatibility stacks stay active-only.""" diff --git a/tests/test_product_completion_gap_contract.py b/tests/test_product_completion_gap_contract.py new file mode 100644 index 000000000..839393f30 --- /dev/null +++ b/tests/test_product_completion_gap_contract.py @@ -0,0 +1,111 @@ +"""Regression contract for the dated commercial-completion gap baseline.""" + +from __future__ import annotations + +import pathlib +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +BASELINE = ROOT / "docs/product-technical-gap-baseline.md" + + +class ProductCompletionGapContractTests(unittest.TestCase): + """Keep the exact repository snapshot and completion tracks reviewable.""" + + def test_baseline_records_current_inventory_and_completion_issues(self) -> None: + """The dated baseline must not retain superseded queue counts or omit buyer tracks.""" + text = BASELINE.read_text(encoding="utf-8") + + for phrase in ( + "158 open pull requests", + "44 non-draft", + "114 draft", + "#198", + "#199", + "#200", + "#201", + "#202", + "#203", + "durable WARC/PROV replay", + "stable BAP/MCP runtime API", + "signed cross-platform Chromium distribution", + "enterprise control and experience plane", + "commercial acceptance gate", + ): + with self.subTest(phrase=phrase): + self.assertIn(phrase, text) + + for stale_phrase in ( + "100 open pull requests", + "22 non-draft", + "78 draft", + "148 open pull requests", + "79 draft PRs", + "150 open pull requests", + "40 non-draft", + "110 draft", + ): + with self.subTest(stale_phrase=stale_phrase): + self.assertNotIn(stale_phrase, text) + + def test_evidence_commands_reproduce_inventory_checks_and_review_state(self) -> None: + """The evidence procedure must paginate the queue and inspect each exact PR head.""" + text = BASELINE.read_text(encoding="utf-8") + evidence = text.split("## Evidence commands", 1)[1].split("\n## ", 1)[0] + shell = evidence.split("```bash", 1)[1].split("```", 1)[0] + + for phrase in ( + "--paginate --slurp 'repos/ContextualWisdomLab/OriginWeave/pulls?state=open&per_page=100'", + "set -euo pipefail", + 'EVIDENCE_DIR="$(mktemp -d /tmp/originweave-evidence.XXXXXX)"', + '"$EVIDENCE_DIR/open-pr-pages.json"', + "jq '[.[][]]' \"$EVIDENCE_DIR/open-pr-pages.json\"", + '"repos/ContextualWisdomLab/OriginWeave/pulls/$PR"', + '"repos/ContextualWisdomLab/OriginWeave/commits/$HEAD_SHA/check-runs?per_page=100"', + '"repos/ContextualWisdomLab/OriginWeave/commits/$HEAD_SHA/statuses?per_page=100"', + '"repos/ContextualWisdomLab/OriginWeave/pulls/$PR/reviews?per_page=100"', + '"repos/ContextualWisdomLab/OriginWeave/actions/runs?head_sha=$HEAD_SHA&per_page=100"', + "check_runs: [$checks[][].check_runs[]?],", + "legacy_statuses: [$statuses[][][]?]", + "workflow_runs: [$workflow_runs[][].workflow_runs[]?],", + "reviewThreads(first: 100, after: $endCursor)", + "rules/branches/main?per_page=100", + '"$EVIDENCE_DIR/main-branch-rule-pages.json"', + '"$EVIDENCE_DIR/collaborator-pages.json"', + '"$EVIDENCE_DIR/collaborators.json"', + '"$EVIDENCE_DIR/pr-${PR}-merge-verdict.json.tmp"', + '.state == "APPROVED"', + ".submitted_at != null", + ".commit_id == $head", + "group_by(.reviewer)", + "required_approving_review_count", + "require_last_push_approval", + "last_push_approval_authority", + '"github_rule_evaluation_required"', + "if $pull_request_parameters.require_last_push_approval == true then false", + "$pr[0].user.login", + '.type == "workflows"', + ".parameters.workflows", + "required_status_checks", + '"$EVIDENCE_DIR/pr-${PR}-merge-verdict.json"', + "for ATTEMPT in 1 2 3; do", + "RECHECKED_HEAD_SHA=", + "RECHECKED_BASE_SHA=", + 'if [[ "$RECHECKED_HEAD_SHA" == "$HEAD_SHA" && "$RECHECKED_BASE_SHA" == "$BASE_SHA" ]]; then', + ): + with self.subTest(phrase=phrase): + self.assertIn(phrase, shell) + + self.assertNotIn("while :; do", shell) + self.assertNotIn("/tmp/originweave-open-pr", shell) + self.assertNotIn("check_runs: [$checks[]?.check_runs[]?],", shell) + self.assertNotIn("legacy_statuses: [$statuses[][]?]", shell) + self.assertNotIn("workflow_runs: [$workflow_runs[]?.workflow_runs[]?],", shell) + self.assertNotIn("$reviews[][]?\n | select(.state", shell) + self.assertNotIn("head-commit.json", shell) + self.assertNotIn("$head_commit[0].committer.login", shell) + self.assertNotIn("$head_commit[0].author.login", shell) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_product_documentation_contract.py b/tests/test_product_documentation_contract.py index 1313189ea..5a1c1133c 100644 --- a/tests/test_product_documentation_contract.py +++ b/tests/test_product_documentation_contract.py @@ -11,6 +11,14 @@ class ProductDocumentationContractTests(unittest.TestCase): """Keep product requirements, technical design, diagrams, and traceability discoverable.""" + @staticmethod + def _subsection(text: str, heading: str) -> str: + """Return one fourth-level documentation subsection.""" + start = text.index(heading) + len(heading) + remainder = text[start:] + end = remainder.find("\n#### ") + return remainder if end == -1 else remainder[:end] + def test_authoritative_product_documentation_graph_exists(self) -> None: """Major product decisions must not require reconstructing chat or PR history.""" required_paths = { @@ -25,10 +33,50 @@ def test_authoritative_product_documentation_graph_exists(self) -> None: "docs/OPERABILITY.md", "docs/API_CONTRACT.md", "docs/RELEASE_AND_ROLLBACK.md", + "docs/product-technical-gap-baseline.md", } missing = sorted(path for path in required_paths if not (ROOT / path).is_file()) self.assertEqual(missing, []) + def test_product_technical_gap_baseline_records_live_delivery_state(self) -> None: + """Buyers and maintainers must see implementation gaps and current delivery blockers together.""" + baseline = ROOT / "docs/product-technical-gap-baseline.md" + self.assertTrue(baseline.is_file()) + text = baseline.read_text(encoding="utf-8") + for phrase in ( + "Observed snapshot: 2026-08-24", + "Protected-main truth", + "Open pull requests", + "Open issues", + "#195", + "#149", + "reviewer-provisioning gap", + "Phase 1", + ): + with self.subTest(phrase=phrase): + self.assertIn(phrase, text) + + protected_main = text.split("### Open pull requests", 1)[0] + open_pull_requests = text.split("### Open pull requests", 1)[1].split( + "### Review and merge authority", 1 + )[0] + self.assertIn("Phase 1 is **in progress**, not shipped.", protected_main) + self.assertIn( + "It remains draft evidence and cannot be treated as shipped behavior.", + open_pull_requests, + ) + bidi_status = self._subsection( + open_pull_requests, "#### #195/#198 WebDriver BiDi opening path status" + ) + vpn_status = self._subsection( + open_pull_requests, "#### #149 VPN/profile intent status" + ) + self.assertIn("Phase 1 is **in progress**, not shipped.", bidi_status) + self.assertIn( + "It remains draft evidence and cannot be treated as shipped behavior.", + vpn_status, + ) + 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") @@ -39,6 +87,7 @@ def test_root_architecture_links_the_authoritative_product_graph(self) -> None: "docs/uml/README.md", "docs/erd/README.md", "docs/traceability/README.md", + "docs/product-technical-gap-baseline.md", ): with self.subTest(link=link): self.assertIn(link, architecture) diff --git a/tests/test_rust_toolchain_contract.py b/tests/test_rust_toolchain_contract.py new file mode 100644 index 000000000..058add241 --- /dev/null +++ b/tests/test_rust_toolchain_contract.py @@ -0,0 +1,53 @@ +"""Regression contracts for the reproducible Rust compiler baseline.""" + +from __future__ import annotations + +import tomllib +import unittest +from pathlib import Path + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +RUST_TOOLCHAIN = REPOSITORY_ROOT / "rust-toolchain.toml" +CI_WORKFLOW = REPOSITORY_ROOT / ".github" / "workflows" / "ci.yml" +HOURLY_WORKFLOW = REPOSITORY_ROOT / ".github" / "workflows" / "hourly-product-development.yml" +REFRESH_WORKFLOW = REPOSITORY_ROOT / ".github" / "workflows" / "apply-rust-nightly-refresh.yml" +DEPENDABOT = REPOSITORY_ROOT / ".github" / "dependabot.yml" + + +class RustToolchainContractTests(unittest.TestCase): + """Keep stable builds reproducible and branch coverage intentionally fresh.""" + + def test_stable_toolchain_is_exact_and_automatically_tracked(self) -> None: + """The stable compiler changes only through a reviewable manifest update.""" + + manifest = tomllib.loads(RUST_TOOLCHAIN.read_text(encoding="utf-8")) + self.assertEqual(manifest["toolchain"]["channel"], "1.97.1") + + dependabot = DEPENDABOT.read_text(encoding="utf-8") + self.assertIn('package-ecosystem: "rust-toolchain"', dependabot) + self.assertIn('directory: "/"', dependabot) + self.assertIn('interval: "weekly"', dependabot) + + def test_branch_coverage_uses_one_current_date_pinned_nightly(self) -> None: + """Every branch-coverage command uses the same reviewed nightly snapshot.""" + + workflow = CI_WORKFLOW.read_text(encoding="utf-8") + self.assertEqual(workflow.count("nightly-2026-08-18"), 3) + self.assertNotIn("nightly-2026-08-01", workflow) + + hourly_workflow = HOURLY_WORKFLOW.read_text(encoding="utf-8") + self.assertEqual(hourly_workflow.count("nightly-2026-08-18"), 2) + self.assertNotIn("nightly-2026-08-01", hourly_workflow) + + def test_nightly_refresh_accepts_only_old_or_already_refreshed_source(self) -> None: + """The one-shot materializer remains valid after the source is refreshed.""" + workflow = REFRESH_WORKFLOW.read_text(encoding="utf-8") + self.assertIn("old_count = source.count(old)", workflow) + self.assertIn("new_count = source.count(new)", workflow) + self.assertIn("if old_count == 2 and new_count == 0:", workflow) + self.assertIn("elif old_count == 0 and new_count == 2:", workflow) + + +if __name__ == "__main__": # pragma: no cover + unittest.main() From 7851b8b53cf8b5b0a9d6c5064a94cab4414f43e7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 02:36:25 -0700 Subject: [PATCH 60/62] feat(evidence): restore WARC dependency and ADR bindings --- Cargo.lock | 1 + crates/originweave-evidence/Cargo.toml | 1 + docs/adr/0106-provenance-evidence-model.md | 8 ++++++++ 3 files changed, 10 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index e2ada3c4e..805549630 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -279,6 +279,7 @@ name = "originweave-evidence" version = "0.1.0" dependencies = [ "originweave-core", + "sha2", ] [[package]] diff --git a/crates/originweave-evidence/Cargo.toml b/crates/originweave-evidence/Cargo.toml index a69386c38..35c21a7fb 100644 --- a/crates/originweave-evidence/Cargo.toml +++ b/crates/originweave-evidence/Cargo.toml @@ -12,6 +12,7 @@ publish = false [dependencies] originweave-core = { path = "../originweave-core" } +sha2 = "=0.10.9" [lints] workspace = true diff --git a/docs/adr/0106-provenance-evidence-model.md b/docs/adr/0106-provenance-evidence-model.md index 09cb0d7ca..d2494ad29 100644 --- a/docs/adr/0106-provenance-evidence-model.md +++ b/docs/adr/0106-provenance-evidence-model.md @@ -9,6 +9,14 @@ OriginWeave must not merely act; it must let a user, operator, auditor, or downstream system establish what was observed, authorized, executed, and verified. Browser logs alone are not sufficient because they collapse observation, policy, action, network identity, approvals, and post-conditions. At the same time, evidence can itself contain sensitive or attacker-controlled content. The product needs a durable model compatible with web-archive and provenance concepts without claiming that every conceptual record is already persisted. +## Active implementation boundary + +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. + ## Decision drivers - `Browse. Act. Prove.` requires evidence as a first-class product output. From 760d5c30ccdd64b5fe07384dfe877ead4b5290d8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 02:38:51 -0700 Subject: [PATCH 61/62] feat(evidence): expose current-stack WARC resource contract --- crates/originweave-evidence/src/lib.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/originweave-evidence/src/lib.rs b/crates/originweave-evidence/src/lib.rs index 8474b3618..d57dc524f 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_resource_record; pub use extraction_schema::{ ExtractionCardinality, ExtractionField, ExtractionNormalizationRule, ExtractionSchema, @@ -20,6 +21,11 @@ pub use sensitive_access::{ SensitiveAccessEvidence, SensitiveAccessEvidenceInput, SensitiveAccessOutcome, SensitiveEvidenceError, }; +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, + WarcTruncationReason, +}; use std::collections::BTreeMap; From efb5dfae50e2a32e74ac92cf76bdcf252dece98c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 02:42:00 -0700 Subject: [PATCH 62/62] test(evidence): adapt WARC URI regressions to stricter provenance --- .../tests/warc_target_uri_presentation.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/originweave-evidence/tests/warc_target_uri_presentation.rs b/crates/originweave-evidence/tests/warc_target_uri_presentation.rs index b31452482..12dfc0c36 100644 --- a/crates/originweave-evidence/tests/warc_target_uri_presentation.rs +++ b/crates/originweave-evidence/tests/warc_target_uri_presentation.rs @@ -22,12 +22,12 @@ fn provenance(source_url: &str) -> ProvenanceRecord { #[test] fn warc_target_uri_rejects_invisible_formatting_characters_before_serialization() { + let source_provenance = provenance("https://example.com/valid"); for formatting_character in [ '\u{00ad}', '\u{061c}', '\u{200b}', '\u{200e}', '\u{202e}', '\u{2066}', '\u{2060}', '\u{feff}', ] { let target_uri = format!("https://example.com/item{formatting_character}shadow"); - let source_provenance = provenance(&target_uri); assert_eq!( WarcResourceRecord::new( @@ -36,7 +36,7 @@ fn warc_target_uri_rejects_invisible_formatting_characters_before_serialization( &target_uri, "text/plain", Vec::new(), - source_provenance, + source_provenance.clone(), ), Err(WarcResourceRecordError::InvalidTargetUri), "target_uri={target_uri:?}" @@ -77,7 +77,7 @@ fn warc_target_uri_rejects_raw_unicode_because_warc_uses_rfc3986_uri_syntax() { target_uri, "text/plain", Vec::new(), - provenance(target_uri), + provenance("https://example.com/valid"), ), Err(WarcResourceRecordError::InvalidTargetUri), ); @@ -85,6 +85,7 @@ fn warc_target_uri_rejects_raw_unicode_because_warc_uses_rfc3986_uri_syntax() { #[test] fn warc_target_uri_rejects_ascii_characters_outside_rfc3986_uri_syntax() { + let source_provenance = provenance("https://example.com/valid"); for invalid_character in ['<', '>', '"', '{', '}', '|', '^', '`'] { let target_uri = format!("https://example.com/item{invalid_character}shadow"); @@ -95,7 +96,7 @@ fn warc_target_uri_rejects_ascii_characters_outside_rfc3986_uri_syntax() { &target_uri, "text/plain", Vec::new(), - provenance(&target_uri), + source_provenance.clone(), ), Err(WarcResourceRecordError::InvalidTargetUri), "target_uri={target_uri:?}" @@ -105,6 +106,7 @@ fn warc_target_uri_rejects_ascii_characters_outside_rfc3986_uri_syntax() { #[test] fn warc_target_uri_rejects_general_delimiters_in_path_segments() { + let source_provenance = provenance("https://example.com/valid"); for target_uri in [ "https://example.com/[segment]", "https://example.com/item]shadow", @@ -116,7 +118,7 @@ fn warc_target_uri_rejects_general_delimiters_in_path_segments() { target_uri, "text/plain", Vec::new(), - provenance(target_uri), + source_provenance.clone(), ), Err(WarcResourceRecordError::InvalidTargetUri), "target_uri={target_uri:?}"