From a84d7f3979759c1583c5a8ef6ffd3140746de3f2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 10:21:19 -0700 Subject: [PATCH 1/5] test(core): require bounded BiDi pointer click command --- .../webdriver_bidi_pointer_click_command.rs | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 crates/originweave-core/tests/webdriver_bidi_pointer_click_command.rs diff --git a/crates/originweave-core/tests/webdriver_bidi_pointer_click_command.rs b/crates/originweave-core/tests/webdriver_bidi_pointer_click_command.rs new file mode 100644 index 000000000..dd32ce80c --- /dev/null +++ b/crates/originweave-core/tests/webdriver_bidi_pointer_click_command.rs @@ -0,0 +1,82 @@ +use std::error::Error; + +use originweave_core::{ + MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES, MAX_WEBDRIVER_BIDI_COMMAND_ID, + UNICODE_PROTOCOL_FORMAT_INJECTION_CHARS, WEBDRIVER_BIDI_PERFORM_ACTIONS_METHOD, + WebDriverBiDiPointerClickCommand, WebDriverBiDiPointerClickCommandError, + WebDriverBiDiRemoteNodeReference, +}; + +#[test] +fn pointer_click_command_serializes_exact_bidi_envelope() -> Result<(), Box> { + let node = WebDriverBiDiRemoteNodeReference::new("node", Some("shared-node-42"))?; + let command = WebDriverBiDiPointerClickCommand::new(42, "context-a", &node)?; + + assert_eq!(command.command_id(), 42); + assert_eq!(command.method(), WEBDRIVER_BIDI_PERFORM_ACTIONS_METHOD); + assert_eq!(command.browsing_context(), "context-a"); + assert_eq!( + command.as_json(), + r#"{"id":42,"method":"input.performActions","params":{"context":"context-a","actions":[{"type":"pointer","id":"originweave-mouse","parameters":{"pointerType":"mouse"},"actions":[{"type":"pointerMove","x":0,"y":0,"origin":{"type":"element","element":{"sharedId":"shared-node-42"}}},{"type":"pointerDown","button":0},{"type":"pointerUp","button":0}]}]}}"# + ); + Ok(()) +} + +#[test] +fn pointer_click_command_rejects_invalid_command_and_context() -> Result<(), Box> { + let node = WebDriverBiDiRemoteNodeReference::new("node", Some("shared-node-42"))?; + + assert_eq!( + WebDriverBiDiPointerClickCommand::new( + MAX_WEBDRIVER_BIDI_COMMAND_ID + 1, + "context-a", + &node, + ), + Err(WebDriverBiDiPointerClickCommandError::InvalidCommandId) + ); + + for invalid in ["", "context with space", "context\nline"] { + assert_eq!( + WebDriverBiDiPointerClickCommand::new(1, invalid, &node), + Err(WebDriverBiDiPointerClickCommandError::InvalidBrowsingContext) + ); + } + + let overlong = "c".repeat(MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES + 1); + assert_eq!( + WebDriverBiDiPointerClickCommand::new(1, &overlong, &node), + Err(WebDriverBiDiPointerClickCommandError::InvalidBrowsingContext) + ); + for character in UNICODE_PROTOCOL_FORMAT_INJECTION_CHARS { + let context = format!("context{character}"); + assert_eq!( + WebDriverBiDiPointerClickCommand::new(1, &context, &node), + Err(WebDriverBiDiPointerClickCommandError::InvalidBrowsingContext) + ); + } + Ok(()) +} + +#[test] +fn pointer_click_command_accepts_maximum_context_and_escaped_shared_id() +-> Result<(), Box> { + let context = "c".repeat(MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES); + let node = WebDriverBiDiRemoteNodeReference::new("node", Some(r#"node-"quoted"\path"#))?; + let command = + WebDriverBiDiPointerClickCommand::new(MAX_WEBDRIVER_BIDI_COMMAND_ID, &context, &node)?; + + assert!(command.as_json().contains(&context)); + assert!(command.as_json().contains(r#"node-\"quoted\"\\path"#)); + Ok(()) +} + +#[test] +fn pointer_click_command_error_contract_is_source_free() { + for error in [ + WebDriverBiDiPointerClickCommandError::InvalidCommandId, + WebDriverBiDiPointerClickCommandError::InvalidBrowsingContext, + ] { + assert!(error.source().is_none()); + assert!(!error.to_string().is_empty()); + } +} From 96277e4f5fcd957a91e7db7e3a69c18d849963c1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 10:25:41 -0700 Subject: [PATCH 2/5] feat(core): restore bounded BiDi pointer click command --- .../src/webdriver_bidi_command.rs | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/crates/originweave-core/src/webdriver_bidi_command.rs b/crates/originweave-core/src/webdriver_bidi_command.rs index 9a019cc45..074472348 100644 --- a/crates/originweave-core/src/webdriver_bidi_command.rs +++ b/crates/originweave-core/src/webdriver_bidi_command.rs @@ -10,6 +10,103 @@ use crate::{ /// Maximum WebDriver BiDi command identifier representable by the protocol `js-uint` type. pub const MAX_WEBDRIVER_BIDI_COMMAND_ID: u64 = 9_007_199_254_740_991; +/// WebDriver BiDi method used for one bounded typed pointer action sequence. +pub const WEBDRIVER_BIDI_PERFORM_ACTIONS_METHOD: &str = "input.performActions"; + +/// Fail-closed validation errors for one serialized WebDriver BiDi pointer click command. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum WebDriverBiDiPointerClickCommandError { + /// The command identifier exceeds WebDriver BiDi's unsigned safe-integer range. + InvalidCommandId, + /// The browsing-context identifier is empty, over budget, or contains disallowed text. + InvalidBrowsingContext, +} + +impl Display for WebDriverBiDiPointerClickCommandError { + fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result { + formatter.write_str(match self { + Self::InvalidCommandId => "WebDriver BiDi command id is outside the js-uint range", + Self::InvalidBrowsingContext => { + "WebDriver BiDi browsing context is empty, over budget, or contains disallowed text" + } + }) + } +} + +impl Error for WebDriverBiDiPointerClickCommandError {} + +/// Deterministic command for one primary-button click on an admitted remote node. +/// +/// The fixed mouse action sequence moves to the element origin, presses button zero, and releases +/// button zero. Construction accepts an already admitted remote node reference and does not grant +/// browser-session, context, origin, document-epoch, policy, approval, or Agent authority. A trusted +/// adapter must bind this inert command to current authority before transport. +#[derive(Debug, PartialEq, Eq)] +pub struct WebDriverBiDiPointerClickCommand { + command_id: u64, + browsing_context: String, + json: String, +} + +impl WebDriverBiDiPointerClickCommand { + /// Validate and serialize one bounded `input.performActions` pointer click command. + pub fn new( + command_id: u64, + browsing_context: &str, + node: &crate::WebDriverBiDiRemoteNodeReference, + ) -> Result { + if command_id > MAX_WEBDRIVER_BIDI_COMMAND_ID { + return Err(WebDriverBiDiPointerClickCommandError::InvalidCommandId); + } + if browsing_context.is_empty() + || browsing_context.len() > MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES + || contains_disallowed_protocol_text(browsing_context, false) + { + return Err(WebDriverBiDiPointerClickCommandError::InvalidBrowsingContext); + } + + let mut json = String::from("{\"id\":"); + json.push_str(&command_id.to_string()); + json.push_str(",\"method\":\""); + json.push_str(WEBDRIVER_BIDI_PERFORM_ACTIONS_METHOD); + json.push_str("\",\"params\":{\"context\":"); + push_json_string(&mut json, browsing_context); + json.push_str(",\"actions\":[{\"type\":\"pointer\",\"id\":\"originweave-mouse\",\"parameters\":{\"pointerType\":\"mouse\"},\"actions\":[{\"type\":\"pointerMove\",\"x\":0,\"y\":0,\"origin\":{\"type\":\"element\",\"element\":{\"sharedId\":"); + push_json_string(&mut json, node.shared_id()); + json.push_str("}}},{\"type\":\"pointerDown\",\"button\":0},{\"type\":\"pointerUp\",\"button\":0}]}]}}"); + + Ok(Self { + command_id, + browsing_context: browsing_context.to_owned(), + json, + }) + } + + /// Return the validated command identifier. + #[must_use] + pub const fn command_id(&self) -> u64 { + self.command_id + } + + /// Return the exact WebDriver BiDi method serialized by this command. + #[must_use] + pub const fn method(&self) -> &'static str { + WEBDRIVER_BIDI_PERFORM_ACTIONS_METHOD + } + + /// Return the exact validated browsing-context identifier. + #[must_use] + pub fn browsing_context(&self) -> &str { + &self.browsing_context + } + + /// Return the deterministic JSON command envelope. + #[must_use] + pub fn as_json(&self) -> &str { + &self.json + } +} + /// Fail-closed validation errors for one serialized WebDriver BiDi `locateNodes` command. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum WebDriverBiDiLocateNodesCommandError { From 18d662ee4dcf904616abba88ceef9993800caaed Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 10:26:05 -0700 Subject: [PATCH 3/5] feat(core): export bounded BiDi pointer click command --- crates/originweave-core/src/lib.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index b9d69c8b0..423654544 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -71,10 +71,11 @@ pub use browser_registry::{ pub use contracts::*; pub use webdriver_bidi_command::{ CorrelatedWebDriverBiDiLocateNodesResponse, MAX_WEBDRIVER_BIDI_COMMAND_ID, - ValidatedWebDriverBiDiLocateNodesResponse, WebDriverBiDiCommandResponseKind, - WebDriverBiDiLocateNodesCommand, WebDriverBiDiLocateNodesCommandError, - WebDriverBiDiLocateNodesResponseCorrelationError, - WebDriverBiDiLocateNodesResponseEnvelopeError, + ValidatedWebDriverBiDiLocateNodesResponse, WEBDRIVER_BIDI_PERFORM_ACTIONS_METHOD, + WebDriverBiDiCommandResponseKind, WebDriverBiDiLocateNodesCommand, + WebDriverBiDiLocateNodesCommandError, WebDriverBiDiLocateNodesResponseCorrelationError, + WebDriverBiDiLocateNodesResponseEnvelopeError, WebDriverBiDiPointerClickCommand, + WebDriverBiDiPointerClickCommandError, }; pub use webdriver_bidi_error_code::WebDriverBiDiErrorCode; pub use webdriver_bidi_response_document::{ From 172a18a03343757dbb6334654edff128e09cd945 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 02:41:36 +0900 Subject: [PATCH 4/5] fix(core): repair typed correlation exact-head regression --- ...ver_bidi_json_envelope_public_boundary_tests.rs | 14 +++++++------- .../src/webdriver_bidi_session_status_response.rs | 8 +++++--- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs b/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs index 6b6487cb0..879fb32bd 100644 --- a/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs +++ b/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs @@ -9,12 +9,12 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use crate::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeError, - WebDriverBiDiJsonEnvelopeKind, WebDriverBiDiSessionStatusResponseError, - WebDriverBiDiSessionStatusResult, WebDriverBiDiTcpConnectionPlan, - WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, - WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandKind, WebDriverBiDiJsonEnvelope, + WebDriverBiDiJsonEnvelopeError, WebDriverBiDiJsonEnvelopeKind, + WebDriverBiDiSessionStatusResponseError, WebDriverBiDiSessionStatusResult, + WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMessageAssembler, + WebDriverBiDiWebSocketMessageAssembly, WebDriverBiDiWebSocketTextMessage, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -134,7 +134,7 @@ fn public_json_envelope_unit_build_covers_fail_closed_json_edges() -> Result<(), fn public_session_status_empty_result_fails_closed_from_unit_build() -> Result<(), Box> { let text = read_text_over_loopback(EMPTY_STATUS_RESULT)?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); - correlation.register_command(7)?; + correlation.register_command_for(7, WebDriverBiDiCommandKind::SessionStatus)?; let parsed = WebDriverBiDiSessionStatusResult::parse_and_correlate(&text, &mut correlation); assert!(matches!( diff --git a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs index e08a7efbb..b1e5596ba 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs @@ -71,9 +71,11 @@ impl WebDriverBiDiSessionStatusResult { retain_validated_error_code(envelope.error_code()).and_then(|error_code| { let completed = correlation .correlate_response_for(&envelope, WebDriverBiDiCommandKind::SessionStatus) - .map_err(|source| { - WebDriverBiDiSessionStatusResponseError::Correlation { source } - })?; + .map_err( + |source| WebDriverBiDiSessionStatusResponseError::Correlation { + source, + }, + )?; Err( WebDriverBiDiSessionStatusResponseError::RemoteProtocolError { command_id: completed.command_id(), From d24b8dc627b088811cc8b04938ff770203a79d16 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 00:04:37 +0900 Subject: [PATCH 5/5] fix(network): encode valid BiDi response routing at parent boundary --- crates/originweave-network/src/lib.rs | 1 + .../src/webdriver_bidi_command_correlation.rs | 43 ++++++++--------- .../src/webdriver_bidi_json_envelope.rs | 48 ++++++++++++++----- 3 files changed, 58 insertions(+), 34 deletions(-) diff --git a/crates/originweave-network/src/lib.rs b/crates/originweave-network/src/lib.rs index 1d3fc2d24..1ff23ce4d 100644 --- a/crates/originweave-network/src/lib.rs +++ b/crates/originweave-network/src/lib.rs @@ -48,6 +48,7 @@ pub use webdriver_bidi_connection::{ WebDriverBiDiTcpConnection, WebDriverBiDiTcpConnectionError, WebDriverBiDiTcpConnectionEvidence, WebDriverBiDiTcpConnectionPlan, }; +pub(crate) use webdriver_bidi_json_envelope::WebDriverBiDiJsonEnvelopeRouting; pub use webdriver_bidi_json_envelope::{ MAX_WEBDRIVER_BIDI_JS_UINT, MAX_WEBDRIVER_BIDI_JSON_DEPTH, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeError, WebDriverBiDiJsonEnvelopeKind, diff --git a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs index 4dd9bcd78..6dfdc6ea3 100644 --- a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs +++ b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs @@ -1,6 +1,8 @@ use std::{collections::BTreeMap, error::Error, fmt}; -use crate::{MAX_WEBDRIVER_BIDI_JS_UINT, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeKind}; +use crate::{ + MAX_WEBDRIVER_BIDI_JS_UINT, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeRouting, +}; /// Maximum number of local WebDriver BiDi commands retained as outstanding at once. /// @@ -21,6 +23,8 @@ pub enum WebDriverBiDiCommandKind { SessionStatus, /// WebDriver BiDi `session.end`. SessionEnd, + /// WebDriver BiDi `input.performActions` pointer click. + PointerClick, } /// Outcome of a response after it has consumed the matching outstanding command identifier. @@ -183,30 +187,25 @@ impl WebDriverBiDiCommandCorrelation { envelope: &WebDriverBiDiJsonEnvelope, expected_kind: WebDriverBiDiCommandKind, ) -> Result { - match envelope.kind() { - WebDriverBiDiJsonEnvelopeKind::Event => { + match envelope.routing() { + WebDriverBiDiJsonEnvelopeRouting::Event => { Err(WebDriverBiDiCommandCorrelationError::EventIsNotResponse) } - WebDriverBiDiJsonEnvelopeKind::Error => { - let Some(command_id) = envelope.command_id() else { - return Err(WebDriverBiDiCommandCorrelationError::UncorrelatableErrorResponse); - }; - self.complete( - command_id, - expected_kind, - WebDriverBiDiCorrelatedResponseOutcome::Error, - ) - } - WebDriverBiDiJsonEnvelopeKind::Success => { - let Some(command_id) = envelope.command_id() else { - return Err(WebDriverBiDiCommandCorrelationError::CommandNotOutstanding); - }; - self.complete( - command_id, - expected_kind, - WebDriverBiDiCorrelatedResponseOutcome::Success, - ) + WebDriverBiDiJsonEnvelopeRouting::CommandError { command_id: None } => { + Err(WebDriverBiDiCommandCorrelationError::UncorrelatableErrorResponse) } + WebDriverBiDiJsonEnvelopeRouting::CommandError { + command_id: Some(command_id), + } => self.complete( + command_id, + expected_kind, + WebDriverBiDiCorrelatedResponseOutcome::Error, + ), + WebDriverBiDiJsonEnvelopeRouting::CommandSuccess { command_id } => self.complete( + command_id, + expected_kind, + WebDriverBiDiCorrelatedResponseOutcome::Success, + ), } } diff --git a/crates/originweave-network/src/webdriver_bidi_json_envelope.rs b/crates/originweave-network/src/webdriver_bidi_json_envelope.rs index e99adf2c9..7e878980f 100644 --- a/crates/originweave-network/src/webdriver_bidi_json_envelope.rs +++ b/crates/originweave-network/src/webdriver_bidi_json_envelope.rs @@ -23,6 +23,18 @@ pub enum WebDriverBiDiJsonEnvelopeKind { Event, } +/// Structurally valid command/event routing retained after common-envelope validation. +/// +/// Keeping success ids inside the success variant prevents an impossible `success` + missing-id +/// state from leaking into downstream command correlation. Error ids remain optional because the +/// WebDriver BiDi protocol explicitly permits `null` there, while events carry no command id. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum WebDriverBiDiJsonEnvelopeRouting { + CommandSuccess { command_id: u64 }, + CommandError { command_id: Option }, + Event, +} + /// Credential-minimal classification of one complete WebDriver BiDi local-end JSON envelope. /// /// Result and parameter bodies are deliberately validated and discarded at this boundary. They @@ -30,8 +42,7 @@ pub enum WebDriverBiDiJsonEnvelopeKind { /// as generic JSON values that could become ambient browser or Agent authority. #[derive(Eq, PartialEq)] pub struct WebDriverBiDiJsonEnvelope { - kind: WebDriverBiDiJsonEnvelopeKind, - command_id: Option, + routing: WebDriverBiDiJsonEnvelopeRouting, method: Option, error_code: Option, } @@ -40,8 +51,8 @@ impl fmt::Debug for WebDriverBiDiJsonEnvelope { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter .debug_struct("WebDriverBiDiJsonEnvelope") - .field("kind", &self.kind) - .field("command_id", &self.command_id) + .field("kind", &self.kind()) + .field("command_id", &self.command_id()) .field("has_method", &self.method.is_some()) .field("has_error_code", &self.error_code.is_some()) .finish() @@ -73,7 +84,15 @@ impl WebDriverBiDiJsonEnvelope { /// Return the classified local-end envelope kind. #[must_use] pub const fn kind(&self) -> WebDriverBiDiJsonEnvelopeKind { - self.kind + match self.routing { + WebDriverBiDiJsonEnvelopeRouting::CommandSuccess { .. } => { + WebDriverBiDiJsonEnvelopeKind::Success + } + WebDriverBiDiJsonEnvelopeRouting::CommandError { .. } => { + WebDriverBiDiJsonEnvelopeKind::Error + } + WebDriverBiDiJsonEnvelopeRouting::Event => WebDriverBiDiJsonEnvelopeKind::Event, + } } /// Return the command identifier for success and correlatable error responses. @@ -81,7 +100,15 @@ impl WebDriverBiDiJsonEnvelope { /// Events and error responses whose protocol `id` is `null` return `None`. #[must_use] pub const fn command_id(&self) -> Option { - self.command_id + match self.routing { + WebDriverBiDiJsonEnvelopeRouting::CommandSuccess { command_id } => Some(command_id), + WebDriverBiDiJsonEnvelopeRouting::CommandError { command_id } => command_id, + WebDriverBiDiJsonEnvelopeRouting::Event => None, + } + } + + pub(crate) const fn routing(&self) -> WebDriverBiDiJsonEnvelopeRouting { + self.routing } /// Borrow the event method when this is an event envelope. @@ -208,8 +235,7 @@ impl TopLevelFields { let command_id = required_js_uint(self.id, "id")?; require_object(self.result, "result")?; Ok(WebDriverBiDiJsonEnvelope { - kind: WebDriverBiDiJsonEnvelopeKind::Success, - command_id: Some(command_id), + routing: WebDriverBiDiJsonEnvelopeRouting::CommandSuccess { command_id }, method: None, error_code: None, }) @@ -223,8 +249,7 @@ impl TopLevelFields { require_text_value(stacktrace, "stacktrace")?; } Ok(WebDriverBiDiJsonEnvelope { - kind: WebDriverBiDiJsonEnvelopeKind::Error, - command_id, + routing: WebDriverBiDiJsonEnvelopeRouting::CommandError { command_id }, method: None, error_code: Some(error_code), }) @@ -234,8 +259,7 @@ impl TopLevelFields { let method = required_text(self.method, "method")?; require_object(self.params, "params")?; Ok(WebDriverBiDiJsonEnvelope { - kind: WebDriverBiDiJsonEnvelopeKind::Event, - command_id: None, + routing: WebDriverBiDiJsonEnvelopeRouting::Event, method: Some(method), error_code: None, })