diff --git a/CHANGELOG.md b/CHANGELOG.md index f98e29765..bedf905a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ 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. - 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 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 diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index 1e2c19663..19a3a3ddd 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -20,7 +20,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, 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()); +}