From a439c9b50da460c6bde624cf42d3fe7a5588ef2f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 14:00:16 +0900 Subject: [PATCH 01/16] test(core): require OriginWeave protocol version binding --- .../tests/browser_protocol_adapter.rs | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/crates/originweave-core/tests/browser_protocol_adapter.rs b/crates/originweave-core/tests/browser_protocol_adapter.rs index b18d22318..f555927ae 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, + BrowserProtocolVersionRequirementError, OriginWeaveProtocolVersion, MAX_BROWSER_PROTOCOL_METADATA_BYTES, }; +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,31 @@ 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 +143,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 +165,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 +187,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 +201,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 +212,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 +223,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 +238,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 +250,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 +267,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 +280,7 @@ fn capability_order_does_not_change_descriptor_identity() -> Result<(), Box Date: Wed, 12 Aug 2026 14:02:41 +0900 Subject: [PATCH 02/16] test(core): apply canonical protocol-version formatting --- .../tests/browser_protocol_adapter.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/crates/originweave-core/tests/browser_protocol_adapter.rs b/crates/originweave-core/tests/browser_protocol_adapter.rs index f555927ae..5cf457a66 100644 --- a/crates/originweave-core/tests/browser_protocol_adapter.rs +++ b/crates/originweave-core/tests/browser_protocol_adapter.rs @@ -5,8 +5,8 @@ use std::error::Error; use originweave_core::{ BrowserProtocolAdapterDescriptor, BrowserProtocolCapability, BrowserProtocolCapabilityRequirementError, BrowserProtocolDescriptorError, BrowserProtocolKind, - BrowserProtocolVersionRequirementError, OriginWeaveProtocolVersion, - MAX_BROWSER_PROTOCOL_METADATA_BYTES, + BrowserProtocolVersionRequirementError, MAX_BROWSER_PROTOCOL_METADATA_BYTES, + OriginWeaveProtocolVersion, }; const CURRENT_ORIGINWEAVE_PROTOCOL_VERSION: OriginWeaveProtocolVersion = @@ -127,10 +127,12 @@ fn required_originweave_protocol_version_fails_closed() -> Result<(), Box Date: Wed, 12 Aug 2026 14:04:43 +0900 Subject: [PATCH 03/16] feat(core): bind browser adapters to protocol version --- .../originweave-core/src/browser_protocol.rs | 112 ++++++++++++++++-- 1 file changed, 103 insertions(+), 9 deletions(-) diff --git a/crates/originweave-core/src/browser_protocol.rs b/crates/originweave-core/src/browser_protocol.rs index 6af83a411..9d4733b02 100644 --- a/crates/originweave-core/src/browser_protocol.rs +++ b/crates/originweave-core/src/browser_protocol.rs @@ -3,6 +3,45 @@ use std::fmt; /// 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) + } +} + /// Browser automation protocol family used by one versioned adapter. /// /// The protocol family is descriptive metadata only. Selecting a kind does not @@ -33,12 +72,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 +88,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 +127,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 +141,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 +177,26 @@ 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 one explicitly declared adapter capability before later use. /// /// This method never infers support from the browser protocol family. An @@ -174,6 +243,31 @@ 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 one browser protocol capability from an adapter. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum BrowserProtocolCapabilityRequirementError { From fb342952a422edef4ed8ea5119938f78dc72578a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 14:05:01 +0900 Subject: [PATCH 04/16] feat(core): export protocol version contract --- crates/originweave-core/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index 775abfcad..ed33e341e 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -17,7 +17,8 @@ mod contracts; pub use browser_protocol::{ BrowserProtocolAdapterDescriptor, BrowserProtocolCapability, BrowserProtocolCapabilityRequirementError, BrowserProtocolDescriptorError, BrowserProtocolKind, - MAX_BROWSER_PROTOCOL_METADATA_BYTES, + BrowserProtocolVersionRequirementError, MAX_BROWSER_PROTOCOL_METADATA_BYTES, + OriginWeaveProtocolVersion, }; pub use browser_registry::{ BrowserAuthorityRegistry, BrowserRegistryError, MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES, From c4cd417266860e006e068e9f58d86b98779b646d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 14:09:00 +0900 Subject: [PATCH 05/16] style(core): apply canonical protocol-version formatting --- crates/originweave-core/src/browser_protocol.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/originweave-core/src/browser_protocol.rs b/crates/originweave-core/src/browser_protocol.rs index 9d4733b02..4b7fe0f00 100644 --- a/crates/originweave-core/src/browser_protocol.rs +++ b/crates/originweave-core/src/browser_protocol.rs @@ -190,10 +190,12 @@ impl BrowserProtocolAdapterDescriptor { if self.originweave_protocol_version == required { Ok(()) } else { - Err(BrowserProtocolVersionRequirementError::ProtocolVersionMismatch { - required, - actual: self.originweave_protocol_version, - }) + Err( + BrowserProtocolVersionRequirementError::ProtocolVersionMismatch { + required, + actual: self.originweave_protocol_version, + }, + ) } } From ea45a91230e003babc33fff08acb9ada4b07957a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 14:09:36 +0900 Subject: [PATCH 06/16] test(core): exercise runtime protocol-version construction --- .../tests/protocol_version_runtime_coverage.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 crates/originweave-core/tests/protocol_version_runtime_coverage.rs 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"); +} From ac780f08ff825fae08c64d993eaaab6fe6817e3b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 14:32:14 +0900 Subject: [PATCH 07/16] test(core): require canonical protocol version parsing --- .../tests/protocol_version_parsing.rs | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 crates/originweave-core/tests/protocol_version_parsing.rs 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()); +} From eec3f462b6df93e1dd10a05aeee23393ddd7bfb2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 14:36:55 +0900 Subject: [PATCH 08/16] feat(core): parse canonical protocol versions --- .../originweave-core/src/browser_protocol.rs | 49 ++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/crates/originweave-core/src/browser_protocol.rs b/crates/originweave-core/src/browser_protocol.rs index 4b7fe0f00..981597e03 100644 --- a/crates/originweave-core/src/browser_protocol.rs +++ b/crates/originweave-core/src/browser_protocol.rs @@ -1,4 +1,4 @@ -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; @@ -42,6 +42,53 @@ impl fmt::Display for OriginWeaveProtocolVersion { } } +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 From aeb9629704dfc1d6dcc14a6e9de2ddb85853a302 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 14:37:16 +0900 Subject: [PATCH 09/16] feat(core): export protocol version parse error --- crates/originweave-core/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index ed33e341e..5b6275eec 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -18,7 +18,7 @@ pub use browser_protocol::{ BrowserProtocolAdapterDescriptor, BrowserProtocolCapability, BrowserProtocolCapabilityRequirementError, BrowserProtocolDescriptorError, BrowserProtocolKind, BrowserProtocolVersionRequirementError, MAX_BROWSER_PROTOCOL_METADATA_BYTES, - OriginWeaveProtocolVersion, + OriginWeaveProtocolVersion, OriginWeaveProtocolVersionParseError, }; pub use browser_registry::{ BrowserAuthorityRegistry, BrowserRegistryError, MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES, From ea17243bf3e0a332bc4a62e25207c80697fde067 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 14:40:58 +0900 Subject: [PATCH 10/16] docs(changelog): record canonical protocol version parsing --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cabdcc5d..7d197d67e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - 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. - 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. From 66f6ad78004471ffb4cc881a20f67ac512dc4fb2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 14:46:05 +0900 Subject: [PATCH 11/16] test(core): require exact runtime browser revisions --- .../browser_protocol_runtime_revision.rs | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 crates/originweave-core/tests/browser_protocol_runtime_revision.rs diff --git a/crates/originweave-core/tests/browser_protocol_runtime_revision.rs b/crates/originweave-core/tests/browser_protocol_runtime_revision.rs new file mode 100644 index 000000000..db5e13172 --- /dev/null +++ b/crates/originweave-core/tests/browser_protocol_runtime_revision.rs @@ -0,0 +1,99 @@ +use std::error::Error; + +use originweave_core::{ + BrowserProtocolAdapterDescriptor, BrowserProtocolCapability, BrowserProtocolKind, + BrowserProtocolRuntimeRequirementError, OriginWeaveProtocolVersion, +}; + +const ORIGINWEAVE_PROTOCOL_VERSION: OriginWeaveProtocolVersion = + OriginWeaveProtocolVersion::new(0, 1); +const ADAPTER_VERSION: &str = "originweave-bidi-v1"; +const PROTOCOL_REVISION: &str = "webdriver-bidi-wd-2026-06-01"; +const BROWSER_REVISION: &str = "chromium-r1639810"; + +fn descriptor() -> 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()); + } +} From 03f7b188b4290ed6eb748df102bbe20119e8fa6b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 14:49:30 +0900 Subject: [PATCH 12/16] style(core): apply canonical runtime revision test formatting --- .../tests/browser_protocol_runtime_revision.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/originweave-core/tests/browser_protocol_runtime_revision.rs b/crates/originweave-core/tests/browser_protocol_runtime_revision.rs index db5e13172..60a04acdb 100644 --- a/crates/originweave-core/tests/browser_protocol_runtime_revision.rs +++ b/crates/originweave-core/tests/browser_protocol_runtime_revision.rs @@ -44,10 +44,7 @@ fn runtime_revision_drift_fails_closed() -> Result<(), Box> { Err(BrowserProtocolRuntimeRequirementError::BrowserRevisionMismatch) ); assert_eq!( - descriptor.require_runtime_revisions( - "webdriver-bidi-wd-2026-07-01", - "chromium-r1639811" - ), + descriptor.require_runtime_revisions("webdriver-bidi-wd-2026-07-01", "chromium-r1639811"), Err(BrowserProtocolRuntimeRequirementError::ProtocolRevisionMismatch) ); Ok(()) From e7c3e3e2dfa58b9a14f3a4027511cea50cd0324a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 14:52:11 +0900 Subject: [PATCH 13/16] feat(core): validate browser runtime revisions --- .../originweave-core/src/browser_protocol.rs | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/crates/originweave-core/src/browser_protocol.rs b/crates/originweave-core/src/browser_protocol.rs index 981597e03..bbb6ca8ce 100644 --- a/crates/originweave-core/src/browser_protocol.rs +++ b/crates/originweave-core/src/browser_protocol.rs @@ -246,6 +246,33 @@ impl BrowserProtocolAdapterDescriptor { } } + /// 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 @@ -317,6 +344,40 @@ impl fmt::Display for BrowserProtocolVersionRequirementError { 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 { From 4b1ce55bd946a3fb57c50c9ea4b2456590e3dccf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 14:52:42 +0900 Subject: [PATCH 14/16] feat(core): export browser runtime revision error --- crates/originweave-core/src/lib.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index 5b6275eec..6a8c7b6bd 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -17,8 +17,9 @@ mod contracts; pub use browser_protocol::{ BrowserProtocolAdapterDescriptor, BrowserProtocolCapability, BrowserProtocolCapabilityRequirementError, BrowserProtocolDescriptorError, BrowserProtocolKind, - BrowserProtocolVersionRequirementError, MAX_BROWSER_PROTOCOL_METADATA_BYTES, - OriginWeaveProtocolVersion, OriginWeaveProtocolVersionParseError, + BrowserProtocolRuntimeRequirementError, BrowserProtocolVersionRequirementError, + MAX_BROWSER_PROTOCOL_METADATA_BYTES, OriginWeaveProtocolVersion, + OriginWeaveProtocolVersionParseError, }; pub use browser_registry::{ BrowserAuthorityRegistry, BrowserRegistryError, MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES, From f0fc8f9cfc66dd8b7664b058a8243cc2bf9d95e5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 15:10:20 +0900 Subject: [PATCH 15/16] style(core): apply canonical runtime revision formatting --- crates/originweave-core/src/browser_protocol.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/originweave-core/src/browser_protocol.rs b/crates/originweave-core/src/browser_protocol.rs index bbb6ca8ce..a23e6c3e1 100644 --- a/crates/originweave-core/src/browser_protocol.rs +++ b/crates/originweave-core/src/browser_protocol.rs @@ -363,9 +363,8 @@ impl fmt::Display for BrowserProtocolRuntimeRequirementError { 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::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", ), From 415b8862ba5210a7bc55c38da00c87f512fbd932 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 06:42:19 +0900 Subject: [PATCH 16/16] docs: record runtime revision boundary --- CHANGELOG.md | 1 + tests/test_repository_contract.py | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bedf905a5..63d231262 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - 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/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()