From 4a7f46f7969d5d419b6d1d45600b87e65d625914 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 08:32:39 +0900 Subject: [PATCH 01/20] test(core): require typed browser operation capability binding --- ...owser_typed_operation_protocol_dispatch.rs | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 crates/originweave-core/tests/browser_typed_operation_protocol_dispatch.rs diff --git a/crates/originweave-core/tests/browser_typed_operation_protocol_dispatch.rs b/crates/originweave-core/tests/browser_typed_operation_protocol_dispatch.rs new file mode 100644 index 000000000..0202f8ce4 --- /dev/null +++ b/crates/originweave-core/tests/browser_typed_operation_protocol_dispatch.rs @@ -0,0 +1,140 @@ +use std::{cell::Cell, error::Error, io}; + +use originweave_core::{ + BrowserAuthorityRegistry, BrowserContextDispatchTarget, BrowserContextOriginDispatchTarget, + BrowserContextOriginEpochDispatchTarget, BrowserContextProtocolDispatchError, + BrowserProtocolAdapterDescriptor, BrowserProtocolCapability, BrowserProtocolKind, + BrowserProtocolOperation, BrowserProtocolRuntimeMetadata, DocumentEpoch, Origin, + 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"; + +fn descriptor( + capabilities: &[BrowserProtocolCapability], +) -> Result> { + Ok(BrowserProtocolAdapterDescriptor::new( + BrowserProtocolKind::WebDriverBiDi, + ORIGINWEAVE_PROTOCOL_VERSION, + ADAPTER_VERSION, + PROTOCOL_REVISION, + BROWSER_REVISION, + capabilities, + )?) +} + +fn runtime_metadata() -> BrowserProtocolRuntimeMetadata<'static> { + BrowserProtocolRuntimeMetadata::new( + BrowserProtocolKind::WebDriverBiDi, + ADAPTER_VERSION, + PROTOCOL_REVISION, + BROWSER_REVISION, + ) +} + +fn origin(value: &str) -> Result> { + Origin::parse(value).map_err(|_| { + Box::new(io::Error::new( + io::ErrorKind::InvalidInput, + "invalid controlled origin fixture", + )) as Box + }) +} + +fn typed_input_target<'a>( + registry: &mut BrowserAuthorityRegistry, + expected_origin: &'a Origin, +) -> Result, Box> { + let session = registry.register_session("webdriver-session")?; + let context = registry.register_context(session, "top-level-context")?; + let epoch = registry.bind_context_origin(session, context, expected_origin)?; + Ok(BrowserContextOriginEpochDispatchTarget::new( + BrowserContextOriginDispatchTarget::new( + BrowserContextDispatchTarget::new(session, context), + expected_origin, + ), + epoch, + )) +} + +#[test] +fn typed_operations_map_to_exact_transport_capabilities() { + assert_eq!( + BrowserProtocolOperation::Navigate.required_capability(), + BrowserProtocolCapability::Navigation + ); + assert_eq!( + BrowserProtocolOperation::ObserveSemantics.required_capability(), + BrowserProtocolCapability::SemanticObservation + ); + assert_eq!( + BrowserProtocolOperation::DispatchTypedInput.required_capability(), + BrowserProtocolCapability::TypedInput + ); + assert_eq!( + BrowserProtocolOperation::ObserveNetwork.required_capability(), + BrowserProtocolCapability::NetworkObservation + ); +} + +#[test] +fn typed_operation_dispatch_derives_the_required_capability() -> Result<(), Box> { + let descriptor = descriptor(&[BrowserProtocolCapability::TypedInput])?; + let mut registry = BrowserAuthorityRegistry::new(); + let expected_origin = origin("https://app.example")?; + let target = typed_input_target(&mut registry, &expected_origin)?; + + let result = descriptor.dispatch_operation_if_context_origin_epoch_current( + ®istry, + target, + ORIGINWEAVE_PROTOCOL_VERSION, + runtime_metadata(), + BrowserProtocolOperation::DispatchTypedInput, + |validated: ValidatedBrowserProtocolUse, operation, epoch: DocumentEpoch| { + ( + operation, + validated.capability(), + epoch.value(), + ) + }, + )?; + + assert_eq!( + result, + ( + BrowserProtocolOperation::DispatchTypedInput, + BrowserProtocolCapability::TypedInput, + 1, + ) + ); + Ok(()) +} + +#[test] +fn unsupported_typed_operation_fails_before_dispatch_callback() -> Result<(), Box> { + let descriptor = descriptor(&[BrowserProtocolCapability::SemanticObservation])?; + let mut registry = BrowserAuthorityRegistry::new(); + let expected_origin = origin("https://app.example")?; + let target = typed_input_target(&mut registry, &expected_origin)?; + let dispatch_called = Cell::new(false); + + let result = descriptor.dispatch_operation_if_context_origin_epoch_current( + ®istry, + target, + ORIGINWEAVE_PROTOCOL_VERSION, + runtime_metadata(), + BrowserProtocolOperation::DispatchTypedInput, + |_validated, _operation, _epoch| dispatch_called.set(true), + ); + + assert!(matches!( + result, + Err(BrowserContextProtocolDispatchError::ProtocolValidation(_)) + )); + assert!(!dispatch_called.get()); + Ok(()) +} From c6d370e6d57b09bf8008f428583f5246cb44b57a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 08:52:39 +0900 Subject: [PATCH 02/20] style(core): format typed browser operation contract --- .../tests/browser_typed_operation_protocol_dispatch.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/crates/originweave-core/tests/browser_typed_operation_protocol_dispatch.rs b/crates/originweave-core/tests/browser_typed_operation_protocol_dispatch.rs index 0202f8ce4..6b555eba7 100644 --- a/crates/originweave-core/tests/browser_typed_operation_protocol_dispatch.rs +++ b/crates/originweave-core/tests/browser_typed_operation_protocol_dispatch.rs @@ -95,11 +95,7 @@ fn typed_operation_dispatch_derives_the_required_capability() -> Result<(), Box< runtime_metadata(), BrowserProtocolOperation::DispatchTypedInput, |validated: ValidatedBrowserProtocolUse, operation, epoch: DocumentEpoch| { - ( - operation, - validated.capability(), - epoch.value(), - ) + (operation, validated.capability(), epoch.value()) }, )?; From 7a6f0e281a8f5877238475bab735d1a82c99962a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 08:58:16 +0900 Subject: [PATCH 03/20] feat(core): derive adapter capability from typed browser operation --- .../src/browser_protocol_operation.rs | 73 +++++++++++++++++++ crates/originweave-core/src/lib.rs | 2 + 2 files changed, 75 insertions(+) create mode 100644 crates/originweave-core/src/browser_protocol_operation.rs diff --git a/crates/originweave-core/src/browser_protocol_operation.rs b/crates/originweave-core/src/browser_protocol_operation.rs new file mode 100644 index 000000000..5f7f62529 --- /dev/null +++ b/crates/originweave-core/src/browser_protocol_operation.rs @@ -0,0 +1,73 @@ +use crate::{ + BrowserAuthorityRegistry, BrowserContextOriginEpochDispatchTarget, + BrowserContextProtocolDispatchError, BrowserProtocolAdapterDescriptor, + BrowserProtocolCapability, BrowserProtocolRuntimeMetadata, DocumentEpoch, + OriginWeaveProtocolVersion, ValidatedBrowserProtocolUse, +}; + +/// One typed high-level browser operation whose transport prerequisite is derived internally. +/// +/// This value carries operation semantics only. It grants no browser session, context, origin, +/// policy, approval, secret, network, or adapter authority and does not perform browser I/O. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BrowserProtocolOperation { + /// Navigate one controlled browser context. + Navigate, + /// Produce one bounded semantic browser observation. + ObserveSemantics, + /// Dispatch policy-authorized typed browser input. + DispatchTypedInput, + /// Produce bounded browser-network evidence. + ObserveNetwork, +} + +impl BrowserProtocolOperation { + /// Return the exact adapter capability required for this typed operation. + #[must_use] + pub const fn required_capability(self) -> BrowserProtocolCapability { + match self { + Self::Navigate => BrowserProtocolCapability::Navigation, + Self::ObserveSemantics => BrowserProtocolCapability::SemanticObservation, + Self::DispatchTypedInput => BrowserProtocolCapability::TypedInput, + Self::ObserveNetwork => BrowserProtocolCapability::NetworkObservation, + } + } +} + +impl BrowserProtocolAdapterDescriptor { + /// Revalidate exact browser authority and derive adapter capability from one typed operation. + /// + /// The existing context/origin/document-epoch boundary runs first, followed by exact runtime + /// protocol metadata and the capability derived from `operation`. The callback can run only + /// after all prerequisites pass and receives the same typed operation together with the + /// non-cloneable protocol-use proof and freshly revalidated document epoch. + /// + /// This method does not authenticate Chromium or the adapter, grant policy approval, validate + /// semantic-node state, authorize destination/network activity, perform browser I/O, or prove + /// an action post-condition. The operation value itself grants no authority. + pub fn dispatch_operation_if_context_origin_epoch_current( + &self, + authority_registry: &BrowserAuthorityRegistry, + target: BrowserContextOriginEpochDispatchTarget<'_>, + required_originweave_protocol_version: OriginWeaveProtocolVersion, + runtime_metadata: BrowserProtocolRuntimeMetadata<'_>, + operation: BrowserProtocolOperation, + dispatch: F, + ) -> Result + where + F: FnOnce( + ValidatedBrowserProtocolUse, + BrowserProtocolOperation, + DocumentEpoch, + ) -> R, + { + self.dispatch_if_context_origin_epoch_current( + authority_registry, + target, + required_originweave_protocol_version, + runtime_metadata, + operation.required_capability(), + |validated, epoch| dispatch(validated, operation, epoch), + ) + } +} diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index f59ac5f8e..3992b8717 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -10,6 +10,7 @@ mod browser_protocol; mod browser_protocol_dispatch; +mod browser_protocol_operation; mod browser_registry; #[cfg(test)] mod browser_registry_coverage; @@ -27,6 +28,7 @@ pub use browser_protocol_dispatch::{ BrowserContextOriginEpochDispatchTarget, BrowserContextProtocolDispatchError, BrowserProtocolRuntimeMetadata, }; +pub use browser_protocol_operation::BrowserProtocolOperation; pub use browser_registry::{ BrowserAuthorityRegistry, BrowserRegistryError, MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES, }; From 4c4868fb890cf53bb4bb93c5e7cf584793442d6c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 09:00:29 +0900 Subject: [PATCH 04/20] style(core): format typed operation dispatch boundary --- crates/originweave-core/src/browser_protocol_operation.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/crates/originweave-core/src/browser_protocol_operation.rs b/crates/originweave-core/src/browser_protocol_operation.rs index 5f7f62529..6eaf32dd2 100644 --- a/crates/originweave-core/src/browser_protocol_operation.rs +++ b/crates/originweave-core/src/browser_protocol_operation.rs @@ -55,11 +55,7 @@ impl BrowserProtocolAdapterDescriptor { dispatch: F, ) -> Result where - F: FnOnce( - ValidatedBrowserProtocolUse, - BrowserProtocolOperation, - DocumentEpoch, - ) -> R, + F: FnOnce(ValidatedBrowserProtocolUse, BrowserProtocolOperation, DocumentEpoch) -> R, { self.dispatch_if_context_origin_epoch_current( authority_registry, From 9acbac713b4b3221c2a577e8b84df5ecff7d0a7e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 09:04:14 +0900 Subject: [PATCH 05/20] docs(changelog): record typed browser operation binding --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28d0ef113..da31bfd48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - 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. - Context-bound browser-protocol dispatch composition that revalidates the exact OriginWeave browser session/context pair, carries the registry's current document epoch into the immediate callback, and separately requires the same exact runtime protocol metadata/capability checks before dispatch without claiming origin, destination, typed-input, transport-authentication, or post-condition authority. +- Typed browser-protocol operations that derive the exact adapter capability inside the strongest current context/origin/document-epoch dispatch boundary, preventing callers from independently selecting mismatched operation and capability metadata without performing browser I/O or granting policy 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. From 993529296d9e95e360571ed6cd7c11dea9b8a70f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 15:22:31 +0900 Subject: [PATCH 06/20] test: define buyer-visible browser operation vocabulary --- ...owser_typed_operation_protocol_dispatch.rs | 54 ++++++++++++------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/crates/originweave-core/tests/browser_typed_operation_protocol_dispatch.rs b/crates/originweave-core/tests/browser_typed_operation_protocol_dispatch.rs index 6b555eba7..6223b067d 100644 --- a/crates/originweave-core/tests/browser_typed_operation_protocol_dispatch.rs +++ b/crates/originweave-core/tests/browser_typed_operation_protocol_dispatch.rs @@ -62,23 +62,37 @@ fn typed_input_target<'a>( } #[test] -fn typed_operations_map_to_exact_transport_capabilities() { - assert_eq!( - BrowserProtocolOperation::Navigate.required_capability(), - BrowserProtocolCapability::Navigation - ); - assert_eq!( - BrowserProtocolOperation::ObserveSemantics.required_capability(), - BrowserProtocolCapability::SemanticObservation - ); - assert_eq!( - BrowserProtocolOperation::DispatchTypedInput.required_capability(), - BrowserProtocolCapability::TypedInput - ); - assert_eq!( - BrowserProtocolOperation::ObserveNetwork.required_capability(), - BrowserProtocolCapability::NetworkObservation - ); +fn buyer_visible_operations_map_to_exact_transport_capabilities() { + let expected = [ + ( + BrowserProtocolOperation::Navigate, + BrowserProtocolCapability::Navigation, + ), + ( + BrowserProtocolOperation::QueryNodes, + BrowserProtocolCapability::SemanticObservation, + ), + ( + BrowserProtocolOperation::ClickNode, + BrowserProtocolCapability::TypedInput, + ), + ( + BrowserProtocolOperation::TypeText, + BrowserProtocolCapability::TypedInput, + ), + ( + BrowserProtocolOperation::WaitForState, + BrowserProtocolCapability::SemanticObservation, + ), + ( + BrowserProtocolOperation::ObserveNetwork, + BrowserProtocolCapability::NetworkObservation, + ), + ]; + + for (operation, capability) in expected { + assert_eq!(operation.required_capability(), capability); + } } #[test] @@ -93,7 +107,7 @@ fn typed_operation_dispatch_derives_the_required_capability() -> Result<(), Box< target, ORIGINWEAVE_PROTOCOL_VERSION, runtime_metadata(), - BrowserProtocolOperation::DispatchTypedInput, + BrowserProtocolOperation::TypeText, |validated: ValidatedBrowserProtocolUse, operation, epoch: DocumentEpoch| { (operation, validated.capability(), epoch.value()) }, @@ -102,7 +116,7 @@ fn typed_operation_dispatch_derives_the_required_capability() -> Result<(), Box< assert_eq!( result, ( - BrowserProtocolOperation::DispatchTypedInput, + BrowserProtocolOperation::TypeText, BrowserProtocolCapability::TypedInput, 1, ) @@ -123,7 +137,7 @@ fn unsupported_typed_operation_fails_before_dispatch_callback() -> Result<(), Bo target, ORIGINWEAVE_PROTOCOL_VERSION, runtime_metadata(), - BrowserProtocolOperation::DispatchTypedInput, + BrowserProtocolOperation::ClickNode, |_validated, _operation, _epoch| dispatch_called.set(true), ); From 06635ab949ebf0be9501ca738272d7fca1dbd0ba Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 17:04:38 +0900 Subject: [PATCH 07/20] feat(core): bind buyer-visible browser operation vocabulary --- .../src/browser_protocol_operation.rs | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/crates/originweave-core/src/browser_protocol_operation.rs b/crates/originweave-core/src/browser_protocol_operation.rs index 6eaf32dd2..77edd06f1 100644 --- a/crates/originweave-core/src/browser_protocol_operation.rs +++ b/crates/originweave-core/src/browser_protocol_operation.rs @@ -5,30 +5,40 @@ use crate::{ OriginWeaveProtocolVersion, ValidatedBrowserProtocolUse, }; -/// One typed high-level browser operation whose transport prerequisite is derived internally. +/// One typed buyer-visible browser operation whose transport prerequisite is derived internally. /// /// This value carries operation semantics only. It grants no browser session, context, origin, -/// policy, approval, secret, network, or adapter authority and does not perform browser I/O. +/// semantic-node, policy, approval, secret, network, or adapter authority and does not perform +/// browser I/O. The vocabulary intentionally mirrors the bounded first Chromium vertical slice so +/// callers cannot hide materially different actions behind a coarse transport capability. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum BrowserProtocolOperation { /// Navigate one controlled browser context. Navigate, - /// Produce one bounded semantic browser observation. - ObserveSemantics, - /// Dispatch policy-authorized typed browser input. - DispatchTypedInput, + /// Query bounded semantic nodes from the current document. + QueryNodes, + /// Dispatch one policy-authorized click to a separately validated semantic node. + ClickNode, + /// Dispatch policy-authorized text input to a separately validated semantic node. + TypeText, + /// Observe bounded semantic state until a separately specified condition is satisfied. + WaitForState, /// Produce bounded browser-network evidence. ObserveNetwork, } impl BrowserProtocolOperation { /// Return the exact adapter capability required for this typed operation. + /// + /// This mapping is a transport prerequisite only. A matching capability does not authorize the + /// operation itself: node freshness, policy, approval, destination, and post-condition checks + /// remain independent authority boundaries. #[must_use] pub const fn required_capability(self) -> BrowserProtocolCapability { match self { Self::Navigate => BrowserProtocolCapability::Navigation, - Self::ObserveSemantics => BrowserProtocolCapability::SemanticObservation, - Self::DispatchTypedInput => BrowserProtocolCapability::TypedInput, + Self::QueryNodes | Self::WaitForState => BrowserProtocolCapability::SemanticObservation, + Self::ClickNode | Self::TypeText => BrowserProtocolCapability::TypedInput, Self::ObserveNetwork => BrowserProtocolCapability::NetworkObservation, } } From d1d5c736a1e50154f59d1ba718108d8610be8165 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 19:10:29 +0900 Subject: [PATCH 08/20] test(core): require bounded BiDi accessibility query --- .../webdriver_bidi_accessibility_query.rs | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 crates/originweave-core/tests/webdriver_bidi_accessibility_query.rs diff --git a/crates/originweave-core/tests/webdriver_bidi_accessibility_query.rs b/crates/originweave-core/tests/webdriver_bidi_accessibility_query.rs new file mode 100644 index 000000000..c3e632116 --- /dev/null +++ b/crates/originweave-core/tests/webdriver_bidi_accessibility_query.rs @@ -0,0 +1,111 @@ +use std::error::Error; + +use originweave_core::{ + MAX_BROWSER_ACCESSIBILITY_QUERY_NAME_BYTES, MAX_BROWSER_ACCESSIBILITY_QUERY_NODE_COUNT, + MAX_BROWSER_ACCESSIBILITY_QUERY_ROLE_BYTES, WEBDRIVER_BIDI_LOCATE_NODES_METHOD, + WebDriverBiDiAccessibilityQuery, WebDriverBiDiAccessibilityQueryError, +}; + +#[test] +fn accessibility_query_exposes_exact_bidi_method_and_locator_contract() -> Result<(), Box> { + let query = WebDriverBiDiAccessibilityQuery::new( + Some("textbox"), + Some("Task text"), + 32, + )?; + + assert_eq!(query.method(), WEBDRIVER_BIDI_LOCATE_NODES_METHOD); + assert_eq!(query.method(), "browsingContext.locateNodes"); + assert_eq!(query.locator_type(), "accessibility"); + assert_eq!(query.role(), Some("textbox")); + assert_eq!(query.name(), Some("Task text")); + assert_eq!(query.max_node_count(), 32); + Ok(()) +} + +#[test] +fn role_only_and_name_only_queries_are_valid() -> Result<(), Box> { + let role_only = WebDriverBiDiAccessibilityQuery::new(Some("button"), None, 1)?; + assert_eq!(role_only.role(), Some("button")); + assert_eq!(role_only.name(), None); + + let name_only = WebDriverBiDiAccessibilityQuery::new(None, Some("Submit task"), 1)?; + assert_eq!(name_only.role(), None); + assert_eq!(name_only.name(), Some("Submit task")); + Ok(()) +} + +#[test] +fn missing_or_empty_accessibility_locator_fields_fail_closed() { + assert_eq!( + WebDriverBiDiAccessibilityQuery::new(None, None, 1), + Err(WebDriverBiDiAccessibilityQueryError::MissingLocatorValue) + ); + assert_eq!( + WebDriverBiDiAccessibilityQuery::new(Some(""), None, 1), + Err(WebDriverBiDiAccessibilityQueryError::EmptyRole) + ); + assert_eq!( + WebDriverBiDiAccessibilityQuery::new(None, Some(""), 1), + Err(WebDriverBiDiAccessibilityQueryError::EmptyName) + ); +} + +#[test] +fn accessibility_locator_text_is_bounded_by_utf8_bytes() -> Result<(), Box> { + let maximum_role = "r".repeat(MAX_BROWSER_ACCESSIBILITY_QUERY_ROLE_BYTES); + let maximum_name = "n".repeat(MAX_BROWSER_ACCESSIBILITY_QUERY_NAME_BYTES); + let query = WebDriverBiDiAccessibilityQuery::new( + Some(&maximum_role), + Some(&maximum_name), + MAX_BROWSER_ACCESSIBILITY_QUERY_NODE_COUNT, + )?; + assert_eq!(query.role(), Some(maximum_role.as_str())); + assert_eq!(query.name(), Some(maximum_name.as_str())); + + let overlong_role = "r".repeat(MAX_BROWSER_ACCESSIBILITY_QUERY_ROLE_BYTES + 1); + assert_eq!( + WebDriverBiDiAccessibilityQuery::new(Some(&overlong_role), None, 1), + Err(WebDriverBiDiAccessibilityQueryError::RoleTooLong) + ); + + let overlong_name = "n".repeat(MAX_BROWSER_ACCESSIBILITY_QUERY_NAME_BYTES + 1); + assert_eq!( + WebDriverBiDiAccessibilityQuery::new(None, Some(&overlong_name), 1), + Err(WebDriverBiDiAccessibilityQueryError::NameTooLong) + ); + Ok(()) +} + +#[test] +fn accessibility_query_node_count_is_finite_and_nonzero() { + assert_eq!( + WebDriverBiDiAccessibilityQuery::new(Some("button"), None, 0), + Err(WebDriverBiDiAccessibilityQueryError::InvalidNodeCount) + ); + assert_eq!( + WebDriverBiDiAccessibilityQuery::new( + Some("button"), + None, + MAX_BROWSER_ACCESSIBILITY_QUERY_NODE_COUNT + 1, + ), + Err(WebDriverBiDiAccessibilityQueryError::InvalidNodeCount) + ); +} + +#[test] +fn accessibility_query_error_contract_is_source_free() { + let errors = [ + WebDriverBiDiAccessibilityQueryError::MissingLocatorValue, + WebDriverBiDiAccessibilityQueryError::EmptyRole, + WebDriverBiDiAccessibilityQueryError::RoleTooLong, + WebDriverBiDiAccessibilityQueryError::EmptyName, + WebDriverBiDiAccessibilityQueryError::NameTooLong, + WebDriverBiDiAccessibilityQueryError::InvalidNodeCount, + ]; + + for error in errors { + assert!(error.source().is_none()); + assert!(!error.to_string().is_empty()); + } +} From 341ccdcb0980240fcdb0e8630fd5b4475abd4960 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 19:12:49 +0900 Subject: [PATCH 09/20] style(core): apply canonical BiDi query test formatting --- .../tests/webdriver_bidi_accessibility_query.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/crates/originweave-core/tests/webdriver_bidi_accessibility_query.rs b/crates/originweave-core/tests/webdriver_bidi_accessibility_query.rs index c3e632116..34412314b 100644 --- a/crates/originweave-core/tests/webdriver_bidi_accessibility_query.rs +++ b/crates/originweave-core/tests/webdriver_bidi_accessibility_query.rs @@ -7,12 +7,9 @@ use originweave_core::{ }; #[test] -fn accessibility_query_exposes_exact_bidi_method_and_locator_contract() -> Result<(), Box> { - let query = WebDriverBiDiAccessibilityQuery::new( - Some("textbox"), - Some("Task text"), - 32, - )?; +fn accessibility_query_exposes_exact_bidi_method_and_locator_contract() -> Result<(), Box> +{ + let query = WebDriverBiDiAccessibilityQuery::new(Some("textbox"), Some("Task text"), 32)?; assert_eq!(query.method(), WEBDRIVER_BIDI_LOCATE_NODES_METHOD); assert_eq!(query.method(), "browsingContext.locateNodes"); From ad3b5a217f98c966f9c502936b308968fd7e76c0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 19:14:57 +0900 Subject: [PATCH 10/20] feat(core): bound WebDriver BiDi accessibility query --- .../src/browser_protocol_operation.rs | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) diff --git a/crates/originweave-core/src/browser_protocol_operation.rs b/crates/originweave-core/src/browser_protocol_operation.rs index 77edd06f1..f7a614988 100644 --- a/crates/originweave-core/src/browser_protocol_operation.rs +++ b/crates/originweave-core/src/browser_protocol_operation.rs @@ -1,3 +1,6 @@ +use std::error::Error; +use std::fmt::{Display, Formatter}; + use crate::{ BrowserAuthorityRegistry, BrowserContextOriginEpochDispatchTarget, BrowserContextProtocolDispatchError, BrowserProtocolAdapterDescriptor, @@ -5,6 +8,133 @@ use crate::{ OriginWeaveProtocolVersion, ValidatedBrowserProtocolUse, }; +/// Exact WebDriver BiDi method used by the bounded accessibility-query contract. +pub const WEBDRIVER_BIDI_LOCATE_NODES_METHOD: &str = "browsingContext.locateNodes"; +/// Maximum UTF-8 bytes accepted for one accessibility-role query value. +pub const MAX_BROWSER_ACCESSIBILITY_QUERY_ROLE_BYTES: usize = 64; +/// Maximum UTF-8 bytes accepted for one accessibility-name query value. +pub const MAX_BROWSER_ACCESSIBILITY_QUERY_NAME_BYTES: usize = 512; +/// Maximum number of nodes one bounded accessibility query may request. +pub const MAX_BROWSER_ACCESSIBILITY_QUERY_NODE_COUNT: u16 = 128; + +/// Fail-closed validation errors for one bounded WebDriver BiDi accessibility query. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum WebDriverBiDiAccessibilityQueryError { + /// Neither an accessibility role nor an accessible name was supplied. + MissingLocatorValue, + /// An explicitly supplied accessibility role was empty. + EmptyRole, + /// The accessibility role exceeded the local UTF-8 byte budget. + RoleTooLong, + /// An explicitly supplied accessible name was empty. + EmptyName, + /// The accessible name exceeded the local UTF-8 byte budget. + NameTooLong, + /// The requested node count was zero or exceeded the local result budget. + InvalidNodeCount, +} + +impl Display for WebDriverBiDiAccessibilityQueryError { + fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result { + let message = match self { + Self::MissingLocatorValue => "accessibility query requires a role or accessible name", + Self::EmptyRole => "accessibility query role must not be empty", + Self::RoleTooLong => "accessibility query role exceeds the local byte budget", + Self::EmptyName => "accessibility query name must not be empty", + Self::NameTooLong => "accessibility query name exceeds the local byte budget", + Self::InvalidNodeCount => "accessibility query node count is outside the local budget", + }; + formatter.write_str(message) + } +} + +impl Error for WebDriverBiDiAccessibilityQueryError {} + +/// Bounded transport parameters for WebDriver BiDi accessibility-node lookup. +/// +/// This value captures only the reviewed `browsingContext.locateNodes` accessibility-locator +/// parameters needed by the first Chromium observation slice. It accepts an exact role, an exact +/// accessible name, or both, together with a finite result count. Text budgets are OriginWeave +/// resource limits rather than claims about upstream protocol maxima. +/// +/// Construction grants no browser session, context, origin, semantic-node, policy, capability, or +/// network authority and performs no browser I/O. A trusted adapter must still bind the query to an +/// exact current browsing context through the separately reviewed authority and protocol boundary. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WebDriverBiDiAccessibilityQuery { + role: Option, + name: Option, + max_node_count: u16, +} + +impl WebDriverBiDiAccessibilityQuery { + /// Validate one bounded accessibility lookup request. + /// + /// Explicit empty values fail closed rather than being treated as absent. Role and name limits + /// are measured in UTF-8 bytes so later serialization cannot exceed the reviewed local budget + /// through multi-byte text. At least one selector value and one result slot are required. + pub fn new( + role: Option<&str>, + name: Option<&str>, + max_node_count: u16, + ) -> Result { + if role.is_some_and(str::is_empty) { + return Err(WebDriverBiDiAccessibilityQueryError::EmptyRole); + } + if role.is_some_and(|value| value.len() > MAX_BROWSER_ACCESSIBILITY_QUERY_ROLE_BYTES) { + return Err(WebDriverBiDiAccessibilityQueryError::RoleTooLong); + } + if name.is_some_and(str::is_empty) { + return Err(WebDriverBiDiAccessibilityQueryError::EmptyName); + } + if name.is_some_and(|value| value.len() > MAX_BROWSER_ACCESSIBILITY_QUERY_NAME_BYTES) { + return Err(WebDriverBiDiAccessibilityQueryError::NameTooLong); + } + if role.is_none() && name.is_none() { + return Err(WebDriverBiDiAccessibilityQueryError::MissingLocatorValue); + } + if max_node_count == 0 || max_node_count > MAX_BROWSER_ACCESSIBILITY_QUERY_NODE_COUNT { + return Err(WebDriverBiDiAccessibilityQueryError::InvalidNodeCount); + } + + Ok(Self { + role: role.map(str::to_owned), + name: name.map(str::to_owned), + max_node_count, + }) + } + + /// Return the exact upstream method associated with this query contract. + #[must_use] + pub const fn method(&self) -> &'static str { + WEBDRIVER_BIDI_LOCATE_NODES_METHOD + } + + /// Return the exact WebDriver BiDi locator type represented by this value. + #[must_use] + pub const fn locator_type(&self) -> &'static str { + "accessibility" + } + + /// Return the exact optional accessibility role requested by the caller. + #[must_use] + pub fn role(&self) -> Option<&str> { + self.role.as_deref() + } + + /// Return the exact optional accessible name requested by the caller. + #[must_use] + pub fn name(&self) -> Option<&str> { + self.name.as_deref() + } + + /// Return the finite maximum number of nodes requested from the adapter. + #[must_use] + pub const fn max_node_count(&self) -> u16 { + self.max_node_count + } +} + /// One typed buyer-visible browser operation whose transport prerequisite is derived internally. /// /// This value carries operation semantics only. It grants no browser session, context, origin, From 750348a499f87c4a057acf0f3cd69ad6537a0148 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 19:15:22 +0900 Subject: [PATCH 11/20] feat(core): export bounded BiDi accessibility query --- crates/originweave-core/src/lib.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index 3992b8717..4b809e315 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -28,7 +28,12 @@ pub use browser_protocol_dispatch::{ BrowserContextOriginEpochDispatchTarget, BrowserContextProtocolDispatchError, BrowserProtocolRuntimeMetadata, }; -pub use browser_protocol_operation::BrowserProtocolOperation; +pub use browser_protocol_operation::{ + BrowserProtocolOperation, WebDriverBiDiAccessibilityQuery, + WebDriverBiDiAccessibilityQueryError, MAX_BROWSER_ACCESSIBILITY_QUERY_NAME_BYTES, + MAX_BROWSER_ACCESSIBILITY_QUERY_NODE_COUNT, MAX_BROWSER_ACCESSIBILITY_QUERY_ROLE_BYTES, + WEBDRIVER_BIDI_LOCATE_NODES_METHOD, +}; pub use browser_registry::{ BrowserAuthorityRegistry, BrowserRegistryError, MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES, }; From 7fc113d26f864ac14e6793409d79328bbe954a2c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 19:16:46 +0900 Subject: [PATCH 12/20] style(core): apply canonical BiDi query exports --- crates/originweave-core/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index 4b809e315..4268f6d27 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -29,10 +29,10 @@ pub use browser_protocol_dispatch::{ BrowserProtocolRuntimeMetadata, }; pub use browser_protocol_operation::{ - BrowserProtocolOperation, WebDriverBiDiAccessibilityQuery, - WebDriverBiDiAccessibilityQueryError, MAX_BROWSER_ACCESSIBILITY_QUERY_NAME_BYTES, + BrowserProtocolOperation, MAX_BROWSER_ACCESSIBILITY_QUERY_NAME_BYTES, MAX_BROWSER_ACCESSIBILITY_QUERY_NODE_COUNT, MAX_BROWSER_ACCESSIBILITY_QUERY_ROLE_BYTES, - WEBDRIVER_BIDI_LOCATE_NODES_METHOD, + WEBDRIVER_BIDI_LOCATE_NODES_METHOD, WebDriverBiDiAccessibilityQuery, + WebDriverBiDiAccessibilityQueryError, }; pub use browser_registry::{ BrowserAuthorityRegistry, BrowserRegistryError, MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES, From fd66444e8d0b9275d109ffa581ebdcfed0d4e301 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 19:22:26 +0900 Subject: [PATCH 13/20] test(core): require minimal BiDi node serialization --- .../tests/webdriver_bidi_accessibility_query.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/crates/originweave-core/tests/webdriver_bidi_accessibility_query.rs b/crates/originweave-core/tests/webdriver_bidi_accessibility_query.rs index 34412314b..26f1d23e0 100644 --- a/crates/originweave-core/tests/webdriver_bidi_accessibility_query.rs +++ b/crates/originweave-core/tests/webdriver_bidi_accessibility_query.rs @@ -3,7 +3,9 @@ use std::error::Error; use originweave_core::{ MAX_BROWSER_ACCESSIBILITY_QUERY_NAME_BYTES, MAX_BROWSER_ACCESSIBILITY_QUERY_NODE_COUNT, MAX_BROWSER_ACCESSIBILITY_QUERY_ROLE_BYTES, WEBDRIVER_BIDI_LOCATE_NODES_METHOD, - WebDriverBiDiAccessibilityQuery, WebDriverBiDiAccessibilityQueryError, + WEBDRIVER_BIDI_QUERY_INCLUDE_SHADOW_TREE, WEBDRIVER_BIDI_QUERY_MAX_DOM_DEPTH, + WEBDRIVER_BIDI_QUERY_MAX_OBJECT_DEPTH, WebDriverBiDiAccessibilityQuery, + WebDriverBiDiAccessibilityQueryError, }; #[test] @@ -20,6 +22,19 @@ fn accessibility_query_exposes_exact_bidi_method_and_locator_contract() -> Resul Ok(()) } +#[test] +fn accessibility_query_fixes_minimal_serialization_options() -> Result<(), Box> { + let query = WebDriverBiDiAccessibilityQuery::new(Some("textbox"), None, 8)?; + + assert_eq!(WEBDRIVER_BIDI_QUERY_MAX_DOM_DEPTH, 0); + assert_eq!(WEBDRIVER_BIDI_QUERY_MAX_OBJECT_DEPTH, 0); + assert_eq!(WEBDRIVER_BIDI_QUERY_INCLUDE_SHADOW_TREE, "none"); + assert_eq!(query.serialization_max_dom_depth(), 0); + assert_eq!(query.serialization_max_object_depth(), 0); + assert_eq!(query.serialization_include_shadow_tree(), "none"); + Ok(()) +} + #[test] fn role_only_and_name_only_queries_are_valid() -> Result<(), Box> { let role_only = WebDriverBiDiAccessibilityQuery::new(Some("button"), None, 1)?; From 75356e0fc10ed2486a463381c48b6e37fc50ff92 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 19:23:49 +0900 Subject: [PATCH 14/20] feat(core): minimize BiDi node serialization surface --- .../src/browser_protocol_operation.rs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/crates/originweave-core/src/browser_protocol_operation.rs b/crates/originweave-core/src/browser_protocol_operation.rs index f7a614988..e7e1662d4 100644 --- a/crates/originweave-core/src/browser_protocol_operation.rs +++ b/crates/originweave-core/src/browser_protocol_operation.rs @@ -16,6 +16,12 @@ pub const MAX_BROWSER_ACCESSIBILITY_QUERY_ROLE_BYTES: usize = 64; pub const MAX_BROWSER_ACCESSIBILITY_QUERY_NAME_BYTES: usize = 512; /// Maximum number of nodes one bounded accessibility query may request. pub const MAX_BROWSER_ACCESSIBILITY_QUERY_NODE_COUNT: u16 = 128; +/// Fixed DOM serialization depth for the first bounded BiDi node-query slice. +pub const WEBDRIVER_BIDI_QUERY_MAX_DOM_DEPTH: u16 = 0; +/// Fixed object serialization depth for the first bounded BiDi node-query slice. +pub const WEBDRIVER_BIDI_QUERY_MAX_OBJECT_DEPTH: u16 = 0; +/// Fixed shadow-tree serialization mode for the first bounded BiDi node-query slice. +pub const WEBDRIVER_BIDI_QUERY_INCLUDE_SHADOW_TREE: &str = "none"; /// Fail-closed validation errors for one bounded WebDriver BiDi accessibility query. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -57,6 +63,11 @@ impl Error for WebDriverBiDiAccessibilityQueryError {} /// accessible name, or both, together with a finite result count. Text budgets are OriginWeave /// resource limits rather than claims about upstream protocol maxima. /// +/// The first slice also fixes WebDriver BiDi serialization to zero DOM depth, zero object depth, +/// and no shadow-tree expansion. Those settings intentionally minimize the remote-value surface a +/// future transport adapter may request; they do not themselves parse, validate, or authorize any +/// returned node. +/// /// Construction grants no browser session, context, origin, semantic-node, policy, capability, or /// network authority and performs no browser I/O. A trusted adapter must still bind the query to an /// exact current browsing context through the separately reviewed authority and protocol boundary. @@ -116,6 +127,24 @@ impl WebDriverBiDiAccessibilityQuery { "accessibility" } + /// Return the fixed maximum DOM serialization depth for returned remote nodes. + #[must_use] + pub const fn serialization_max_dom_depth(&self) -> u16 { + WEBDRIVER_BIDI_QUERY_MAX_DOM_DEPTH + } + + /// Return the fixed maximum object serialization depth for returned remote nodes. + #[must_use] + pub const fn serialization_max_object_depth(&self) -> u16 { + WEBDRIVER_BIDI_QUERY_MAX_OBJECT_DEPTH + } + + /// Return the fixed shadow-tree serialization mode for returned remote nodes. + #[must_use] + pub const fn serialization_include_shadow_tree(&self) -> &'static str { + WEBDRIVER_BIDI_QUERY_INCLUDE_SHADOW_TREE + } + /// Return the exact optional accessibility role requested by the caller. #[must_use] pub fn role(&self) -> Option<&str> { From 8a18184935e3ae9219b5de7e049a87776b8ef0f7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 19:24:05 +0900 Subject: [PATCH 15/20] feat(core): export minimal BiDi serialization limits --- 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 4268f6d27..e43cb26bf 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -31,8 +31,9 @@ pub use browser_protocol_dispatch::{ pub use browser_protocol_operation::{ BrowserProtocolOperation, MAX_BROWSER_ACCESSIBILITY_QUERY_NAME_BYTES, MAX_BROWSER_ACCESSIBILITY_QUERY_NODE_COUNT, MAX_BROWSER_ACCESSIBILITY_QUERY_ROLE_BYTES, - WEBDRIVER_BIDI_LOCATE_NODES_METHOD, WebDriverBiDiAccessibilityQuery, - WebDriverBiDiAccessibilityQueryError, + WEBDRIVER_BIDI_LOCATE_NODES_METHOD, WEBDRIVER_BIDI_QUERY_INCLUDE_SHADOW_TREE, + WEBDRIVER_BIDI_QUERY_MAX_DOM_DEPTH, WEBDRIVER_BIDI_QUERY_MAX_OBJECT_DEPTH, + WebDriverBiDiAccessibilityQuery, WebDriverBiDiAccessibilityQueryError, }; pub use browser_registry::{ BrowserAuthorityRegistry, BrowserRegistryError, MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES, From b649787d6ea151ab622c98329b408765ff6a808f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 20:12:46 +0900 Subject: [PATCH 16/20] test(bidi): reject over-budget locateNodes results --- .../tests/webdriver_bidi_accessibility_query.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/crates/originweave-core/tests/webdriver_bidi_accessibility_query.rs b/crates/originweave-core/tests/webdriver_bidi_accessibility_query.rs index 26f1d23e0..720c3596f 100644 --- a/crates/originweave-core/tests/webdriver_bidi_accessibility_query.rs +++ b/crates/originweave-core/tests/webdriver_bidi_accessibility_query.rs @@ -105,6 +105,19 @@ fn accessibility_query_node_count_is_finite_and_nonzero() { ); } +#[test] +fn accessibility_query_revalidates_returned_node_count() -> Result<(), Box> { + let query = WebDriverBiDiAccessibilityQuery::new(Some("button"), None, 2)?; + + assert_eq!(query.validate_result_count(0), Ok(())); + assert_eq!(query.validate_result_count(2), Ok(())); + assert_eq!( + query.validate_result_count(3), + Err(WebDriverBiDiAccessibilityQueryError::ResultNodeCountExceeded) + ); + Ok(()) +} + #[test] fn accessibility_query_error_contract_is_source_free() { let errors = [ @@ -114,6 +127,7 @@ fn accessibility_query_error_contract_is_source_free() { WebDriverBiDiAccessibilityQueryError::EmptyName, WebDriverBiDiAccessibilityQueryError::NameTooLong, WebDriverBiDiAccessibilityQueryError::InvalidNodeCount, + WebDriverBiDiAccessibilityQueryError::ResultNodeCountExceeded, ]; for error in errors { From 7faa2c6bb3760b7cd67cdae45e5f29ff749aea7f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 20:14:08 +0900 Subject: [PATCH 17/20] fix(bidi): revalidate locateNodes result budget --- .../src/browser_protocol_operation.rs | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/crates/originweave-core/src/browser_protocol_operation.rs b/crates/originweave-core/src/browser_protocol_operation.rs index e7e1662d4..d921d2d6b 100644 --- a/crates/originweave-core/src/browser_protocol_operation.rs +++ b/crates/originweave-core/src/browser_protocol_operation.rs @@ -38,6 +38,8 @@ pub enum WebDriverBiDiAccessibilityQueryError { NameTooLong, /// The requested node count was zero or exceeded the local result budget. InvalidNodeCount, + /// The untrusted adapter returned more nodes than the reviewed request budget allowed. + ResultNodeCountExceeded, } impl Display for WebDriverBiDiAccessibilityQueryError { @@ -49,6 +51,9 @@ impl Display for WebDriverBiDiAccessibilityQueryError { Self::EmptyName => "accessibility query name must not be empty", Self::NameTooLong => "accessibility query name exceeds the local byte budget", Self::InvalidNodeCount => "accessibility query node count is outside the local budget", + Self::ResultNodeCountExceeded => { + "accessibility query result exceeds the requested node budget" + } }; formatter.write_str(message) } @@ -65,8 +70,8 @@ impl Error for WebDriverBiDiAccessibilityQueryError {} /// /// The first slice also fixes WebDriver BiDi serialization to zero DOM depth, zero object depth, /// and no shadow-tree expansion. Those settings intentionally minimize the remote-value surface a -/// future transport adapter may request; they do not themselves parse, validate, or authorize any -/// returned node. +/// future transport adapter may request. The adapter must additionally revalidate the returned +/// node count against this exact query before it retains or normalizes any returned node data. /// /// Construction grants no browser session, context, origin, semantic-node, policy, capability, or /// network authority and performs no browser I/O. A trusted adapter must still bind the query to an @@ -162,6 +167,21 @@ impl WebDriverBiDiAccessibilityQuery { pub const fn max_node_count(&self) -> u16 { self.max_node_count } + + /// Revalidate an untrusted `locateNodes` result count against this exact request budget. + /// + /// A conforming browser is expected to honor `maxNodeCount`, but an adapter boundary must not + /// treat that expectation as resource authority. Zero through the requested maximum are valid; + /// any larger returned array fails closed before later node normalization or retention. + pub fn validate_result_count( + &self, + returned_node_count: usize, + ) -> Result<(), WebDriverBiDiAccessibilityQueryError> { + if returned_node_count > usize::from(self.max_node_count) { + return Err(WebDriverBiDiAccessibilityQueryError::ResultNodeCountExceeded); + } + Ok(()) + } } /// One typed buyer-visible browser operation whose transport prerequisite is derived internally. From d99181bb0b7c893c2eba6eaf7c13723dc196ac53 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 20:14:47 +0900 Subject: [PATCH 18/20] docs(changelog): record BiDi result budget revalidation --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index da31bfd48..861c04eef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - 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. - Context-bound browser-protocol dispatch composition that revalidates the exact OriginWeave browser session/context pair, carries the registry's current document epoch into the immediate callback, and separately requires the same exact runtime protocol metadata/capability checks before dispatch without claiming origin, destination, typed-input, transport-authentication, or post-condition authority. - Typed browser-protocol operations that derive the exact adapter capability inside the strongest current context/origin/document-epoch dispatch boundary, preventing callers from independently selecting mismatched operation and capability metadata without performing browser I/O or granting policy authority. +- Bounded WebDriver BiDi accessibility-node query parameters for `browsingContext.locateNodes`, with reviewed selector/result budgets, minimal remote-value serialization options, and fail-closed revalidation that rejects an untrusted adapter response whose returned node count exceeds the exact request budget before node normalization or retention. - 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. From b45c40b3919d923cea1bdfadd7453ea17617dc11 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 00:08:01 +0900 Subject: [PATCH 19/20] test(bidi): require bounded remote node references --- .../webdriver_bidi_remote_node_reference.rs | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 crates/originweave-core/tests/webdriver_bidi_remote_node_reference.rs diff --git a/crates/originweave-core/tests/webdriver_bidi_remote_node_reference.rs b/crates/originweave-core/tests/webdriver_bidi_remote_node_reference.rs new file mode 100644 index 000000000..cb0fd2d47 --- /dev/null +++ b/crates/originweave-core/tests/webdriver_bidi_remote_node_reference.rs @@ -0,0 +1,79 @@ +use std::error::Error; + +use originweave_core::{ + MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES, WEBDRIVER_BIDI_NODE_REMOTE_VALUE_TYPE, + WebDriverBiDiRemoteNodeReference, WebDriverBiDiRemoteNodeReferenceError, +}; + +#[test] +fn remote_node_reference_requires_exact_node_type_and_shared_id() -> Result<(), Box> { + let reference = WebDriverBiDiRemoteNodeReference::new("node", Some("shared-node-42"))?; + + assert_eq!(reference.remote_type(), WEBDRIVER_BIDI_NODE_REMOTE_VALUE_TYPE); + assert_eq!(reference.remote_type(), "node"); + assert_eq!(reference.shared_id(), "shared-node-42"); + Ok(()) +} + +#[test] +fn remote_node_reference_rejects_non_node_remote_values() { + assert_eq!( + WebDriverBiDiRemoteNodeReference::new("object", Some("shared-node-42")), + Err(WebDriverBiDiRemoteNodeReferenceError::UnexpectedRemoteType) + ); +} + +#[test] +fn remote_node_reference_requires_a_usable_shared_id() { + assert_eq!( + WebDriverBiDiRemoteNodeReference::new("node", None), + Err(WebDriverBiDiRemoteNodeReferenceError::MissingSharedId) + ); + assert_eq!( + WebDriverBiDiRemoteNodeReference::new("node", Some("")), + Err(WebDriverBiDiRemoteNodeReferenceError::InvalidSharedId) + ); +} + +#[test] +fn remote_node_reference_reuses_the_registry_identifier_budget() -> Result<(), Box> { + let maximum = "n".repeat(MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES); + let reference = WebDriverBiDiRemoteNodeReference::new("node", Some(&maximum))?; + assert_eq!(reference.shared_id(), maximum); + + let overlong = "n".repeat(MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES + 1); + assert_eq!( + WebDriverBiDiRemoteNodeReference::new("node", Some(&overlong)), + Err(WebDriverBiDiRemoteNodeReferenceError::InvalidSharedId) + ); + Ok(()) +} + +#[test] +fn remote_node_reference_bounds_multibyte_shared_ids_by_utf8_bytes() -> Result<(), Box> { + let exact = "한".repeat(MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES / "한".len()); + let reference = WebDriverBiDiRemoteNodeReference::new("node", Some(&exact))?; + assert_eq!(reference.shared_id(), exact); + + let overlong = format!("{exact}한"); + assert!(overlong.len() > MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES); + assert_eq!( + WebDriverBiDiRemoteNodeReference::new("node", Some(&overlong)), + Err(WebDriverBiDiRemoteNodeReferenceError::InvalidSharedId) + ); + Ok(()) +} + +#[test] +fn remote_node_reference_error_contract_is_source_free() { + let errors = [ + WebDriverBiDiRemoteNodeReferenceError::UnexpectedRemoteType, + WebDriverBiDiRemoteNodeReferenceError::MissingSharedId, + WebDriverBiDiRemoteNodeReferenceError::InvalidSharedId, + ]; + + for error in errors { + assert!(error.source().is_none()); + assert!(!error.to_string().is_empty()); + } +} From 64d763004dc096f3bf8a3aea4cff47e07fbe60f8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 16 Aug 2026 15:12:54 +0000 Subject: [PATCH 20/20] feat(core): admit bounded BiDi remote node references Close the locateNodes result-item gap by requiring the exact node remote type and a usable sharedId within the registry identifier budget before an untrusted adapter value can be retained as a later handle. Co-authored-by: Seongho Bae --- CHANGELOG.md | 1 + .../src/browser_protocol_operation.rs | 89 ++++++++++++++++++- crates/originweave-core/src/lib.rs | 8 +- .../webdriver_bidi_remote_node_reference.rs | 5 +- docs/doctoring.md | 2 + docs/doctoring/browser-agent-protocols.md | 2 +- 6 files changed, 101 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 861c04eef..4ca0efeed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Context-bound browser-protocol dispatch composition that revalidates the exact OriginWeave browser session/context pair, carries the registry's current document epoch into the immediate callback, and separately requires the same exact runtime protocol metadata/capability checks before dispatch without claiming origin, destination, typed-input, transport-authentication, or post-condition authority. - Typed browser-protocol operations that derive the exact adapter capability inside the strongest current context/origin/document-epoch dispatch boundary, preventing callers from independently selecting mismatched operation and capability metadata without performing browser I/O or granting policy authority. - Bounded WebDriver BiDi accessibility-node query parameters for `browsingContext.locateNodes`, with reviewed selector/result budgets, minimal remote-value serialization options, and fail-closed revalidation that rejects an untrusted adapter response whose returned node count exceeds the exact request budget before node normalization or retention. +- Fail-closed WebDriver BiDi `script.NodeRemoteValue` admission that requires the exact remote type `node` and a non-empty `sharedId` within the same UTF-8 identifier budget as browser session and context identifiers, so an untrusted `locateNodes` item cannot be retained as a later typed-input handle without a usable shared node identity. - 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_operation.rs b/crates/originweave-core/src/browser_protocol_operation.rs index d921d2d6b..b2f61e8f6 100644 --- a/crates/originweave-core/src/browser_protocol_operation.rs +++ b/crates/originweave-core/src/browser_protocol_operation.rs @@ -5,7 +5,7 @@ use crate::{ BrowserAuthorityRegistry, BrowserContextOriginEpochDispatchTarget, BrowserContextProtocolDispatchError, BrowserProtocolAdapterDescriptor, BrowserProtocolCapability, BrowserProtocolRuntimeMetadata, DocumentEpoch, - OriginWeaveProtocolVersion, ValidatedBrowserProtocolUse, + MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES, OriginWeaveProtocolVersion, ValidatedBrowserProtocolUse, }; /// Exact WebDriver BiDi method used by the bounded accessibility-query contract. @@ -184,6 +184,93 @@ impl WebDriverBiDiAccessibilityQuery { } } +/// Exact WebDriver BiDi remote-value type admitted as a later node handle. +pub const WEBDRIVER_BIDI_NODE_REMOTE_VALUE_TYPE: &str = "node"; + +/// Fail-closed validation errors for one untrusted WebDriver BiDi node remote value. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum WebDriverBiDiRemoteNodeReferenceError { + /// The remote value type was not the exact `node` type. + UnexpectedRemoteType, + /// The remote value omitted `sharedId`. + MissingSharedId, + /// The shared identifier was empty or exceeded the local UTF-8 byte budget. + InvalidSharedId, +} + +impl Display for WebDriverBiDiRemoteNodeReferenceError { + fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result { + let message = match self { + Self::UnexpectedRemoteType => { + "remote node reference type must be the exact node remote value" + } + Self::MissingSharedId => "remote node reference requires a shared id", + Self::InvalidSharedId => { + "remote node reference shared id is empty or exceeds the local byte budget" + } + }; + formatter.write_str(message) + } +} + +impl Error for WebDriverBiDiRemoteNodeReferenceError {} + +/// Bounded admission of one untrusted WebDriver BiDi `script.NodeRemoteValue`. +/// +/// The 1 June 2026 WebDriver BiDi Working Draft returns `script.NodeRemoteValue` items from +/// `browsingContext.locateNodes`. Those values have a required `type` of `node` and an optional +/// `sharedId`. OriginWeave admits a result item only when the type is exactly `node` and a +/// non-empty `sharedId` fits the same UTF-8 identifier budget used by browser session and context +/// identifiers. +/// +/// Requiring `sharedId` is a local fail-closed policy: the Working Draft permits omitting it, but a +/// later typed-input adapter cannot refer to the same node across realms without that shared +/// identity. Construction grants no session, context, origin, document-epoch, semantic-node, +/// policy, or network authority and performs no browser I/O. The admitted value remains an +/// untrusted transport handle until a separately reviewed authority boundary binds it. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WebDriverBiDiRemoteNodeReference { + shared_id: String, +} + +impl WebDriverBiDiRemoteNodeReference { + /// Admit one untrusted locateNodes remote value as a later node handle. + /// + /// The remote type is checked first so a non-node value cannot be retained even when it carries + /// a well-formed shared identifier. A missing shared identifier is distinct from an empty or + /// over-budget identifier so callers can distinguish protocol omission from local + /// resource-budget rejection. + pub fn new( + remote_type: &str, + shared_id: Option<&str>, + ) -> Result { + if remote_type != WEBDRIVER_BIDI_NODE_REMOTE_VALUE_TYPE { + return Err(WebDriverBiDiRemoteNodeReferenceError::UnexpectedRemoteType); + } + let Some(shared_id) = shared_id else { + return Err(WebDriverBiDiRemoteNodeReferenceError::MissingSharedId); + }; + if shared_id.is_empty() || shared_id.len() > MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES { + return Err(WebDriverBiDiRemoteNodeReferenceError::InvalidSharedId); + } + Ok(Self { + shared_id: shared_id.to_owned(), + }) + } + + /// Return the exact admitted WebDriver BiDi remote-value type. + #[must_use] + pub const fn remote_type(&self) -> &'static str { + WEBDRIVER_BIDI_NODE_REMOTE_VALUE_TYPE + } + + /// Return the exact shared node identifier admitted from the remote value. + #[must_use] + pub fn shared_id(&self) -> &str { + &self.shared_id + } +} + /// One typed buyer-visible browser operation whose transport prerequisite is derived internally. /// /// This value carries operation semantics only. It grants no browser session, context, origin, diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index e43cb26bf..49a8ca0ff 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -31,9 +31,11 @@ pub use browser_protocol_dispatch::{ pub use browser_protocol_operation::{ BrowserProtocolOperation, MAX_BROWSER_ACCESSIBILITY_QUERY_NAME_BYTES, MAX_BROWSER_ACCESSIBILITY_QUERY_NODE_COUNT, MAX_BROWSER_ACCESSIBILITY_QUERY_ROLE_BYTES, - WEBDRIVER_BIDI_LOCATE_NODES_METHOD, WEBDRIVER_BIDI_QUERY_INCLUDE_SHADOW_TREE, - WEBDRIVER_BIDI_QUERY_MAX_DOM_DEPTH, WEBDRIVER_BIDI_QUERY_MAX_OBJECT_DEPTH, - WebDriverBiDiAccessibilityQuery, WebDriverBiDiAccessibilityQueryError, + WEBDRIVER_BIDI_LOCATE_NODES_METHOD, WEBDRIVER_BIDI_NODE_REMOTE_VALUE_TYPE, + WEBDRIVER_BIDI_QUERY_INCLUDE_SHADOW_TREE, WEBDRIVER_BIDI_QUERY_MAX_DOM_DEPTH, + WEBDRIVER_BIDI_QUERY_MAX_OBJECT_DEPTH, WebDriverBiDiAccessibilityQuery, + WebDriverBiDiAccessibilityQueryError, WebDriverBiDiRemoteNodeReference, + WebDriverBiDiRemoteNodeReferenceError, }; pub use browser_registry::{ BrowserAuthorityRegistry, BrowserRegistryError, MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES, diff --git a/crates/originweave-core/tests/webdriver_bidi_remote_node_reference.rs b/crates/originweave-core/tests/webdriver_bidi_remote_node_reference.rs index cb0fd2d47..2aa8a377a 100644 --- a/crates/originweave-core/tests/webdriver_bidi_remote_node_reference.rs +++ b/crates/originweave-core/tests/webdriver_bidi_remote_node_reference.rs @@ -9,7 +9,10 @@ use originweave_core::{ fn remote_node_reference_requires_exact_node_type_and_shared_id() -> Result<(), Box> { let reference = WebDriverBiDiRemoteNodeReference::new("node", Some("shared-node-42"))?; - assert_eq!(reference.remote_type(), WEBDRIVER_BIDI_NODE_REMOTE_VALUE_TYPE); + assert_eq!( + reference.remote_type(), + WEBDRIVER_BIDI_NODE_REMOTE_VALUE_TYPE + ); assert_eq!(reference.remote_type(), "node"); assert_eq!(reference.shared_id(), "shared-node-42"); Ok(()) diff --git a/docs/doctoring.md b/docs/doctoring.md index 75c107ef0..28faa6d81 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -8,6 +8,8 @@ This document records external evidence that changes OriginWeave architecture, t The 1 June 2026 WebDriver BiDi Working Draft defines a bidirectional remote-control protocol, events, commands, and user contexts. Because it remains a W3C Working Draft, OriginWeave places BiDi behind a versioned adapter and Web Platform Tests-derived contract tests rather than make it the internal authority model. +The same Working Draft defines `script.NodeRemoteValue` with a required `type` of `node` and an optional `sharedId`, and `browsingContext.locateNodes` returns a list of those remote values. A `script.SharedReference` is the protocol's node identity across realms; when both `handle` and `sharedId` are present, the protocol respects only `sharedId`. OriginWeave therefore admits a `locateNodes` result item only when the remote type is exactly `node` and a non-empty `sharedId` fits the same UTF-8 identifier budget used by browser session and context identifiers. Requiring `sharedId` is a local fail-closed policy, not a claim that the Working Draft makes the field mandatory. The admitted value is an untrusted transport handle, not an OriginWeave session, context, origin, or document-epoch node identity. + ### Browser origin equivalence The WHATWG URL host parser and Chromium canonicalizer classify shortened decimal, integer, hexadecimal, legacy octal-looking, and mixed-component numeric hosts as IPv4 or broken IPv4 candidates rather than ordinary DNS names. Chromium's regression suite includes values such as `192`, `0xC0a80001`, `030052000001`, and mixed hexadecimal components. A non-final empty `0x` component can participate in Chromium's multi-part IPv4 truncation behavior, but a final `0x` label does not produce an IPv4 number because stripping its prefix leaves no digits; it remains a domain label. Chromium also warns that broken IP-like hosts must not be connected because another resolver could accept them. OriginWeave therefore admits only canonical dotted-decimal IPv4 into its policy origin type, rejects browser-special numeric spellings before DNS validation, and preserves final non-numeric DNS labels such as `0x`. diff --git a/docs/doctoring/browser-agent-protocols.md b/docs/doctoring/browser-agent-protocols.md index 5173a32e6..d31b6ba2f 100644 --- a/docs/doctoring/browser-agent-protocols.md +++ b/docs/doctoring/browser-agent-protocols.md @@ -46,7 +46,7 @@ The main [`docs/doctoring.md`](../doctoring.md) records the stable W3C PROV-O Re ## Product consequences -1. Version adapter contracts independently from OriginWeave session/context/action/evidence types. +1. Version adapter contracts independently from OriginWeave session/context/action/evidence types. Admit a BiDi `script.NodeRemoteValue` only as an untrusted transport handle when its type is exactly `node` and a usable `sharedId` is present; do not treat a realm-local `handle` or a missing shared identifier as OriginWeave node authority. 2. Pin exact Chromium/CDP compatibility evidence at release time. 3. Keep WebDriver BiDi's Working Draft status visible in compatibility claims. 4. Keep WebMCP experimental/optional and propagate untrusted-content semantics.