From 319372cd62f107fda4eb042f74f59a9d351027cb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 20:36:28 -0700 Subject: [PATCH 01/34] test(network): require session.status command send --- .../webdriver_bidi_session_status_command.rs | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_session_status_command.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_session_status_command.rs b/crates/originweave-network/tests/webdriver_bidi_session_status_command.rs new file mode 100644 index 000000000..f21e4eb5c --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_session_status_command.rs @@ -0,0 +1,131 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::WebDriverBiDiWebSocketEndpoint; +use originweave_network::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiCorrelatedResponseOutcome, + WebDriverBiDiJsonEnvelope, WebDriverBiDiSessionStatusCommand, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler, + WebDriverBiDiWebSocketMessageAssembly, +}; + +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 STATUS_RESPONSE: &[u8] = br#"{"type":"success","id":7,"result":{"ready":true,"message":"ready"}}"#; + +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 length = usize::from(header[1] & 0x7f); + if length > 125 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "test command unexpectedly required extended framing", + )); + } + 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) +} + +#[test] +fn session_status_command_round_trips_over_the_verified_websocket_and_correlation_boundary() +-> Result<(), Box> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + 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)?; + let command = read_masked_text_frame(&mut stream)?; + if command != br#"{"id":7,"method":"session.status","params":{}}"# { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("unexpected session.status command: {}", String::from_utf8_lossy(&command)), + )); + } + stream.write_all(&[0x81, STATUS_RESPONSE.len() as u8])?; + stream.write_all(STATUS_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 mut correlation = WebDriverBiDiCommandCorrelation::new(); + let command = WebDriverBiDiSessionStatusCommand::new(7)?; + let established = command.send( + established, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + Duration::from_millis(500), + )?; + assert_eq!(correlation.outstanding_count(), 1); + + 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!( + "session.status response produced unexpected assembly state: {other:?}" + )) + .into()); + } + }; + let envelope = WebDriverBiDiJsonEnvelope::parse(&text)?; + let completed = correlation.correlate_response(&envelope)?; + assert_eq!(completed.command_id(), 7); + assert_eq!( + completed.outcome(), + WebDriverBiDiCorrelatedResponseOutcome::Success + ); + assert_eq!(correlation.outstanding_count(), 0); + + server + .join() + .map_err(|_| io::Error::other("session.status command test server panicked"))??; + Ok(()) +} From 03511d92d367b0042dbfc27c7a60b705be425ed7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 20:40:01 -0700 Subject: [PATCH 02/34] test(network): format session.status RED regression --- .../tests/webdriver_bidi_session_status_command.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_status_command.rs b/crates/originweave-network/tests/webdriver_bidi_session_status_command.rs index f21e4eb5c..dccc2eda7 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_status_command.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_status_command.rs @@ -18,7 +18,8 @@ use originweave_network::{ 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 STATUS_RESPONSE: &[u8] = br#"{"type":"success","id":7,"result":{"ready":true,"message":"ready"}}"#; +const STATUS_RESPONSE: &[u8] = + br#"{"type":"success","id":7,"result":{"ready":true,"message":"ready"}}"#; fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { stream.set_read_timeout(Some(Duration::from_secs(2)))?; @@ -76,7 +77,10 @@ fn session_status_command_round_trips_over_the_verified_websocket_and_correlatio if command != br#"{"id":7,"method":"session.status","params":{}}"# { return Err(io::Error::new( io::ErrorKind::InvalidData, - format!("unexpected session.status command: {}", String::from_utf8_lossy(&command)), + format!( + "unexpected session.status command: {}", + String::from_utf8_lossy(&command) + ), )); } stream.write_all(&[0x81, STATUS_RESPONSE.len() as u8])?; From 748a5c57c5ed1c66727fd84b0c951f97cb07225c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 20:45:17 -0700 Subject: [PATCH 03/34] feat(network): send typed BiDi session.status command --- crates/originweave-network/src/lib.rs | 11 +- .../webdriver_bidi_session_status_command.rs | 177 ++++++++++++++++++ 2 files changed, 185 insertions(+), 3 deletions(-) create mode 100644 crates/originweave-network/src/webdriver_bidi_session_status_command.rs diff --git a/crates/originweave-network/src/lib.rs b/crates/originweave-network/src/lib.rs index 5e077b3a8..8669e9af1 100644 --- a/crates/originweave-network/src/lib.rs +++ b/crates/originweave-network/src/lib.rs @@ -7,9 +7,10 @@ //! `originweave-core` into one bounded exact TCP connection, binds and validates //! 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, and tracks bounded command-response -//! correlation without exposing generic JSON bodies or granting browser, TLS, -//! policy, secret, or Agent authority. +//! classifies complete local-end JSON envelopes, tracks bounded command-response +//! correlation, and sends one narrowly typed `session.status` command without +//! exposing generic JSON bodies or granting browser, TLS, policy, secret, or +//! Agent authority. #![forbid(unsafe_code)] #![deny(missing_docs)] @@ -18,6 +19,7 @@ mod connection; mod webdriver_bidi_command_correlation; mod webdriver_bidi_connection; mod webdriver_bidi_json_envelope; +mod webdriver_bidi_session_status_command; mod webdriver_bidi_websocket_frame; mod webdriver_bidi_websocket_handshake; mod webdriver_bidi_websocket_message; @@ -43,6 +45,9 @@ pub use webdriver_bidi_json_envelope::{ MAX_WEBDRIVER_BIDI_JS_UINT, MAX_WEBDRIVER_BIDI_JSON_DEPTH, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeError, WebDriverBiDiJsonEnvelopeKind, }; +pub use webdriver_bidi_session_status_command::{ + WebDriverBiDiSessionStatusCommand, WebDriverBiDiSessionStatusCommandError, +}; pub use webdriver_bidi_websocket_frame::{ MAX_WEBSOCKET_FRAME_PAYLOAD_SIZE, MAX_WEBSOCKET_FRAME_TIMEOUT, WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketFrame, diff --git a/crates/originweave-network/src/webdriver_bidi_session_status_command.rs b/crates/originweave-network/src/webdriver_bidi_session_status_command.rs new file mode 100644 index 000000000..f752a1172 --- /dev/null +++ b/crates/originweave-network/src/webdriver_bidi_session_status_command.rs @@ -0,0 +1,177 @@ +use std::{error::Error, fmt, time::Duration}; + +use crate::{ + MAX_WEBDRIVER_BIDI_JS_UINT, WebDriverBiDiCommandCorrelation, + WebDriverBiDiCommandCorrelationError, WebDriverBiDiWebSocketEstablished, + WebDriverBiDiWebSocketFrameError, WebDriverBiDiWebSocketMaskKey, +}; + +const SESSION_STATUS_METHOD: &str = "session.status"; + +/// One bounded WebDriver BiDi `session.status` command. +/// +/// The command is deliberately concrete rather than a generic JSON or arbitrary-method escape +/// hatch. It carries only a WebDriver BiDi `js-uint` correlation identifier and always serializes +/// the standards-defined empty parameter map. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct WebDriverBiDiSessionStatusCommand { + command_id: u64, +} + +impl WebDriverBiDiSessionStatusCommand { + /// Construct one `session.status` command with a JavaScript-safe correlation identifier. + pub fn new(command_id: u64) -> Result { + if command_id > MAX_WEBDRIVER_BIDI_JS_UINT { + return Err(WebDriverBiDiSessionStatusCommandError::CommandIdOutOfRange { + command_id, + maximum_command_id: MAX_WEBDRIVER_BIDI_JS_UINT, + }); + } + Ok(Self { command_id }) + } + + /// Return the exact local correlation identifier serialized for this command. + #[must_use] + pub const fn command_id(&self) -> u64 { + self.command_id + } + + /// Register and write this exact command on an already established verified BiDi stream. + /// + /// Registration occurs before the first possible remote side effect. A correlation failure + /// therefore writes nothing. Once registration succeeds, any frame-write failure consumes the + /// transport and intentionally leaves the identifier outstanding: a partial or fully emitted + /// frame is ambiguous, so silently retiring the id could allow unsafe reuse. Callers must treat + /// the failed stream/correlation pairing as unusable or explicitly tear down its session state. + pub fn send( + self, + established: WebDriverBiDiWebSocketEstablished, + correlation: &mut WebDriverBiDiCommandCorrelation, + masking_key: WebDriverBiDiWebSocketMaskKey, + frame_timeout: Duration, + ) -> Result { + correlation + .register_command(self.command_id) + .map_err(|source| WebDriverBiDiSessionStatusCommandError::Correlation { source })?; + let message = self.serialized(); + established + .write_text_frame(&message, masking_key, frame_timeout) + .map_err(|source| WebDriverBiDiSessionStatusCommandError::FrameWrite { source }) + } + + fn serialized(self) -> String { + format!( + "{{\"id\":{},\"method\":\"{SESSION_STATUS_METHOD}\",\"params\":{{}}}}", + self.command_id + ) + } +} + +/// Fail-closed errors while constructing or sending one typed `session.status` command. +#[derive(Debug)] +pub enum WebDriverBiDiSessionStatusCommandError { + /// The requested command identifier is outside WebDriver BiDi's `js-uint` range. + CommandIdOutOfRange { + /// Rejected command identifier. + command_id: u64, + /// Largest JavaScript-safe identifier admitted by this boundary. + maximum_command_id: u64, + }, + /// The bounded local correlation registry rejected the command before network I/O. + Correlation { + /// Exact typed correlation failure. + source: WebDriverBiDiCommandCorrelationError, + }, + /// Writing the already-registered command frame failed and the transport is not reusable. + FrameWrite { + /// Exact typed bounded WebSocket frame-write failure. + source: WebDriverBiDiWebSocketFrameError, + }, +} + +impl fmt::Display for WebDriverBiDiSessionStatusCommandError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::CommandIdOutOfRange { .. } => formatter + .write_str("WebDriver BiDi session.status command id is outside the js-uint range"), + Self::Correlation { .. } => formatter + .write_str("WebDriver BiDi session.status command correlation was rejected"), + Self::FrameWrite { .. } => formatter + .write_str("WebDriver BiDi session.status command frame write failed"), + } + } +} + +impl Error for WebDriverBiDiSessionStatusCommandError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::CommandIdOutOfRange { .. } => None, + Self::Correlation { source } => Some(source), + Self::FrameWrite { source } => Some(source), + } + } +} + +#[cfg(test)] +mod tests { + use std::io; + + use super::*; + + #[test] + fn constructor_rejects_ids_outside_the_webdriver_bidi_js_uint_range() { + let rejected = WebDriverBiDiSessionStatusCommand::new(MAX_WEBDRIVER_BIDI_JS_UINT + 1); + assert!(matches!( + rejected, + Err(WebDriverBiDiSessionStatusCommandError::CommandIdOutOfRange { + command_id, + maximum_command_id: MAX_WEBDRIVER_BIDI_JS_UINT, + }) if command_id == MAX_WEBDRIVER_BIDI_JS_UINT + 1 + )); + } + + #[test] + fn command_serialization_is_static_and_exact() -> Result<(), WebDriverBiDiSessionStatusCommandError> { + let command = WebDriverBiDiSessionStatusCommand::new(42)?; + assert_eq!(command.command_id(), 42); + assert_eq!( + command.serialized(), + r#"{"id":42,"method":"session.status","params":{}}"# + ); + Ok(()) + } + + #[test] + fn command_errors_have_stable_messages_and_typed_sources() { + let range = WebDriverBiDiSessionStatusCommandError::CommandIdOutOfRange { + command_id: MAX_WEBDRIVER_BIDI_JS_UINT + 1, + maximum_command_id: MAX_WEBDRIVER_BIDI_JS_UINT, + }; + assert_eq!( + range.to_string(), + "WebDriver BiDi session.status command id is outside the js-uint range" + ); + assert!(range.source().is_none()); + + let correlation = WebDriverBiDiSessionStatusCommandError::Correlation { + source: WebDriverBiDiCommandCorrelationError::CommandAlreadyOutstanding, + }; + assert_eq!( + correlation.to_string(), + "WebDriver BiDi session.status command correlation was rejected" + ); + assert!(correlation.source().is_some()); + + let frame = WebDriverBiDiSessionStatusCommandError::FrameWrite { + source: WebDriverBiDiWebSocketFrameError::FrameWriteFailed { + bytes_written: 0, + source: io::Error::other("test frame failure"), + }, + }; + assert_eq!( + frame.to_string(), + "WebDriver BiDi session.status command frame write failed" + ); + assert!(frame.source().is_some()); + } +} From 4b6eeea561383fcbc4adfb42afec16a978b1654f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 20:47:55 -0700 Subject: [PATCH 04/34] style(network): apply canonical session.status rustfmt --- .../webdriver_bidi_session_status_command.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_session_status_command.rs b/crates/originweave-network/src/webdriver_bidi_session_status_command.rs index f752a1172..606730c95 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_status_command.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_status_command.rs @@ -22,10 +22,12 @@ impl WebDriverBiDiSessionStatusCommand { /// Construct one `session.status` command with a JavaScript-safe correlation identifier. pub fn new(command_id: u64) -> Result { if command_id > MAX_WEBDRIVER_BIDI_JS_UINT { - return Err(WebDriverBiDiSessionStatusCommandError::CommandIdOutOfRange { - command_id, - maximum_command_id: MAX_WEBDRIVER_BIDI_JS_UINT, - }); + return Err( + WebDriverBiDiSessionStatusCommandError::CommandIdOutOfRange { + command_id, + maximum_command_id: MAX_WEBDRIVER_BIDI_JS_UINT, + }, + ); } Ok(Self { command_id }) } @@ -96,8 +98,9 @@ impl fmt::Display for WebDriverBiDiSessionStatusCommandError { .write_str("WebDriver BiDi session.status command id is outside the js-uint range"), Self::Correlation { .. } => formatter .write_str("WebDriver BiDi session.status command correlation was rejected"), - Self::FrameWrite { .. } => formatter - .write_str("WebDriver BiDi session.status command frame write failed"), + Self::FrameWrite { .. } => { + formatter.write_str("WebDriver BiDi session.status command frame write failed") + } } } } @@ -131,7 +134,8 @@ mod tests { } #[test] - fn command_serialization_is_static_and_exact() -> Result<(), WebDriverBiDiSessionStatusCommandError> { + fn command_serialization_is_static_and_exact() + -> Result<(), WebDriverBiDiSessionStatusCommandError> { let command = WebDriverBiDiSessionStatusCommand::new(42)?; assert_eq!(command.command_id(), 42); assert_eq!( From 2607a2238eb08fb661ae9b5d2d222c4c5db8765d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 21:07:39 -0700 Subject: [PATCH 05/34] test(network): cover session.status fail-closed send paths --- ...er_bidi_session_status_command_failures.rs | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs b/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs new file mode 100644 index 000000000..fd03092ed --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs @@ -0,0 +1,119 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::WebDriverBiDiWebSocketEndpoint; +use originweave_network::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiSessionStatusCommand, + WebDriverBiDiSessionStatusCommandError, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketEstablished, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, +}; + +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"; + +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 establish_with_handshake_only_server() -> Result< + ( + WebDriverBiDiWebSocketEstablished, + thread::JoinHandle>, + ), + Box, +> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + 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) + }); + + 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))?; + Ok((established, server)) +} + +#[test] +fn session_status_rejects_duplicate_correlation_before_any_frame_write() -> Result<(), Box> { + let (established, server) = establish_with_handshake_only_server()?; + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + correlation.register_command(7)?; + let command = WebDriverBiDiSessionStatusCommand::new(7)?; + + let error = command + .send( + established, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + Duration::from_millis(500), + ) + .err() + .ok_or_else(|| io::Error::other("duplicate correlation unexpectedly sent a command"))?; + assert!(matches!( + error, + WebDriverBiDiSessionStatusCommandError::Correlation { .. } + )); + assert_eq!(correlation.outstanding_count(), 1); + + server + .join() + .map_err(|_| io::Error::other("duplicate-correlation test server panicked"))??; + Ok(()) +} + +#[test] +fn session_status_preserves_registration_when_frame_timeout_is_invalid() -> Result<(), Box> { + let (established, server) = establish_with_handshake_only_server()?; + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + let command = WebDriverBiDiSessionStatusCommand::new(11)?; + + let error = command + .send( + established, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([5, 6, 7, 8]), + Duration::ZERO, + ) + .err() + .ok_or_else(|| io::Error::other("zero frame timeout unexpectedly sent a command"))?; + assert!(matches!( + error, + WebDriverBiDiSessionStatusCommandError::FrameWrite { .. } + )); + assert_eq!(correlation.outstanding_count(), 1); + + server + .join() + .map_err(|_| io::Error::other("invalid-timeout test server panicked"))??; + Ok(()) +} From 491176ca5c2550b0d566b49ec043f566cbefc70b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 21:08:10 -0700 Subject: [PATCH 06/34] test(network): remove vacuous session.status coverage branch --- .../src/webdriver_bidi_session_status_command.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_session_status_command.rs b/crates/originweave-network/src/webdriver_bidi_session_status_command.rs index 606730c95..5dc722a85 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_status_command.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_status_command.rs @@ -127,9 +127,9 @@ mod tests { assert!(matches!( rejected, Err(WebDriverBiDiSessionStatusCommandError::CommandIdOutOfRange { - command_id, maximum_command_id: MAX_WEBDRIVER_BIDI_JS_UINT, - }) if command_id == MAX_WEBDRIVER_BIDI_JS_UINT + 1 + .. + }) )); } From 9c8dd8be63c9bbaab0689916c8729f523f23fdcd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 21:13:11 -0700 Subject: [PATCH 07/34] test(network): keep production coverage scoped to production code --- .../src/webdriver_bidi_session_status_command.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_session_status_command.rs b/crates/originweave-network/src/webdriver_bidi_session_status_command.rs index 5dc722a85..960c1ac57 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_status_command.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_status_command.rs @@ -116,6 +116,7 @@ impl Error for WebDriverBiDiSessionStatusCommandError { } #[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] mod tests { use std::io; @@ -126,10 +127,12 @@ mod tests { let rejected = WebDriverBiDiSessionStatusCommand::new(MAX_WEBDRIVER_BIDI_JS_UINT + 1); assert!(matches!( rejected, - Err(WebDriverBiDiSessionStatusCommandError::CommandIdOutOfRange { - maximum_command_id: MAX_WEBDRIVER_BIDI_JS_UINT, - .. - }) + Err( + WebDriverBiDiSessionStatusCommandError::CommandIdOutOfRange { + maximum_command_id: MAX_WEBDRIVER_BIDI_JS_UINT, + .. + } + ) )); } From bdc338b0c0c06375875a9a15913bf7922b322ab8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 21:13:42 -0700 Subject: [PATCH 08/34] style(network): apply canonical session.status test formatting --- .../tests/webdriver_bidi_session_status_command_failures.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs b/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs index fd03092ed..e6fb4c793 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs @@ -64,7 +64,8 @@ fn establish_with_handshake_only_server() -> Result< } #[test] -fn session_status_rejects_duplicate_correlation_before_any_frame_write() -> Result<(), Box> { +fn session_status_rejects_duplicate_correlation_before_any_frame_write() +-> Result<(), Box> { let (established, server) = establish_with_handshake_only_server()?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); correlation.register_command(7)?; @@ -92,7 +93,8 @@ fn session_status_rejects_duplicate_correlation_before_any_frame_write() -> Resu } #[test] -fn session_status_preserves_registration_when_frame_timeout_is_invalid() -> Result<(), Box> { +fn session_status_preserves_registration_when_frame_timeout_is_invalid() +-> Result<(), Box> { let (established, server) = establish_with_handshake_only_server()?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); let command = WebDriverBiDiSessionStatusCommand::new(11)?; From c4c969fc7cda850bc5da7b566c9d4f2e7ef326d0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 22:07:44 -0700 Subject: [PATCH 09/34] fix(network): remove unstable coverage attribute --- .../src/webdriver_bidi_session_status_command.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_session_status_command.rs b/crates/originweave-network/src/webdriver_bidi_session_status_command.rs index 960c1ac57..0a173c29a 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_status_command.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_status_command.rs @@ -116,7 +116,6 @@ impl Error for WebDriverBiDiSessionStatusCommandError { } #[cfg(test)] -#[cfg_attr(coverage_nightly, coverage(off))] mod tests { use std::io; @@ -137,15 +136,15 @@ mod tests { } #[test] - fn command_serialization_is_static_and_exact() - -> Result<(), WebDriverBiDiSessionStatusCommandError> { - let command = WebDriverBiDiSessionStatusCommand::new(42)?; - assert_eq!(command.command_id(), 42); + fn command_serialization_is_static_and_exact() { + let serialized = WebDriverBiDiSessionStatusCommand::new(42).map(|command| { + assert_eq!(command.command_id(), 42); + command.serialized() + }); assert_eq!( - command.serialized(), - r#"{"id":42,"method":"session.status","params":{}}"# + serialized.as_deref(), + Ok(r#"{"id":42,"method":"session.status","params":{}}"#) ); - Ok(()) } #[test] From 45956f20c788ade61e1e8fbf4f0de5571ad2a6f7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 22:10:14 -0700 Subject: [PATCH 10/34] test(network): avoid residual branch in session.status serialization --- .../src/webdriver_bidi_session_status_command.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_session_status_command.rs b/crates/originweave-network/src/webdriver_bidi_session_status_command.rs index 0a173c29a..fa9626702 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_status_command.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_status_command.rs @@ -137,13 +137,11 @@ mod tests { #[test] fn command_serialization_is_static_and_exact() { - let serialized = WebDriverBiDiSessionStatusCommand::new(42).map(|command| { - assert_eq!(command.command_id(), 42); - command.serialized() - }); + let command = WebDriverBiDiSessionStatusCommand { command_id: 42 }; + assert_eq!(command.command_id(), 42); assert_eq!( - serialized.as_deref(), - Ok(r#"{"id":42,"method":"session.status","params":{}}"#) + command.serialized(), + r#"{"id":42,"method":"session.status","params":{}}"# ); } From 4e0d5e86aafeb4cb69f4531bef3d8d2952bf541d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 22:13:34 -0700 Subject: [PATCH 11/34] test(network): name session.status handshake fixture type --- ...webdriver_bidi_session_status_command_failures.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs b/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs index e6fb4c793..1d947d0c9 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs @@ -17,6 +17,10 @@ use originweave_network::{ 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"; +type HandshakeOnlyServer = ( + WebDriverBiDiWebSocketEstablished, + thread::JoinHandle>, +); fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { stream.set_read_timeout(Some(Duration::from_secs(2)))?; @@ -35,13 +39,7 @@ fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { Ok(()) } -fn establish_with_handshake_only_server() -> Result< - ( - WebDriverBiDiWebSocketEstablished, - thread::JoinHandle>, - ), - Box, -> { +fn establish_with_handshake_only_server() -> Result> { let listener = TcpListener::bind(("127.0.0.1", 0))?; let local_addr = listener.local_addr()?; let server = thread::spawn(move || -> io::Result<()> { From d0876f1d450651f7362baf259e45bc7256bd2e97 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 23:06:27 -0700 Subject: [PATCH 12/34] test(network): cover both session.status id-range paths --- .../webdriver_bidi_session_status_command.rs | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_session_status_command.rs b/crates/originweave-network/src/webdriver_bidi_session_status_command.rs index fa9626702..890e7b5bc 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_status_command.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_status_command.rs @@ -122,17 +122,18 @@ mod tests { use super::*; #[test] - fn constructor_rejects_ids_outside_the_webdriver_bidi_js_uint_range() { + fn constructor_enforces_the_webdriver_bidi_js_uint_range() { + let accepted = WebDriverBiDiSessionStatusCommand::new(MAX_WEBDRIVER_BIDI_JS_UINT); + assert_eq!( + accepted.ok().map(|command| command.command_id()), + Some(MAX_WEBDRIVER_BIDI_JS_UINT) + ); + let rejected = WebDriverBiDiSessionStatusCommand::new(MAX_WEBDRIVER_BIDI_JS_UINT + 1); - assert!(matches!( - rejected, - Err( - WebDriverBiDiSessionStatusCommandError::CommandIdOutOfRange { - maximum_command_id: MAX_WEBDRIVER_BIDI_JS_UINT, - .. - } - ) - )); + assert_eq!( + rejected.err().map(|error| error.to_string()).as_deref(), + Some("WebDriver BiDi session.status command id is outside the js-uint range") + ); } #[test] From 0230ae574b3026e19790e72830cf23e5a02ce674 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 23:07:06 -0700 Subject: [PATCH 13/34] test(network): exercise rejected session.status ids externally --- ...ver_bidi_session_status_command_failures.rs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs b/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs index 1d947d0c9..32ddc1643 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs @@ -8,10 +8,11 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use originweave_network::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiSessionStatusCommand, - WebDriverBiDiSessionStatusCommandError, WebDriverBiDiTcpConnectionPlan, - WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketEstablished, - WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, + MAX_WEBDRIVER_BIDI_JS_UINT, WebDriverBiDiCommandCorrelation, + WebDriverBiDiSessionStatusCommand, WebDriverBiDiSessionStatusCommandError, + WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketMaskKey, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -61,6 +62,15 @@ fn establish_with_handshake_only_server() -> Result Result<(), Box> { From f6ad0191567b6e1370b3d62135c5e4ecf2e1f019 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 23:09:30 -0700 Subject: [PATCH 14/34] style(network): apply canonical rustfmt --- .../webdriver_bidi_session_status_command_failures.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs b/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs index 32ddc1643..b796a4ee5 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs @@ -8,11 +8,10 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use originweave_network::{ - MAX_WEBDRIVER_BIDI_JS_UINT, WebDriverBiDiCommandCorrelation, - WebDriverBiDiSessionStatusCommand, WebDriverBiDiSessionStatusCommandError, - WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, - WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMaskKey, + MAX_WEBDRIVER_BIDI_JS_UINT, WebDriverBiDiCommandCorrelation, WebDriverBiDiSessionStatusCommand, + WebDriverBiDiSessionStatusCommandError, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketEstablished, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; From 04d6c1235aaef60ab5129302d15ec92dcd460414 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 03:48:55 +0900 Subject: [PATCH 15/34] test(network): bind session.status response kind --- .../tests/webdriver_bidi_session_status_command.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_status_command.rs b/crates/originweave-network/tests/webdriver_bidi_session_status_command.rs index dccc2eda7..75c1dbb25 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_status_command.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_status_command.rs @@ -8,8 +8,9 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use originweave_network::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiCorrelatedResponseOutcome, - WebDriverBiDiJsonEnvelope, WebDriverBiDiSessionStatusCommand, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandKind, + WebDriverBiDiCorrelatedResponseOutcome, WebDriverBiDiJsonEnvelope, + WebDriverBiDiSessionStatusCommand, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, @@ -120,7 +121,8 @@ fn session_status_command_round_trips_over_the_verified_websocket_and_correlatio } }; let envelope = WebDriverBiDiJsonEnvelope::parse(&text)?; - let completed = correlation.correlate_response(&envelope)?; + let completed = + correlation.correlate_response_for(&envelope, WebDriverBiDiCommandKind::SessionStatus)?; assert_eq!(completed.command_id(), 7); assert_eq!( completed.outcome(), From 8b5b4542512fcec57c0d96515856eee2457b9f9b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 03:49:34 +0900 Subject: [PATCH 16/34] test(network): bind duplicate status command kind --- .../webdriver_bidi_session_status_command_failures.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs b/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs index b796a4ee5..05b9cb6b7 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs @@ -8,10 +8,11 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use originweave_network::{ - MAX_WEBDRIVER_BIDI_JS_UINT, WebDriverBiDiCommandCorrelation, WebDriverBiDiSessionStatusCommand, - WebDriverBiDiSessionStatusCommandError, WebDriverBiDiTcpConnectionPlan, - WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketEstablished, - WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, + MAX_WEBDRIVER_BIDI_JS_UINT, WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandKind, + WebDriverBiDiSessionStatusCommand, WebDriverBiDiSessionStatusCommandError, + WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketMaskKey, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -75,7 +76,7 @@ fn session_status_rejects_duplicate_correlation_before_any_frame_write() -> Result<(), Box> { let (established, server) = establish_with_handshake_only_server()?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); - correlation.register_command(7)?; + correlation.register_command_for(7, WebDriverBiDiCommandKind::SessionStatus)?; let command = WebDriverBiDiSessionStatusCommand::new(7)?; let error = command From 9e4a64f81f573d45effbbcd2a1f624ee176ca38f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 03:51:27 +0900 Subject: [PATCH 17/34] fix(network): bind session.status correlation kind --- .../src/webdriver_bidi_session_status_command.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_session_status_command.rs b/crates/originweave-network/src/webdriver_bidi_session_status_command.rs index 890e7b5bc..5f98fa734 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_status_command.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_status_command.rs @@ -2,8 +2,9 @@ use std::{error::Error, fmt, time::Duration}; use crate::{ MAX_WEBDRIVER_BIDI_JS_UINT, WebDriverBiDiCommandCorrelation, - WebDriverBiDiCommandCorrelationError, WebDriverBiDiWebSocketEstablished, - WebDriverBiDiWebSocketFrameError, WebDriverBiDiWebSocketMaskKey, + WebDriverBiDiCommandCorrelationError, WebDriverBiDiCommandKind, + WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketFrameError, + WebDriverBiDiWebSocketMaskKey, }; const SESSION_STATUS_METHOD: &str = "session.status"; @@ -53,7 +54,7 @@ impl WebDriverBiDiSessionStatusCommand { frame_timeout: Duration, ) -> Result { correlation - .register_command(self.command_id) + .register_command_for(self.command_id, WebDriverBiDiCommandKind::SessionStatus) .map_err(|source| WebDriverBiDiSessionStatusCommandError::Correlation { source })?; let message = self.serialized(); established From f9ec8f478997aa110f58dad243a1488d9ec674b4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 03:51:57 +0900 Subject: [PATCH 18/34] fix(network): expose command kind to status slice --- crates/originweave-network/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/originweave-network/src/lib.rs b/crates/originweave-network/src/lib.rs index 8669e9af1..0b1c43f3a 100644 --- a/crates/originweave-network/src/lib.rs +++ b/crates/originweave-network/src/lib.rs @@ -34,8 +34,8 @@ pub use connection::{ }; pub use webdriver_bidi_command_correlation::{ MAX_WEBDRIVER_BIDI_OUTSTANDING_COMMANDS, WebDriverBiDiCommandCorrelation, - WebDriverBiDiCommandCorrelationError, WebDriverBiDiCorrelatedResponse, - WebDriverBiDiCorrelatedResponseOutcome, + WebDriverBiDiCommandCorrelationError, WebDriverBiDiCommandKind, + WebDriverBiDiCorrelatedResponse, WebDriverBiDiCorrelatedResponseOutcome, }; pub use webdriver_bidi_connection::{ WebDriverBiDiTcpConnection, WebDriverBiDiTcpConnectionError, From eca63e014b0dc27c54c25abce1e2aa38166485dd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 17:02:04 +0900 Subject: [PATCH 19/34] style(network): preserve canonical trailing newline after restack --- crates/originweave-network/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/originweave-network/src/lib.rs b/crates/originweave-network/src/lib.rs index b0142217b..79084c139 100644 --- a/crates/originweave-network/src/lib.rs +++ b/crates/originweave-network/src/lib.rs @@ -67,4 +67,4 @@ pub use webdriver_bidi_websocket_message::{ WebDriverBiDiWebSocketMessageAssembly, WebDriverBiDiWebSocketMessageError, WebDriverBiDiWebSocketTextMessage, }; -pub use webdriver_bidi_websocket_opening_recovery::WebDriverBiDiWebSocketOpeningWriteRecoveryDisposition; \ No newline at end of file +pub use webdriver_bidi_websocket_opening_recovery::WebDriverBiDiWebSocketOpeningWriteRecoveryDisposition; From 06cf25f74c30af9cc6a382860a9431bf5aaa1222 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 17:05:30 +0900 Subject: [PATCH 20/34] test(network): reject invalid BiDi frame deadline before correlation --- .../tests/webdriver_bidi_session_status_command_failures.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs b/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs index 05b9cb6b7..573932eeb 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs @@ -101,7 +101,7 @@ fn session_status_rejects_duplicate_correlation_before_any_frame_write() } #[test] -fn session_status_preserves_registration_when_frame_timeout_is_invalid() +fn session_status_rejects_invalid_frame_timeout_before_correlation_registration() -> Result<(), Box> { let (established, server) = establish_with_handshake_only_server()?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); @@ -120,7 +120,7 @@ fn session_status_preserves_registration_when_frame_timeout_is_invalid() error, WebDriverBiDiSessionStatusCommandError::FrameWrite { .. } )); - assert_eq!(correlation.outstanding_count(), 1); + assert_eq!(correlation.outstanding_count(), 0); server .join() From 0f71dac05f5809bd47786df77fc84d9d2ebe0ea2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 17:06:47 +0900 Subject: [PATCH 21/34] fix(network): reject invalid BiDi frame deadline before correlation --- .../webdriver_bidi_session_status_command.rs | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_session_status_command.rs b/crates/originweave-network/src/webdriver_bidi_session_status_command.rs index 5f98fa734..57c26eec6 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_status_command.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_status_command.rs @@ -1,7 +1,7 @@ use std::{error::Error, fmt, time::Duration}; use crate::{ - MAX_WEBDRIVER_BIDI_JS_UINT, WebDriverBiDiCommandCorrelation, + MAX_WEBDRIVER_BIDI_JS_UINT, MAX_WEBSOCKET_FRAME_TIMEOUT, WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, WebDriverBiDiCommandKind, WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketFrameError, WebDriverBiDiWebSocketMaskKey, @@ -41,11 +41,12 @@ impl WebDriverBiDiSessionStatusCommand { /// Register and write this exact command on an already established verified BiDi stream. /// - /// Registration occurs before the first possible remote side effect. A correlation failure - /// therefore writes nothing. Once registration succeeds, any frame-write failure consumes the - /// transport and intentionally leaves the identifier outstanding: a partial or fully emitted - /// frame is ambiguous, so silently retiring the id could allow unsafe reuse. Callers must treat - /// the failed stream/correlation pairing as unusable or explicitly tear down its session state. + /// Locally invalid frame deadlines fail before correlation registration and before any remote + /// side effect. Correlation then registers the command before the first possible frame write. + /// Once registration succeeds, a later frame-write failure leaves the identifier outstanding: + /// partial or full emission is ambiguous, so silently retiring the id could allow unsafe reuse. + /// Callers must treat the failed stream/correlation pairing as unusable or explicitly tear down + /// its session state. pub fn send( self, established: WebDriverBiDiWebSocketEstablished, @@ -53,6 +54,14 @@ impl WebDriverBiDiSessionStatusCommand { masking_key: WebDriverBiDiWebSocketMaskKey, frame_timeout: Duration, ) -> Result { + if frame_timeout.is_zero() || frame_timeout > MAX_WEBSOCKET_FRAME_TIMEOUT { + return Err(WebDriverBiDiSessionStatusCommandError::FrameWrite { + source: WebDriverBiDiWebSocketFrameError::InvalidFrameTimeout { + frame_timeout, + maximum_timeout: MAX_WEBSOCKET_FRAME_TIMEOUT, + }, + }); + } correlation .register_command_for(self.command_id, WebDriverBiDiCommandKind::SessionStatus) .map_err(|source| WebDriverBiDiSessionStatusCommandError::Correlation { source })?; From 2b7348251e2daed0a68916bdcd6b4eb557f73394 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 17:07:21 +0900 Subject: [PATCH 22/34] test(network): cover both invalid BiDi frame deadline bounds --- ...er_bidi_session_status_command_failures.rs | 55 ++++++++++--------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs b/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs index 573932eeb..1583b8433 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs @@ -8,11 +8,11 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use originweave_network::{ - MAX_WEBDRIVER_BIDI_JS_UINT, WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandKind, - WebDriverBiDiSessionStatusCommand, WebDriverBiDiSessionStatusCommandError, - WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, - WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMaskKey, + MAX_WEBDRIVER_BIDI_JS_UINT, MAX_WEBSOCKET_FRAME_TIMEOUT, WebDriverBiDiCommandCorrelation, + WebDriverBiDiCommandKind, WebDriverBiDiSessionStatusCommand, + WebDriverBiDiSessionStatusCommandError, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketEstablished, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -103,27 +103,32 @@ fn session_status_rejects_duplicate_correlation_before_any_frame_write() #[test] fn session_status_rejects_invalid_frame_timeout_before_correlation_registration() -> Result<(), Box> { - let (established, server) = establish_with_handshake_only_server()?; - let mut correlation = WebDriverBiDiCommandCorrelation::new(); - let command = WebDriverBiDiSessionStatusCommand::new(11)?; + for (command_id, frame_timeout) in [ + (11, Duration::ZERO), + (12, MAX_WEBSOCKET_FRAME_TIMEOUT + Duration::from_millis(1)), + ] { + let (established, server) = establish_with_handshake_only_server()?; + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + let command = WebDriverBiDiSessionStatusCommand::new(command_id)?; - let error = command - .send( - established, - &mut correlation, - WebDriverBiDiWebSocketMaskKey::new([5, 6, 7, 8]), - Duration::ZERO, - ) - .err() - .ok_or_else(|| io::Error::other("zero frame timeout unexpectedly sent a command"))?; - assert!(matches!( - error, - WebDriverBiDiSessionStatusCommandError::FrameWrite { .. } - )); - assert_eq!(correlation.outstanding_count(), 0); + let error = command + .send( + established, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([5, 6, 7, 8]), + frame_timeout, + ) + .err() + .ok_or_else(|| io::Error::other("invalid frame timeout unexpectedly sent a command"))?; + assert!(matches!( + error, + WebDriverBiDiSessionStatusCommandError::FrameWrite { .. } + )); + assert_eq!(correlation.outstanding_count(), 0); - server - .join() - .map_err(|_| io::Error::other("invalid-timeout test server panicked"))??; + server + .join() + .map_err(|_| io::Error::other("invalid-timeout test server panicked"))??; + } Ok(()) } From 3c9124baaa9b75f3613ac6040f002196dbea9f16 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 17:07:48 +0900 Subject: [PATCH 23/34] docs(network): distinguish BiDi frame preflight from write ambiguity --- .../src/webdriver_bidi_session_status_command.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_session_status_command.rs b/crates/originweave-network/src/webdriver_bidi_session_status_command.rs index 57c26eec6..93d72b01c 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_status_command.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_status_command.rs @@ -94,9 +94,9 @@ pub enum WebDriverBiDiSessionStatusCommandError { /// Exact typed correlation failure. source: WebDriverBiDiCommandCorrelationError, }, - /// Writing the already-registered command frame failed and the transport is not reusable. + /// Frame preflight validation or a later write operation failed. FrameWrite { - /// Exact typed bounded WebSocket frame-write failure. + /// Exact typed bounded WebSocket frame validation/write failure. source: WebDriverBiDiWebSocketFrameError, }, } From b293b35535b463a15b42b18ae35b178a418d9249 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 18:05:13 +0900 Subject: [PATCH 24/34] test(network): expose pre-I/O mask reuse correlation leak --- ...er_bidi_session_status_command_failures.rs | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs b/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs index 1583b8433..e1e7bf974 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs @@ -40,6 +40,32 @@ fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { 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 length = usize::from(header[1] & 0x7f); + if length > 125 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "test frame unexpectedly required extended framing", + )); + } + 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_with_handshake_only_server() -> Result> { let listener = TcpListener::bind(("127.0.0.1", 0))?; let local_addr = listener.local_addr()?; @@ -132,3 +158,58 @@ fn session_status_rejects_invalid_frame_timeout_before_correlation_registration( } Ok(()) } + +#[test] +fn session_status_rejects_reused_mask_key_before_correlation_registration() +-> Result<(), Box> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + 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)?; + let seed = read_masked_text_frame(&mut stream)?; + if seed != b"{}" { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "unexpected seed frame before reused-key regression", + )); + } + Ok(()) + }); + + 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 repeated_key = WebDriverBiDiWebSocketMaskKey::new([9, 10, 11, 12]); + let established = established.write_text_frame("{}", repeated_key, Duration::from_millis(500))?; + + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + let command = WebDriverBiDiSessionStatusCommand::new(13)?; + let error = command + .send( + established, + &mut correlation, + repeated_key, + Duration::from_millis(500), + ) + .err() + .ok_or_else(|| io::Error::other("reused masking key unexpectedly sent session.status"))?; + assert!(matches!( + error, + WebDriverBiDiSessionStatusCommandError::FrameWrite { .. } + )); + assert_eq!(correlation.outstanding_count(), 0); + + server + .join() + .map_err(|_| io::Error::other("reused-mask-key test server panicked"))??; + Ok(()) +} From 8c7b9750d81d1fb6d9b75437b4eda0c605d45929 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 18:08:32 +0900 Subject: [PATCH 25/34] fix(network): retire correlation on pre-I/O mask rejection --- .../webdriver_bidi_session_status_command.rs | 43 ++++++++++++++++--- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_session_status_command.rs b/crates/originweave-network/src/webdriver_bidi_session_status_command.rs index 93d72b01c..f1dad8be3 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_status_command.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_status_command.rs @@ -43,10 +43,11 @@ impl WebDriverBiDiSessionStatusCommand { /// /// Locally invalid frame deadlines fail before correlation registration and before any remote /// side effect. Correlation then registers the command before the first possible frame write. - /// Once registration succeeds, a later frame-write failure leaves the identifier outstanding: - /// partial or full emission is ambiguous, so silently retiring the id could allow unsafe reuse. - /// Callers must treat the failed stream/correlation pairing as unusable or explicitly tear down - /// its session state. + /// A frame-owner preflight rejection that proves no write began retires this exact command + /// again; currently that covers adjacent client masking-key reuse. Once frame emission can have + /// begun, a later failure leaves the identifier outstanding because partial or full emission is + /// ambiguous. Callers must treat that failed stream/correlation pairing as unusable or + /// explicitly tear down its session state. pub fn send( self, established: WebDriverBiDiWebSocketEstablished, @@ -66,9 +67,19 @@ impl WebDriverBiDiSessionStatusCommand { .register_command_for(self.command_id, WebDriverBiDiCommandKind::SessionStatus) .map_err(|source| WebDriverBiDiSessionStatusCommandError::Correlation { source })?; let message = self.serialized(); - established - .write_text_frame(&message, masking_key, frame_timeout) - .map_err(|source| WebDriverBiDiSessionStatusCommandError::FrameWrite { source }) + match established.write_text_frame(&message, masking_key, frame_timeout) { + Ok(established) => Ok(established), + Err(source) => { + if frame_failure_precedes_possible_write(&source) { + correlation + .retire_command_for(self.command_id, WebDriverBiDiCommandKind::SessionStatus) + .map_err(|source| { + WebDriverBiDiSessionStatusCommandError::Correlation { source } + })?; + } + Err(WebDriverBiDiSessionStatusCommandError::FrameWrite { source }) + } + } } fn serialized(self) -> String { @@ -79,6 +90,10 @@ impl WebDriverBiDiSessionStatusCommand { } } +fn frame_failure_precedes_possible_write(source: &WebDriverBiDiWebSocketFrameError) -> bool { + matches!(source, WebDriverBiDiWebSocketFrameError::MalformedFrame { .. }) +} + /// Fail-closed errors while constructing or sending one typed `session.status` command. #[derive(Debug)] pub enum WebDriverBiDiSessionStatusCommandError { @@ -189,4 +204,18 @@ mod tests { ); assert!(frame.source().is_some()); } + + #[test] + fn only_frame_preflight_malformed_errors_retire_registered_correlation() { + let preflight = WebDriverBiDiWebSocketFrameError::MalformedFrame { + reason: "test preflight rejection", + }; + assert!(frame_failure_precedes_possible_write(&preflight)); + + let ambiguous = WebDriverBiDiWebSocketFrameError::FrameWriteFailed { + bytes_written: 1, + source: io::Error::other("test ambiguous write failure"), + }; + assert!(!frame_failure_precedes_possible_write(&ambiguous)); + } } From b9e38b4ca535470122b98fabd93929c6292af299 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 18:10:18 +0900 Subject: [PATCH 26/34] test(network): name mask-reuse outcome precisely --- .../tests/webdriver_bidi_session_status_command_failures.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs b/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs index e1e7bf974..adf12b184 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs @@ -160,7 +160,7 @@ fn session_status_rejects_invalid_frame_timeout_before_correlation_registration( } #[test] -fn session_status_rejects_reused_mask_key_before_correlation_registration() +fn session_status_reused_mask_key_rejection_does_not_leave_correlation_outstanding() -> Result<(), Box> { let listener = TcpListener::bind(("127.0.0.1", 0))?; let local_addr = listener.local_addr()?; From 0c6a6daa144b1dfccb58fc59b4a23631ad104558 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 18:30:37 +0900 Subject: [PATCH 27/34] docs(network): doctor session.status transport contract --- docs/doctoring.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/doctoring.md b/docs/doctoring.md index 6f3882e52..e6a8ff809 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -6,15 +6,17 @@ This document records external evidence that changes OriginWeave architecture, t ### Browser automation and interoperability -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 live WebDriver BiDi Editor's Draft dated 1 September 2026 defines the current bidirectional remote-control protocol, events, commands, and user contexts. The 1 June 2026 W3C Working Draft remains the most recent dated published Working Draft referenced by this repository, but it is not treated as the current editor text. Because WebDriver BiDi remains a draft protocol, OriginWeave keeps it 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 and contains no control, whitespace, or reviewed Unicode format characters. Requiring `sharedId` and rejecting control, whitespace, and format characters is a local fail-closed policy, not a claim that the Working Draft makes those fields mandatory or forbids whitespace. The admitted value is an untrusted transport handle, not an OriginWeave session, context, origin, or document-epoch node identity. The same-call QueryNodes admission boundary first obtains a non-cloneable SemanticObservation protocol-use proof and transfers that proof by ownership into `bind_current_nodes`, which refuses Navigation and TypedInput proofs before translating each admitted `sharedId` through the session-scoped registry into an `ObservedNodeHandle` only after the exact current session, browsing context, canonical origin, and document epoch are revalidated and the returned item count still fits the reviewed query budget. That composition still performs no browser I/O and does not authorize typed input. +The same Editor's 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 and contains no control, whitespace, or reviewed Unicode format characters. Requiring `sharedId` and rejecting control, whitespace, and format characters is a local fail-closed policy, not a claim that the Editor's Draft makes those fields mandatory or forbids whitespace. The admitted value is an untrusted transport handle, not an OriginWeave session, context, origin, or document-epoch node identity. The same-call QueryNodes admission boundary first obtains a non-cloneable SemanticObservation protocol-use proof and transfers that proof by ownership into `bind_current_nodes`, which refuses Navigation and TypedInput proofs before translating each admitted `sharedId` through the session-scoped registry into an `ObservedNodeHandle` only after the exact current session, browsing context, canonical origin, and document epoch are revalidated and the returned item count still fits the reviewed query budget. That composition still performs no browser I/O and does not authorize typed input. + +The current Editor's Draft defines `session.status` as a static command whose command type is the exact method `session.status` with `EmptyParams`; the result contains `ready` as a boolean and `message` as text. OriginWeave's first outbound command slice therefore serializes only that standards-defined method and empty parameter object. A successful WebSocket frame write is transport progress, not command completion: the command remains outstanding until a later admitted WebDriver BiDi response is parsed and correlated to the exact command id. Local validation failures that prove no command bytes could have been emitted may release only the corresponding just-registered correlation; partial or ambiguous write failures retain correlation because remote receipt cannot be disproved. WAI-ARIA 1.2 defines host-language `role` values as a token list: user agents split on whitespace and use the first matching non-abstract role. OriginWeave's first `locateNodes` accessibility query asks for one exact role, so a role containing whitespace, a control character, or a Unicode format character is rejected rather than interpreted as a fallback-role list. Accessible Name and Description Computation 1.2, a W3C Working Draft as of 5 August 2026, treats accessible names as ordinary strings that may contain spaces and treats whitespace-only `aria-roledescription` values as absent. OriginWeave therefore keeps ordinary spaces in accessible-name locators, rejects control and reviewed format characters that would become protocol-text injection or bidirectional spoofing, and rejects whitespace-only names as non-selectors. UTS #39 Revision 32 is the current Unicode security-mechanisms standard and marks Default_Ignorable and bidirectional format characters as restricted in identifier profiles. UAX #9 defines the bidirectional format controls that can reorder displayed protocol text. UTR #36 Revision 15 remains a stabilized historical security-considerations report; its identifier recommendations are superseded by UTS #39 rather than cited as current normative profile rules. OriginWeave therefore rejects the reviewed format-character set in roles, shared identifiers, and registry external identifiers, and rejects those same characters inside accessible names while still allowing ordinary U+0020 spaces. -RFC 6455 carries the WebSocket opening handshake over HTTP/1.1, and RFC 9110 permits `obs-text` octets (`%x80-FF`) in field values while retaining ASCII field-name and delimiter syntax. RFC 6455 also specifies that unknown opening-handshake header fields are ignored. OriginWeave therefore treats unknown extension-field values as opaque compatibility data rather than requiring the entire opening response to be UTF-8, while keeping the authority-bearing `Upgrade`, `Connection`, and `Sec-WebSocket-Accept` checks fail closed: opaque replacement material cannot satisfy the reviewed ASCII token or exact accept-value contracts. Ignoring an unknown field never grants browser, network, secret, approval, or Agent authority. +RFC 6455 carries the WebSocket opening handshake over HTTP/1.1, and RFC 9110 permits `obs-text` octets (`%x80-FF`) in field values while retaining ASCII field-name and delimiter syntax. RFC 6455 also specifies that unknown opening-handshake header fields are ignored. OriginWeave therefore treats unknown extension-field values as opaque compatibility data rather than requiring the entire opening response to be UTF-8, while keeping the authority-bearing `Upgrade`, `Connection`, and `Sec-WebSocket-Accept` checks fail closed: opaque replacement material cannot satisfy the reviewed ASCII token or exact accept-value contracts. Ignoring an unknown field never grants browser, network, secret, approval, or Agent authority. RFC 6455 section 5.3 additionally requires every client-to-server frame to use a fresh unpredictable 32-bit masking key derived from strong entropy. OriginWeave's frame owner enforces that normative masking requirement and also rejects immediate reuse of the preceding key as a local fail-closed stuck-randomness defense; the adjacent-reuse rule is stronger local policy, not an RFC 6455 requirement. ### Browser origin equivalence @@ -174,6 +176,8 @@ World Wide Web Consortium. (2023, June 6). *Accessible Rich Internet Application World Wide Web Consortium. (2026, June 1). *WebDriver BiDi* (W3C Working Draft). https://www.w3.org/TR/2026/WD-webdriver-bidi-20260601/ +World Wide Web Consortium. (2026, September 1). *WebDriver BiDi* [Editor's Draft]. https://w3c.github.io/webdriver-bidi/ + World Wide Web Consortium. (2026, August 5). *Accessible name and description computation 1.2* (W3C Working Draft). https://www.w3.org/TR/2026/WD-accname-1.2-20260805/ Xu, J., Sun, Q., Schwendeman, P., Nielsen, S., Cetin, E., & Tang, Y. (2025). *TRINITY: An evolved LLM coordinator* [Preprint]. arXiv. https://doi.org/10.48550/arXiv.2512.04695 From 117d7593d66243c391bde04791cf521a92a34e18 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 18:31:30 +0900 Subject: [PATCH 28/34] docs(network): record bounded session.status command --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74e3f7a56..ca3145e9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Added - Bounded RFC 6455 WebDriver BiDi opening-response validation on the exact peer-verified stream: it admits only HTTP/1.1 `101`, case-insensitive `Upgrade`/`Connection` tokens, and the client-key-correlated `Sec-WebSocket-Accept` value within monotonic time and header-size ceilings; it restores blocking mode and still does not implement WebSocket frames or grant browser/Agent authority. +- Typed outbound WebDriver BiDi `session.status` over the bounded client WebSocket stream: it serializes only the standards-defined method with empty params, preserves exact typed command-id correlation, rejects invalid frame deadlines before registration, retires only the just-registered id when a local masking-key preflight proves no command bytes were emitted, and keeps correlation outstanding after partial or ambiguous writes; frame-write success is not treated as command completion or browser/Agent authority. - Bounded WebDriver BiDi loopback TCP transport that consumes one exact no-DNS connect target, retries only explicitly recoverable local transport failures within repository timeout and attempt ceilings, exposes the stream only after operating-system peer inspection and exact peer verification, supports a consuming handoff of the original stream with typed credential-free peer/session/TLS and bounded-attempt evidence, preserves typed causal errors, and performs no DNS, proxy/PAC, process authentication, TLS, WebSocket, BiDi message, browser-action, or Agent-authority step. - Exact WebDriver BiDi socket-peer verification that consumes an approved no-DNS connect target, requires the observed IP address and port to match exactly, preserves the TLS requirement and exact correlated session id, and remains inert metadata that does not authenticate an OS process, does not negotiate TLS, perform a WebSocket handshake, or grant Agent authority. - Explicit no-DNS WebDriver BiDi loopback connection targets that derive exact IPv4/IPv6 loopback `SocketAddr` metadata from a session-correlated endpoint, reject `localhost` as requiring separately trusted name resolution, preserve the TLS requirement and exact session id, perform no socket I/O, and grant no Agent authority. From 588514bfa2150381bff8f3ccbb81e6072fde6218 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 18:36:03 +0900 Subject: [PATCH 29/34] docs(network): correct current BiDi editor draft date --- docs/doctoring.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/doctoring.md b/docs/doctoring.md index e6a8ff809..d01bc4c29 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -6,7 +6,7 @@ This document records external evidence that changes OriginWeave architecture, t ### Browser automation and interoperability -The live WebDriver BiDi Editor's Draft dated 1 September 2026 defines the current bidirectional remote-control protocol, events, commands, and user contexts. The 1 June 2026 W3C Working Draft remains the most recent dated published Working Draft referenced by this repository, but it is not treated as the current editor text. Because WebDriver BiDi remains a draft protocol, OriginWeave keeps it behind a versioned adapter and Web Platform Tests-derived contract tests rather than make it the internal authority model. +The live WebDriver BiDi Editor's Draft dated 3 September 2026 defines the current bidirectional remote-control protocol, events, commands, and user contexts. The 1 June 2026 W3C Working Draft remains the most recent dated published Working Draft referenced by this repository, but it is not treated as the current editor text. Because WebDriver BiDi remains a draft protocol, OriginWeave keeps it behind a versioned adapter and Web Platform Tests-derived contract tests rather than make it the internal authority model. The same Editor's 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 and contains no control, whitespace, or reviewed Unicode format characters. Requiring `sharedId` and rejecting control, whitespace, and format characters is a local fail-closed policy, not a claim that the Editor's Draft makes those fields mandatory or forbids whitespace. The admitted value is an untrusted transport handle, not an OriginWeave session, context, origin, or document-epoch node identity. The same-call QueryNodes admission boundary first obtains a non-cloneable SemanticObservation protocol-use proof and transfers that proof by ownership into `bind_current_nodes`, which refuses Navigation and TypedInput proofs before translating each admitted `sharedId` through the session-scoped registry into an `ObservedNodeHandle` only after the exact current session, browsing context, canonical origin, and document epoch are revalidated and the returned item count still fits the reviewed query budget. That composition still performs no browser I/O and does not authorize typed input. @@ -112,7 +112,7 @@ Chromium Authors. (n.d.). *Proxy support in Chrome* [Source documentation]. Chro Chromium Authors. (2026). *URL canonicalizer unit tests* [Source code]. Chromium. https://chromium.googlesource.com/chromium/src/+/446d05d21720f0b3505ec21057b3e9f909784262/url/url_canon_unittest.cc -Cooper, D., Santesson, S., Farrell, S., Boeyen, S., Housley, R., & Polk, W. (2008). *Internet X.509 public key infrastructure certificate and certificate revocation list (CRL) profile* (RFC 5280). Internet Engineering Task Force. https://doi.org/10.17487/RFC5280 +Cooper, D., Santesson, S., Farrell, S., Boeyen, R., Housley, R., & Polk, W. (2008). *Internet X.509 public key infrastructure certificate and certificate revocation list (CRL) profile* (RFC 5280). Internet Engineering Task Force. https://doi.org/10.17487/RFC5280 Cotton, M., Vegoda, L., Bonica, R., & Haberman, B. (2013). *Special-purpose IP address registries* (RFC 6890). Internet Engineering Task Force. https://doi.org/10.17487/RFC6890 @@ -176,7 +176,7 @@ World Wide Web Consortium. (2023, June 6). *Accessible Rich Internet Application World Wide Web Consortium. (2026, June 1). *WebDriver BiDi* (W3C Working Draft). https://www.w3.org/TR/2026/WD-webdriver-bidi-20260601/ -World Wide Web Consortium. (2026, September 1). *WebDriver BiDi* [Editor's Draft]. https://w3c.github.io/webdriver-bidi/ +World Wide Web Consortium. (2026, September 3). *WebDriver BiDi* [Editor's Draft]. https://w3c.github.io/webdriver-bidi/ World Wide Web Consortium. (2026, August 5). *Accessible name and description computation 1.2* (W3C Working Draft). https://www.w3.org/TR/2026/WD-accname-1.2-20260805/ From 422ac8cce072cb016af16ed37e0de357a9ce854d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 18:37:28 +0900 Subject: [PATCH 30/34] docs: preserve RFC 5280 reference author --- docs/doctoring.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/doctoring.md b/docs/doctoring.md index d01bc4c29..d00b046a7 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -112,7 +112,7 @@ Chromium Authors. (n.d.). *Proxy support in Chrome* [Source documentation]. Chro Chromium Authors. (2026). *URL canonicalizer unit tests* [Source code]. Chromium. https://chromium.googlesource.com/chromium/src/+/446d05d21720f0b3505ec21057b3e9f909784262/url/url_canon_unittest.cc -Cooper, D., Santesson, S., Farrell, S., Boeyen, R., Housley, R., & Polk, W. (2008). *Internet X.509 public key infrastructure certificate and certificate revocation list (CRL) profile* (RFC 5280). Internet Engineering Task Force. https://doi.org/10.17487/RFC5280 +Cooper, D., Santesson, S., Farrell, S., Boeyen, S., Housley, R., & Polk, W. (2008). *Internet X.509 public key infrastructure certificate and certificate revocation list (CRL) profile* (RFC 5280). Internet Engineering Task Force. https://doi.org/10.17487/RFC5280 Cotton, M., Vegoda, L., Bonica, R., & Haberman, B. (2013). *Special-purpose IP address registries* (RFC 6890). Internet Engineering Task Force. https://doi.org/10.17487/RFC6890 From 2636564a5b815e3b03c21bd61d835df8b850aa12 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 12:24:46 +0900 Subject: [PATCH 31/34] test(network): close session status coverage gap Commit-Message-Assisted-by: Claude (via Claude Code) Signed-off-by: Seongho Bae --- CHANGELOG.md | 1 + .../src/webdriver_bidi_session_status_command.rs | 10 ++-------- docs/doctoring.md | 2 +- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a3a113de..b08afef30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Changed +- Kept the `session.status` frame-failure coverage contract focused on observable correlation state, avoiding assertion-internal uncovered branches without weakening preflight retirement or ambiguous-write retention checks. - Separated logical origin authority from resolved network destination authority; an origin grant no longer implies permission to connect to every resolver result. - Separated resolved-address authorization from direct transport evidence; an approved IP now becomes a usable stream only after the operating system reports the exact requested IP and port. - Separated exact TCP peer proof from authenticated TLS service identity; an observed peer becomes an authenticated HTTPS stream only after explicit-root, fixed-time, SAN-bound WebPKI verification over that same stream. diff --git a/crates/originweave-network/src/webdriver_bidi_session_status_command.rs b/crates/originweave-network/src/webdriver_bidi_session_status_command.rs index f149df7d0..e6341c416 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_status_command.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_status_command.rs @@ -218,10 +218,7 @@ mod tests { let preflight = WebDriverBiDiWebSocketFrameError::MalformedFrame { reason: "test preflight rejection", }; - assert!(matches!( - map_frame_failure(&mut correlation, 1, preflight), - WebDriverBiDiSessionStatusCommandError::FrameWrite { .. } - )); + map_frame_failure(&mut correlation, 1, preflight); assert_eq!(correlation.outstanding_count(), 0); assert!( @@ -233,10 +230,7 @@ mod tests { bytes_written: 1, source: io::Error::other("test ambiguous write failure"), }; - assert!(matches!( - map_frame_failure(&mut correlation, 2, ambiguous), - WebDriverBiDiSessionStatusCommandError::FrameWrite { .. } - )); + map_frame_failure(&mut correlation, 2, ambiguous); assert_eq!(correlation.outstanding_count(), 1); } } diff --git a/docs/doctoring.md b/docs/doctoring.md index d00b046a7..7a7a44c24 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -10,7 +10,7 @@ The live WebDriver BiDi Editor's Draft dated 3 September 2026 defines the curren The same Editor's 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 and contains no control, whitespace, or reviewed Unicode format characters. Requiring `sharedId` and rejecting control, whitespace, and format characters is a local fail-closed policy, not a claim that the Editor's Draft makes those fields mandatory or forbids whitespace. The admitted value is an untrusted transport handle, not an OriginWeave session, context, origin, or document-epoch node identity. The same-call QueryNodes admission boundary first obtains a non-cloneable SemanticObservation protocol-use proof and transfers that proof by ownership into `bind_current_nodes`, which refuses Navigation and TypedInput proofs before translating each admitted `sharedId` through the session-scoped registry into an `ObservedNodeHandle` only after the exact current session, browsing context, canonical origin, and document epoch are revalidated and the returned item count still fits the reviewed query budget. That composition still performs no browser I/O and does not authorize typed input. -The current Editor's Draft defines `session.status` as a static command whose command type is the exact method `session.status` with `EmptyParams`; the result contains `ready` as a boolean and `message` as text. OriginWeave's first outbound command slice therefore serializes only that standards-defined method and empty parameter object. A successful WebSocket frame write is transport progress, not command completion: the command remains outstanding until a later admitted WebDriver BiDi response is parsed and correlated to the exact command id. Local validation failures that prove no command bytes could have been emitted may release only the corresponding just-registered correlation; partial or ambiguous write failures retain correlation because remote receipt cannot be disproved. +The current Editor's Draft defines `session.status` as a static command whose command type is the exact method `session.status` with `EmptyParams`; the result contains `ready` as a boolean and `message` as text. OriginWeave's first outbound command slice therefore serializes only that standards-defined method and empty parameter object. A successful WebSocket frame write is transport progress, not command completion: the command remains outstanding until a later admitted WebDriver BiDi response is parsed and correlated to the exact command id. Local validation failures that prove no command bytes could have been emitted may release only the corresponding just-registered correlation; partial or ambiguous write failures retain correlation because remote receipt cannot be disproved. Regression coverage verifies that distinction from the resulting correlation count rather than from an internal error wrapper. WAI-ARIA 1.2 defines host-language `role` values as a token list: user agents split on whitespace and use the first matching non-abstract role. OriginWeave's first `locateNodes` accessibility query asks for one exact role, so a role containing whitespace, a control character, or a Unicode format character is rejected rather than interpreted as a fallback-role list. Accessible Name and Description Computation 1.2, a W3C Working Draft as of 5 August 2026, treats accessible names as ordinary strings that may contain spaces and treats whitespace-only `aria-roledescription` values as absent. OriginWeave therefore keeps ordinary spaces in accessible-name locators, rejects control and reviewed format characters that would become protocol-text injection or bidirectional spoofing, and rejects whitespace-only names as non-selectors. From e77150f4de6534887098fb9de7e02ecea7fbb59c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 06:08:50 +0900 Subject: [PATCH 32/34] fix: bind session.status to sender connection --- .../webdriver_bidi_session_status_command.rs | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_session_status_command.rs b/crates/originweave-network/src/webdriver_bidi_session_status_command.rs index e6341c416..092d06ec1 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_status_command.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_status_command.rs @@ -42,12 +42,14 @@ impl WebDriverBiDiSessionStatusCommand { /// Register and write this exact command on an already established verified BiDi stream. /// /// Locally invalid frame deadlines fail before correlation registration and before any remote - /// side effect. Correlation then registers the command before the first possible frame write. - /// A frame-owner preflight rejection that proves no write began retires this exact command - /// again; currently that covers adjacent client masking-key reuse. Once frame emission can have - /// begun, a later failure leaves the identifier outstanding because partial or full emission is - /// ambiguous. Callers must treat that failed stream/correlation pairing as unusable or - /// explicitly tear down its session state. + /// side effect. Correlation then registers the command together with the established + /// connection generation before the first possible frame write, so a response received on a + /// same-session replacement connection cannot consume this pending command. A frame-owner + /// preflight rejection that proves no write began retires this exact command again; currently + /// that covers adjacent client masking-key reuse. Once frame emission can have begun, a later + /// failure leaves the identifier outstanding because partial or full emission is ambiguous. + /// Callers must treat that failed stream/correlation pairing as unusable or explicitly tear down + /// its session state. pub fn send( self, established: WebDriverBiDiWebSocketEstablished, @@ -64,7 +66,11 @@ impl WebDriverBiDiSessionStatusCommand { }); } correlation - .register_command_for(self.command_id, WebDriverBiDiCommandKind::SessionStatus) + .register_command_for_connection( + self.command_id, + WebDriverBiDiCommandKind::SessionStatus, + established.transport_evidence().connection_generation(), + ) .map_err(|source| WebDriverBiDiSessionStatusCommandError::Correlation { source })?; let message = self.serialized(); match established.write_text_frame(&message, masking_key, frame_timeout) { From 583ffee2ee471bdb49762225d810ff9b5d6a9a77 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 06:39:15 +0900 Subject: [PATCH 33/34] repair(network): restore BiDi connection provenance prerequisite --- .../src/webdriver_bidi_command_correlation.rs | 150 ++++++++++++++++-- .../src/webdriver_bidi_connection.rs | 60 ++++++- .../src/webdriver_bidi_connection/error.rs | 13 +- .../generation_exhaustion_tests.rs | 91 +++++++++++ .../src/webdriver_bidi_connection/tests.rs | 45 ++++-- 5 files changed, 325 insertions(+), 34 deletions(-) create mode 100644 crates/originweave-network/src/webdriver_bidi_connection/generation_exhaustion_tests.rs diff --git a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs index 43d5e911e..2b66551b0 100644 --- a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs +++ b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs @@ -2,6 +2,7 @@ use std::{collections::BTreeMap, error::Error, fmt}; use crate::{ MAX_WEBDRIVER_BIDI_JS_UINT, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeRouting, + webdriver_bidi_connection::WebDriverBiDiConnectionGeneration, }; /// Maximum number of local WebDriver BiDi commands retained as outstanding at once. @@ -25,6 +26,12 @@ pub enum WebDriverBiDiCommandKind { SessionEnd, } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct OutstandingCommand { + kind: WebDriverBiDiCommandKind, + connection_generation: Option, +} + /// Outcome of a response after it has consumed the matching outstanding command identifier. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum WebDriverBiDiCorrelatedResponseOutcome { @@ -36,12 +43,15 @@ pub enum WebDriverBiDiCorrelatedResponseOutcome { /// Credential-free evidence that one parsed response consumed one outstanding local command. /// -/// This value carries only the matched command identifier and success/error classification. It -/// does not retain result bodies, error text, browser authority, transport authority, or secrets. +/// This value carries only the matched command identifier and success/error classification. A +/// private process-local connection generation is retained when the command owner bound one before +/// I/O so later transport evidence can be compared without accepting caller-supplied provenance. +/// It does not retain result bodies, error text, browser authority, transport authority, or secrets. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct WebDriverBiDiCorrelatedResponse { command_id: u64, outcome: WebDriverBiDiCorrelatedResponseOutcome, + connection_generation: Option, } impl WebDriverBiDiCorrelatedResponse { @@ -76,6 +86,16 @@ pub enum WebDriverBiDiCommandCorrelationError { /// Command family actually registered for the outstanding identifier. actual: WebDriverBiDiCommandKind, }, + /// A connection-bound consumer found an outstanding command with no connection provenance. + CommandConnectionProvenanceMissing { + /// Exact outstanding local command identifier. + command_id: u64, + }, + /// The response was received on a different verified connection from the outstanding command. + ResponseConnectionMismatch { + /// Exact outstanding local command identifier left untouched after rejection. + command_id: u64, + }, /// An event is not a command response and cannot consume correlation state. EventIsNotResponse, /// A protocol error with a `null` id cannot be attributed to one outstanding command. @@ -92,6 +112,12 @@ impl fmt::Display for WebDriverBiDiCommandCorrelationError { Self::CommandKindMismatch { .. } => { "WebDriver BiDi response command kind does not match the outstanding command" } + Self::CommandConnectionProvenanceMissing { .. } => { + "WebDriver BiDi outstanding command lacks connection provenance" + } + Self::ResponseConnectionMismatch { .. } => { + "WebDriver BiDi response arrived on a different connection" + } Self::EventIsNotResponse => "WebDriver BiDi event cannot be correlated as a response", Self::UncorrelatableErrorResponse => { "WebDriver BiDi error response has no correlatable command id" @@ -106,14 +132,17 @@ impl Error for WebDriverBiDiCommandCorrelationError {} /// Bounded local WebDriver BiDi command-response correlation state. /// /// Register an id together with its exact typed command family only after the caller has committed -/// to that outbound command. A success or correlatable error response consumes the id exactly once -/// only through a matching typed consumer. Events, null-id errors, and command-kind mismatches leave -/// outstanding state untouched. This type performs no I/O, retry, command serialization, browser -/// authentication, or authority grant. Debug output reports only the outstanding-count summary; -/// command identifiers and command families remain private correlation state. +/// to that outbound command. Connection-owning command adapters may additionally bind the private +/// generation of the exact established transport before I/O. A success or correlatable error +/// response consumes the id exactly once only through a matching typed consumer. Events, null-id +/// errors, command-kind mismatches, missing connection provenance, and responses received on a +/// different verified connection leave outstanding state untouched. This type performs no I/O, +/// retry, command serialization, browser authentication, or authority grant. Debug output reports +/// only the outstanding-count summary; command identifiers, families, and generations remain +/// private correlation state. #[derive(Default)] pub struct WebDriverBiDiCommandCorrelation { - outstanding: BTreeMap, + outstanding: BTreeMap, } impl fmt::Debug for WebDriverBiDiCommandCorrelation { @@ -147,6 +176,24 @@ impl WebDriverBiDiCommandCorrelation { &mut self, command_id: u64, command_kind: WebDriverBiDiCommandKind, + ) -> Result<(), WebDriverBiDiCommandCorrelationError> { + self.register(command_id, command_kind, None) + } + + pub(crate) fn register_command_for_connection( + &mut self, + command_id: u64, + command_kind: WebDriverBiDiCommandKind, + connection_generation: WebDriverBiDiConnectionGeneration, + ) -> Result<(), WebDriverBiDiCommandCorrelationError> { + self.register(command_id, command_kind, Some(connection_generation)) + } + + fn register( + &mut self, + command_id: u64, + command_kind: WebDriverBiDiCommandKind, + connection_generation: Option, ) -> Result<(), WebDriverBiDiCommandCorrelationError> { if command_id > MAX_WEBDRIVER_BIDI_JS_UINT { return Err(WebDriverBiDiCommandCorrelationError::CommandIdOutOfRange); @@ -157,7 +204,13 @@ impl WebDriverBiDiCommandCorrelation { if self.outstanding.len() >= MAX_WEBDRIVER_BIDI_OUTSTANDING_COMMANDS { return Err(WebDriverBiDiCommandCorrelationError::OutstandingCommandLimit); } - let _previous = self.outstanding.insert(command_id, command_kind); + let _previous = self.outstanding.insert( + command_id, + OutstandingCommand { + kind: command_kind, + connection_generation, + }, + ); Ok(()) } @@ -179,7 +232,8 @@ impl WebDriverBiDiCommandCorrelation { /// /// Successful responses and error responses with ids consume exactly one matching command. /// Unknown ids and command-kind mismatches fail without consuming state. Events and null-id - /// errors fail before touching the map. + /// errors fail before touching the map. This generic path does not claim received-connection + /// provenance; connection-sensitive command owners must use their connection-bound path. pub fn correlate_response_for( &mut self, envelope: &WebDriverBiDiJsonEnvelope, @@ -207,23 +261,54 @@ impl WebDriverBiDiCommandCorrelation { } } + pub(crate) fn correlate_response_for_connection( + &mut self, + envelope: &WebDriverBiDiJsonEnvelope, + expected_kind: WebDriverBiDiCommandKind, + received_connection_generation: WebDriverBiDiConnectionGeneration, + ) -> Result { + match envelope.routing() { + WebDriverBiDiJsonEnvelopeRouting::Event => { + Err(WebDriverBiDiCommandCorrelationError::EventIsNotResponse) + } + WebDriverBiDiJsonEnvelopeRouting::CommandError { command_id: None } => { + Err(WebDriverBiDiCommandCorrelationError::UncorrelatableErrorResponse) + } + WebDriverBiDiJsonEnvelopeRouting::CommandError { + command_id: Some(command_id), + } => self.complete_on_connection( + command_id, + expected_kind, + WebDriverBiDiCorrelatedResponseOutcome::Error, + received_connection_generation, + ), + WebDriverBiDiJsonEnvelopeRouting::CommandSuccess { command_id } => self + .complete_on_connection( + command_id, + expected_kind, + WebDriverBiDiCorrelatedResponseOutcome::Success, + received_connection_generation, + ), + } + } + fn require_command_kind( &self, command_id: u64, expected_kind: WebDriverBiDiCommandKind, - ) -> Result<(), WebDriverBiDiCommandCorrelationError> { + ) -> Result { let actual = self .outstanding .get(&command_id) .copied() .ok_or(WebDriverBiDiCommandCorrelationError::CommandNotOutstanding)?; - if actual != expected_kind { + if actual.kind != expected_kind { return Err(WebDriverBiDiCommandCorrelationError::CommandKindMismatch { expected: expected_kind, - actual, + actual: actual.kind, }); } - Ok(()) + Ok(actual) } fn complete( @@ -232,11 +317,36 @@ impl WebDriverBiDiCommandCorrelation { expected_kind: WebDriverBiDiCommandKind, outcome: WebDriverBiDiCorrelatedResponseOutcome, ) -> Result { - self.require_command_kind(command_id, expected_kind)?; + let outstanding = self.require_command_kind(command_id, expected_kind)?; let _removed = self.outstanding.remove(&command_id); Ok(WebDriverBiDiCorrelatedResponse { command_id, outcome, + connection_generation: outstanding.connection_generation, + }) + } + + fn complete_on_connection( + &mut self, + command_id: u64, + expected_kind: WebDriverBiDiCommandKind, + outcome: WebDriverBiDiCorrelatedResponseOutcome, + received_connection_generation: WebDriverBiDiConnectionGeneration, + ) -> Result { + let outstanding = self.require_command_kind(command_id, expected_kind)?; + let expected_connection_generation = outstanding.connection_generation.ok_or( + WebDriverBiDiCommandCorrelationError::CommandConnectionProvenanceMissing { command_id }, + )?; + if expected_connection_generation != received_connection_generation { + return Err( + WebDriverBiDiCommandCorrelationError::ResponseConnectionMismatch { command_id }, + ); + } + let _removed = self.outstanding.remove(&command_id); + Ok(WebDriverBiDiCorrelatedResponse { + command_id, + outcome, + connection_generation: Some(expected_connection_generation), }) } } @@ -271,6 +381,16 @@ mod tests { }, "WebDriver BiDi response command kind does not match the outstanding command", ), + ( + WebDriverBiDiCommandCorrelationError::CommandConnectionProvenanceMissing { + command_id: 7, + }, + "WebDriver BiDi outstanding command lacks connection provenance", + ), + ( + WebDriverBiDiCommandCorrelationError::ResponseConnectionMismatch { command_id: 7 }, + "WebDriver BiDi response arrived on a different connection", + ), ( WebDriverBiDiCommandCorrelationError::EventIsNotResponse, "WebDriver BiDi event cannot be correlated as a response", diff --git a/crates/originweave-network/src/webdriver_bidi_connection.rs b/crates/originweave-network/src/webdriver_bidi_connection.rs index 5d39bb5e3..f1701a923 100644 --- a/crates/originweave-network/src/webdriver_bidi_connection.rs +++ b/crates/originweave-network/src/webdriver_bidi_connection.rs @@ -1,6 +1,7 @@ use std::{ io, net::{SocketAddr, TcpStream}, + sync::atomic::{AtomicU64, Ordering}, time::Duration, }; @@ -12,9 +13,31 @@ mod error; pub use error::WebDriverBiDiTcpConnectionError; +#[cfg(test)] +mod generation_exhaustion_tests; #[cfg(test)] mod tests; +static NEXT_CONNECTION_GENERATION: AtomicU64 = AtomicU64::new(1); + +/// Process-local identity of one verified WebDriver BiDi transport generation. +/// +/// The value is minted only by the connection owner, is never accepted from callers, and exists +/// solely to prevent evidence from distinct sockets being combined across later protocol stages. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct WebDriverBiDiConnectionGeneration(u64); + +fn allocate_connection_generation( + counter: &AtomicU64, +) -> Result { + counter + .try_update(Ordering::Relaxed, Ordering::Relaxed, |current| { + current.checked_add(1) + }) + .map(WebDriverBiDiConnectionGeneration) + .map_err(|_| WebDriverBiDiTcpConnectionError::ConnectionGenerationExhausted) +} + fn is_retryable_connect_error(kind: io::ErrorKind) -> bool { matches!( kind, @@ -32,7 +55,9 @@ fn is_retryable_connect_error(kind: io::ErrorKind) -> bool { /// produced by `originweave-core`. It applies the same bounded per-attempt timeout and retry /// ceilings as the general direct-network connector, opens only the exact [`SocketAddr`] carried by /// that target, and does not expose the stream until the operating system's observed peer has been -/// verified by the consumed target. +/// verified by the consumed target. Each verified stream also receives one process-local monotonic +/// connection generation that later transport stages can retain as non-forgeable correlation +/// provenance; the generation is not public authority and is never accepted from callers. /// /// This boundary performs no DNS lookup, proxy or PAC routing, Chromium/ChromeDriver process /// authentication, TLS negotiation, WebSocket upgrade, BiDi framing, browser policy decision, or @@ -83,6 +108,14 @@ impl WebDriverBiDiTcpConnectionPlan { fn connect_with( self, connector: &dyn WebDriverBiDiSocketConnector, + ) -> Result { + self.connect_with_generation_counter(connector, &NEXT_CONNECTION_GENERATION) + } + + fn connect_with_generation_counter( + self, + connector: &dyn WebDriverBiDiSocketConnector, + generation_counter: &AtomicU64, ) -> Result { let socket_address = self.target.socket_addr(); let connect_timeout = self.connect_timeout; @@ -107,11 +140,13 @@ impl WebDriverBiDiTcpConnectionPlan { attempt_number, source, })?; + let connection_generation = allocate_connection_generation(generation_counter)?; return Ok(WebDriverBiDiTcpConnection { stream, verified_peer, attempt_number, connect_timeout, + connection_generation, }); } Err(source) @@ -170,13 +205,16 @@ impl WebDriverBiDiSocketConnector for SystemWebDriverBiDiConnector { /// /// This wrapper proves only exact transport-destination equality for one bounded connection. The /// caller must still establish any required TLS channel, complete a WebSocket handshake, bind the -/// transport to the expected browser process/session, and pass separate action-policy checks. +/// transport to the expected browser process/session, and pass separate action-policy checks. A +/// private process-local connection generation follows this exact stream so later evidence cannot +/// be mixed with another connection that happens to use the same session or command identifier. #[derive(Debug)] pub struct WebDriverBiDiTcpConnection { stream: TcpStream, verified_peer: VerifiedWebDriverBiDiSocketPeer, attempt_number: u8, connect_timeout: Duration, + connection_generation: WebDriverBiDiConnectionGeneration, } impl WebDriverBiDiTcpConnection { @@ -207,15 +245,17 @@ impl WebDriverBiDiTcpConnection { /// Consume the wrapper into the original verified stream and credential-free transport evidence. /// /// This handoff does not clone the socket or create reusable connection authority. The returned - /// evidence records only the already-verified peer plus bounded connection-attempt metadata; it - /// does not authenticate a browser process, establish TLS, complete WebSocket framing, or grant - /// browser or Agent authority. + /// evidence records the already-verified peer, bounded connection-attempt metadata, and one + /// private process-local connection generation for downstream provenance matching. It does not + /// authenticate a browser process, establish TLS, complete WebSocket framing, or grant browser + /// or Agent authority. #[must_use] pub fn into_parts(self) -> (TcpStream, WebDriverBiDiTcpConnectionEvidence) { let evidence = WebDriverBiDiTcpConnectionEvidence { verified_peer: self.verified_peer, attempt_number: self.attempt_number, connect_timeout: self.connect_timeout, + connection_generation: self.connection_generation, }; (self.stream, evidence) } @@ -224,13 +264,15 @@ impl WebDriverBiDiTcpConnection { /// Credential-free evidence retained when a verified WebDriver BiDi TCP stream is consumed. /// /// This value records exact peer/session/TLS-requirement metadata inherited from the consumed -/// no-DNS target together with the successful bounded attempt and per-attempt timeout. It is -/// transport evidence only and grants no process, TLS, WebSocket, browser-action, or Agent authority. +/// no-DNS target together with the successful bounded attempt, per-attempt timeout, and a private +/// process-local connection generation. It is transport evidence only and grants no process, TLS, +/// WebSocket, browser-action, or Agent authority. #[derive(Debug)] pub struct WebDriverBiDiTcpConnectionEvidence { verified_peer: VerifiedWebDriverBiDiSocketPeer, attempt_number: u8, connect_timeout: Duration, + connection_generation: WebDriverBiDiConnectionGeneration, } impl WebDriverBiDiTcpConnectionEvidence { @@ -251,4 +293,8 @@ impl WebDriverBiDiTcpConnectionEvidence { pub const fn connect_timeout(&self) -> Duration { self.connect_timeout } + + pub(crate) const fn connection_generation(&self) -> WebDriverBiDiConnectionGeneration { + self.connection_generation + } } diff --git a/crates/originweave-network/src/webdriver_bidi_connection/error.rs b/crates/originweave-network/src/webdriver_bidi_connection/error.rs index 226cd1d2b..75e574acf 100644 --- a/crates/originweave-network/src/webdriver_bidi_connection/error.rs +++ b/crates/originweave-network/src/webdriver_bidi_connection/error.rs @@ -19,6 +19,8 @@ pub enum WebDriverBiDiTcpConnectionError { /// The largest accepted attempt count. maximum_attempts: u8, }, + /// The process-local connection-generation space was exhausted before a distinct identity could be minted. + ConnectionGenerationExhausted, /// The final bounded connection attempt timed out. ConnectionTimedOut { /// Exact approved socket address submitted to the operating system. @@ -66,7 +68,9 @@ impl WebDriverBiDiTcpConnectionError { | Self::ConnectionFailed { attempt_count, .. } => Some(*attempt_count), Self::PeerInspectionFailed { attempt_number, .. } | Self::PeerMismatch { attempt_number, .. } => Some(*attempt_number), - Self::InvalidConnectTimeout { .. } | Self::InvalidAttemptCount { .. } => None, + Self::InvalidConnectTimeout { .. } + | Self::InvalidAttemptCount { .. } + | Self::ConnectionGenerationExhausted => None, } } } @@ -88,6 +92,9 @@ impl fmt::Display for WebDriverBiDiTcpConnectionError { formatter, "WebDriver BiDi connection attempt count {attempt_count} is outside 1..={maximum_attempts}", ), + Self::ConnectionGenerationExhausted => { + formatter.write_str("WebDriver BiDi connection generation space is exhausted") + } Self::ConnectionTimedOut { socket_address, attempt_count, @@ -128,7 +135,9 @@ impl std::error::Error for WebDriverBiDiTcpConnectionError { | Self::ConnectionFailed { source, .. } | Self::PeerInspectionFailed { source, .. } => Some(source), Self::PeerMismatch { source, .. } => Some(source), - Self::InvalidConnectTimeout { .. } | Self::InvalidAttemptCount { .. } => None, + Self::InvalidConnectTimeout { .. } + | Self::InvalidAttemptCount { .. } + | Self::ConnectionGenerationExhausted => None, } } } diff --git a/crates/originweave-network/src/webdriver_bidi_connection/generation_exhaustion_tests.rs b/crates/originweave-network/src/webdriver_bidi_connection/generation_exhaustion_tests.rs new file mode 100644 index 000000000..071216ad5 --- /dev/null +++ b/crates/originweave-network/src/webdriver_bidi_connection/generation_exhaustion_tests.rs @@ -0,0 +1,91 @@ +#![allow(clippy::expect_used)] + +use std::{ + cell::{Cell, RefCell}, + io, + net::{SocketAddr, TcpListener, TcpStream}, + sync::atomic::AtomicU64, + time::Duration, +}; + +use originweave_core::{WebDriverBiDiWebSocketConnectTarget, WebDriverBiDiWebSocketEndpoint}; + +use super::{ + WebDriverBiDiSocketConnector, WebDriverBiDiTcpConnectionError, WebDriverBiDiTcpConnectionPlan, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; + +struct VerifiedConnector { + stream: RefCell>, + connect_calls: Cell, + peer_calls: Cell, +} + +impl VerifiedConnector { + fn new(stream: TcpStream) -> Self { + Self { + stream: RefCell::new(Some(stream)), + connect_calls: Cell::new(0), + peer_calls: Cell::new(0), + } + } +} + +impl WebDriverBiDiSocketConnector for VerifiedConnector { + fn connect_timeout( + &self, + _socket_address: &SocketAddr, + _timeout: Duration, + ) -> io::Result { + self.connect_calls.set(self.connect_calls.get() + 1); + self.stream + .borrow_mut() + .take() + .ok_or_else(|| io::Error::other("test stream already consumed")) + } + + fn peer_addr(&self, _stream: &TcpStream) -> io::Result { + self.peer_calls.set(self.peer_calls.get() + 1); + Ok(SocketAddr::from(([127, 0, 0, 1], 9515))) + } +} + +fn loopback_stream() -> TcpStream { + let listener = TcpListener::bind(("127.0.0.1", 0)).expect("bind loopback listener"); + let address = listener + .local_addr() + .expect("read loopback listener address"); + let client = TcpStream::connect(address).expect("connect loopback client"); + let (server, _) = listener.accept().expect("accept loopback client"); + drop(server); + client +} + +fn connect_target() -> WebDriverBiDiWebSocketConnectTarget { + let endpoint = format!("ws://127.0.0.1:9515/session/{SESSION_ID}"); + WebDriverBiDiWebSocketEndpoint::new(&endpoint) + .expect("admit endpoint") + .correlate_session_id(SESSION_ID) + .expect("correlate endpoint") + .into_explicit_connect_target() + .expect("derive explicit connect target") +} + +#[test] +fn verified_connection_fails_closed_when_generation_space_is_exhausted() { + let connector = VerifiedConnector::new(loopback_stream()); + let exhausted_counter = AtomicU64::new(u64::MAX); + let error = + WebDriverBiDiTcpConnectionPlan::new(connect_target(), Duration::from_millis(250), 1) + .expect("valid plan") + .connect_with_generation_counter(&connector, &exhausted_counter) + .expect_err("generation exhaustion must fail after exact peer verification"); + + assert!(matches!( + error, + WebDriverBiDiTcpConnectionError::ConnectionGenerationExhausted + )); + assert_eq!(connector.connect_calls.get(), 1); + assert_eq!(connector.peer_calls.get(), 1); +} diff --git a/crates/originweave-network/src/webdriver_bidi_connection/tests.rs b/crates/originweave-network/src/webdriver_bidi_connection/tests.rs index e2d287ca1..63564283f 100644 --- a/crates/originweave-network/src/webdriver_bidi_connection/tests.rs +++ b/crates/originweave-network/src/webdriver_bidi_connection/tests.rs @@ -6,14 +6,16 @@ use std::{ error::Error, io, net::{SocketAddr, TcpListener, TcpStream}, + sync::atomic::AtomicU64, time::Duration, }; use originweave_core::{WebDriverBiDiWebSocketConnectTarget, WebDriverBiDiWebSocketEndpoint}; use super::{ - WebDriverBiDiSocketConnector, WebDriverBiDiTcpConnectionError, WebDriverBiDiTcpConnectionPlan, - is_retryable_connect_error, + WebDriverBiDiConnectionGeneration, WebDriverBiDiSocketConnector, + WebDriverBiDiTcpConnectionError, WebDriverBiDiTcpConnectionPlan, + allocate_connection_generation, is_retryable_connect_error, }; use crate::connection::{MAX_CONNECT_TIMEOUT, MAX_CONNECTION_ATTEMPTS}; @@ -115,6 +117,25 @@ fn plan(maximum_attempts: u8) -> WebDriverBiDiTcpConnectionPlan { .expect("valid test plan") } +#[test] +fn connection_generation_allocator_is_monotonic_and_fails_before_reuse() { + let counter = AtomicU64::new(41); + assert_eq!( + allocate_connection_generation(&counter).ok(), + Some(WebDriverBiDiConnectionGeneration(41)) + ); + assert_eq!( + allocate_connection_generation(&counter).ok(), + Some(WebDriverBiDiConnectionGeneration(42)) + ); + + let exhausted = AtomicU64::new(u64::MAX); + assert!(matches!( + allocate_connection_generation(&exhausted), + Err(WebDriverBiDiTcpConnectionError::ConnectionGenerationExhausted) + )); +} + #[test] fn validates_timeout_and_attempt_bounds_before_io() { let zero_timeout = @@ -316,6 +337,7 @@ fn error_display_source_and_attempt_contracts_cover_every_variant() { attempt_count: 0, maximum_attempts: MAX_CONNECTION_ATTEMPTS, }, + WebDriverBiDiTcpConnectionError::ConnectionGenerationExhausted, WebDriverBiDiTcpConnectionError::ConnectionTimedOut { socket_address: socket_address(), attempt_count: 2, @@ -341,21 +363,24 @@ fn error_display_source_and_attempt_contracts_cover_every_variant() { let messages: Vec = errors.iter().map(ToString::to_string).collect(); assert!(messages[0].contains("outside 1ns")); assert!(messages[1].contains("attempt count 0")); - assert!(messages[2].contains("timed out after 2 attempts")); - assert!(messages[3].contains("failed after 3 attempts")); - assert!(messages[4].contains("peer inspection failed")); - assert!(messages[5].contains("did not match the approved target")); + assert!(messages[2].contains("generation space is exhausted")); + assert!(messages[3].contains("timed out after 2 attempts")); + assert!(messages[4].contains("failed after 3 attempts")); + assert!(messages[5].contains("peer inspection failed")); + assert!(messages[6].contains("did not match the approved target")); assert_eq!(errors[0].attempt_count(), None); assert_eq!(errors[1].attempt_count(), None); - assert_eq!(errors[2].attempt_count(), Some(2)); - assert_eq!(errors[3].attempt_count(), Some(3)); - assert_eq!(errors[4].attempt_count(), Some(1)); + assert_eq!(errors[2].attempt_count(), None); + assert_eq!(errors[3].attempt_count(), Some(2)); + assert_eq!(errors[4].attempt_count(), Some(3)); assert_eq!(errors[5].attempt_count(), Some(1)); + assert_eq!(errors[6].attempt_count(), Some(1)); assert!(errors[0].source().is_none()); assert!(errors[1].source().is_none()); - assert!(errors[2].source().is_some()); + assert!(errors[2].source().is_none()); assert!(errors[3].source().is_some()); assert!(errors[4].source().is_some()); assert!(errors[5].source().is_some()); + assert!(errors[6].source().is_some()); } From 65ac3ab94daceebd8843c0727676fbbd71200256 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 07:32:23 +0900 Subject: [PATCH 34/34] fix(network): keep response provenance in child slice --- .../src/webdriver_bidi_command_correlation.rs | 75 +++---------------- 1 file changed, 10 insertions(+), 65 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs index 2b66551b0..67a6420c0 100644 --- a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs +++ b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs @@ -45,8 +45,9 @@ pub enum WebDriverBiDiCorrelatedResponseOutcome { /// /// This value carries only the matched command identifier and success/error classification. A /// private process-local connection generation is retained when the command owner bound one before -/// I/O so later transport evidence can be compared without accepting caller-supplied provenance. -/// It does not retain result bodies, error text, browser authority, transport authority, or secrets. +/// I/O so a later response-provenance owner can compare transport evidence without accepting +/// caller-supplied provenance. It does not retain result bodies, error text, browser authority, +/// transport authority, or secrets. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct WebDriverBiDiCorrelatedResponse { command_id: u64, @@ -133,13 +134,12 @@ impl Error for WebDriverBiDiCommandCorrelationError {} /// /// Register an id together with its exact typed command family only after the caller has committed /// to that outbound command. Connection-owning command adapters may additionally bind the private -/// generation of the exact established transport before I/O. A success or correlatable error -/// response consumes the id exactly once only through a matching typed consumer. Events, null-id -/// errors, command-kind mismatches, missing connection provenance, and responses received on a -/// different verified connection leave outstanding state untouched. This type performs no I/O, -/// retry, command serialization, browser authentication, or authority grant. Debug output reports -/// only the outstanding-count summary; command identifiers, families, and generations remain -/// private correlation state. +/// generation of the exact established transport before I/O. Generic success or correlatable error +/// responses consume the id exactly once through a matching typed consumer; a later slice that owns +/// received-connection evidence adds the connection-sensitive consuming boundary. This type +/// performs no I/O, retry, command serialization, browser authentication, or authority grant. Debug +/// output reports only the outstanding-count summary; command identifiers, families, and +/// generations remain private correlation state. #[derive(Default)] pub struct WebDriverBiDiCommandCorrelation { outstanding: BTreeMap, @@ -233,7 +233,7 @@ impl WebDriverBiDiCommandCorrelation { /// Successful responses and error responses with ids consume exactly one matching command. /// Unknown ids and command-kind mismatches fail without consuming state. Events and null-id /// errors fail before touching the map. This generic path does not claim received-connection - /// provenance; connection-sensitive command owners must use their connection-bound path. + /// provenance; connection-sensitive response handling belongs to its owning child slice. pub fn correlate_response_for( &mut self, envelope: &WebDriverBiDiJsonEnvelope, @@ -261,37 +261,6 @@ impl WebDriverBiDiCommandCorrelation { } } - pub(crate) fn correlate_response_for_connection( - &mut self, - envelope: &WebDriverBiDiJsonEnvelope, - expected_kind: WebDriverBiDiCommandKind, - received_connection_generation: WebDriverBiDiConnectionGeneration, - ) -> Result { - match envelope.routing() { - WebDriverBiDiJsonEnvelopeRouting::Event => { - Err(WebDriverBiDiCommandCorrelationError::EventIsNotResponse) - } - WebDriverBiDiJsonEnvelopeRouting::CommandError { command_id: None } => { - Err(WebDriverBiDiCommandCorrelationError::UncorrelatableErrorResponse) - } - WebDriverBiDiJsonEnvelopeRouting::CommandError { - command_id: Some(command_id), - } => self.complete_on_connection( - command_id, - expected_kind, - WebDriverBiDiCorrelatedResponseOutcome::Error, - received_connection_generation, - ), - WebDriverBiDiJsonEnvelopeRouting::CommandSuccess { command_id } => self - .complete_on_connection( - command_id, - expected_kind, - WebDriverBiDiCorrelatedResponseOutcome::Success, - received_connection_generation, - ), - } - } - fn require_command_kind( &self, command_id: u64, @@ -325,30 +294,6 @@ impl WebDriverBiDiCommandCorrelation { connection_generation: outstanding.connection_generation, }) } - - fn complete_on_connection( - &mut self, - command_id: u64, - expected_kind: WebDriverBiDiCommandKind, - outcome: WebDriverBiDiCorrelatedResponseOutcome, - received_connection_generation: WebDriverBiDiConnectionGeneration, - ) -> Result { - let outstanding = self.require_command_kind(command_id, expected_kind)?; - let expected_connection_generation = outstanding.connection_generation.ok_or( - WebDriverBiDiCommandCorrelationError::CommandConnectionProvenanceMissing { command_id }, - )?; - if expected_connection_generation != received_connection_generation { - return Err( - WebDriverBiDiCommandCorrelationError::ResponseConnectionMismatch { command_id }, - ); - } - let _removed = self.outstanding.remove(&command_id); - Ok(WebDriverBiDiCorrelatedResponse { - command_id, - outcome, - connection_generation: Some(expected_connection_generation), - }) - } } #[cfg(test)]