diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b3f92b50..fadbbc8b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - 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. - 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. - Runtime browser-adapter version binding at the atomic protocol-use boundary: the caller-supplied bounded adapter-version token must exactly match the reviewed descriptor version before runtime revision or capability checks can succeed, preventing adapter-build drift from silently reusing otherwise matching protocol/browser metadata without authenticating or attesting the adapter process. +- Same-call browser-protocol dispatch gating that validates current protocol family, adapter version, pinned protocol/browser revisions, OriginWeave generation, and required capability before invoking one callback, transferring the non-cloneable validation proof by ownership without turning metadata validation into browser or Agent authority. - Credential-safe browser-protocol validation evidence that copies only the already validated protocol family, OriginWeave generation, adapter version, pinned protocol/browser revisions, and exact capability into cloneable audit metadata without recreating the non-cloneable validation prerequisite or granting 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. diff --git a/crates/originweave-core/src/browser_protocol_dispatch.rs b/crates/originweave-core/src/browser_protocol_dispatch.rs new file mode 100644 index 000000000..6664545b3 --- /dev/null +++ b/crates/originweave-core/src/browser_protocol_dispatch.rs @@ -0,0 +1,71 @@ +use crate::{ + BrowserProtocolAdapterDescriptor, BrowserProtocolCapability, BrowserProtocolKind, + BrowserProtocolUseValidationError, OriginWeaveProtocolVersion, ValidatedBrowserProtocolUse, +}; + +/// Current runtime metadata sampled from the browser-protocol adapter about to perform I/O. +/// +/// This value is untrusted descriptive input. Constructing it does not validate or authenticate an +/// adapter, browser, or protocol revision and grants no browser or Agent authority. The descriptor +/// validates every field against its reviewed metadata before a dispatch callback can run. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct BrowserProtocolRuntimeMetadata<'a> { + kind: BrowserProtocolKind, + adapter_version: &'a str, + protocol_revision: &'a str, + browser_revision: &'a str, +} + +impl<'a> BrowserProtocolRuntimeMetadata<'a> { + /// Build one runtime metadata snapshot for immediate validation and dispatch. + /// + /// String syntax and descriptor equality are intentionally checked later by + /// [`BrowserProtocolAdapterDescriptor::dispatch_if_runtime_matches`], so malformed caller data + /// remains representable as input that the fail-closed boundary can reject deterministically. + pub const fn new( + kind: BrowserProtocolKind, + adapter_version: &'a str, + protocol_revision: &'a str, + browser_revision: &'a str, + ) -> Self { + Self { + kind, + adapter_version, + protocol_revision, + browser_revision, + } + } +} + +impl BrowserProtocolAdapterDescriptor { + /// Validate current browser-protocol metadata and immediately invoke one dispatch callback. + /// + /// `runtime_metadata` must be sampled from the trusted adapter that is about to perform the + /// operation. Validation occurs before `dispatch` is invoked, and the callback receives the + /// resulting non-cloneable [`ValidatedBrowserProtocolUse`] by ownership so this boundary does + /// not turn successful validation into reusable ambient authority. + /// + /// A successful callback invocation does not authenticate the adapter process, authorize a + /// browser session, browsing context, origin, destination, secret, or approval, or prove a + /// browser post-condition. Those remain separate higher-level execution boundaries. + pub fn dispatch_if_runtime_matches( + &self, + required_originweave_protocol_version: OriginWeaveProtocolVersion, + runtime_metadata: BrowserProtocolRuntimeMetadata<'_>, + required_capability: BrowserProtocolCapability, + dispatch: F, + ) -> Result + where + F: FnOnce(ValidatedBrowserProtocolUse) -> R, + { + let validated = self.validate_use( + required_originweave_protocol_version, + runtime_metadata.kind, + runtime_metadata.adapter_version, + runtime_metadata.protocol_revision, + runtime_metadata.browser_revision, + required_capability, + )?; + Ok(dispatch(validated)) + } +} diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index f490e4ee6..6be2e355f 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -9,6 +9,7 @@ #![deny(missing_docs)] mod browser_protocol; +mod browser_protocol_dispatch; mod browser_registry; #[cfg(test)] mod browser_registry_coverage; @@ -22,6 +23,7 @@ pub use browser_protocol::{ BrowserProtocolVersionRequirementError, MAX_BROWSER_PROTOCOL_METADATA_BYTES, OriginWeaveProtocolVersion, OriginWeaveProtocolVersionParseError, ValidatedBrowserProtocolUse, }; +pub use browser_protocol_dispatch::BrowserProtocolRuntimeMetadata; pub use browser_registry::{ BrowserAuthorityRegistry, BrowserRegistryError, MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES, }; diff --git a/crates/originweave-core/tests/browser_protocol_runtime_dispatch.rs b/crates/originweave-core/tests/browser_protocol_runtime_dispatch.rs new file mode 100644 index 000000000..0ca669d7d --- /dev/null +++ b/crates/originweave-core/tests/browser_protocol_runtime_dispatch.rs @@ -0,0 +1,121 @@ +use std::{cell::Cell, error::Error}; + +use originweave_core::{ + BrowserProtocolAdapterDescriptor, BrowserProtocolCapability, BrowserProtocolKind, + BrowserProtocolRuntimeMetadata, BrowserProtocolUseValidationError, OriginWeaveProtocolVersion, + ValidatedBrowserProtocolUse, +}; + +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"; + +type DispatchOutcome = Result<(String, BrowserProtocolCapability), &'static str>; +type DispatchFn = fn(ValidatedBrowserProtocolUse) -> DispatchOutcome; + +thread_local! { + static DISPATCH_CALLED: Cell = const { Cell::new(false) }; +} + +fn descriptor() -> Result> { + Ok(BrowserProtocolAdapterDescriptor::new( + BrowserProtocolKind::WebDriverBiDi, + ORIGINWEAVE_PROTOCOL_VERSION, + ADAPTER_VERSION, + PROTOCOL_REVISION, + BROWSER_REVISION, + &[BrowserProtocolCapability::Navigation], + )?) +} + +fn runtime_metadata(adapter_version: &str) -> BrowserProtocolRuntimeMetadata<'_> { + BrowserProtocolRuntimeMetadata::new( + BrowserProtocolKind::WebDriverBiDi, + adapter_version, + PROTOCOL_REVISION, + BROWSER_REVISION, + ) +} + +fn reset_dispatch_marker() { + DISPATCH_CALLED.with(|called| called.set(false)); +} + +fn dispatch_was_called() -> bool { + DISPATCH_CALLED.with(Cell::get) +} + +fn successful_dispatch(validated: ValidatedBrowserProtocolUse) -> DispatchOutcome { + DISPATCH_CALLED.with(|called| called.set(true)); + Ok(( + validated.adapter_version().to_owned(), + validated.capability(), + )) +} + +fn failing_dispatch(_: ValidatedBrowserProtocolUse) -> DispatchOutcome { + DISPATCH_CALLED.with(|called| called.set(true)); + Err("adapter-failure") +} + +#[test] +fn exact_runtime_validation_hands_single_use_proof_to_dispatch() -> Result<(), Box> { + let descriptor = descriptor()?; + reset_dispatch_marker(); + + let dispatch_result = descriptor.dispatch_if_runtime_matches( + ORIGINWEAVE_PROTOCOL_VERSION, + runtime_metadata(ADAPTER_VERSION), + BrowserProtocolCapability::Navigation, + successful_dispatch as DispatchFn, + )?; + + assert!(dispatch_was_called()); + assert_eq!( + dispatch_result, + Ok(( + ADAPTER_VERSION.to_owned(), + BrowserProtocolCapability::Navigation + )) + ); + Ok(()) +} + +#[test] +fn runtime_mismatch_prevents_dispatch_callback() -> Result<(), Box> { + let descriptor = descriptor()?; + reset_dispatch_marker(); + + let result = descriptor.dispatch_if_runtime_matches( + ORIGINWEAVE_PROTOCOL_VERSION, + runtime_metadata("originweave-bidi-v2"), + BrowserProtocolCapability::Navigation, + successful_dispatch as DispatchFn, + ); + + assert_eq!( + result, + Err(BrowserProtocolUseValidationError::AdapterVersionMismatch) + ); + assert!(!dispatch_was_called()); + Ok(()) +} + +#[test] +fn adapter_callback_failure_remains_separate_after_validation() -> Result<(), Box> { + let descriptor = descriptor()?; + reset_dispatch_marker(); + + let dispatch_result = descriptor.dispatch_if_runtime_matches( + ORIGINWEAVE_PROTOCOL_VERSION, + runtime_metadata(ADAPTER_VERSION), + BrowserProtocolCapability::Navigation, + failing_dispatch as DispatchFn, + )?; + + assert!(dispatch_was_called()); + assert_eq!(dispatch_result, Err("adapter-failure")); + Ok(()) +}