diff --git a/CHANGELOG.md b/CHANGELOG.md index f98e29765..63d231262 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ All notable changes to OriginWeave are documented in this file. The format follo - 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. - Versioned browser-protocol adapter metadata that distinguishes WebDriver BiDi from pinned CDP, binds bounded adapter/browser revision tokens to an explicit duplicate-free capability set, normalizes capability-set identity independently of caller ordering, and exposes typed fail-closed capability requirements without granting browser, action, network, or secret authority by protocol kind alone. +- Canonical OriginWeave protocol-version parsing for exact `originweave/.` syntax, with typed fail-closed rejection of malformed, ambiguous, overflowed, or noncanonical serialized generations; parsing does not negotiate compatibility or grant adapter authority. +- Public `require_runtime_revisions` validation that fails closed when caller-supplied runtime protocol or browser revision evidence is malformed or differs from the descriptor's pinned revisions, preserving typed malformed-versus-drift errors without authenticating or attesting the adapter process. - 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. - Deterministic fail-closed policy evaluation for untrusted instructions, origin grants, crawler restrictions, execution-mode and purpose consistency, approvals, and brokered secrets. diff --git a/crates/originweave-core/src/browser_protocol.rs b/crates/originweave-core/src/browser_protocol.rs index 6af83a411..a23e6c3e1 100644 --- a/crates/originweave-core/src/browser_protocol.rs +++ b/crates/originweave-core/src/browser_protocol.rs @@ -1,8 +1,94 @@ -use std::fmt; +use std::{fmt, str::FromStr}; /// Maximum UTF-8 byte length for browser protocol adapter metadata tokens. pub const MAX_BROWSER_PROTOCOL_METADATA_BYTES: usize = 128; +/// One OriginWeave Protocol generation. +/// +/// This value identifies the OriginWeave contract spoken by an adapter. It is +/// deliberately independent from the upstream WebDriver BiDi/CDP revision and +/// from the browser build. Constructing a version does not make that version +/// supported; callers must compare it with the exact version required by the +/// surrounding OriginWeave protocol boundary. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct OriginWeaveProtocolVersion { + major: u16, + minor: u16, +} + +impl OriginWeaveProtocolVersion { + /// Construct an OriginWeave Protocol generation identifier. + #[must_use] + pub const fn new(major: u16, minor: u16) -> Self { + Self { major, minor } + } + + /// Return the protocol major version. + #[must_use] + pub const fn major(self) -> u16 { + self.major + } + + /// Return the protocol minor version. + #[must_use] + pub const fn minor(self) -> u16 { + self.minor + } +} + +impl fmt::Display for OriginWeaveProtocolVersion { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(formatter, "originweave/{}.{}", self.major, self.minor) + } +} + +impl FromStr for OriginWeaveProtocolVersion { + type Err = OriginWeaveProtocolVersionParseError; + + fn from_str(value: &str) -> Result { + let Some(remainder) = value.strip_prefix("originweave/") else { + return Err(OriginWeaveProtocolVersionParseError::InvalidFormat); + }; + let Some((major_text, minor_text)) = remainder.split_once('.') else { + return Err(OriginWeaveProtocolVersionParseError::InvalidFormat); + }; + if minor_text.contains('.') { + return Err(OriginWeaveProtocolVersionParseError::InvalidFormat); + } + let Ok(major) = major_text.parse::() else { + return Err(OriginWeaveProtocolVersionParseError::InvalidFormat); + }; + let Ok(minor) = minor_text.parse::() else { + return Err(OriginWeaveProtocolVersionParseError::InvalidFormat); + }; + + let version = Self::new(major, minor); + if version.to_string() != value { + return Err(OriginWeaveProtocolVersionParseError::InvalidFormat); + } + Ok(version) + } +} + +/// Failure to parse a canonical serialized OriginWeave Protocol generation. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum OriginWeaveProtocolVersionParseError { + /// The value did not use the exact canonical `originweave/.` syntax. + InvalidFormat, +} + +impl fmt::Display for OriginWeaveProtocolVersionParseError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidFormat => formatter.write_str( + "OriginWeave protocol version must use canonical originweave/. syntax", + ), + } + } +} + +impl std::error::Error for OriginWeaveProtocolVersionParseError {} + /// Browser automation protocol family used by one versioned adapter. /// /// The protocol family is descriptive metadata only. Selecting a kind does not @@ -33,12 +119,13 @@ pub enum BrowserProtocolCapability { /// /// This value is deliberately not browser authority. It contains no browser /// session, context, origin, node handle, action grant, credential, or network -/// permission. Higher layers may use it to fail closed when a required adapter -/// capability is absent, while all OriginWeave authority remains separately -/// validated. +/// permission. Higher layers may use it to fail closed when the adapter targets +/// the wrong OriginWeave Protocol generation or lacks a required browser +/// capability, while all OriginWeave authority remains separately validated. #[derive(Debug, Clone, PartialEq, Eq)] pub struct BrowserProtocolAdapterDescriptor { kind: BrowserProtocolKind, + originweave_protocol_version: OriginWeaveProtocolVersion, adapter_version: String, protocol_revision: String, browser_revision: String, @@ -48,14 +135,16 @@ pub struct BrowserProtocolAdapterDescriptor { impl BrowserProtocolAdapterDescriptor { /// Construct one explicit adapter descriptor. /// - /// Adapter version, upstream protocol revision, and browser revision are - /// separate bounded ASCII metadata tokens. This prevents an OriginWeave - /// adapter release from being mistaken for the WebDriver BiDi/CDP revision - /// or the pinned browser build it was validated against. The declared - /// capability list must be non-empty and duplicate-free and is normalized - /// into one stable order so caller ordering cannot change descriptor identity. + /// The OriginWeave Protocol generation, adapter version, upstream protocol + /// revision, and browser revision are distinct metadata. This prevents an + /// OriginWeave contract version from being mistaken for the WebDriver + /// BiDi/CDP revision or the pinned browser build it was validated against. + /// The declared capability list must be non-empty and duplicate-free and is + /// normalized into one stable order so caller ordering cannot change + /// descriptor identity. pub fn new( kind: BrowserProtocolKind, + originweave_protocol_version: OriginWeaveProtocolVersion, adapter_version: &str, protocol_revision: &str, browser_revision: &str, @@ -85,6 +174,7 @@ impl BrowserProtocolAdapterDescriptor { Ok(Self { kind, + originweave_protocol_version, adapter_version: adapter_version.to_owned(), protocol_revision: protocol_revision.to_owned(), browser_revision: browser_revision.to_owned(), @@ -98,6 +188,12 @@ impl BrowserProtocolAdapterDescriptor { self.kind } + /// Return the exact OriginWeave Protocol generation implemented by this adapter. + #[must_use] + pub const fn originweave_protocol_version(&self) -> OriginWeaveProtocolVersion { + self.originweave_protocol_version + } + /// Return the bounded OriginWeave adapter-version metadata token. #[must_use] pub fn adapter_version(&self) -> &str { @@ -128,6 +224,55 @@ impl BrowserProtocolAdapterDescriptor { self.capabilities.contains(&capability) } + /// Require one exact OriginWeave Protocol generation before later adapter use. + /// + /// Pre-alpha compatibility is deliberately exact at this boundary. A caller + /// may add a separately reviewed compatibility transform later, but this + /// descriptor never silently treats a different major or minor generation + /// as equivalent. + pub fn require_originweave_protocol_version( + &self, + required: OriginWeaveProtocolVersion, + ) -> Result<(), BrowserProtocolVersionRequirementError> { + if self.originweave_protocol_version == required { + Ok(()) + } else { + Err( + BrowserProtocolVersionRequirementError::ProtocolVersionMismatch { + required, + actual: self.originweave_protocol_version, + }, + ) + } + } + + /// Require exact runtime browser-protocol and browser revisions before use. + /// + /// The caller must derive both values from the trusted runtime adapter that + /// is about to perform browser work. This deterministic comparison does not + /// authenticate or attest that caller. It only prevents a descriptor pinned + /// to one validated upstream-protocol/browser pair from being silently used + /// when the supplied runtime evidence is malformed or has drifted. + pub fn require_runtime_revisions( + &self, + protocol_revision: &str, + browser_revision: &str, + ) -> Result<(), BrowserProtocolRuntimeRequirementError> { + if !metadata_token_is_valid(protocol_revision) { + return Err(BrowserProtocolRuntimeRequirementError::InvalidProtocolRevision); + } + if !metadata_token_is_valid(browser_revision) { + return Err(BrowserProtocolRuntimeRequirementError::InvalidBrowserRevision); + } + if self.protocol_revision != protocol_revision { + return Err(BrowserProtocolRuntimeRequirementError::ProtocolRevisionMismatch); + } + if self.browser_revision != browser_revision { + return Err(BrowserProtocolRuntimeRequirementError::BrowserRevisionMismatch); + } + Ok(()) + } + /// Require one explicitly declared adapter capability before later use. /// /// This method never infers support from the browser protocol family. An @@ -174,6 +319,64 @@ fn metadata_token_is_valid(value: &str) -> bool { && value.bytes().any(|byte| byte.is_ascii_alphanumeric()) } +/// Failure to require one exact OriginWeave Protocol generation from an adapter. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BrowserProtocolVersionRequirementError { + /// The adapter targets a different OriginWeave Protocol generation. + ProtocolVersionMismatch { + /// Exact OriginWeave Protocol generation required by the caller. + required: OriginWeaveProtocolVersion, + /// Exact OriginWeave Protocol generation declared by the adapter. + actual: OriginWeaveProtocolVersion, + }, +} + +impl fmt::Display for BrowserProtocolVersionRequirementError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::ProtocolVersionMismatch { required, actual } => write!( + formatter, + "browser protocol adapter targets {actual} but {required} is required" + ), + } + } +} + +impl std::error::Error for BrowserProtocolVersionRequirementError {} + +/// Failure to require exact pinned runtime revision evidence from an adapter. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BrowserProtocolRuntimeRequirementError { + /// The runtime upstream-protocol revision token was malformed. + InvalidProtocolRevision, + /// The runtime browser revision token was malformed. + InvalidBrowserRevision, + /// The runtime upstream-protocol revision differs from the pinned descriptor. + ProtocolRevisionMismatch, + /// The runtime browser revision differs from the pinned descriptor. + BrowserRevisionMismatch, +} + +impl fmt::Display for BrowserProtocolRuntimeRequirementError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidProtocolRevision => formatter.write_str( + "runtime browser protocol revision must be a bounded ASCII metadata token", + ), + Self::InvalidBrowserRevision => formatter + .write_str("runtime browser revision must be a bounded ASCII metadata token"), + Self::ProtocolRevisionMismatch => formatter.write_str( + "runtime browser protocol revision does not match the pinned adapter revision", + ), + Self::BrowserRevisionMismatch => formatter.write_str( + "runtime browser revision does not match the pinned adapter browser revision", + ), + } + } +} + +impl std::error::Error for BrowserProtocolRuntimeRequirementError {} + /// Failure to require one browser protocol capability from an adapter. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum BrowserProtocolCapabilityRequirementError { diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index 7f4d93af7..1101a87a1 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -19,7 +19,9 @@ mod extension_authority; pub use browser_protocol::{ BrowserProtocolAdapterDescriptor, BrowserProtocolCapability, BrowserProtocolCapabilityRequirementError, BrowserProtocolDescriptorError, BrowserProtocolKind, - MAX_BROWSER_PROTOCOL_METADATA_BYTES, + BrowserProtocolRuntimeRequirementError, BrowserProtocolVersionRequirementError, + MAX_BROWSER_PROTOCOL_METADATA_BYTES, OriginWeaveProtocolVersion, + OriginWeaveProtocolVersionParseError, }; pub use browser_registry::{ BrowserAuthorityRegistry, BrowserRegistryError, MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES, diff --git a/crates/originweave-core/tests/browser_protocol_adapter.rs b/crates/originweave-core/tests/browser_protocol_adapter.rs index b18d22318..5cf457a66 100644 --- a/crates/originweave-core/tests/browser_protocol_adapter.rs +++ b/crates/originweave-core/tests/browser_protocol_adapter.rs @@ -5,19 +5,39 @@ use std::error::Error; use originweave_core::{ BrowserProtocolAdapterDescriptor, BrowserProtocolCapability, BrowserProtocolCapabilityRequirementError, BrowserProtocolDescriptorError, BrowserProtocolKind, - MAX_BROWSER_PROTOCOL_METADATA_BYTES, + BrowserProtocolVersionRequirementError, MAX_BROWSER_PROTOCOL_METADATA_BYTES, + OriginWeaveProtocolVersion, }; +const CURRENT_ORIGINWEAVE_PROTOCOL_VERSION: OriginWeaveProtocolVersion = + OriginWeaveProtocolVersion::new(0, 1); +const FUTURE_ORIGINWEAVE_PROTOCOL_VERSION: OriginWeaveProtocolVersion = + OriginWeaveProtocolVersion::new(0, 2); const BIDI_ADAPTER_VERSION: &str = "originweave-bidi-v1"; const BIDI_PROTOCOL_REVISION: &str = "webdriver-bidi-wd-2026-06-01"; const CDP_ADAPTER_VERSION: &str = "originweave-cdp-v1"; const CDP_PROTOCOL_REVISION: &str = "cdp-browser-r1639810"; const BROWSER_REVISION: &str = "chromium-r1639810"; +#[test] +fn originweave_protocol_version_is_explicit_and_canonical() { + assert_eq!(CURRENT_ORIGINWEAVE_PROTOCOL_VERSION.major(), 0); + assert_eq!(CURRENT_ORIGINWEAVE_PROTOCOL_VERSION.minor(), 1); + assert_eq!( + CURRENT_ORIGINWEAVE_PROTOCOL_VERSION.to_string(), + "originweave/0.1" + ); + assert_ne!( + CURRENT_ORIGINWEAVE_PROTOCOL_VERSION, + FUTURE_ORIGINWEAVE_PROTOCOL_VERSION + ); +} + #[test] fn webdriver_bidi_descriptor_is_explicit_and_capability_bounded() -> Result<(), Box> { let descriptor = BrowserProtocolAdapterDescriptor::new( BrowserProtocolKind::WebDriverBiDi, + CURRENT_ORIGINWEAVE_PROTOCOL_VERSION, BIDI_ADAPTER_VERSION, BIDI_PROTOCOL_REVISION, BROWSER_REVISION, @@ -29,6 +49,10 @@ fn webdriver_bidi_descriptor_is_explicit_and_capability_bounded() -> Result<(), )?; assert_eq!(descriptor.kind(), BrowserProtocolKind::WebDriverBiDi); + assert_eq!( + descriptor.originweave_protocol_version(), + CURRENT_ORIGINWEAVE_PROTOCOL_VERSION + ); assert_eq!(descriptor.adapter_version(), BIDI_ADAPTER_VERSION); assert_eq!(descriptor.protocol_revision(), BIDI_PROTOCOL_REVISION); assert_eq!(descriptor.browser_revision(), BROWSER_REVISION); @@ -44,6 +68,7 @@ fn webdriver_bidi_descriptor_is_explicit_and_capability_bounded() -> Result<(), fn cdp_capability_is_not_inferred_from_protocol_kind() -> Result<(), Box> { let descriptor = BrowserProtocolAdapterDescriptor::new( BrowserProtocolKind::ChromeDevToolsProtocol, + CURRENT_ORIGINWEAVE_PROTOCOL_VERSION, CDP_ADAPTER_VERSION, CDP_PROTOCOL_REVISION, BROWSER_REVISION, @@ -63,6 +88,7 @@ fn required_capability_fails_closed_without_side_effectful_fallback() -> Result< { let descriptor = BrowserProtocolAdapterDescriptor::new( BrowserProtocolKind::WebDriverBiDi, + CURRENT_ORIGINWEAVE_PROTOCOL_VERSION, BIDI_ADAPTER_VERSION, BIDI_PROTOCOL_REVISION, BROWSER_REVISION, @@ -84,6 +110,33 @@ fn required_capability_fails_closed_without_side_effectful_fallback() -> Result< Ok(()) } +#[test] +fn required_originweave_protocol_version_fails_closed() -> Result<(), Box> { + let descriptor = BrowserProtocolAdapterDescriptor::new( + BrowserProtocolKind::WebDriverBiDi, + CURRENT_ORIGINWEAVE_PROTOCOL_VERSION, + BIDI_ADAPTER_VERSION, + BIDI_PROTOCOL_REVISION, + BROWSER_REVISION, + &[BrowserProtocolCapability::Navigation], + )?; + + assert_eq!( + descriptor.require_originweave_protocol_version(CURRENT_ORIGINWEAVE_PROTOCOL_VERSION), + Ok(()) + ); + assert_eq!( + descriptor.require_originweave_protocol_version(FUTURE_ORIGINWEAVE_PROTOCOL_VERSION), + Err( + BrowserProtocolVersionRequirementError::ProtocolVersionMismatch { + required: FUTURE_ORIGINWEAVE_PROTOCOL_VERSION, + actual: CURRENT_ORIGINWEAVE_PROTOCOL_VERSION, + } + ) + ); + Ok(()) +} + #[test] fn malformed_or_ambiguous_metadata_fails_closed() { let valid_capabilities = [BrowserProtocolCapability::Navigation]; @@ -92,6 +145,7 @@ fn malformed_or_ambiguous_metadata_fails_closed() { assert_eq!( BrowserProtocolAdapterDescriptor::new( BrowserProtocolKind::WebDriverBiDi, + CURRENT_ORIGINWEAVE_PROTOCOL_VERSION, adapter_version, BIDI_PROTOCOL_REVISION, BROWSER_REVISION, @@ -113,6 +167,7 @@ fn malformed_or_ambiguous_metadata_fails_closed() { assert_eq!( BrowserProtocolAdapterDescriptor::new( BrowserProtocolKind::WebDriverBiDi, + CURRENT_ORIGINWEAVE_PROTOCOL_VERSION, BIDI_ADAPTER_VERSION, protocol_revision, BROWSER_REVISION, @@ -134,6 +189,7 @@ fn malformed_or_ambiguous_metadata_fails_closed() { assert_eq!( BrowserProtocolAdapterDescriptor::new( BrowserProtocolKind::WebDriverBiDi, + CURRENT_ORIGINWEAVE_PROTOCOL_VERSION, BIDI_ADAPTER_VERSION, BIDI_PROTOCOL_REVISION, browser_revision, @@ -147,6 +203,7 @@ fn malformed_or_ambiguous_metadata_fails_closed() { assert_eq!( BrowserProtocolAdapterDescriptor::new( BrowserProtocolKind::WebDriverBiDi, + CURRENT_ORIGINWEAVE_PROTOCOL_VERSION, &oversized, BIDI_PROTOCOL_REVISION, BROWSER_REVISION, @@ -157,6 +214,7 @@ fn malformed_or_ambiguous_metadata_fails_closed() { assert_eq!( BrowserProtocolAdapterDescriptor::new( BrowserProtocolKind::WebDriverBiDi, + CURRENT_ORIGINWEAVE_PROTOCOL_VERSION, BIDI_ADAPTER_VERSION, &oversized, BROWSER_REVISION, @@ -167,6 +225,7 @@ fn malformed_or_ambiguous_metadata_fails_closed() { assert_eq!( BrowserProtocolAdapterDescriptor::new( BrowserProtocolKind::WebDriverBiDi, + CURRENT_ORIGINWEAVE_PROTOCOL_VERSION, BIDI_ADAPTER_VERSION, BIDI_PROTOCOL_REVISION, &oversized, @@ -181,6 +240,7 @@ fn capability_set_must_be_nonempty_and_canonical() { assert_eq!( BrowserProtocolAdapterDescriptor::new( BrowserProtocolKind::WebDriverBiDi, + CURRENT_ORIGINWEAVE_PROTOCOL_VERSION, BIDI_ADAPTER_VERSION, BIDI_PROTOCOL_REVISION, BROWSER_REVISION, @@ -192,6 +252,7 @@ fn capability_set_must_be_nonempty_and_canonical() { assert_eq!( BrowserProtocolAdapterDescriptor::new( BrowserProtocolKind::WebDriverBiDi, + CURRENT_ORIGINWEAVE_PROTOCOL_VERSION, BIDI_ADAPTER_VERSION, BIDI_PROTOCOL_REVISION, BROWSER_REVISION, @@ -208,6 +269,7 @@ fn capability_set_must_be_nonempty_and_canonical() { fn capability_order_does_not_change_descriptor_identity() -> Result<(), Box> { let forward = BrowserProtocolAdapterDescriptor::new( BrowserProtocolKind::WebDriverBiDi, + CURRENT_ORIGINWEAVE_PROTOCOL_VERSION, BIDI_ADAPTER_VERSION, BIDI_PROTOCOL_REVISION, BROWSER_REVISION, @@ -220,6 +282,7 @@ fn capability_order_does_not_change_descriptor_identity() -> Result<(), Box Result> { + Ok(BrowserProtocolAdapterDescriptor::new( + BrowserProtocolKind::WebDriverBiDi, + ORIGINWEAVE_PROTOCOL_VERSION, + ADAPTER_VERSION, + PROTOCOL_REVISION, + BROWSER_REVISION, + &[BrowserProtocolCapability::Navigation], + )?) +} + +#[test] +fn exact_runtime_revisions_are_required_before_adapter_use() -> Result<(), Box> { + let descriptor = descriptor()?; + assert_eq!( + descriptor.require_runtime_revisions(PROTOCOL_REVISION, BROWSER_REVISION), + Ok(()) + ); + Ok(()) +} + +#[test] +fn runtime_revision_drift_fails_closed() -> Result<(), Box> { + let descriptor = descriptor()?; + assert_eq!( + descriptor.require_runtime_revisions("webdriver-bidi-wd-2026-07-01", BROWSER_REVISION), + Err(BrowserProtocolRuntimeRequirementError::ProtocolRevisionMismatch) + ); + assert_eq!( + descriptor.require_runtime_revisions(PROTOCOL_REVISION, "chromium-r1639811"), + Err(BrowserProtocolRuntimeRequirementError::BrowserRevisionMismatch) + ); + assert_eq!( + descriptor.require_runtime_revisions("webdriver-bidi-wd-2026-07-01", "chromium-r1639811"), + Err(BrowserProtocolRuntimeRequirementError::ProtocolRevisionMismatch) + ); + Ok(()) +} + +#[test] +fn malformed_runtime_revision_evidence_fails_before_comparison() -> Result<(), Box> { + let descriptor = descriptor()?; + assert_eq!( + descriptor.require_runtime_revisions("webdriver bidi current", BROWSER_REVISION), + Err(BrowserProtocolRuntimeRequirementError::InvalidProtocolRevision) + ); + assert_eq!( + descriptor.require_runtime_revisions(PROTOCOL_REVISION, "chromium/current"), + Err(BrowserProtocolRuntimeRequirementError::InvalidBrowserRevision) + ); + assert_eq!( + descriptor.require_runtime_revisions("", ""), + Err(BrowserProtocolRuntimeRequirementError::InvalidProtocolRevision) + ); + Ok(()) +} + +#[test] +fn runtime_requirement_errors_are_stable_and_source_free() { + let cases = [ + ( + BrowserProtocolRuntimeRequirementError::InvalidProtocolRevision, + "runtime browser protocol revision must be a bounded ASCII metadata token", + ), + ( + BrowserProtocolRuntimeRequirementError::InvalidBrowserRevision, + "runtime browser revision must be a bounded ASCII metadata token", + ), + ( + BrowserProtocolRuntimeRequirementError::ProtocolRevisionMismatch, + "runtime browser protocol revision does not match the pinned adapter revision", + ), + ( + BrowserProtocolRuntimeRequirementError::BrowserRevisionMismatch, + "runtime browser revision does not match the pinned adapter browser revision", + ), + ]; + + for (error, expected) in cases { + assert_eq!(error.to_string(), expected); + assert!(error.source().is_none()); + } +} diff --git a/crates/originweave-core/tests/protocol_version_parsing.rs b/crates/originweave-core/tests/protocol_version_parsing.rs new file mode 100644 index 000000000..189bde6be --- /dev/null +++ b/crates/originweave-core/tests/protocol_version_parsing.rs @@ -0,0 +1,59 @@ +#![allow(clippy::expect_used)] + +use std::error::Error; +use std::str::FromStr; + +use originweave_core::{OriginWeaveProtocolVersion, OriginWeaveProtocolVersionParseError}; + +#[test] +fn canonical_protocol_versions_parse_and_round_trip() -> Result<(), Box> { + let current = OriginWeaveProtocolVersion::from_str("originweave/0.1")?; + assert_eq!(current, OriginWeaveProtocolVersion::new(0, 1)); + assert_eq!(current.to_string(), "originweave/0.1"); + + let maximum = OriginWeaveProtocolVersion::from_str("originweave/65535.65535")?; + assert_eq!(maximum, OriginWeaveProtocolVersion::new(u16::MAX, u16::MAX)); + assert_eq!(maximum.to_string(), "originweave/65535.65535"); + Ok(()) +} + +#[test] +fn malformed_or_noncanonical_protocol_versions_fail_closed() { + let malformed = [ + "", + "originweave/", + "originweave/0", + "originweave/0.", + "originweave/.1", + "originweave/0.1.0", + "OriginWeave/0.1", + "originweave/00.1", + "originweave/0.01", + "originweave/+0.1", + "originweave/0.+1", + "originweave/-0.1", + "originweave/0.-1", + "originweave/65536.1", + "originweave/0.65536", + " originweave/0.1", + "originweave/0.1 ", + "originweave/0.1", + ]; + + for value in malformed { + assert_eq!( + OriginWeaveProtocolVersion::from_str(value), + Err(OriginWeaveProtocolVersionParseError::InvalidFormat) + ); + } +} + +#[test] +fn protocol_version_parse_error_is_stable_and_source_free() { + let error = OriginWeaveProtocolVersionParseError::InvalidFormat; + assert_eq!( + error.to_string(), + "OriginWeave protocol version must use canonical originweave/. syntax" + ); + assert!(error.source().is_none()); +} diff --git a/crates/originweave-core/tests/protocol_version_runtime_coverage.rs b/crates/originweave-core/tests/protocol_version_runtime_coverage.rs new file mode 100644 index 000000000..aeca15dec --- /dev/null +++ b/crates/originweave-core/tests/protocol_version_runtime_coverage.rs @@ -0,0 +1,12 @@ +use originweave_core::OriginWeaveProtocolVersion; + +#[test] +fn protocol_version_can_be_constructed_from_runtime_values() { + let major = std::hint::black_box(0_u16); + let minor = std::hint::black_box(1_u16); + let version = OriginWeaveProtocolVersion::new(major, minor); + + assert_eq!(version.major(), 0); + assert_eq!(version.minor(), 1); + assert_eq!(version.to_string(), "originweave/0.1"); +} diff --git a/tests/test_repository_contract.py b/tests/test_repository_contract.py index 360e11143..4c529bd21 100644 --- a/tests/test_repository_contract.py +++ b/tests/test_repository_contract.py @@ -175,6 +175,12 @@ def test_product_name_is_consistent_in_binding_documents(self) -> None: self.assertNotIn("TraceWeave", text, relative) self.assertNotIn("ProofRail", text, relative) + def test_runtime_revision_boundary_is_recorded_in_the_changelog(self) -> None: + """The public runtime-revision boundary must remain visible in release history.""" + + changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8") + self.assertIn("require_runtime_revisions", changelog) + def test_database_contract_requires_two_word_snake_case(self) -> None: """Persistent naming policy must include the mandated canonical form.""" @@ -185,4 +191,4 @@ def test_database_contract_requires_two_word_snake_case(self) -> None: if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main()