diff --git a/CHANGELOG.md b/CHANGELOG.md index 63d231262..143ea6088 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - 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. +- Atomic browser-protocol use validation that requires the exact OriginWeave protocol generation, caller-supplied runtime protocol family, exact pinned runtime protocol/browser revisions, and an explicitly declared capability in deterministic fail-closed order before producing non-cloneable validation evidence; this metadata proof does not authenticate the adapter or grant browser/Agent 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 a23e6c3e1..d4650ac30 100644 --- a/crates/originweave-core/src/browser_protocol.rs +++ b/crates/originweave-core/src/browser_protocol.rs @@ -289,6 +289,95 @@ impl BrowserProtocolAdapterDescriptor { Err(BrowserProtocolCapabilityRequirementError::UnsupportedCapability(capability)) } } + + /// Validate all adapter metadata prerequisites for one immediate browser operation. + /// + /// Validation is intentionally ordered and fail closed: the exact + /// OriginWeave Protocol generation is checked first, then the caller-supplied + /// runtime protocol family, then the supplied runtime protocol/browser + /// revisions, and finally the required adapter capability. Success returns + /// a non-cloneable value that a later trusted transport can consume as proof + /// that these metadata prerequisites were checked together. It is not + /// browser or Agent authority and does not authenticate or attest the caller + /// supplying runtime metadata. + pub fn validate_use( + &self, + required_originweave_protocol_version: OriginWeaveProtocolVersion, + runtime_kind: BrowserProtocolKind, + runtime_protocol_revision: &str, + runtime_browser_revision: &str, + required_capability: BrowserProtocolCapability, + ) -> Result { + self.require_originweave_protocol_version(required_originweave_protocol_version) + .map_err(BrowserProtocolUseValidationError::ProtocolVersion)?; + if self.kind != runtime_kind { + return Err(BrowserProtocolUseValidationError::ProtocolKindMismatch { + descriptor_kind: self.kind, + runtime_kind, + }); + } + self.require_runtime_revisions(runtime_protocol_revision, runtime_browser_revision) + .map_err(BrowserProtocolUseValidationError::RuntimeRevision)?; + self.require_capability(required_capability) + .map_err(BrowserProtocolUseValidationError::Capability)?; + + Ok(ValidatedBrowserProtocolUse { + descriptor: self.clone(), + capability: required_capability, + }) + } +} + +/// Snapshot proving that one descriptor passed all browser-protocol metadata checks for one use. +/// +/// Only [`BrowserProtocolAdapterDescriptor::validate_use`] can construct this +/// value. It intentionally does not implement `Clone`: a future trusted browser +/// transport can consume the value by ownership at the operation boundary +/// rather than treating it as reusable ambient authority. The value still does +/// not authenticate an adapter or attest that supplied runtime metadata came +/// from the running browser process. +#[derive(Debug, PartialEq, Eq)] +pub struct ValidatedBrowserProtocolUse { + descriptor: BrowserProtocolAdapterDescriptor, + capability: BrowserProtocolCapability, +} + +impl ValidatedBrowserProtocolUse { + /// Return the validated browser protocol family. + #[must_use] + pub const fn kind(&self) -> BrowserProtocolKind { + self.descriptor.kind + } + + /// Return the validated OriginWeave Protocol generation. + #[must_use] + pub const fn originweave_protocol_version(&self) -> OriginWeaveProtocolVersion { + self.descriptor.originweave_protocol_version + } + + /// Return the validated bounded adapter-version metadata token. + #[must_use] + pub fn adapter_version(&self) -> &str { + &self.descriptor.adapter_version + } + + /// Return the validated bounded upstream protocol-revision metadata token. + #[must_use] + pub fn protocol_revision(&self) -> &str { + &self.descriptor.protocol_revision + } + + /// Return the validated bounded browser-revision metadata token. + #[must_use] + pub fn browser_revision(&self) -> &str { + &self.descriptor.browser_revision + } + + /// Return the exact adapter capability validated for this use. + #[must_use] + pub const fn capability(&self) -> BrowserProtocolCapability { + self.capability + } } const fn capability_rank(capability: BrowserProtocolCapability) -> u8 { @@ -398,6 +487,47 @@ impl fmt::Display for BrowserProtocolCapabilityRequirementError { impl std::error::Error for BrowserProtocolCapabilityRequirementError {} +/// Failure to validate all browser-protocol metadata prerequisites for one use. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BrowserProtocolUseValidationError { + /// The descriptor targets the wrong OriginWeave Protocol generation. + ProtocolVersion(BrowserProtocolVersionRequirementError), + /// The runtime transport reports a different protocol family than the descriptor. + ProtocolKindMismatch { + /// Browser protocol family pinned by the adapter descriptor. + descriptor_kind: BrowserProtocolKind, + /// Browser protocol family reported by the runtime transport. + runtime_kind: BrowserProtocolKind, + }, + /// The supplied runtime protocol or browser revision is invalid or has drifted. + RuntimeRevision(BrowserProtocolRuntimeRequirementError), + /// The descriptor does not explicitly declare the required capability. + Capability(BrowserProtocolCapabilityRequirementError), +} + +impl fmt::Display for BrowserProtocolUseValidationError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::ProtocolVersion(error) => error.fmt(formatter), + Self::ProtocolKindMismatch { .. } => formatter + .write_str("runtime browser protocol kind does not match the pinned adapter kind"), + Self::RuntimeRevision(error) => error.fmt(formatter), + Self::Capability(error) => error.fmt(formatter), + } + } +} + +impl std::error::Error for BrowserProtocolUseValidationError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::ProtocolVersion(error) => Some(error), + Self::ProtocolKindMismatch { .. } => None, + Self::RuntimeRevision(error) => Some(error), + Self::Capability(error) => Some(error), + } + } +} + /// Failure to construct canonical browser protocol adapter metadata. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum BrowserProtocolDescriptorError { diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index 1101a87a1..66b120985 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -19,9 +19,9 @@ mod extension_authority; pub use browser_protocol::{ BrowserProtocolAdapterDescriptor, BrowserProtocolCapability, BrowserProtocolCapabilityRequirementError, BrowserProtocolDescriptorError, BrowserProtocolKind, - BrowserProtocolRuntimeRequirementError, BrowserProtocolVersionRequirementError, - MAX_BROWSER_PROTOCOL_METADATA_BYTES, OriginWeaveProtocolVersion, - OriginWeaveProtocolVersionParseError, + BrowserProtocolRuntimeRequirementError, BrowserProtocolUseValidationError, + BrowserProtocolVersionRequirementError, MAX_BROWSER_PROTOCOL_METADATA_BYTES, + OriginWeaveProtocolVersion, OriginWeaveProtocolVersionParseError, ValidatedBrowserProtocolUse, }; pub use browser_registry::{ BrowserAuthorityRegistry, BrowserRegistryError, MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES, diff --git a/crates/originweave-core/tests/browser_protocol_use_validation.rs b/crates/originweave-core/tests/browser_protocol_use_validation.rs new file mode 100644 index 000000000..8994253ec --- /dev/null +++ b/crates/originweave-core/tests/browser_protocol_use_validation.rs @@ -0,0 +1,185 @@ +use std::error::Error; + +use originweave_core::{ + BrowserProtocolAdapterDescriptor, BrowserProtocolCapability, + BrowserProtocolCapabilityRequirementError, BrowserProtocolKind, + BrowserProtocolRuntimeRequirementError, BrowserProtocolUseValidationError, + BrowserProtocolVersionRequirementError, 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, + BrowserProtocolCapability::TypedInput, + ], + )?) +} + +#[test] +fn validated_use_binds_all_required_adapter_metadata() -> Result<(), Box> { + let descriptor = descriptor()?; + let validated = descriptor.validate_use( + ORIGINWEAVE_PROTOCOL_VERSION, + BrowserProtocolKind::WebDriverBiDi, + PROTOCOL_REVISION, + BROWSER_REVISION, + BrowserProtocolCapability::Navigation, + )?; + + assert_eq!(validated.kind(), BrowserProtocolKind::WebDriverBiDi); + assert_eq!( + validated.originweave_protocol_version(), + ORIGINWEAVE_PROTOCOL_VERSION + ); + assert_eq!(validated.adapter_version(), ADAPTER_VERSION); + assert_eq!(validated.protocol_revision(), PROTOCOL_REVISION); + assert_eq!(validated.browser_revision(), BROWSER_REVISION); + assert_eq!( + validated.capability(), + BrowserProtocolCapability::Navigation + ); + Ok(()) +} + +#[test] +fn protocol_generation_mismatch_precedes_runtime_and_capability_checks() +-> Result<(), Box> { + let descriptor = descriptor()?; + let wrong_generation = OriginWeaveProtocolVersion::new(0, 2); + + assert_eq!( + descriptor.validate_use( + wrong_generation, + BrowserProtocolKind::ChromeDevToolsProtocol, + "runtime revision with spaces", + "browser/revision", + BrowserProtocolCapability::NetworkObservation, + ), + Err(BrowserProtocolUseValidationError::ProtocolVersion( + BrowserProtocolVersionRequirementError::ProtocolVersionMismatch { + required: wrong_generation, + actual: ORIGINWEAVE_PROTOCOL_VERSION, + } + )) + ); + Ok(()) +} + +#[test] +fn runtime_protocol_kind_mismatch_precedes_revision_and_capability_checks() +-> Result<(), Box> { + let descriptor = descriptor()?; + + let error = descriptor.validate_use( + ORIGINWEAVE_PROTOCOL_VERSION, + BrowserProtocolKind::ChromeDevToolsProtocol, + "runtime revision with spaces", + "browser/revision", + BrowserProtocolCapability::NetworkObservation, + ); + + assert_eq!( + error, + Err(BrowserProtocolUseValidationError::ProtocolKindMismatch { + descriptor_kind: BrowserProtocolKind::WebDriverBiDi, + runtime_kind: BrowserProtocolKind::ChromeDevToolsProtocol, + }) + ); + let error = error.err().ok_or("expected protocol kind mismatch")?; + assert_eq!( + error.to_string(), + "runtime browser protocol kind does not match the pinned adapter kind" + ); + assert!(error.source().is_none()); + Ok(()) +} + +#[test] +fn runtime_revision_validation_precedes_capability_check() -> Result<(), Box> { + let descriptor = descriptor()?; + + assert_eq!( + descriptor.validate_use( + ORIGINWEAVE_PROTOCOL_VERSION, + BrowserProtocolKind::WebDriverBiDi, + "webdriver-bidi-wd-2026-07-01", + BROWSER_REVISION, + BrowserProtocolCapability::NetworkObservation, + ), + Err(BrowserProtocolUseValidationError::RuntimeRevision( + BrowserProtocolRuntimeRequirementError::ProtocolRevisionMismatch, + )) + ); + Ok(()) +} + +#[test] +fn undeclared_capability_cannot_produce_validated_use() -> Result<(), Box> { + let descriptor = descriptor()?; + + assert_eq!( + descriptor.validate_use( + ORIGINWEAVE_PROTOCOL_VERSION, + BrowserProtocolKind::WebDriverBiDi, + PROTOCOL_REVISION, + BROWSER_REVISION, + BrowserProtocolCapability::NetworkObservation, + ), + Err(BrowserProtocolUseValidationError::Capability( + BrowserProtocolCapabilityRequirementError::UnsupportedCapability( + BrowserProtocolCapability::NetworkObservation, + ), + )) + ); + Ok(()) +} + +#[test] +fn validation_errors_preserve_stable_typed_sources() { + let wrong_generation = OriginWeaveProtocolVersion::new(0, 2); + let cases = [ + ( + BrowserProtocolUseValidationError::ProtocolVersion( + BrowserProtocolVersionRequirementError::ProtocolVersionMismatch { + required: wrong_generation, + actual: ORIGINWEAVE_PROTOCOL_VERSION, + }, + ), + "browser protocol adapter targets originweave/0.1 but originweave/0.2 is required", + ), + ( + BrowserProtocolUseValidationError::RuntimeRevision( + BrowserProtocolRuntimeRequirementError::ProtocolRevisionMismatch, + ), + "runtime browser protocol revision does not match the pinned adapter revision", + ), + ( + BrowserProtocolUseValidationError::Capability( + BrowserProtocolCapabilityRequirementError::UnsupportedCapability( + BrowserProtocolCapability::NetworkObservation, + ), + ), + "browser protocol adapter does not declare required network-observation capability", + ), + ]; + + for (error, expected) in cases { + assert_eq!(error.to_string(), expected); + assert_eq!( + error.source().map(ToString::to_string).as_deref(), + Some(expected) + ); + } +}