From 476978598850753485750fe1c73b8e3720ffcade Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 11:15:55 -0700 Subject: [PATCH 1/9] test(network): require correlated pointer click response --- .../webdriver_bidi_pointer_click_response.rs | 262 ++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_pointer_click_response.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_pointer_click_response.rs b/crates/originweave-network/tests/webdriver_bidi_pointer_click_response.rs new file mode 100644 index 000000000..12f3412c8 --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_pointer_click_response.rs @@ -0,0 +1,262 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::{ + WebDriverBiDiPointerClickCommand, WebDriverBiDiRemoteNodeReference, + WebDriverBiDiWebSocketEndpoint, +}; +use originweave_network::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiPointerClickResponseError, + WebDriverBiDiPointerClickResult, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler, + WebDriverBiDiWebSocketMessageAssembly, WebDriverBiDiWebSocketTextMessage, + send_webdriver_bidi_pointer_click, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; +const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; +const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; +const CLICK_SUCCESS_RESPONSE: &[u8] = + br#"{"type":"success","id":42,"result":{"vendorExtension":{"observed":false}}}"#; +const CLICK_REMOTE_ERROR_RESPONSE: &[u8] = + br#"{"type":"error","id":42,"error":"element click intercepted","message":"blocked"}"#; +const CLICK_UNKNOWN_ID_RESPONSE: &[u8] = + br#"{"type":"success","id":43,"result":{"vendorExtension":true}}"#; +const CLICK_MALFORMED_RESPONSE: &[u8] = br#"{"type":"success","id":42}"#; + +fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { + stream.set_read_timeout(Some(Duration::from_secs(2)))?; + let mut request = Vec::new(); + let mut buffer = [0_u8; 512]; + while !request.ends_with(b"\r\n\r\n") { + let count = stream.read(&mut buffer)?; + if count == 0 { + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "client opening request ended before the header terminator", + )); + } + request.extend_from_slice(&buffer[..count]); + } + Ok(()) +} + +fn read_masked_text_frame(stream: &mut TcpStream) -> io::Result> { + let mut header = [0_u8; 2]; + stream.read_exact(&mut header)?; + if header[0] != 0x81 || header[1] & 0x80 == 0 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "expected one final masked client text frame", + )); + } + + let marker = header[1] & 0x7f; + let length = match marker { + 0..=125 => usize::from(marker), + 126 => { + let mut extended = [0_u8; 2]; + stream.read_exact(&mut extended)?; + let length = usize::from(u16::from_be_bytes(extended)); + if length <= 125 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "client text frame used non-minimal 16-bit length encoding", + )); + } + length + } + 127 => { + let mut extended = [0_u8; 8]; + stream.read_exact(&mut extended)?; + let length = u64::from_be_bytes(extended); + if length <= u64::from(u16::MAX) || length > usize::MAX as u64 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "client text frame used invalid 64-bit length encoding", + )); + } + length as usize + } + _ => unreachable!(), + }; + + let mut mask = [0_u8; 4]; + stream.read_exact(&mut mask)?; + let mut payload = vec![0_u8; length]; + stream.read_exact(&mut payload)?; + for (index, byte) in payload.iter_mut().enumerate() { + *byte ^= mask[index % mask.len()]; + } + Ok(payload) +} + +fn send_click_and_read_response( + response: &'static [u8], +) -> Result< + ( + WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiCommandCorrelation, + ), + Box, +> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let expected = WebDriverBiDiPointerClickCommand::new( + 42, + "context-a", + &WebDriverBiDiRemoteNodeReference::new("node", Some("shared-node-42"))?, + )?; + let expected_json = expected.as_json().as_bytes().to_vec(); + + let server = thread::spawn(move || -> io::Result<()> { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + let command = read_masked_text_frame(&mut stream)?; + if command != expected_json { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "unexpected input.performActions pointer-click command", + )); + } + stream.write_all(&[0x81, response.len() as u8])?; + stream.write_all(response) + }); + + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? + .correlate_session_id(SESSION_ID)? + .into_explicit_connect_target()?; + let connection = + WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; + let key = WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?; + let established = WebDriverBiDiWebSocketHandshakePlan::new(connection, key)? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?; + + let command = WebDriverBiDiPointerClickCommand::new( + 42, + "context-a", + &WebDriverBiDiRemoteNodeReference::new("node", Some("shared-node-42"))?, + )?; + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + let established = send_webdriver_bidi_pointer_click( + &command, + established, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + Duration::from_millis(500), + )?; + + let (_established, frame) = established.read_frame(Duration::from_millis(500))?; + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); + let text = match assembler.push_frame(frame)? { + WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + other => { + return Err(io::Error::other(format!( + "pointer-click response produced unexpected assembly state: {other:?}" + )) + .into()); + } + }; + + server + .join() + .map_err(|_| io::Error::other("pointer-click response test server panicked"))??; + Ok((text, correlation)) +} + +#[test] +fn pointer_click_success_accepts_extensible_empty_result_and_consumes_exact_correlation() +-> Result<(), Box> { + let (text, mut correlation) = send_click_and_read_response(CLICK_SUCCESS_RESPONSE)?; + assert_eq!(correlation.outstanding_count(), 1); + + let result = WebDriverBiDiPointerClickResult::parse_and_correlate(&text, &mut correlation)?; + assert_eq!(result.command_id(), 42); + assert_eq!(correlation.outstanding_count(), 0); + Ok(()) +} + +#[test] +fn pointer_click_remote_error_consumes_only_the_correlated_command() -> Result<(), Box> { + let (text, mut correlation) = send_click_and_read_response(CLICK_REMOTE_ERROR_RESPONSE)?; + let parsed = WebDriverBiDiPointerClickResult::parse_and_correlate(&text, &mut correlation); + let error = match parsed { + Ok(_) => { + return Err( + io::Error::other("remote error was accepted as pointer-click success").into(), + ); + } + Err(error) => error, + }; + + assert!(matches!( + error, + WebDriverBiDiPointerClickResponseError::RemoteProtocolError { command_id: 42 } + )); + assert_eq!( + error.to_string(), + "WebDriver BiDi pointer-click returned a protocol error" + ); + assert!(error.source().is_none()); + assert_eq!(correlation.outstanding_count(), 0); + Ok(()) +} + +#[test] +fn malformed_pointer_click_envelope_fails_before_consuming_correlation() +-> Result<(), Box> { + let (text, mut correlation) = send_click_and_read_response(CLICK_MALFORMED_RESPONSE)?; + let parsed = WebDriverBiDiPointerClickResult::parse_and_correlate(&text, &mut correlation); + let error = match parsed { + Ok(_) => { + return Err(io::Error::other("malformed pointer-click response was accepted").into()); + } + Err(error) => error, + }; + + assert!(matches!( + error, + WebDriverBiDiPointerClickResponseError::Envelope { .. } + )); + assert_eq!( + error.to_string(), + "WebDriver BiDi pointer-click envelope is invalid" + ); + assert!(error.source().is_some()); + assert_eq!(correlation.outstanding_count(), 1); + Ok(()) +} + +#[test] +fn unknown_pointer_click_response_id_does_not_consume_the_outstanding_command() +-> Result<(), Box> { + let (text, mut correlation) = send_click_and_read_response(CLICK_UNKNOWN_ID_RESPONSE)?; + let parsed = WebDriverBiDiPointerClickResult::parse_and_correlate(&text, &mut correlation); + let error = match parsed { + Ok(_) => { + return Err(io::Error::other("unknown pointer-click response id was accepted").into()); + } + Err(error) => error, + }; + + assert!(matches!( + error, + WebDriverBiDiPointerClickResponseError::Correlation { .. } + )); + assert_eq!( + error.to_string(), + "WebDriver BiDi pointer-click response correlation failed" + ); + assert!(error.source().is_some()); + assert_eq!(correlation.outstanding_count(), 1); + Ok(()) +} From 3e7988cd45b8eea8b00975d499c10fe51137dbf8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 11:19:21 -0700 Subject: [PATCH 2/9] feat(network): correlate pointer click response --- .../webdriver_bidi_pointer_click_response.rs | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 crates/originweave-network/src/webdriver_bidi_pointer_click_response.rs diff --git a/crates/originweave-network/src/webdriver_bidi_pointer_click_response.rs b/crates/originweave-network/src/webdriver_bidi_pointer_click_response.rs new file mode 100644 index 000000000..41e8aa3e8 --- /dev/null +++ b/crates/originweave-network/src/webdriver_bidi_pointer_click_response.rs @@ -0,0 +1,137 @@ +use std::{error::Error, fmt}; + +use crate::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, + WebDriverBiDiCorrelatedResponseOutcome, WebDriverBiDiJsonEnvelope, + WebDriverBiDiJsonEnvelopeError, WebDriverBiDiWebSocketTextMessage, +}; + +/// Typed protocol acknowledgment for one correlated WebDriver BiDi `input.performActions` +/// primary-button pointer-click command. +/// +/// WebDriver BiDi defines `input.PerformActionsResult` as the extensible `EmptyResult` object. The +/// common local-end envelope parser already validates the complete JSON document and requires a +/// success `result` object, so this command-specific boundary intentionally retains no generic +/// result body and accepts extension members. This value proves only that the remote end returned a +/// correlated protocol success; it does not prove target activation, DOM mutation, navigation, or +/// any other OriginWeave post-condition. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct WebDriverBiDiPointerClickResult { + command_id: u64, +} + +impl WebDriverBiDiPointerClickResult { + /// Parse one bounded local-end message and consume its exact outstanding command on response. + /// + /// Complete JSON and common WebDriver BiDi envelope validation occur before correlation state + /// can be consumed. Successful responses retain only the matched command id. A correlatable + /// protocol-error response consumes its matching id and returns a typed remote failure, while + /// events, null-id errors, malformed envelopes, and unknown ids fail closed without consuming + /// unrelated outstanding state. + pub fn parse_and_correlate( + message: &WebDriverBiDiWebSocketTextMessage, + correlation: &mut WebDriverBiDiCommandCorrelation, + ) -> Result { + let envelope = WebDriverBiDiJsonEnvelope::parse(message) + .map_err(|source| WebDriverBiDiPointerClickResponseError::Envelope { source })?; + let completed = correlation + .correlate_response(&envelope) + .map_err(|source| WebDriverBiDiPointerClickResponseError::Correlation { source })?; + + match completed.outcome() { + WebDriverBiDiCorrelatedResponseOutcome::Success => Ok(Self { + command_id: completed.command_id(), + }), + WebDriverBiDiCorrelatedResponseOutcome::Error => { + Err(WebDriverBiDiPointerClickResponseError::RemoteProtocolError { + command_id: completed.command_id(), + }) + } + } + } + + /// Return the exact local command identifier consumed by this protocol acknowledgment. + #[must_use] + pub const fn command_id(&self) -> u64 { + self.command_id + } +} + +/// Fail-closed failures while admitting one typed WebDriver BiDi pointer-click response. +#[derive(Debug)] +pub enum WebDriverBiDiPointerClickResponseError { + /// Common local-end JSON envelope validation failed before correlation state was touched. + Envelope { + /// Exact common-envelope validation failure. + source: WebDriverBiDiJsonEnvelopeError, + }, + /// Exact command-response correlation failed without consuming unrelated state. + Correlation { + /// Exact typed correlation failure. + source: WebDriverBiDiCommandCorrelationError, + }, + /// The remote end returned a correlatable WebDriver BiDi protocol error for this command. + RemoteProtocolError { + /// Exact local command identifier consumed by the protocol-error response. + command_id: u64, + }, +} + +impl fmt::Display for WebDriverBiDiPointerClickResponseError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Envelope { .. } => { + formatter.write_str("WebDriver BiDi pointer-click envelope is invalid") + } + Self::Correlation { .. } => { + formatter.write_str("WebDriver BiDi pointer-click response correlation failed") + } + Self::RemoteProtocolError { .. } => { + formatter.write_str("WebDriver BiDi pointer-click returned a protocol error") + } + } + } +} + +impl Error for WebDriverBiDiPointerClickResponseError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::Envelope { source } => Some(source), + Self::Correlation { source } => Some(source), + Self::RemoteProtocolError { .. } => None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn response_errors_have_stable_messages_and_typed_sources() { + let envelope = WebDriverBiDiPointerClickResponseError::Envelope { + source: WebDriverBiDiJsonEnvelopeError::InvalidJson, + }; + assert_eq!( + envelope.to_string(), + "WebDriver BiDi pointer-click envelope is invalid" + ); + assert!(envelope.source().is_some()); + + let correlation = WebDriverBiDiPointerClickResponseError::Correlation { + source: WebDriverBiDiCommandCorrelationError::CommandNotOutstanding, + }; + assert_eq!( + correlation.to_string(), + "WebDriver BiDi pointer-click response correlation failed" + ); + assert!(correlation.source().is_some()); + + let remote = WebDriverBiDiPointerClickResponseError::RemoteProtocolError { command_id: 42 }; + assert_eq!( + remote.to_string(), + "WebDriver BiDi pointer-click returned a protocol error" + ); + assert!(remote.source().is_none()); + } +} From 19869286a98d08dd5ae821c7bf0afc0570d60742 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 11:19:43 -0700 Subject: [PATCH 3/9] feat(network): export pointer click response --- crates/originweave-network/src/lib.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/crates/originweave-network/src/lib.rs b/crates/originweave-network/src/lib.rs index 6aebd9c53..c4b2e0281 100644 --- a/crates/originweave-network/src/lib.rs +++ b/crates/originweave-network/src/lib.rs @@ -8,12 +8,13 @@ //! the RFC 6455 opening exchange, provides bounded masked client writes and //! unmasked server-frame reads, assembles bounded WebDriver BiDi text messages, //! classifies complete local-end JSON envelopes, tracks bounded command-response -//! correlation, transports a narrowly typed pointer click, sends narrowly typed -//! `session.status` and `session.end` commands, admits typed correlated status and -//! end responses, observes bounded peer Close or clean-EOF transport cessation, -//! and keeps protocol/transport evidence separate from explicit operational -//! teardown observations without exposing generic JSON bodies or granting browser, -//! TLS, policy, secret, process, profile, or Agent authority. +//! correlation, transports a narrowly typed pointer click, admits its typed +//! correlated protocol acknowledgment, sends narrowly typed `session.status` and +//! `session.end` commands, admits typed correlated status and end responses, +//! observes bounded peer Close or clean-EOF transport cessation, and keeps +//! protocol/transport evidence separate from explicit operational teardown +//! observations without exposing generic JSON bodies or granting browser, TLS, +//! policy, secret, process, profile, or Agent authority. #![forbid(unsafe_code)] #![deny(missing_docs)] @@ -22,6 +23,7 @@ mod connection; mod webdriver_bidi_command_correlation; mod webdriver_bidi_connection; mod webdriver_bidi_json_envelope; +mod webdriver_bidi_pointer_click_response; mod webdriver_bidi_pointer_click_transport; mod webdriver_bidi_session_end_command; mod webdriver_bidi_session_end_response; @@ -54,6 +56,9 @@ pub use webdriver_bidi_json_envelope::{ MAX_WEBDRIVER_BIDI_JS_UINT, MAX_WEBDRIVER_BIDI_JSON_DEPTH, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeError, WebDriverBiDiJsonEnvelopeKind, }; +pub use webdriver_bidi_pointer_click_response::{ + WebDriverBiDiPointerClickResponseError, WebDriverBiDiPointerClickResult, +}; pub use webdriver_bidi_pointer_click_transport::{ WebDriverBiDiPointerClickSendError, send_webdriver_bidi_pointer_click, }; From f22d81402553f9ebde2a5c07b294e5c6b6cb8e8b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 11:21:15 -0700 Subject: [PATCH 4/9] style(network): apply canonical pointer response formatting --- .../src/webdriver_bidi_pointer_click_response.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_pointer_click_response.rs b/crates/originweave-network/src/webdriver_bidi_pointer_click_response.rs index 41e8aa3e8..fb53a976d 100644 --- a/crates/originweave-network/src/webdriver_bidi_pointer_click_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_pointer_click_response.rs @@ -42,11 +42,11 @@ impl WebDriverBiDiPointerClickResult { WebDriverBiDiCorrelatedResponseOutcome::Success => Ok(Self { command_id: completed.command_id(), }), - WebDriverBiDiCorrelatedResponseOutcome::Error => { - Err(WebDriverBiDiPointerClickResponseError::RemoteProtocolError { + WebDriverBiDiCorrelatedResponseOutcome::Error => Err( + WebDriverBiDiPointerClickResponseError::RemoteProtocolError { command_id: completed.command_id(), - }) - } + }, + ), } } From 5fe865628a98dc9993f242a331cccbeec6bc5ffd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 22:16:50 +0900 Subject: [PATCH 5/9] fix(network): repair typed pointer response restack checks --- ...ver_bidi_json_envelope_public_boundary_tests.rs | 14 +++++++------- .../src/webdriver_bidi_pointer_click_response.rs | 5 ++--- .../src/webdriver_bidi_pointer_click_transport.rs | 4 ++-- .../src/webdriver_bidi_session_status_response.rs | 8 +++++--- .../webdriver_bidi_pointer_click_send_failures.rs | 8 ++++---- 5 files changed, 20 insertions(+), 19 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_pointer_click_response.rs b/crates/originweave-network/src/webdriver_bidi_pointer_click_response.rs index f251af400..980388968 100644 --- a/crates/originweave-network/src/webdriver_bidi_pointer_click_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_pointer_click_response.rs @@ -2,9 +2,8 @@ use std::{error::Error, fmt}; use crate::{ WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, - WebDriverBiDiCommandKind, WebDriverBiDiCorrelatedResponseOutcome, - WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeError, - WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiCommandKind, WebDriverBiDiCorrelatedResponseOutcome, WebDriverBiDiJsonEnvelope, + WebDriverBiDiJsonEnvelopeError, WebDriverBiDiWebSocketTextMessage, }; /// Typed protocol acknowledgment for one correlated WebDriver BiDi `input.performActions` diff --git a/crates/originweave-network/src/webdriver_bidi_pointer_click_transport.rs b/crates/originweave-network/src/webdriver_bidi_pointer_click_transport.rs index 429e76127..1c74717af 100644 --- a/crates/originweave-network/src/webdriver_bidi_pointer_click_transport.rs +++ b/crates/originweave-network/src/webdriver_bidi_pointer_click_transport.rs @@ -4,8 +4,8 @@ use originweave_core::WebDriverBiDiPointerClickCommand; use crate::{ WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, - WebDriverBiDiCommandKind, WebDriverBiDiWebSocketEstablished, - WebDriverBiDiWebSocketFrameError, WebDriverBiDiWebSocketMaskKey, + WebDriverBiDiCommandKind, WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketFrameError, + WebDriverBiDiWebSocketMaskKey, }; /// Fail-closed errors while transporting one already validated pointer-click command. 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(), diff --git a/crates/originweave-network/tests/webdriver_bidi_pointer_click_send_failures.rs b/crates/originweave-network/tests/webdriver_bidi_pointer_click_send_failures.rs index 9d4a0ee38..c2d9cffcc 100644 --- a/crates/originweave-network/tests/webdriver_bidi_pointer_click_send_failures.rs +++ b/crates/originweave-network/tests/webdriver_bidi_pointer_click_send_failures.rs @@ -12,10 +12,10 @@ use originweave_core::{ }; use originweave_network::{ WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, - WebDriverBiDiCommandKind, WebDriverBiDiPointerClickSendError, - WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, - WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMaskKey, send_webdriver_bidi_pointer_click, + WebDriverBiDiCommandKind, WebDriverBiDiPointerClickSendError, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketEstablished, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, + send_webdriver_bidi_pointer_click, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; From 8193fcd50125d9e9a43b4755e0f7626801b74374 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 00:09:28 +0900 Subject: [PATCH 6/9] test: reject pointer reply from replacement connection --- ...er_click_response_connection_provenance.rs | 188 ++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_pointer_click_response_connection_provenance.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_pointer_click_response_connection_provenance.rs b/crates/originweave-network/tests/webdriver_bidi_pointer_click_response_connection_provenance.rs new file mode 100644 index 000000000..814f4f9d9 --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_pointer_click_response_connection_provenance.rs @@ -0,0 +1,188 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{SocketAddr, TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::{ + WebDriverBiDiPointerClickCommand, WebDriverBiDiRemoteNodeReference, + WebDriverBiDiWebSocketEndpoint, +}; +use originweave_network::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiPointerClickResult, + WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler, + WebDriverBiDiWebSocketMessageAssembly, WebDriverBiDiWebSocketTextMessage, + send_webdriver_bidi_pointer_click, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; +const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; +const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; +const CLICK_SUCCESS_RESPONSE: &[u8] = + br#"{"type":"success","id":42,"result":{"vendorExtension":{"observed":false}}}"#; + +fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { + stream.set_read_timeout(Some(Duration::from_secs(2)))?; + let mut request = Vec::new(); + let mut buffer = [0_u8; 512]; + while !request.ends_with(b"\r\n\r\n") { + let count = stream.read(&mut buffer)?; + if count == 0 { + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "client opening request ended before the header terminator", + )); + } + request.extend_from_slice(&buffer[..count]); + } + Ok(()) +} + +fn read_masked_text_frame(stream: &mut TcpStream) -> io::Result> { + let mut header = [0_u8; 2]; + stream.read_exact(&mut header)?; + if header[0] != 0x81 || header[1] & 0x80 == 0 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "expected one final masked client text frame", + )); + } + let marker = header[1] & 0x7f; + let length = match marker { + 0..=125 => usize::from(marker), + 126 => { + let mut extended = [0_u8; 2]; + stream.read_exact(&mut extended)?; + usize::from(u16::from_be_bytes(extended)) + } + 127 => { + let mut extended = [0_u8; 8]; + stream.read_exact(&mut extended)?; + let length = u64::from_be_bytes(extended); + usize::try_from(length).map_err(|_| { + io::Error::new(io::ErrorKind::InvalidData, "pointer frame length exceeds usize") + })? + } + _ => unreachable!(), + }; + let mut mask = [0_u8; 4]; + stream.read_exact(&mut mask)?; + let mut payload = vec![0_u8; length]; + stream.read_exact(&mut payload)?; + for (index, byte) in payload.iter_mut().enumerate() { + *byte ^= mask[index % mask.len()]; + } + Ok(payload) +} + +fn establish(local_addr: SocketAddr) -> Result> { + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? + .correlate_session_id(SESSION_ID)? + .into_explicit_connect_target()?; + let connection = + WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; + Ok(WebDriverBiDiWebSocketHandshakePlan::new( + connection, + WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?, + )? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?) +} + +fn receive_replacement_response( + listener: TcpListener, +) -> Result> { + let local_addr = listener.local_addr()?; + let server = thread::spawn(move || -> io::Result<()> { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + stream.write_all(&[0x81, CLICK_SUCCESS_RESPONSE.len() as u8])?; + stream.write_all(CLICK_SUCCESS_RESPONSE) + }); + + let established = establish(local_addr)?; + let (_established, frame) = established.read_frame(Duration::from_millis(500))?; + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); + let text = match assembler.push_frame(frame)? { + WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + other => { + return Err(io::Error::other(format!( + "replacement pointer connection produced unexpected assembly state: {other:?}" + )) + .into()); + } + }; + server + .join() + .map_err(|_| io::Error::other("replacement pointer server panicked"))??; + Ok(text) +} + +#[test] +fn pointer_response_from_same_session_replacement_connection_cannot_consume_original_pending_command( +) -> Result<(), Box> { + let original_listener = TcpListener::bind(("127.0.0.1", 0))?; + let original_addr = original_listener.local_addr()?; + let expected = WebDriverBiDiPointerClickCommand::new( + 42, + "context-a", + &WebDriverBiDiRemoteNodeReference::new("node", Some("shared-node-42"))?, + )?; + let expected_json = expected.as_json().as_bytes().to_vec(); + let original_server = thread::spawn(move || -> io::Result<()> { + let (mut stream, _) = original_listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + let command = read_masked_text_frame(&mut stream)?; + if command != expected_json { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "unexpected pointer command on original connection", + )); + } + Ok(()) + }); + + let original = establish(original_addr)?; + let command = WebDriverBiDiPointerClickCommand::new( + 42, + "context-a", + &WebDriverBiDiRemoteNodeReference::new("node", Some("shared-node-42"))?, + )?; + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + let _original = send_webdriver_bidi_pointer_click( + &command, + original, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + Duration::from_millis(500), + )?; + original_server + .join() + .map_err(|_| io::Error::other("original pointer server panicked"))??; + assert_eq!(correlation.outstanding_count(), 1); + + let replacement_listener = TcpListener::bind(("127.0.0.1", 0))?; + let replacement_response = receive_replacement_response(replacement_listener)?; + let parsed = WebDriverBiDiPointerClickResult::parse_and_correlate( + &replacement_response, + &mut correlation, + ); + + assert!( + parsed.is_err(), + "same-session replacement connection unexpectedly consumed the original pointer command" + ); + assert_eq!( + correlation.outstanding_count(), + 1, + "foreign-connection rejection must leave the original pointer command pending" + ); + Ok(()) +} From d9396f0519aea4b6222af4a4bbfa004714dcace5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 00:17:38 +0900 Subject: [PATCH 7/9] test(network): require exact pointer receipt rejection and recovery Commit-Message-Assisted-by: Codex (via Codex) Signed-off-by: Seongho Bae --- ...er_click_response_connection_provenance.rs | 95 +++++++++++-------- 1 file changed, 55 insertions(+), 40 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_pointer_click_response_connection_provenance.rs b/crates/originweave-network/tests/webdriver_bidi_pointer_click_response_connection_provenance.rs index 814f4f9d9..3e04d1ded 100644 --- a/crates/originweave-network/tests/webdriver_bidi_pointer_click_response_connection_provenance.rs +++ b/crates/originweave-network/tests/webdriver_bidi_pointer_click_response_connection_provenance.rs @@ -11,12 +11,13 @@ use originweave_core::{ WebDriverBiDiWebSocketEndpoint, }; use originweave_network::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiPointerClickResult, - WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, - WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler, - WebDriverBiDiWebSocketMessageAssembly, WebDriverBiDiWebSocketTextMessage, - send_webdriver_bidi_pointer_click, + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, + WebDriverBiDiCommandKind, WebDriverBiDiPointerClickResponseError, + WebDriverBiDiPointerClickResult, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketEstablished, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, + WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, + WebDriverBiDiWebSocketTextMessage, send_webdriver_bidi_pointer_click, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -24,6 +25,8 @@ const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; const CLICK_SUCCESS_RESPONSE: &[u8] = br#"{"type":"success","id":42,"result":{"vendorExtension":{"observed":false}}}"#; +const CLICK_ERROR_RESPONSE: &[u8] = + br#"{"type":"error","id":42,"error":"invalid argument","message":"blocked","stacktrace":"remote"}"#; fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { stream.set_read_timeout(Some(Duration::from_secs(2)))?; @@ -64,7 +67,10 @@ fn read_masked_text_frame(stream: &mut TcpStream) -> io::Result> { stream.read_exact(&mut extended)?; let length = u64::from_be_bytes(extended); usize::try_from(length).map_err(|_| { - io::Error::new(io::ErrorKind::InvalidData, "pointer frame length exceeds usize") + io::Error::new( + io::ErrorKind::InvalidData, + "pointer frame length exceeds usize", + ) })? } _ => unreachable!(), @@ -94,19 +100,9 @@ fn establish(local_addr: SocketAddr) -> Result Result> { - let local_addr = listener.local_addr()?; - let server = thread::spawn(move || -> io::Result<()> { - let (mut stream, _) = listener.accept()?; - read_opening_request(&mut stream)?; - stream.write_all(OPENING_RESPONSE)?; - stream.write_all(&[0x81, CLICK_SUCCESS_RESPONSE.len() as u8])?; - stream.write_all(CLICK_SUCCESS_RESPONSE) - }); - - let established = establish(local_addr)?; let (_established, frame) = established.read_frame(Duration::from_millis(500))?; let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); let text = match assembler.push_frame(frame)? { @@ -118,15 +114,10 @@ fn receive_replacement_response( .into()); } }; - server - .join() - .map_err(|_| io::Error::other("replacement pointer server panicked"))??; Ok(text) } -#[test] -fn pointer_response_from_same_session_replacement_connection_cannot_consume_original_pending_command( -) -> Result<(), Box> { +fn assert_replacement_rejected(foreign_response: &'static [u8]) -> Result<(), Box> { let original_listener = TcpListener::bind(("127.0.0.1", 0))?; let original_addr = original_listener.local_addr()?; let expected = WebDriverBiDiPointerClickCommand::new( @@ -146,7 +137,13 @@ fn pointer_response_from_same_session_replacement_connection_cannot_consume_orig "unexpected pointer command on original connection", )); } - Ok(()) + let (mut replacement, _) = original_listener.accept()?; + read_opening_request(&mut replacement)?; + replacement.write_all(OPENING_RESPONSE)?; + replacement.write_all(&[0x81, foreign_response.len() as u8])?; + replacement.write_all(foreign_response)?; + stream.write_all(&[0x81, CLICK_SUCCESS_RESPONSE.len() as u8])?; + stream.write_all(CLICK_SUCCESS_RESPONSE) }); let original = establish(original_addr)?; @@ -156,33 +153,51 @@ fn pointer_response_from_same_session_replacement_connection_cannot_consume_orig &WebDriverBiDiRemoteNodeReference::new("node", Some("shared-node-42"))?, )?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); - let _original = send_webdriver_bidi_pointer_click( + correlation.register_command_for(43, WebDriverBiDiCommandKind::SessionStatus)?; + let original = send_webdriver_bidi_pointer_click( &command, original, &mut correlation, WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), Duration::from_millis(500), )?; - original_server - .join() - .map_err(|_| io::Error::other("original pointer server panicked"))??; - assert_eq!(correlation.outstanding_count(), 1); + assert_eq!(correlation.outstanding_count(), 2); - let replacement_listener = TcpListener::bind(("127.0.0.1", 0))?; - let replacement_response = receive_replacement_response(replacement_listener)?; + let replacement_response = read_response(establish(original_addr)?)?; let parsed = WebDriverBiDiPointerClickResult::parse_and_correlate( &replacement_response, &mut correlation, ); + let original_response = read_response(original)?; + original_server + .join() + .map_err(|_| io::Error::other("original pointer server panicked"))??; assert!( - parsed.is_err(), - "same-session replacement connection unexpectedly consumed the original pointer command" - ); - assert_eq!( - correlation.outstanding_count(), - 1, - "foreign-connection rejection must leave the original pointer command pending" + matches!( + parsed, + Err(WebDriverBiDiPointerClickResponseError::Correlation { + source: WebDriverBiDiCommandCorrelationError::ResponseConnectionMismatch { + command_id: 42 + } + }) + ), + "replacement response must fail for exact connection mismatch: {parsed:?}" ); + assert_eq!(correlation.outstanding_count(), 2); + let accepted = + WebDriverBiDiPointerClickResult::parse_and_correlate(&original_response, &mut correlation)?; + assert_eq!(accepted.command_id(), 42); + assert_eq!(correlation.outstanding_count(), 1); Ok(()) } + +#[test] +fn replacement_success_cannot_consume_original_pointer_command() -> Result<(), Box> { + assert_replacement_rejected(CLICK_SUCCESS_RESPONSE) +} + +#[test] +fn replacement_error_cannot_consume_original_pointer_command() -> Result<(), Box> { + assert_replacement_rejected(CLICK_ERROR_RESPONSE) +} From 588fe73167fb1eb0ad8db7a75acfe48bd5397bd2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 00:19:38 +0900 Subject: [PATCH 8/9] fix(network): require connection-bound pointer responses Commit-Message-Assisted-by: Codex (via Codex) Signed-off-by: Seongho Bae --- .../webdriver_bidi_pointer_click_response.rs | 14 ++++++++++---- .../webdriver_bidi_pointer_click_response.rs | 18 +++++++++--------- ...ter_click_response_connection_provenance.rs | 18 +++++++++--------- 3 files changed, 28 insertions(+), 22 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_pointer_click_response.rs b/crates/originweave-network/src/webdriver_bidi_pointer_click_response.rs index 980388968..4f4168db7 100644 --- a/crates/originweave-network/src/webdriver_bidi_pointer_click_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_pointer_click_response.rs @@ -3,7 +3,7 @@ use std::{error::Error, fmt}; use crate::{ WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, WebDriverBiDiCommandKind, WebDriverBiDiCorrelatedResponseOutcome, WebDriverBiDiJsonEnvelope, - WebDriverBiDiJsonEnvelopeError, WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiJsonEnvelopeError, WebDriverBiDiReceivedTextMessage, }; /// Typed protocol acknowledgment for one correlated WebDriver BiDi `input.performActions` @@ -23,19 +23,25 @@ pub struct WebDriverBiDiPointerClickResult { impl WebDriverBiDiPointerClickResult { /// Parse one bounded local-end message and consume its exact outstanding pointer-click command. /// + /// The sealed message must come from the exact connection registered by the pointer sender. + /// Missing connection provenance or a replacement connection leaves the command outstanding. /// Complete JSON and common WebDriver BiDi envelope validation occur before correlation state /// can be consumed. Successful responses retain only the matched command id. A correlatable /// protocol-error response consumes its matching pointer-click id and returns a typed remote /// failure. A response for another typed command family, an event, a null-id error, a malformed /// envelope, or an unknown id fails closed without consuming unrelated outstanding state. pub fn parse_and_correlate( - message: &WebDriverBiDiWebSocketTextMessage, + message: &WebDriverBiDiReceivedTextMessage, correlation: &mut WebDriverBiDiCommandCorrelation, ) -> Result { - let envelope = WebDriverBiDiJsonEnvelope::parse(message) + let envelope = WebDriverBiDiJsonEnvelope::parse(message.message()) .map_err(|source| WebDriverBiDiPointerClickResponseError::Envelope { source })?; let completed = correlation - .correlate_response_for(&envelope, WebDriverBiDiCommandKind::PointerClick) + .correlate_response_for_connection( + &envelope, + WebDriverBiDiCommandKind::PointerClick, + message.connection_generation(), + ) .map_err(|source| WebDriverBiDiPointerClickResponseError::Correlation { source })?; match completed.outcome() { diff --git a/crates/originweave-network/tests/webdriver_bidi_pointer_click_response.rs b/crates/originweave-network/tests/webdriver_bidi_pointer_click_response.rs index cdd3603b5..dce677dd6 100644 --- a/crates/originweave-network/tests/webdriver_bidi_pointer_click_response.rs +++ b/crates/originweave-network/tests/webdriver_bidi_pointer_click_response.rs @@ -11,11 +11,11 @@ use originweave_core::{ WebDriverBiDiWebSocketEndpoint, }; use originweave_network::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiPointerClickResponseError, - WebDriverBiDiPointerClickResult, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiCommandCorrelation, WebDriverBiDiConnectionMessageRead, + WebDriverBiDiPointerClickResponseError, WebDriverBiDiPointerClickResult, + WebDriverBiDiReceivedTextMessage, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler, - WebDriverBiDiWebSocketMessageAssembly, WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageReader, send_webdriver_bidi_pointer_click, }; @@ -101,7 +101,7 @@ fn send_click_and_read_response( response: &'static [u8], ) -> Result< ( - WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiReceivedTextMessage, WebDriverBiDiCommandCorrelation, ), Box, @@ -155,10 +155,10 @@ fn send_click_and_read_response( Duration::from_millis(500), )?; - let (_established, frame) = established.read_frame(Duration::from_millis(500))?; - let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); - let text = match assembler.push_frame(frame)? { - WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + let text = match WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + WebDriverBiDiConnectionMessageRead::Text { message, .. } => message, other => { return Err(io::Error::other(format!( "pointer-click response produced unexpected assembly state: {other:?}" diff --git a/crates/originweave-network/tests/webdriver_bidi_pointer_click_response_connection_provenance.rs b/crates/originweave-network/tests/webdriver_bidi_pointer_click_response_connection_provenance.rs index 3e04d1ded..ac5a2db8d 100644 --- a/crates/originweave-network/tests/webdriver_bidi_pointer_click_response_connection_provenance.rs +++ b/crates/originweave-network/tests/webdriver_bidi_pointer_click_response_connection_provenance.rs @@ -12,12 +12,12 @@ use originweave_core::{ }; use originweave_network::{ WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, - WebDriverBiDiCommandKind, WebDriverBiDiPointerClickResponseError, - WebDriverBiDiPointerClickResult, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiCommandKind, WebDriverBiDiConnectionMessageRead, + WebDriverBiDiPointerClickResponseError, WebDriverBiDiPointerClickResult, + WebDriverBiDiReceivedTextMessage, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, - WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, - WebDriverBiDiWebSocketTextMessage, send_webdriver_bidi_pointer_click, + WebDriverBiDiWebSocketMessageReader, send_webdriver_bidi_pointer_click, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -102,11 +102,11 @@ fn establish(local_addr: SocketAddr) -> Result Result> { - let (_established, frame) = established.read_frame(Duration::from_millis(500))?; - let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); - let text = match assembler.push_frame(frame)? { - WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, +) -> Result> { + let text = match WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + WebDriverBiDiConnectionMessageRead::Text { message, .. } => message, other => { return Err(io::Error::other(format!( "replacement pointer connection produced unexpected assembly state: {other:?}" From 5417ce32ed957aa166807f1023647caccc2920cb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 00:22:09 +0900 Subject: [PATCH 9/9] docs: record pointer receipt repair evidence Commit-Message-Assisted-by: Codex (via Codex) Signed-off-by: Seongho Bae --- CHANGELOG.md | 1 + ...received-response-connection-provenance.md | 12 +++++++++++ .../action-postcondition-evidence.md | 21 +++++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8505bfff4..04110b007 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Changed +- Rejected pointer-click replies received on replacement connections without losing the original pending click; a later reply on the originating connection can still complete that protocol exchange, without claiming that the page changed. - Integrated the current click-transport prerequisites into typed click-response handling, preserving the response contracts and bounded socket-observation test adjustment without claiming a browser post-condition. - Retained the originating connection when sending a pointer click so later response validation can reject acknowledgments received through a replacement connection; sending still does not prove that the click completed. - Integrated the current connection-provenance and pointer-click prerequisites into bounded click transport, retaining both public adapters and keeping frame-write evidence separate from browser-action completion. diff --git a/docs/doctoring/webdriver-bidi-received-response-connection-provenance.md b/docs/doctoring/webdriver-bidi-received-response-connection-provenance.md index e00c903d8..20185e98d 100644 --- a/docs/doctoring/webdriver-bidi-received-response-connection-provenance.md +++ b/docs/doctoring/webdriver-bidi-received-response-connection-provenance.md @@ -66,6 +66,18 @@ Fresh integrated-tree verification passed nine click response/send tests, five f ## References +### Pointer-response receipt repair evidence + +The later #258 repair reuses the selected received-message boundary above. +Published regression `8193fcd5` failed locally when a replacement socket consumed +the original click. Stronger test `d9396f05` holds listener address and session +constant and rejects both foreign success and error with exact mismatch while +requiring original-connection recovery and unrelated-request retention. +Consumer-only `588fe731` failed with missing sender provenance; ordinary parent +merge `0234b587d1bca9286eb5b597f9dab33be47ff518` includes #257 `9451fd8a` and +passes all six focused response tests. These are local implementation findings, +not new standards claims, browser authentication, page effects, or release proof. + Fette, I., & Melnikov, A. (2011). *The WebSocket Protocol* (RFC 6455). Internet Engineering Task Force. https://www.rfc-editor.org/rfc/rfc6455 The Rust Project Developers. (n.d.). *AtomicU64 in std::sync::atomic*. Rust standard library documentation. Retrieved September 5, 2026, from https://doc.rust-lang.org/std/sync/atomic/type.AtomicU64.html diff --git a/docs/traceability/action-postcondition-evidence.md b/docs/traceability/action-postcondition-evidence.md index 9c47f208a..061f77680 100644 --- a/docs/traceability/action-postcondition-evidence.md +++ b/docs/traceability/action-postcondition-evidence.md @@ -26,6 +26,27 @@ The generic value primitives are **IMPLEMENTED_ON_PROTECTED_MAIN**. The complete ## 3. Active executable evidence +### Pointer-click connection-bound receipt checkpoint + +Ordinary merge `0234b587d1bca9286eb5b597f9dab33be47ff518` integrates #257 +`9451fd8a23dec95b31749376bc78c2eaca977fe8` with #258's sealed received-message +consumer. Test commit `d9396f05` strengthened the published `8193fcd5` regression: +two sockets share one listener and session; foreign success and error replies +must produce the exact connection-mismatch error, retain both pending commands, +and permit the original reply to complete only its click. Both cases failed +before repair. Consumer-only `588fe731` also failed original-response acceptance +because the sender lacked connection provenance; this prevents a partial repair +from appearing complete. + +All six focused response and replacement-connection tests pass on the integrated +tree. Existing malformed-envelope, unknown-id, extensible-success and matched +remote-error behavior is retained. No public receipt constructor, caller-supplied +generation, generic fallback, dependency, or new authority boundary was added. +This supersedes the receipt prerequisite below, not its dated evidence or the +remaining outbound session-authority and real-browser postcondition gaps. +Full exact-head verification, hosted acceptance and protected integration remain +separate requirements; focused loopback success does not establish them. + ### Pointer-click originating-connection prerequisite PR #258 test-only head `8193fcd50125d9e9a43b4755e0f7626801b74374`, on