diff --git a/crates/originweave-core/src/browser_protocol.rs b/crates/originweave-core/src/browser_protocol.rs index 6af83a411..77b182be7 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,47 @@ impl BrowserProtocolAdapterDescriptor { self.capabilities.contains(&capability) } + /// Require one exact browser protocol family before later adapter use. + /// + /// This boundary never treats WebDriver BiDi and Chrome DevTools Protocol + /// as interchangeable. A kind mismatch fails closed with a typed error and + /// does not select or authorize another protocol family as a fallback. + pub fn require_kind( + &self, + required: BrowserProtocolKind, + ) -> Result<(), BrowserProtocolKindRequirementError> { + if self.kind == required { + Ok(()) + } else { + Err(BrowserProtocolKindRequirementError::ProtocolKindMismatch { + required, + actual: self.kind, + }) + } + } + + /// 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 @@ -155,6 +245,13 @@ const fn capability_rank(capability: BrowserProtocolCapability) -> u8 { } } +fn protocol_kind_name(kind: BrowserProtocolKind) -> &'static str { + match kind { + BrowserProtocolKind::WebDriverBiDi => "webdriver-bidi", + BrowserProtocolKind::ChromeDevToolsProtocol => "chrome-devtools-protocol", + } +} + fn capability_name(capability: BrowserProtocolCapability) -> &'static str { match capability { BrowserProtocolCapability::Navigation => "navigation", @@ -174,6 +271,58 @@ fn metadata_token_is_valid(value: &str) -> bool { && value.bytes().any(|byte| byte.is_ascii_alphanumeric()) } +/// Failure to require one exact browser protocol family from an adapter. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BrowserProtocolKindRequirementError { + /// The adapter uses a different browser protocol family than required. + ProtocolKindMismatch { + /// Exact browser protocol family required by the caller. + required: BrowserProtocolKind, + /// Exact browser protocol family declared by the adapter. + actual: BrowserProtocolKind, + }, +} + +impl fmt::Display for BrowserProtocolKindRequirementError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::ProtocolKindMismatch { required, actual } => write!( + formatter, + "browser protocol adapter uses {} but {} is required", + protocol_kind_name(*actual), + protocol_kind_name(*required) + ), + } + } +} + +impl std::error::Error for BrowserProtocolKindRequirementError {} + +/// 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 { diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index b5adf4fc3..d7f1c8765 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -19,7 +19,8 @@ mod extension_authority; pub use browser_protocol::{ BrowserProtocolAdapterDescriptor, BrowserProtocolCapability, BrowserProtocolCapabilityRequirementError, BrowserProtocolDescriptorError, BrowserProtocolKind, - MAX_BROWSER_PROTOCOL_METADATA_BYTES, + BrowserProtocolKindRequirementError, BrowserProtocolVersionRequirementError, + MAX_BROWSER_PROTOCOL_METADATA_BYTES, OriginWeaveProtocolVersion, }; pub use browser_registry::{ BrowserAuthorityRegistry, BrowserRegistryError, MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES, diff --git a/crates/originweave-core/src/root.rs b/crates/originweave-core/src/root.rs index 6a907cd27..b4956eac5 100644 --- a/crates/originweave-core/src/root.rs +++ b/crates/originweave-core/src/root.rs @@ -18,12 +18,13 @@ pub use core_contracts::{ AuthorityExtensionAgentGrant as ExtensionAgentGrant, BrowserAuthorityRegistry, BrowserProtocolAdapterDescriptor, BrowserProtocolCapability, BrowserProtocolCapabilityRequirementError, BrowserProtocolDescriptorError, BrowserProtocolKind, + BrowserProtocolKindRequirementError, BrowserProtocolVersionRequirementError, BrowserRegistryError, BrowserSessionId, BrowsingContextId, Capability, DocumentEpoch, ExecutionPurpose, ExtensionAgentCapability, ExtensionId, ExtensionIdError, InstructionSource, MAX_BROWSER_PROTOCOL_METADATA_BYTES, MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES, NodeHandleError, - Origin, OriginError, PolicyContext, RegistryObservedNodeHandle as ObservedNodeHandle, - RiskClass, RobotsDecision, SecretDelivery, SessionMode, - evaluate_extension_authority_access as evaluate_extension_access, + Origin, OriginError, OriginWeaveProtocolVersion, PolicyContext, + RegistryObservedNodeHandle as ObservedNodeHandle, RiskClass, RobotsDecision, SecretDelivery, + SessionMode, evaluate_extension_authority_access as evaluate_extension_access, }; /// Stateless MCP routing validation that maps only explicit tools to typed actions. diff --git a/crates/originweave-core/tests/browser_protocol_adapter.rs b/crates/originweave-core/tests/browser_protocol_adapter.rs index b18d22318..0d6d3f206 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, + BrowserProtocolKindRequirementError, 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, @@ -58,11 +83,38 @@ fn cdp_capability_is_not_inferred_from_protocol_kind() -> Result<(), Box 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_kind(BrowserProtocolKind::WebDriverBiDi), + Ok(()) + ); + assert_eq!( + descriptor.require_kind(BrowserProtocolKind::ChromeDevToolsProtocol), + Err(BrowserProtocolKindRequirementError::ProtocolKindMismatch { + required: BrowserProtocolKind::ChromeDevToolsProtocol, + actual: BrowserProtocolKind::WebDriverBiDi, + }) + ); + Ok(()) +} + #[test] fn required_capability_fails_closed_without_side_effectful_fallback() -> Result<(), Box> { let descriptor = BrowserProtocolAdapterDescriptor::new( BrowserProtocolKind::WebDriverBiDi, + CURRENT_ORIGINWEAVE_PROTOCOL_VERSION, BIDI_ADAPTER_VERSION, BIDI_PROTOCOL_REVISION, BROWSER_REVISION, @@ -84,6 +136,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 +171,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 +193,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 +215,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 +229,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 +240,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 +251,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 +266,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 +278,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 +295,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 +308,7 @@ fn capability_order_does_not_change_descriptor_identity() -> Result<(), Box