From 3247c31bcd47498533faf87c35798de5c4c54052 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 28 Sep 2026 16:28:51 +0900 Subject: [PATCH 1/2] fix: give public boundary errors a stable standard contract Origin, action-intent digest, extension identifier, and evidence failures now implement Display and std::error::Error. Each typed variant keeps one stable message, and the error carries no source that could widen the failure into another authority. --- CHANGELOG.md | 1 + crates/originweave-core/src/lib.rs | 36 +++++++++++++ .../tests/action_intent_error_contract.rs | 15 ++++++ .../tests/extension_id_error_contract.rs | 15 ++++++ .../tests/origin_error_contract.rs | 51 +++++++++++++++++++ crates/originweave-evidence/src/lib.rs | 15 ++++++ .../tests/evidence_error_contract.rs | 25 +++++++++ 7 files changed, 158 insertions(+) create mode 100644 crates/originweave-core/tests/action_intent_error_contract.rs create mode 100644 crates/originweave-core/tests/extension_id_error_contract.rs create mode 100644 crates/originweave-core/tests/origin_error_contract.rs create mode 100644 crates/originweave-evidence/tests/evidence_error_contract.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index f747adeae..8bcb0d8e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Refreshed the product-gap queue to 126 open pull requests (54 ready, 72 draft) after #190, #188, #185, #192, #182, #184, #115, #181, #116, #117, #118, #183, #114, #127, #112, #109, #186, #110, #108, #111, #174, and #113 were merged into their immediate stacked prerequisites. PRs #147, #146, #145, #144, #143, #142, #141, #139, #136, #132, #129, and #128 moved to ready after exact-head checks and thread review; these are queue-consolidation results, not protected-main shipment. ### Added +- Added standard `Display` and `std::error::Error` contracts for core origin, action-intent digest, extension-identifier, and evidence failures, so every public error carries a stable message and can cross a transport boundary without losing its typed variant. - Corrected the 2026-08-26 product-gap snapshot with current #229 presentation-identity evidence, stacked-only #205 integration evidence, current base/head pairs, the 126-PR queue count, explicit root-versus-child merge ordering, and the active GitHub counted-approval gate. - Refreshed the product and technical gap baseline onto the 2026-08-26 live inventory: 126 open pull requests (54 ready, 72 draft), protected-main promotion of #168/#194/#196/#216/#151, a verified maintenance-loop record (supersession closure of #153, conflict reconciliations on #37/#149/#152/#173/#175, issue #212 option-(b) authorization on #43, Strix vuln-0001 homoglyph remediation on #124), provider-rerun outcome evidence, an organization review-pipeline congestion record, and refreshed merge-order queue guidance. Documentation evidence contracts were aligned to the same snapshot so the baseline, its dated markers, and the pinned exact-head rows cannot silently diverge. diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index e33a7e7e5..a57699b03 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -237,6 +237,24 @@ pub enum OriginError { InvalidPort, } +impl fmt::Display for OriginError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(match self { + Self::MissingScheme => "origin must include a scheme", + Self::UnsupportedScheme => "origin scheme must be HTTPS or loopback HTTP", + Self::InsecureRemoteOrigin => "HTTP origins are restricted to loopback hosts", + Self::MissingAuthority => "origin authority is required", + Self::UserInfoNotAllowed => "origin user information is not allowed", + Self::PathNotAllowed => "origin must not include a path, query, or fragment", + Self::InvalidAuthority => "origin authority is invalid", + Self::AmbiguousNumericHost => "origin host uses an ambiguous numeric spelling", + Self::InvalidPort => "origin port must be within 1..=65535", + }) + } +} + +impl std::error::Error for OriginError {} + /// A nonzero identity for one active browser automation session. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct BrowserSessionId(u64); @@ -503,6 +521,16 @@ pub enum ActionIntentDigestError { InvalidFormat, } +impl fmt::Display for ActionIntentDigestError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str( + "action intent digest must be sha256: followed by 64 lowercase hexadecimal digits", + ) + } +} + +impl std::error::Error for ActionIntentDigestError {} + /// The browser execution mode that owns an action. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum SessionMode { @@ -955,6 +983,14 @@ pub enum ExtensionIdError { InvalidExtensionId, } +impl fmt::Display for ExtensionIdError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("extension identifier must be 32 lowercase characters from a through p") + } +} + +impl std::error::Error for ExtensionIdError {} + /// An OriginWeave Agent capability that a browser extension may request explicitly. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum ExtensionAgentCapability { diff --git a/crates/originweave-core/tests/action_intent_error_contract.rs b/crates/originweave-core/tests/action_intent_error_contract.rs new file mode 100644 index 000000000..ca3d13f48 --- /dev/null +++ b/crates/originweave-core/tests/action_intent_error_contract.rs @@ -0,0 +1,15 @@ +use std::error::Error as _; + +use originweave_core::ActionIntentDigestError; + +#[test] +fn action_intent_digest_errors_have_a_stable_standard_error_contract() { + let error = ActionIntentDigestError::InvalidFormat; + assert_eq!( + error.to_string(), + "action intent digest must be sha256: followed by 64 lowercase hexadecimal digits" + ); + assert!(error.source().is_none()); + fn assert_standard_error_contract() {} + assert_standard_error_contract::(); +} diff --git a/crates/originweave-core/tests/extension_id_error_contract.rs b/crates/originweave-core/tests/extension_id_error_contract.rs new file mode 100644 index 000000000..90ce4414d --- /dev/null +++ b/crates/originweave-core/tests/extension_id_error_contract.rs @@ -0,0 +1,15 @@ +use std::error::Error as _; + +use originweave_core::ExtensionIdError; + +#[test] +fn extension_id_errors_have_a_stable_standard_error_contract() { + let error = ExtensionIdError::InvalidExtensionId; + assert_eq!( + error.to_string(), + "extension identifier must be 32 lowercase characters from a through p" + ); + assert!(error.source().is_none()); + fn assert_standard_error_contract() {} + assert_standard_error_contract::(); +} diff --git a/crates/originweave-core/tests/origin_error_contract.rs b/crates/originweave-core/tests/origin_error_contract.rs new file mode 100644 index 000000000..71ee58926 --- /dev/null +++ b/crates/originweave-core/tests/origin_error_contract.rs @@ -0,0 +1,51 @@ +use std::error::Error as _; + +use originweave_core::OriginError; + +#[test] +fn origin_errors_have_a_stable_standard_error_contract() { + let cases = [ + (OriginError::MissingScheme, "origin must include a scheme"), + ( + OriginError::UnsupportedScheme, + "origin scheme must be HTTPS or loopback HTTP", + ), + ( + OriginError::InsecureRemoteOrigin, + "HTTP origins are restricted to loopback hosts", + ), + ( + OriginError::MissingAuthority, + "origin authority is required", + ), + ( + OriginError::UserInfoNotAllowed, + "origin user information is not allowed", + ), + ( + OriginError::PathNotAllowed, + "origin must not include a path, query, or fragment", + ), + (OriginError::InvalidAuthority, "origin authority is invalid"), + ( + OriginError::AmbiguousNumericHost, + "origin host uses an ambiguous numeric spelling", + ), + ( + OriginError::InvalidPort, + "origin port must be within 1..=65535", + ), + ]; + + for (error, expected) in cases { + assert_eq!(error.to_string(), expected); + assert!(error.source().is_none()); + } +} + +fn assert_standard_error_contract() {} + +#[test] +fn origin_error_is_send_sync_and_standard_error_compatible() { + assert_standard_error_contract::(); +} diff --git a/crates/originweave-evidence/src/lib.rs b/crates/originweave-evidence/src/lib.rs index 17c97bec8..e5de11e16 100644 --- a/crates/originweave-evidence/src/lib.rs +++ b/crates/originweave-evidence/src/lib.rs @@ -104,6 +104,21 @@ pub enum EvidenceError { InvalidSourceUrl, } +impl std::fmt::Display for EvidenceError { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let message = match self { + Self::InvalidPath => "evidence path is invalid", + Self::LimitExceeded => "evidence limit exceeded", + Self::EmptyLocator => "evidence locator is empty", + Self::InvalidHash => "evidence hash is invalid", + Self::InvalidSourceUrl => "evidence source URL is invalid", + }; + formatter.write_str(message) + } +} + +impl std::error::Error for EvidenceError {} + /// A redacted network observation suitable for audit logging. #[derive(Debug, Clone, PartialEq, Eq)] pub struct NetworkEvidence { diff --git a/crates/originweave-evidence/tests/evidence_error_contract.rs b/crates/originweave-evidence/tests/evidence_error_contract.rs new file mode 100644 index 000000000..2d7994639 --- /dev/null +++ b/crates/originweave-evidence/tests/evidence_error_contract.rs @@ -0,0 +1,25 @@ +use std::error::Error as _; + +use originweave_evidence::EvidenceError; + +#[test] +fn evidence_errors_have_a_stable_standard_error_contract() { + let cases = [ + (EvidenceError::InvalidPath, "evidence path is invalid"), + (EvidenceError::LimitExceeded, "evidence limit exceeded"), + (EvidenceError::EmptyLocator, "evidence locator is empty"), + (EvidenceError::InvalidHash, "evidence hash is invalid"), + ( + EvidenceError::InvalidSourceUrl, + "evidence source URL is invalid", + ), + ]; + + for (error, expected) in cases { + assert_eq!(error.to_string(), expected); + assert!(error.source().is_none()); + } + + fn assert_standard_error_contract() {} + assert_standard_error_contract::(); +} From b14493be78fd020ce23f28aebcb1a123dabdaec1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 5 Oct 2026 06:27:01 +0900 Subject: [PATCH 2/2] test(evidence): preserve numeric-only provenance source hashes --- CHANGELOG.md | 1 + crates/originweave-evidence/tests/evidence.rs | 13 +++++++++++++ docs/TEST_STRATEGY.md | 2 ++ 3 files changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bcb0d8e7..c4c37b0a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Refreshed the product-gap queue to 126 open pull requests (54 ready, 72 draft) after #190, #188, #185, #192, #182, #184, #115, #181, #116, #117, #118, #183, #114, #127, #112, #109, #186, #110, #108, #111, #174, and #113 were merged into their immediate stacked prerequisites. PRs #147, #146, #145, #144, #143, #142, #141, #139, #136, #132, #129, and #128 moved to ready after exact-head checks and thread review; these are queue-consolidation results, not protected-main shipment. ### Added +- Added a test-only provenance characterization for numeric-only SHA-256 acceptance and exact source-hash retention. Existing production behavior is unchanged; the initially passing case closes a regression-oracle gap rather than repairing a product defect. - Added standard `Display` and `std::error::Error` contracts for core origin, action-intent digest, extension-identifier, and evidence failures, so every public error carries a stable message and can cross a transport boundary without losing its typed variant. - Corrected the 2026-08-26 product-gap snapshot with current #229 presentation-identity evidence, stacked-only #205 integration evidence, current base/head pairs, the 126-PR queue count, explicit root-versus-child merge ordering, and the active GitHub counted-approval gate. - Refreshed the product and technical gap baseline onto the 2026-08-26 live inventory: 126 open pull requests (54 ready, 72 draft), protected-main promotion of #168/#194/#196/#216/#151, a verified maintenance-loop record (supersession closure of #153, conflict reconciliations on #37/#149/#152/#173/#175, issue #212 option-(b) authorization on #43, Strix vuln-0001 homoglyph remediation on #124), provider-rerun outcome evidence, an organization review-pipeline congestion record, and refreshed merge-order queue guidance. Documentation evidence contracts were aligned to the same snapshot so the baseline, its dated markers, and the pinned exact-head rows cannot silently diverge. diff --git a/crates/originweave-evidence/tests/evidence.rs b/crates/originweave-evidence/tests/evidence.rs index 48d49cbc4..0a34ebeef 100644 --- a/crates/originweave-evidence/tests/evidence.rs +++ b/crates/originweave-evidence/tests/evidence.rs @@ -147,6 +147,8 @@ fn provenance_rejects_credential_bearing_or_ambiguous_source_urls() { } } +/// Validate source URLs, nonempty locators and lowercase SHA-256 identifiers, +/// preserving mixed hexadecimal and numeric-only source hashes exactly. #[test] fn provenance_requires_locator_and_sha256_evidence() { let record = ProvenanceRecord::new( @@ -163,6 +165,17 @@ fn provenance_requires_locator_and_sha256_evidence() { assert_eq!(record.source_kind(), EvidenceSourceKind::NetworkResponse); assert_eq!(record.verification_result(), VerificationResult::Verified); + let numeric_hash = "sha256:0123456789012345678901234567890123456789012345678901234567890123"; + let numeric_record = ProvenanceRecord::new( + "https://example.com", + "body", + numeric_hash, + EvidenceSourceKind::NetworkResponse, + VerificationResult::Verified, + ) + .expect("numeric lowercase hexadecimal digest is valid"); + assert_eq!(numeric_record.source_hash(), numeric_hash); + assert_eq!( ProvenanceRecord::new( "https://example.com", diff --git a/docs/TEST_STRATEGY.md b/docs/TEST_STRATEGY.md index ba3c31624..b88f94271 100644 --- a/docs/TEST_STRATEGY.md +++ b/docs/TEST_STRATEGY.md @@ -40,6 +40,8 @@ Use deterministic unit/property tests for: - resource budgets and cumulative mitigation decisions; - evidence locator/digest/value-redaction rules. +The provenance constructor characterization accepts the exact numeric-only identifier `sha256:0123456789012345678901234567890123456789012345678901234567890123` and asserts unchanged `source_hash()` retention, alongside the existing mixed hexadecimal positive and malformed URL, locator and hash negatives. This is a first-GREEN characterization of existing behavior, not a production defect repair. Isolated private mutations requiring at least one `a`–`f` byte or truncating only numeric-only stored hashes establish regression sensitivity; neither mutation is delivered. + ### 3.2 Stateful boundary tests Use deterministic state-transition and concurrency tests for: