From 7e85a7e5f0147f4b712129cd19aaa3d0a0a54634 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 00:05:35 +0900 Subject: [PATCH 01/22] test: reject session.status reply from replacement connection --- ...n_status_response_connection_provenance.rs | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_session_status_response_connection_provenance.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_session_status_response_connection_provenance.rs b/crates/originweave-network/tests/webdriver_bidi_session_status_response_connection_provenance.rs new file mode 100644 index 000000000..a089710a7 --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_session_status_response_connection_provenance.rs @@ -0,0 +1,161 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::WebDriverBiDiWebSocketEndpoint; +use originweave_network::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiSessionStatusCommand, + WebDriverBiDiSessionStatusResult, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler, + WebDriverBiDiWebSocketMessageAssembly, WebDriverBiDiWebSocketTextMessage, +}; + +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":"capacity available"}}"#; + +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, + "session.status fixture 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(local_addr: std::net::SocketAddr) -> Result> { + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? + .correlate_session_id(SESSION_ID)? + .into_explicit_connect_target()?; + let connection = + WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; + Ok(WebDriverBiDiWebSocketHandshakePlan::new( + connection, + WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?, + )? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?) +} + +fn receive_foreign_status_response( + listener: TcpListener, +) -> Result> { + let local_addr = listener.local_addr()?; + let server = thread::spawn(move || -> io::Result<()> { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + stream.write_all(&[0x81, STATUS_RESPONSE.len() as u8])?; + stream.write_all(STATUS_RESPONSE) + }); + + let established = establish(local_addr)?; + let (_established, frame) = established.read_frame(Duration::from_millis(500))?; + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); + let text = match assembler.push_frame(frame)? { + WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + other => { + return Err(io::Error::other(format!( + "replacement connection produced unexpected assembly state: {other:?}" + )) + .into()); + } + }; + server + .join() + .map_err(|_| io::Error::other("replacement-connection server panicked"))??; + Ok(text) +} + +#[test] +fn session_status_response_from_same_session_replacement_connection_cannot_consume_original_pending_command( +) -> Result<(), Box> { + let original_listener = TcpListener::bind(("127.0.0.1", 0))?; + let original_addr = original_listener.local_addr()?; + let original_server = thread::spawn(move || -> io::Result<()> { + let (mut stream, _) = original_listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + let command = read_masked_text_frame(&mut stream)?; + if command != br#"{"id":7,"method":"session.status","params":{}}"# { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "unexpected session.status command on original connection", + )); + } + Ok(()) + }); + + let original = establish(original_addr)?; + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + let _original = WebDriverBiDiSessionStatusCommand::new(7)?.send( + original, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + Duration::from_millis(500), + )?; + original_server + .join() + .map_err(|_| io::Error::other("original-connection server panicked"))??; + assert_eq!(correlation.outstanding_count(), 1); + + let replacement_listener = TcpListener::bind(("127.0.0.1", 0))?; + let replacement_response = receive_foreign_status_response(replacement_listener)?; + let parsed = WebDriverBiDiSessionStatusResult::parse_and_correlate( + &replacement_response, + &mut correlation, + ); + + assert!( + parsed.is_err(), + "same-session replacement connection unexpectedly consumed the original session.status command" + ); + assert_eq!( + correlation.outstanding_count(), + 1, + "foreign-connection rejection must leave the original command pending" + ); + Ok(()) +} From 6b102c1d860629d1b3e1a49cb0c33d94ca825adb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 01:13:33 +0900 Subject: [PATCH 02/22] test: apply canonical status-provenance formatting --- ..._bidi_session_status_response_connection_provenance.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_status_response_connection_provenance.rs b/crates/originweave-network/tests/webdriver_bidi_session_status_response_connection_provenance.rs index a089710a7..8ad11aefa 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_status_response_connection_provenance.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_status_response_connection_provenance.rs @@ -64,7 +64,9 @@ fn read_masked_text_frame(stream: &mut TcpStream) -> io::Result> { Ok(payload) } -fn establish(local_addr: std::net::SocketAddr) -> Result> { +fn establish( + local_addr: std::net::SocketAddr, +) -> Result> { let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? .correlate_session_id(SESSION_ID)? @@ -110,8 +112,8 @@ fn receive_foreign_status_response( } #[test] -fn session_status_response_from_same_session_replacement_connection_cannot_consume_original_pending_command( -) -> Result<(), Box> { +fn session_status_response_from_same_session_replacement_connection_cannot_consume_original_pending_command() +-> Result<(), Box> { let original_listener = TcpListener::bind(("127.0.0.1", 0))?; let original_addr = original_listener.local_addr()?; let original_server = thread::spawn(move || -> io::Result<()> { From e77150f4de6534887098fb9de7e02ecea7fbb59c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 06:08:50 +0900 Subject: [PATCH 03/22] 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 5aa7d1a6c7ad63b62186cc7ca882cf17e215c613 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 06:12:30 +0900 Subject: [PATCH 04/22] merge: adopt session.status sender provenance --- .../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 9307eca134d647d9330f5455058b1888ab40fd06 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 06:13:57 +0900 Subject: [PATCH 05/22] fix: bind session.status responses to connection --- .../webdriver_bidi_session_status_response.rs | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs index b1e5596ba..285bb6d6f 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs @@ -3,7 +3,7 @@ use std::{error::Error, fmt}; use crate::{ WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, WebDriverBiDiCommandKind, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeError, - WebDriverBiDiJsonEnvelopeKind, WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiJsonEnvelopeKind, WebDriverBiDiReceivedTextMessage, }; /// Maximum decoded byte length retained from WebDriver BiDi `session.status` implementation text. @@ -37,27 +37,33 @@ impl fmt::Debug for WebDriverBiDiSessionStatusResult { } impl WebDriverBiDiSessionStatusResult { - /// Parse one bounded local-end message and consume its exact outstanding command on success. + /// Parse one bounded received message and consume its exact outstanding command on success. /// /// Common WebDriver BiDi envelope validation runs first. A successful envelope then undergoes /// command-specific projection of `result.ready` and `result.message`; correlation is consumed /// only after that result is valid, so malformed success bodies cannot silently retire an id. /// A correlatable protocol-error response consumes its matching id and returns a typed remote /// protocol failure retaining the protocol error code but not the implementation-defined remote - /// message or stacktrace. Events, null-id errors, and unknown ids fail closed through the - /// existing correlation boundary. + /// message or stacktrace. Both success and error responses must have arrived on the exact + /// connection generation that registered the command; replacement-connection responses leave + /// the original pending command untouched. Events, null-id errors, and unknown ids fail closed + /// through the existing correlation boundary. pub fn parse_and_correlate( - message: &WebDriverBiDiWebSocketTextMessage, + message: &WebDriverBiDiReceivedTextMessage, correlation: &mut WebDriverBiDiCommandCorrelation, ) -> Result { - let envelope = WebDriverBiDiJsonEnvelope::parse(message) + let envelope = WebDriverBiDiJsonEnvelope::parse(message.message()) .map_err(|source| WebDriverBiDiSessionStatusResponseError::Envelope { source })?; match envelope.kind() { WebDriverBiDiJsonEnvelopeKind::Success => { - let projected = StatusProjection::parse(message.as_str())?; + let projected = StatusProjection::parse(message.message().as_str())?; let completed = correlation - .correlate_response_for(&envelope, WebDriverBiDiCommandKind::SessionStatus) + .correlate_response_for_connection( + &envelope, + WebDriverBiDiCommandKind::SessionStatus, + message.connection_generation(), + ) .map_err( |source| WebDriverBiDiSessionStatusResponseError::Correlation { source }, )?; @@ -70,7 +76,11 @@ impl WebDriverBiDiSessionStatusResult { WebDriverBiDiJsonEnvelopeKind::Error => { retain_validated_error_code(envelope.error_code()).and_then(|error_code| { let completed = correlation - .correlate_response_for(&envelope, WebDriverBiDiCommandKind::SessionStatus) + .correlate_response_for_connection( + &envelope, + WebDriverBiDiCommandKind::SessionStatus, + message.connection_generation(), + ) .map_err( |source| WebDriverBiDiSessionStatusResponseError::Correlation { source, From 0b68198e27bab23f001053d3ff605ace9d6d595a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 06:14:26 +0900 Subject: [PATCH 06/22] test: carry status response connection evidence --- .../webdriver_bidi_session_status_response.rs | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs b/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs index 347dc5fec..45ee995b6 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs @@ -8,12 +8,12 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use originweave_network::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiSessionStatusCommand, + WebDriverBiDiCommandCorrelation, WebDriverBiDiConnectionMessageRead, + WebDriverBiDiReceivedTextMessage, WebDriverBiDiSessionStatusCommand, WebDriverBiDiSessionStatusResponseError, WebDriverBiDiSessionStatusResult, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, - WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, - WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiWebSocketMessageReader, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -72,7 +72,7 @@ fn send_status_and_read_response( response: &'static [u8], ) -> Result< ( - WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiReceivedTextMessage, WebDriverBiDiCommandCorrelation, ), Box, @@ -113,13 +113,13 @@ fn send_status_and_read_response( Duration::from_millis(500), )?; - let (_established, frame) = established.read_frame(Duration::from_millis(500))?; - let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); - let text = match assembler.push_frame(frame)? { - WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + let message = match WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + WebDriverBiDiConnectionMessageRead::Text { message, .. } => message, other => { return Err(io::Error::other(format!( - "session.status response produced unexpected assembly state: {other:?}" + "session.status response produced unexpected message state: {other:?}" )) .into()); } @@ -128,14 +128,14 @@ fn send_status_and_read_response( server .join() .map_err(|_| io::Error::other("session.status response test server panicked"))??; - Ok((text, correlation)) + Ok((message, correlation)) } #[test] fn session_status_success_result_is_typed_correlated_and_message_redacted_in_debug() -> Result<(), Box> { - let (text, mut correlation) = send_status_and_read_response(STATUS_RESPONSE)?; - let result = WebDriverBiDiSessionStatusResult::parse_and_correlate(&text, &mut correlation)?; + let (message, mut correlation) = send_status_and_read_response(STATUS_RESPONSE)?; + let result = WebDriverBiDiSessionStatusResult::parse_and_correlate(&message, &mut correlation)?; assert_eq!(result.command_id(), 7); assert!(result.ready()); @@ -151,8 +151,8 @@ fn session_status_success_result_is_typed_correlated_and_message_redacted_in_deb #[test] fn malformed_status_result_does_not_consume_the_outstanding_command() -> Result<(), Box> { - let (text, mut correlation) = send_status_and_read_response(STATUS_RESPONSE_MISSING_READY)?; - let parsed = WebDriverBiDiSessionStatusResult::parse_and_correlate(&text, &mut correlation); + let (message, mut correlation) = send_status_and_read_response(STATUS_RESPONSE_MISSING_READY)?; + let parsed = WebDriverBiDiSessionStatusResult::parse_and_correlate(&message, &mut correlation); assert!(matches!( parsed, @@ -165,8 +165,8 @@ fn malformed_status_result_does_not_consume_the_outstanding_command() -> Result< #[test] fn empty_status_result_fails_before_consuming_the_outstanding_command() -> Result<(), Box> { - let (text, mut correlation) = send_status_and_read_response(STATUS_RESPONSE_EMPTY_RESULT)?; - let parsed = WebDriverBiDiSessionStatusResult::parse_and_correlate(&text, &mut correlation); + let (message, mut correlation) = send_status_and_read_response(STATUS_RESPONSE_EMPTY_RESULT)?; + let parsed = WebDriverBiDiSessionStatusResult::parse_and_correlate(&message, &mut correlation); assert!(matches!( parsed, From cafa95001cbaa918dce66d3706a955088b392ed9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 06:15:09 +0900 Subject: [PATCH 07/22] test: retain status connection provenance in hostile cases --- ...er_bidi_session_status_response_hostile.rs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_status_response_hostile.rs b/crates/originweave-network/tests/webdriver_bidi_session_status_response_hostile.rs index e588fa6e5..2e5d69e42 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_status_response_hostile.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_status_response_hostile.rs @@ -9,11 +9,11 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use originweave_network::{ MAX_WEBDRIVER_BIDI_SESSION_STATUS_MESSAGE_SIZE, WebDriverBiDiCommandCorrelation, + WebDriverBiDiConnectionMessageRead, WebDriverBiDiReceivedTextMessage, WebDriverBiDiSessionStatusCommand, WebDriverBiDiSessionStatusResponseError, WebDriverBiDiSessionStatusResult, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler, - WebDriverBiDiWebSocketMessageAssembly, WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageReader, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -21,7 +21,7 @@ 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 StatusRead = ( - WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiReceivedTextMessage, WebDriverBiDiCommandCorrelation, ); @@ -132,13 +132,13 @@ fn send_status_and_read_response(response: Vec) -> Result text, + let message = match WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + WebDriverBiDiConnectionMessageRead::Text { message, .. } => message, other => { return Err(io::Error::other(format!( - "session.status response produced unexpected assembly state: {other:?}" + "session.status response produced unexpected message state: {other:?}" )) .into()); } @@ -147,7 +147,7 @@ fn send_status_and_read_response(response: Vec) -> Result, > { - let (text, mut correlation) = send_status_and_read_response(response)?; - let parsed = WebDriverBiDiSessionStatusResult::parse_and_correlate(&text, &mut correlation); + let (message, mut correlation) = send_status_and_read_response(response)?; + let parsed = WebDriverBiDiSessionStatusResult::parse_and_correlate(&message, &mut correlation); Ok((parsed, correlation)) } From 05cbc5c5a6dd777c8a59199450352318360f72ca Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 06:15:35 +0900 Subject: [PATCH 08/22] test: require exact status response connection provenance --- ...n_status_response_connection_provenance.rs | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_status_response_connection_provenance.rs b/crates/originweave-network/tests/webdriver_bidi_session_status_response_connection_provenance.rs index 8ad11aefa..0be86d79a 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_status_response_connection_provenance.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_status_response_connection_provenance.rs @@ -8,11 +8,12 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use originweave_network::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiSessionStatusCommand, + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, + WebDriverBiDiConnectionMessageRead, WebDriverBiDiReceivedTextMessage, + WebDriverBiDiSessionStatusCommand, WebDriverBiDiSessionStatusResponseError, WebDriverBiDiSessionStatusResult, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler, - WebDriverBiDiWebSocketMessageAssembly, WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageReader, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -83,7 +84,7 @@ fn establish( fn receive_foreign_status_response( listener: TcpListener, -) -> Result> { +) -> Result> { let local_addr = listener.local_addr()?; let server = thread::spawn(move || -> io::Result<()> { let (mut stream, _) = listener.accept()?; @@ -94,13 +95,13 @@ fn receive_foreign_status_response( }); let established = establish(local_addr)?; - let (_established, frame) = established.read_frame(Duration::from_millis(500))?; - let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); - let text = match assembler.push_frame(frame)? { - WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + let message = match WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + WebDriverBiDiConnectionMessageRead::Text { message, .. } => message, other => { return Err(io::Error::other(format!( - "replacement connection produced unexpected assembly state: {other:?}" + "replacement connection produced unexpected message state: {other:?}" )) .into()); } @@ -108,7 +109,7 @@ fn receive_foreign_status_response( server .join() .map_err(|_| io::Error::other("replacement-connection server panicked"))??; - Ok(text) + Ok(message) } #[test] @@ -150,10 +151,14 @@ fn session_status_response_from_same_session_replacement_connection_cannot_consu &mut correlation, ); - assert!( - parsed.is_err(), - "same-session replacement connection unexpectedly consumed the original session.status command" - ); + assert!(matches!( + parsed, + Err(WebDriverBiDiSessionStatusResponseError::Correlation { + source: WebDriverBiDiCommandCorrelationError::ResponseConnectionMismatch { + command_id: 7, + }, + }) + )); assert_eq!( correlation.outstanding_count(), 1, From 0e1e47c0360650f1ee5e761e321209bcbdee6e86 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 06:18:01 +0900 Subject: [PATCH 09/22] test: exercise received status boundary --- ...idi_json_envelope_public_boundary_tests.rs | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs b/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs index 1e97bff2f..0c6baf39d 100644 --- a/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs +++ b/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs @@ -9,12 +9,13 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use crate::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandKind, WebDriverBiDiJsonEnvelope, + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandKind, + WebDriverBiDiConnectionMessageRead, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeError, WebDriverBiDiJsonEnvelopeKind, - WebDriverBiDiSessionStatusResponseError, WebDriverBiDiSessionStatusResult, - WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, - WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMessageAssembler, - WebDriverBiDiWebSocketMessageAssembly, WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiReceivedTextMessage, WebDriverBiDiSessionStatusResponseError, + WebDriverBiDiSessionStatusResult, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketMessageReader, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -43,7 +44,7 @@ fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { fn read_text_over_loopback( document: &'static [u8], -) -> Result> { +) -> Result> { if document.len() > 125 { return Err(io::Error::other("unit JSON document exceeded one-byte frame length").into()); } @@ -68,14 +69,14 @@ fn read_text_over_loopback( let established = WebDriverBiDiWebSocketHandshakePlan::new(connection, key)? .write_opening_request(Duration::from_millis(500))? .read_opening_response(Duration::from_millis(500))?; - let (_established, frame) = established.read_frame(Duration::from_millis(500))?; - let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); - let text = match assembler.push_frame(frame)? { - WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + let message = match WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + WebDriverBiDiConnectionMessageRead::Text { message, .. } => message, other => { return Err(io::Error::other(format!( - "validated text frame produced unexpected assembly state: {other:?}" + "validated text frame produced unexpected message state: {other:?}" )) .into()); } @@ -84,14 +85,14 @@ fn read_text_over_loopback( server .join() .map_err(|_| io::Error::other("JSON-envelope unit server panicked"))??; - Ok(text) + Ok(message) } fn parse_over_loopback( document: &'static [u8], ) -> Result, Box> { - let text = read_text_over_loopback(document)?; - Ok(WebDriverBiDiJsonEnvelope::parse(&text)) + let message = read_text_over_loopback(document)?; + Ok(WebDriverBiDiJsonEnvelope::parse(message.message())) } #[test] @@ -150,11 +151,11 @@ fn public_json_envelope_unit_build_covers_fail_closed_json_edges() -> Result<(), #[test] fn public_session_status_empty_result_fails_closed_from_unit_build() -> Result<(), Box> { - let text = read_text_over_loopback(EMPTY_STATUS_RESULT)?; + let message = read_text_over_loopback(EMPTY_STATUS_RESULT)?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); correlation.register_command_for(7, WebDriverBiDiCommandKind::SessionStatus)?; - let parsed = WebDriverBiDiSessionStatusResult::parse_and_correlate(&text, &mut correlation); + let parsed = WebDriverBiDiSessionStatusResult::parse_and_correlate(&message, &mut correlation); assert!(matches!( parsed, Err(WebDriverBiDiSessionStatusResponseError::MissingReady) From aadba11b94ac6bc426702b3caf63f893e28368cc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 06:23:17 +0900 Subject: [PATCH 10/22] docs: record session.status connection provenance --- docs/doctoring.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/doctoring.md b/docs/doctoring.md index bb9166e20..b72c975ec 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -122,6 +122,8 @@ The test-only `serve_opening_exchange` helper reuses the bounded request reader, The #250 integration preserves all five child-owned production and Rust-test blobs from `0eab23d5e388c5c8b984c0021a58316680c9ba8b` and ordinarily adopts #249 `84b9407978ae0f6c115f01170b6069c601b21104`. The pre-integration branch lacked that parent, and native unittest discovery collected zero correlation release checks instead of one. Parent adoption brings the canonical synchronized opening-exchange fixture and the discoverable release TestCase into this response stack without copying either implementation. Command-specific projection still validates the common envelope, required readiness/message fields and bounded status text before consuming the exact typed outstanding correlation; status text is untrusted and is omitted from Debug output. This does not add a connection-bound received-message capability, browser policy authority, runtime process/profile teardown proof or real Chromium acceptance. The connection-provenance repair remains separately owned by its later stack. Local integrated tests and exact-head hosted gates must be evaluated independently; predecessor results do not transfer. +On 6 September 2026, that historical no-connection-provenance state was superseded for the current #250 branch by an executed real-socket regression at `6b102c1d860629d1b3e1a49cb0c33d94ca825adb`: CI `34044758402` / Rust job `101517723552` showed that a same-session replacement WebSocket could consume the original pending `session.status` command. Parent #249 `e77150f4de6534887098fb9de7e02ecea7fbb59c` now registers `SessionStatus` with the established connection generation before frame I/O. #250 adopts that parent by an ordinary two-parent non-force merge and its command-specific response parser accepts only `WebDriverBiDiReceivedTextMessage`, correlating both success and protocol-error envelopes against the received connection generation. The regression requires typed `ResponseConnectionMismatch { command_id: 7 }` and preserves the original outstanding command after rejection. Malformed success projection and invalid protocol-error shape still fail before correlation consumption. This is transport/correlation provenance only: it does not grant browser policy authority, prove Chromium post-conditions or runtime process/profile teardown, or establish protected-main/release acceptance; exact-current hosted gates remain independently required. + ## References Amazon Web Services. (n.d.). *Set up the Amazon EKS Pod Identity Agent*. Retrieved August 6, 2026, from https://docs.aws.amazon.com/eks/latest/userguide/pod-id-agent-setup.html @@ -204,4 +206,4 @@ World Wide Web Consortium. (2026, August 5). *Accessible name and description co 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 -Zhou, S., Xu, F. F., Zhu, H., Zhou, X., Lo, R., Sridhar, A., Cheng, X., Ou, T., Bisk, Y., Fried, D., Alon, U., & Neubig, G. (2023). *WebArena: A realistic web environment for building autonomous agents*. arXiv. https://doi.org/10.48550/arXiv.2307.13854 +Zhou, S., Xu, F. F., Zhu, H., Zhou, X., Lo, R., Sridhar, A., Cheng, X., Ou, T., Bisk, Y., Fried, D., Alon, U., & Neubig, G. (2023). *WebArena: A realistic web environment for building autonomous agents*. arXiv. https://doi.org/10.48550/arXiv.2307.13854 \ No newline at end of file From 583ffee2ee471bdb49762225d810ff9b5d6a9a77 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 06:39:15 +0900 Subject: [PATCH 11/22] 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 615d8408cec07c9d863ba62aa379f02b199e7536 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 07:19:30 +0900 Subject: [PATCH 12/22] fix: format session.status connection provenance boundary --- ...ver_bidi_json_envelope_public_boundary_tests.rs | 5 ++--- .../src/webdriver_bidi_session_status_response.rs | 14 ++++++-------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs b/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs index 0c6baf39d..94e6fd3de 100644 --- a/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs +++ b/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs @@ -9,9 +9,8 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use crate::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandKind, - WebDriverBiDiConnectionMessageRead, WebDriverBiDiJsonEnvelope, - WebDriverBiDiJsonEnvelopeError, WebDriverBiDiJsonEnvelopeKind, + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandKind, WebDriverBiDiConnectionMessageRead, + WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeError, WebDriverBiDiJsonEnvelopeKind, WebDriverBiDiReceivedTextMessage, WebDriverBiDiSessionStatusResponseError, WebDriverBiDiSessionStatusResult, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, diff --git a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs index 285bb6d6f..a1037b9cb 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs @@ -64,9 +64,9 @@ impl WebDriverBiDiSessionStatusResult { WebDriverBiDiCommandKind::SessionStatus, message.connection_generation(), ) - .map_err( - |source| WebDriverBiDiSessionStatusResponseError::Correlation { source }, - )?; + .map_err(|source| WebDriverBiDiSessionStatusResponseError::Correlation { + source, + })?; Ok(Self { command_id: completed.command_id(), ready: projected.ready, @@ -81,11 +81,9 @@ impl WebDriverBiDiSessionStatusResult { WebDriverBiDiCommandKind::SessionStatus, message.connection_generation(), ) - .map_err( - |source| WebDriverBiDiSessionStatusResponseError::Correlation { - source, - }, - )?; + .map_err(|source| { + WebDriverBiDiSessionStatusResponseError::Correlation { source } + })?; Err( WebDriverBiDiSessionStatusResponseError::RemoteProtocolError { command_id: completed.command_id(), From 65ac3ab94daceebd8843c0727676fbbd71200256 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 07:32:23 +0900 Subject: [PATCH 13/22] 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)] From 74536b276b5e55bdd1f788aef5d095b4b1a1ed1c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 13:24:40 +0900 Subject: [PATCH 14/22] test(network): cover sealed reader and correlation rejection paths Reuse the existing sealed-reader integration checks and preserve pending commands after event or missing-provenance rejection. Apply canonical formatting observed in exact-head CI. Co-Authored-By: OpenAI Codex Signed-off-by: Seongho Bae --- .../src/webdriver_bidi_json_envelope.rs | 49 +++++++ .../webdriver_bidi_session_status_response.rs | 25 ++-- .../tests/webdriver_bidi_received_message.rs | 137 ++++++++++++++++++ 3 files changed, 199 insertions(+), 12 deletions(-) create mode 100644 crates/originweave-network/tests/webdriver_bidi_received_message.rs diff --git a/crates/originweave-network/src/webdriver_bidi_json_envelope.rs b/crates/originweave-network/src/webdriver_bidi_json_envelope.rs index 1d92d0ed8..276209cb0 100644 --- a/crates/originweave-network/src/webdriver_bidi_json_envelope.rs +++ b/crates/originweave-network/src/webdriver_bidi_json_envelope.rs @@ -702,6 +702,55 @@ mod tests { WebDriverBiDiJsonEnvelope::parse_str(value) } + #[test] + fn connection_correlation_rejections_preserve_pending_command() -> Result<(), Box> { + use std::{net::TcpListener, time::Duration}; + + use originweave_core::WebDriverBiDiWebSocketEndpoint; + + use crate::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, + WebDriverBiDiCommandKind, WebDriverBiDiTcpConnectionPlan, + }; + + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let session = "01234567-89ab-cdef-0123-456789abcdef"; + let endpoint = format!("ws://{}/session/{session}", listener.local_addr()?); + let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? + .correlate_session_id(session)? + .into_explicit_connect_target()?; + let connection = + WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; + let (_server, _) = listener.accept()?; + let generation = connection.connection_generation(); + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + correlation.register_command_for(7, WebDriverBiDiCommandKind::SessionStatus)?; + + for (document, expected) in [ + ( + r#"{"type":"event","method":"log.entryAdded","params":{}}"#, + WebDriverBiDiCommandCorrelationError::EventIsNotResponse, + ), + ( + r#"{"type":"success","id":7,"result":{}}"#, + WebDriverBiDiCommandCorrelationError::CommandConnectionProvenanceMissing { + command_id: 7, + }, + ), + ] { + assert_eq!( + correlation.correlate_response_for_connection( + &parse(document)?, + WebDriverBiDiCommandKind::SessionStatus, + generation, + ), + Err(expected) + ); + assert_eq!(correlation.outstanding_count(), 1); + } + Ok(()) + } + #[test] fn classifies_all_local_end_envelope_kinds_and_redacts_debug() { let success = parse( diff --git a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs index a1037b9cb..d6a8f8605 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs @@ -64,9 +64,9 @@ impl WebDriverBiDiSessionStatusResult { WebDriverBiDiCommandKind::SessionStatus, message.connection_generation(), ) - .map_err(|source| WebDriverBiDiSessionStatusResponseError::Correlation { - source, - })?; + .map_err( + |source| WebDriverBiDiSessionStatusResponseError::Correlation { source }, + )?; Ok(Self { command_id: completed.command_id(), ready: projected.ready, @@ -75,15 +75,16 @@ impl WebDriverBiDiSessionStatusResult { } WebDriverBiDiJsonEnvelopeKind::Error => { retain_validated_error_code(envelope.error_code()).and_then(|error_code| { - let completed = correlation - .correlate_response_for_connection( - &envelope, - WebDriverBiDiCommandKind::SessionStatus, - message.connection_generation(), - ) - .map_err(|source| { - WebDriverBiDiSessionStatusResponseError::Correlation { source } - })?; + let completed = + correlation + .correlate_response_for_connection( + &envelope, + WebDriverBiDiCommandKind::SessionStatus, + message.connection_generation(), + ) + .map_err(|source| { + WebDriverBiDiSessionStatusResponseError::Correlation { source } + })?; Err( WebDriverBiDiSessionStatusResponseError::RemoteProtocolError { command_id: completed.command_id(), diff --git a/crates/originweave-network/tests/webdriver_bidi_received_message.rs b/crates/originweave-network/tests/webdriver_bidi_received_message.rs new file mode 100644 index 000000000..edf752c06 --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_received_message.rs @@ -0,0 +1,137 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::WebDriverBiDiWebSocketEndpoint; +use originweave_network::{ + WebDriverBiDiConnectionMessageRead, WebDriverBiDiConnectionMessageReadError, + WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMessageReader, +}; + +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 established_for_frames( + frames: Vec>, +) -> Result> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + thread::spawn(move || -> io::Result<()> { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + for frame in frames { + stream.write_all(&frame)?; + } + 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)?; + Ok(WebDriverBiDiWebSocketHandshakePlan::new(connection, key)? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?) +} + +#[test] +fn fragmented_text_and_interleaved_control_remain_on_one_reader() -> Result<(), Box> { + let established = established_for_frames(vec![ + vec![0x01, 0x03, b'a', b'b', b'c'], + vec![0x89, 0x00], + vec![0x80, 0x03, b'd', b'e', b'f'], + ])?; + let reader = WebDriverBiDiWebSocketMessageReader::new(established); + assert!(format!("{reader:?}").contains("connection_bound")); + + let first = reader.read_next(Duration::from_millis(500))?; + assert!(format!("{first:?}").starts_with("Pending")); + let reader = match first { + WebDriverBiDiConnectionMessageRead::Pending(reader) => reader, + _ => return Err(io::Error::other("first fragment did not remain pending").into()), + }; + + let control = reader.read_next(Duration::from_millis(500))?; + assert!(format!("{control:?}").starts_with("Control")); + let reader = match control { + WebDriverBiDiConnectionMessageRead::Control { reader, message } => { + assert_eq!(message.payload(), b""); + reader + } + _ => return Err(io::Error::other("interleaved Ping was not surfaced").into()), + }; + + let completed = reader.read_next(Duration::from_millis(500))?; + let debug = format!("{completed:?}"); + assert!(debug.starts_with("Text")); + assert!(debug.contains("payload_bytes")); + match completed { + WebDriverBiDiConnectionMessageRead::Text { + established, + message: _, + } => drop(established), + _ => return Err(io::Error::other("continuation did not complete text message").into()), + } + Ok(()) +} + +#[test] +fn frame_and_message_failures_remain_typed_and_sourced() -> Result<(), Box> { + let malformed = established_for_frames(vec![vec![0x81, 0x80]])?; + let frame_error = WebDriverBiDiWebSocketMessageReader::new(malformed) + .read_next(Duration::from_millis(500)) + .err() + .ok_or_else(|| io::Error::other("masked server frame was accepted"))?; + assert!(matches!( + frame_error, + WebDriverBiDiConnectionMessageReadError::Frame { .. } + )); + assert_eq!( + frame_error.to_string(), + "connection-bound WebDriver BiDi WebSocket frame read failed" + ); + assert!(frame_error.source().is_some()); + + let binary = established_for_frames(vec![vec![0x82, 0x00]])?; + let message_error = WebDriverBiDiWebSocketMessageReader::new(binary) + .read_next(Duration::from_millis(500)) + .err() + .ok_or_else(|| io::Error::other("binary BiDi message was accepted"))?; + assert!(matches!( + message_error, + WebDriverBiDiConnectionMessageReadError::Message { .. } + )); + assert_eq!( + message_error.to_string(), + "connection-bound WebDriver BiDi WebSocket message assembly failed" + ); + assert!(message_error.source().is_some()); + Ok(()) +} From 9bdd1169a77b259b9f6ab2f48d033f2a328e9ae3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 13:28:01 +0900 Subject: [PATCH 15/22] test(network): use consumed transport evidence in rejection fixture Preserve the failed compile attempt and use the existing evidence accessor without widening production authority. Co-Authored-By: OpenAI Codex Signed-off-by: Seongho Bae --- CHANGELOG.md | 1 + .../originweave-network/src/webdriver_bidi_json_envelope.rs | 3 ++- docs/TEST_STRATEGY.md | 4 ++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70ed49817..c5b28b0c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Added +- Regression checks now exercise fragmented browser replies, interleaved control messages, and rejected replies without losing a pending request. These checks do not establish browser readiness or release acceptance. - The typed browser-status response stack now includes its verified command and opening-exchange prerequisites, including the release-record check that previously did not execute; parsing remains bounded and does not grant browser authority or prove operational readiness. - 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. diff --git a/crates/originweave-network/src/webdriver_bidi_json_envelope.rs b/crates/originweave-network/src/webdriver_bidi_json_envelope.rs index 276209cb0..ecc7eb36b 100644 --- a/crates/originweave-network/src/webdriver_bidi_json_envelope.rs +++ b/crates/originweave-network/src/webdriver_bidi_json_envelope.rs @@ -722,7 +722,8 @@ mod tests { let connection = WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; let (_server, _) = listener.accept()?; - let generation = connection.connection_generation(); + let (_stream, evidence) = connection.into_parts(); + let generation = evidence.connection_generation(); let mut correlation = WebDriverBiDiCommandCorrelation::new(); correlation.register_command_for(7, WebDriverBiDiCommandKind::SessionStatus)?; diff --git a/docs/TEST_STRATEGY.md b/docs/TEST_STRATEGY.md index ba3c31624..9a9856db6 100644 --- a/docs/TEST_STRATEGY.md +++ b/docs/TEST_STRATEGY.md @@ -340,6 +340,10 @@ Do not retry deterministic failures blindly. If multiple distinct fixes fail, re Documentation contracts intentionally validate only durable properties such as required files, links, status vocabularies and authority assertions. Do not create brittle tests that freeze wording without preventing a real documentation defect. +### Active browser-status response checks + +The active response stack exercises fragmented text with an interleaved Ping, malformed frame and message errors, and payload-redacted diagnostics. Separate correlation checks reject an event or a reply to a request lacking connection provenance while preserving the pending request. The fixture obtains its connection identity from a real loopback connection through the existing consuming transport handoff; it does not add a caller-supplied identity constructor. These local checks are not protected-main, real-browser acceptance, or release evidence. + ## 17. Exit criteria for a production capability A capability may be documented as Implemented only when: From 804a7a5f54aa6065410fe4a3644625a585c3093a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 13:34:35 +0900 Subject: [PATCH 16/22] test(network): exercise missing provenance through public status replies Replace the superseded unit fixture with the existing loopback response path and verify original correlation remains usable. Co-Authored-By: OpenAI Codex Signed-off-by: Seongho Bae --- .../src/webdriver_bidi_json_envelope.rs | 50 ------------------- .../webdriver_bidi_session_status_response.rs | 23 +++++++++ 2 files changed, 23 insertions(+), 50 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_json_envelope.rs b/crates/originweave-network/src/webdriver_bidi_json_envelope.rs index ecc7eb36b..1d92d0ed8 100644 --- a/crates/originweave-network/src/webdriver_bidi_json_envelope.rs +++ b/crates/originweave-network/src/webdriver_bidi_json_envelope.rs @@ -702,56 +702,6 @@ mod tests { WebDriverBiDiJsonEnvelope::parse_str(value) } - #[test] - fn connection_correlation_rejections_preserve_pending_command() -> Result<(), Box> { - use std::{net::TcpListener, time::Duration}; - - use originweave_core::WebDriverBiDiWebSocketEndpoint; - - use crate::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, - WebDriverBiDiCommandKind, WebDriverBiDiTcpConnectionPlan, - }; - - let listener = TcpListener::bind(("127.0.0.1", 0))?; - let session = "01234567-89ab-cdef-0123-456789abcdef"; - let endpoint = format!("ws://{}/session/{session}", listener.local_addr()?); - let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? - .correlate_session_id(session)? - .into_explicit_connect_target()?; - let connection = - WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; - let (_server, _) = listener.accept()?; - let (_stream, evidence) = connection.into_parts(); - let generation = evidence.connection_generation(); - let mut correlation = WebDriverBiDiCommandCorrelation::new(); - correlation.register_command_for(7, WebDriverBiDiCommandKind::SessionStatus)?; - - for (document, expected) in [ - ( - r#"{"type":"event","method":"log.entryAdded","params":{}}"#, - WebDriverBiDiCommandCorrelationError::EventIsNotResponse, - ), - ( - r#"{"type":"success","id":7,"result":{}}"#, - WebDriverBiDiCommandCorrelationError::CommandConnectionProvenanceMissing { - command_id: 7, - }, - ), - ] { - assert_eq!( - correlation.correlate_response_for_connection( - &parse(document)?, - WebDriverBiDiCommandKind::SessionStatus, - generation, - ), - Err(expected) - ); - assert_eq!(correlation.outstanding_count(), 1); - } - Ok(()) - } - #[test] fn classifies_all_local_end_envelope_kinds_and_redacts_debug() { let success = parse( diff --git a/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs b/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs index 45ee995b6..9a0eca693 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs @@ -25,6 +25,29 @@ const STATUS_RESPONSE_MISSING_READY: &[u8] = br#"{"type":"success","id":7,"result":{"message":"capacity available"}}"#; const STATUS_RESPONSE_EMPTY_RESULT: &[u8] = br#"{"type":"success","id":7,"result":{}}"#; +#[test] +fn unbound_command_cannot_consume_a_connection_bound_reply() -> Result<(), Box> { + use originweave_network::{WebDriverBiDiCommandCorrelationError, WebDriverBiDiCommandKind}; + + let (message, mut original) = send_status_and_read_response(STATUS_RESPONSE)?; + let mut unbound = WebDriverBiDiCommandCorrelation::new(); + unbound.register_command_for(7, WebDriverBiDiCommandKind::SessionStatus)?; + assert!(matches!( + WebDriverBiDiSessionStatusResult::parse_and_correlate(&message, &mut unbound), + Err(WebDriverBiDiSessionStatusResponseError::Correlation { + source: WebDriverBiDiCommandCorrelationError::CommandConnectionProvenanceMissing { + command_id: 7, + }, + }) + )); + assert_eq!(unbound.outstanding_count(), 1); + assert_eq!(original.outstanding_count(), 1); + let result = WebDriverBiDiSessionStatusResult::parse_and_correlate(&message, &mut original)?; + assert_eq!(result.command_id(), 7); + assert_eq!(original.outstanding_count(), 0); + Ok(()) +} + fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { stream.set_read_timeout(Some(Duration::from_secs(2)))?; let mut request = Vec::new(); From d5c7aea007e3c32146cb3ba88ebf251a283f336f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 13:36:02 +0900 Subject: [PATCH 17/22] test(network): preserve pending status across unroutable replies Co-Authored-By: OpenAI Codex Signed-off-by: Seongho Bae --- .../webdriver_bidi_session_status_response.rs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs b/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs index 9a0eca693..6e733163c 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs @@ -48,6 +48,32 @@ fn unbound_command_cannot_consume_a_connection_bound_reply() -> Result<(), Box Result<(), Box> { + use originweave_network::WebDriverBiDiCommandCorrelationError; + + for (document, expected) in [ + ( + br#"{"type":"event","method":"log.entryAdded","params":{}}"#.as_slice(), + WebDriverBiDiCommandCorrelationError::EventIsNotResponse, + ), + ( + br#"{"type":"error","id":null,"error":"unknown error","message":"remote"}"#.as_slice(), + WebDriverBiDiCommandCorrelationError::UncorrelatableErrorResponse, + ), + ] { + let (message, mut correlation) = send_status_and_read_response(document)?; + let result = + WebDriverBiDiSessionStatusResult::parse_and_correlate(&message, &mut correlation); + assert!(matches!( + result, + Err(WebDriverBiDiSessionStatusResponseError::Correlation { source }) if source == expected + )); + assert_eq!(correlation.outstanding_count(), 1); + } + Ok(()) +} + fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { stream.set_read_timeout(Some(Duration::from_secs(2)))?; let mut request = Vec::new(); From bbdc6ace7a5932adf24836700f806850e6b230bc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 13:36:42 +0900 Subject: [PATCH 18/22] refactor(network): share non-consuming response routing validation Keep generic and connection-bound completion separate while reusing exact event and null-id rejection. Projection and provenance remain prerequisites to consumption. Co-Authored-By: OpenAI Codex Signed-off-by: Seongho Bae --- .../src/webdriver_bidi_command_correlation.rs | 71 ++++++++----------- docs/TEST_STRATEGY.md | 2 +- 2 files changed, 29 insertions(+), 44 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..a3dd0d53b 100644 --- a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs +++ b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs @@ -239,26 +239,8 @@ impl WebDriverBiDiCommandCorrelation { envelope: &WebDriverBiDiJsonEnvelope, expected_kind: WebDriverBiDiCommandKind, ) -> 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( - command_id, - expected_kind, - WebDriverBiDiCorrelatedResponseOutcome::Error, - ), - WebDriverBiDiJsonEnvelopeRouting::CommandSuccess { command_id } => self.complete( - command_id, - expected_kind, - WebDriverBiDiCorrelatedResponseOutcome::Success, - ), - } + let (command_id, outcome) = response_route(envelope)?; + self.complete(command_id, expected_kind, outcome) } pub(crate) fn correlate_response_for_connection( @@ -267,29 +249,13 @@ impl WebDriverBiDiCommandCorrelation { 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, - ), - } + let (command_id, outcome) = response_route(envelope)?; + self.complete_on_connection( + command_id, + expected_kind, + outcome, + received_connection_generation, + ) } fn require_command_kind( @@ -351,6 +317,25 @@ impl WebDriverBiDiCommandCorrelation { } } +fn response_route( + envelope: &WebDriverBiDiJsonEnvelope, +) -> Result<(u64, WebDriverBiDiCorrelatedResponseOutcome), WebDriverBiDiCommandCorrelationError> { + match envelope.routing() { + WebDriverBiDiJsonEnvelopeRouting::Event => { + Err(WebDriverBiDiCommandCorrelationError::EventIsNotResponse) + } + WebDriverBiDiJsonEnvelopeRouting::CommandError { command_id: None } => { + Err(WebDriverBiDiCommandCorrelationError::UncorrelatableErrorResponse) + } + WebDriverBiDiJsonEnvelopeRouting::CommandError { + command_id: Some(command_id), + } => Ok((command_id, WebDriverBiDiCorrelatedResponseOutcome::Error)), + WebDriverBiDiJsonEnvelopeRouting::CommandSuccess { command_id } => { + Ok((command_id, WebDriverBiDiCorrelatedResponseOutcome::Success)) + } + } +} + #[cfg(test)] mod tests { use super::{WebDriverBiDiCommandCorrelationError, WebDriverBiDiCommandKind}; diff --git a/docs/TEST_STRATEGY.md b/docs/TEST_STRATEGY.md index 9a9856db6..73476ff0f 100644 --- a/docs/TEST_STRATEGY.md +++ b/docs/TEST_STRATEGY.md @@ -342,7 +342,7 @@ Documentation contracts intentionally validate only durable properties such as r ### Active browser-status response checks -The active response stack exercises fragmented text with an interleaved Ping, malformed frame and message errors, and payload-redacted diagnostics. Separate correlation checks reject an event or a reply to a request lacking connection provenance while preserving the pending request. The fixture obtains its connection identity from a real loopback connection through the existing consuming transport handoff; it does not add a caller-supplied identity constructor. These local checks are not protected-main, real-browser acceptance, or release evidence. +The active response stack exercises fragmented text with an interleaved Ping, malformed frame and message errors, and payload-redacted diagnostics. Public loopback checks reject events, unattributable errors, and replies to requests lacking connection provenance while preserving the pending request; the original request can still accept its matching reply after rejection by an unbound registry. Generic and connection-bound correlation share routing validation, while result validation and connection checks still precede completion. These local checks are not protected-main, real-browser acceptance, or release evidence. ## 17. Exit criteria for a production capability From edec535b8d8b3c843f9f9baf8562f31ff5c7af23 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 13:55:25 +0900 Subject: [PATCH 19/22] test(network): reject replacement status replies in session end stack Require pending-state preservation and acceptance of the original connection reply after rejection. Co-Authored-By: OpenAI Codex Signed-off-by: Seongho Bae --- .../webdriver_bidi_session_status_response.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs b/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs index 347dc5fec..1c1842e42 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs @@ -175,3 +175,20 @@ fn empty_status_result_fails_before_consuming_the_outstanding_command() -> Resul assert_eq!(correlation.outstanding_count(), 1); Ok(()) } + +#[test] +fn replacement_status_reply_preserves_original_pending_request_and_recovery() +-> Result<(), Box> { + let (original, mut pending) = send_status_and_read_response(STATUS_RESPONSE)?; + let (replacement, _replacement_pending) = send_status_and_read_response(STATUS_RESPONSE)?; + + assert!( + WebDriverBiDiSessionStatusResult::parse_and_correlate(&replacement, &mut pending).is_err(), + "a replacement connection must not complete the original status request" + ); + assert_eq!(pending.outstanding_count(), 1); + let completed = WebDriverBiDiSessionStatusResult::parse_and_correlate(&original, &mut pending)?; + assert_eq!(completed.command_id(), 7); + assert_eq!(pending.outstanding_count(), 0); + Ok(()) +} From 924ad97551750d4a901ded38b89488cc5438e54f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 13:57:25 +0900 Subject: [PATCH 20/22] docs: bound session end parent adoption evidence Require exact replacement-connection rejection without claiming stream liveness or end-response provenance. Co-Authored-By: OpenAI Codex Signed-off-by: Seongho Bae --- CHANGELOG.md | 1 + .../tests/webdriver_bidi_session_status_response.rs | 12 ++++++++---- docs/doctoring.md | 6 ++++++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8a809101..ad12a72bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Added +- The session-ending command stack now retains the status-reply protections from its current parent. A reply from a replacement connection is rejected while the original pending status request remains recoverable; sending the end command still does not prove that the browser session ended. - Typed outbound WebDriver BiDi `session.end` over the bounded client WebSocket stream: it serializes only the standards-defined method with empty params, rejects invalid frame deadlines before correlation registration, retires only the just-registered id when frame preflight proves no command bytes were emitted, preserves exact command-kind correlation across ambiguous writes, and does not treat frame-write success as proof that the browser session ended. - Regression checks now exercise fragmented browser replies, interleaved control messages, and rejected replies without losing a pending request. These checks do not establish browser readiness or release acceptance. - The typed browser-status response stack now includes its verified command and opening-exchange prerequisites, including the release-record check that previously did not execute; parsing remains bounded and does not grant browser authority or prove operational readiness. diff --git a/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs b/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs index ff7a9dd34..6779da6c1 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs @@ -231,10 +231,14 @@ fn replacement_status_reply_preserves_original_pending_request_and_recovery() let (original, mut pending) = send_status_and_read_response(STATUS_RESPONSE)?; let (replacement, _replacement_pending) = send_status_and_read_response(STATUS_RESPONSE)?; - assert!( - WebDriverBiDiSessionStatusResult::parse_and_correlate(&replacement, &mut pending).is_err(), - "a replacement connection must not complete the original status request" - ); + assert!(matches!( + WebDriverBiDiSessionStatusResult::parse_and_correlate(&replacement, &mut pending), + Err(WebDriverBiDiSessionStatusResponseError::Correlation { + source: originweave_network::WebDriverBiDiCommandCorrelationError::ResponseConnectionMismatch { + command_id: 7, + }, + }) + )); assert_eq!(pending.outstanding_count(), 1); let completed = WebDriverBiDiSessionStatusResult::parse_and_correlate(&original, &mut pending)?; assert_eq!(completed.command_id(), 7); diff --git a/docs/doctoring.md b/docs/doctoring.md index 0950b9a2d..ffe0f82ee 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -128,6 +128,12 @@ On 6 September 2026, that historical no-connection-provenance state was supersed The #251 integration adopts #250 `ec433b844a121f8554c062f92267991af9cacb6f` by ordinary merge and retains both release records. Native discovery on predecessor `86e8ad76838f2a64aa7e0cd56ba1f931c8d0c3dc` collected zero command-correlation release tests; the current parent supplies the existing discoverable TestCase and synchronized opening-exchange fixtures. The session-end sender, its public exports and both child-owned Rust integration tests remain unchanged. The sender still registers only the exact typed command, retires correlation only after proven preflight rejection, and leaves ambiguous writes outstanding. A successful frame write does not prove session termination, browser-process exit, profile deletion, authenticated connection provenance or real Chromium acceptance. Local verification and current-head hosted checks remain separate prerequisites; earlier stack results do not transfer. +### Current session-end sender adoption of status-reply provenance + +On 7 September 2026, the session-end sender stack reproduced the inherited status-reply gap at `edec535b8d8b3c843f9f9baf8562f31ff5c7af23`: two real loopback connections using the same session and command id allowed the replacement reply to complete the original pending status request. The ordinary merge of #250 `bbdc6ace7a5932adf24836700f806850e6b230bc` preserves the session-end sender and both original sender integration-test files byte-for-byte while inheriting sealed status receipts and connection-aware correlation. The regression now requires the exact connection-mismatch error, unchanged pending count, and subsequent completion using the original received reply. The server fixtures have already finished; this proves retained reply/correlation recovery, not liveness of an open original stream or same-endpoint replacement coverage. + +The shared routing validation, result projection order, and parent connection safeguards are unchanged. This supersedes the earlier no-received-provenance description for the status path only: the session-end sender still makes no received-acknowledgment, process-exit, profile-cleanup, browser-policy or release-acceptance claim. End-response provenance belongs to its separate consumer. Local tests and exact-head hosted checks must be evaluated independently after this adoption. + ## References Amazon Web Services. (n.d.). *Set up the Amazon EKS Pod Identity Agent*. Retrieved August 6, 2026, from https://docs.aws.amazon.com/eks/latest/userguide/pod-id-agent-setup.html From 6aaf7f3fd15b5f14dc7b320c29a40efe0e618391 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 14:14:20 +0900 Subject: [PATCH 21/22] test(network): reject replacement session-end replies Co-Authored-By: OpenAI Codex Signed-off-by: Seongho Bae --- .../webdriver_bidi_session_end_response.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_end_response.rs b/crates/originweave-network/tests/webdriver_bidi_session_end_response.rs index 042f5137b..0f34a17f5 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_end_response.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_end_response.rs @@ -27,6 +27,24 @@ const END_UNKNOWN_ID_RESPONSE: &[u8] = br#"{"type":"success","id":8,"result":{"vendorExtension":true}}"#; const END_MALFORMED_RESPONSE: &[u8] = br#"{"type":"success","id":7}"#; +#[test] +fn replacement_end_replies_preserve_original_pending_request_and_recovery() +-> Result<(), Box> { + for response in [END_SUCCESS_RESPONSE, END_REMOTE_ERROR_RESPONSE] { + let (original, mut pending) = send_end_and_read_response(END_SUCCESS_RESPONSE)?; + let (replacement, _) = send_end_and_read_response(response)?; + assert!(matches!( + WebDriverBiDiSessionEndResult::parse_and_correlate(&replacement, &mut pending), + Err(WebDriverBiDiSessionEndResponseError::Correlation { .. }) + )); + assert_eq!(pending.outstanding_count(), 1); + let result = WebDriverBiDiSessionEndResult::parse_and_correlate(&original, &mut pending)?; + assert_eq!(result.command_id(), 7); + assert_eq!(pending.outstanding_count(), 0); + } + Ok(()) +} + fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { stream.set_read_timeout(Some(Duration::from_secs(2)))?; let mut request = Vec::new(); From 363a78e36e7690e9ed5bf49829567e00e2ec5d59 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 14:17:51 +0900 Subject: [PATCH 22/22] fix(network): bind session-end replies to their sending connection Co-Authored-By: OpenAI Codex Signed-off-by: Seongho Bae --- CHANGELOG.md | 1 + .../src/webdriver_bidi_session_end_command.rs | 9 ++- .../webdriver_bidi_session_end_response.rs | 16 +++-- .../webdriver_bidi_session_end_response.rs | 70 ++++++++++++++++--- docs/doctoring.md | 6 ++ 5 files changed, 84 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25b3bfae9..6127f0374 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Added +- Session-ending replies from a replacement connection can no longer complete the original pending request. The original reply remains usable, and a protocol acknowledgment still does not prove browser shutdown or cleanup. - The session-ending command stack now retains the status-reply protections from its current parent. A reply from a replacement connection is rejected while the original pending status request remains recoverable; sending the end command still does not prove that the browser session ended. - Typed outbound WebDriver BiDi `session.end` over the bounded client WebSocket stream: it serializes only the standards-defined method with empty params, rejects invalid frame deadlines before correlation registration, retires only the just-registered id when frame preflight proves no command bytes were emitted, preserves exact command-kind correlation across ambiguous writes, and does not treat frame-write success as proof that the browser session ended. - Typed `session.end` response admission that consumes only the exact outstanding command-kind correlation after complete envelope validation, preserves remote protocol errors as failures, and does not claim browser-process exit or resource cleanup from a protocol acknowledgment. diff --git a/crates/originweave-network/src/webdriver_bidi_session_end_command.rs b/crates/originweave-network/src/webdriver_bidi_session_end_command.rs index 29fbe05aa..e9d3f7174 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_end_command.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_end_command.rs @@ -41,7 +41,8 @@ impl WebDriverBiDiSessionEndCommand { /// 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. + /// side effect. Correlation then binds the command to this connection before the first possible + /// frame write. Only a reply received on this same connection can complete that registration. /// A frame-owner preflight rejection that proves no write began retires this exact command /// again. Once frame emission can have begun, a later failure leaves the identifier outstanding /// because partial or full emission is ambiguous. A successful write also leaves the identifier @@ -62,7 +63,11 @@ impl WebDriverBiDiSessionEndCommand { }); } correlation - .register_command_for(self.command_id, WebDriverBiDiCommandKind::SessionEnd) + .register_command_for_connection( + self.command_id, + WebDriverBiDiCommandKind::SessionEnd, + established.transport_evidence().connection_generation(), + ) .map_err(|source| WebDriverBiDiSessionEndCommandError::Correlation { source })?; let message = self.serialized(); match established.write_text_frame(&message, masking_key, frame_timeout) { diff --git a/crates/originweave-network/src/webdriver_bidi_session_end_response.rs b/crates/originweave-network/src/webdriver_bidi_session_end_response.rs index 9ab083230..cd284f134 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_end_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_end_response.rs @@ -3,7 +3,7 @@ use std::{error::Error, fmt}; use crate::{ WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, WebDriverBiDiCommandKind, WebDriverBiDiCorrelatedResponseOutcome, WebDriverBiDiJsonEnvelope, - WebDriverBiDiJsonEnvelopeError, WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiJsonEnvelopeError, WebDriverBiDiReceivedTextMessage, }; /// Typed protocol acknowledgment for one correlated WebDriver BiDi `session.end` command. @@ -26,15 +26,21 @@ impl WebDriverBiDiSessionEndResult { /// can be consumed. Successful responses retain only the matched command id. A correlatable /// protocol-error response consumes its matching id and returns a typed remote failure, while /// events, null-id errors, malformed envelopes, unknown ids, and command-kind mismatches fail - /// closed without consuming unrelated outstanding state. + /// closed without consuming unrelated outstanding state. Only a sealed reply from the same + /// connection that registered the command can consume it; a replacement connection cannot + /// complete the request even when its session and command identifiers match. pub fn parse_and_correlate( - message: &WebDriverBiDiWebSocketTextMessage, + message: &WebDriverBiDiReceivedTextMessage, correlation: &mut WebDriverBiDiCommandCorrelation, ) -> Result { - let envelope = WebDriverBiDiJsonEnvelope::parse(message) + let envelope = WebDriverBiDiJsonEnvelope::parse(message.message()) .map_err(|source| WebDriverBiDiSessionEndResponseError::Envelope { source })?; let completed = correlation - .correlate_response_for(&envelope, WebDriverBiDiCommandKind::SessionEnd) + .correlate_response_for_connection( + &envelope, + WebDriverBiDiCommandKind::SessionEnd, + message.connection_generation(), + ) .map_err(|source| WebDriverBiDiSessionEndResponseError::Correlation { source })?; match completed.outcome() { diff --git a/crates/originweave-network/tests/webdriver_bidi_session_end_response.rs b/crates/originweave-network/tests/webdriver_bidi_session_end_response.rs index 0f34a17f5..610526556 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_end_response.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_end_response.rs @@ -8,12 +8,12 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use originweave_network::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiSessionEndCommand, - WebDriverBiDiSessionEndResponseError, WebDriverBiDiSessionEndResult, - WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, + WebDriverBiDiConnectionMessageRead, WebDriverBiDiReceivedTextMessage, + WebDriverBiDiSessionEndCommand, WebDriverBiDiSessionEndResponseError, + WebDriverBiDiSessionEndResult, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, - WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, - WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiWebSocketMessageReader, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -27,6 +27,50 @@ const END_UNKNOWN_ID_RESPONSE: &[u8] = br#"{"type":"success","id":8,"result":{"vendorExtension":true}}"#; const END_MALFORMED_RESPONSE: &[u8] = br#"{"type":"success","id":7}"#; +#[test] +fn unbound_end_command_cannot_consume_a_connection_bound_reply() -> Result<(), Box> { + use originweave_network::WebDriverBiDiCommandKind; + + let (message, mut original) = send_end_and_read_response(END_SUCCESS_RESPONSE)?; + let mut unbound = WebDriverBiDiCommandCorrelation::new(); + unbound.register_command_for(7, WebDriverBiDiCommandKind::SessionEnd)?; + assert!(matches!( + WebDriverBiDiSessionEndResult::parse_and_correlate(&message, &mut unbound), + Err(WebDriverBiDiSessionEndResponseError::Correlation { + source: WebDriverBiDiCommandCorrelationError::CommandConnectionProvenanceMissing { + command_id: 7, + }, + }) + )); + assert_eq!(unbound.outstanding_count(), 1); + let result = WebDriverBiDiSessionEndResult::parse_and_correlate(&message, &mut original)?; + assert_eq!(result.command_id(), 7); + assert_eq!(original.outstanding_count(), 0); + Ok(()) +} + +#[test] +fn event_and_null_id_error_preserve_the_sent_end_command() -> Result<(), Box> { + for (document, expected) in [ + ( + br#"{"type":"event","method":"log.entryAdded","params":{}}"#.as_slice(), + WebDriverBiDiCommandCorrelationError::EventIsNotResponse, + ), + ( + br#"{"type":"error","id":null,"error":"unknown error","message":"remote"}"#.as_slice(), + WebDriverBiDiCommandCorrelationError::UncorrelatableErrorResponse, + ), + ] { + let (message, mut correlation) = send_end_and_read_response(document)?; + assert!(matches!( + WebDriverBiDiSessionEndResult::parse_and_correlate(&message, &mut correlation), + Err(WebDriverBiDiSessionEndResponseError::Correlation { source }) if source == expected + )); + assert_eq!(correlation.outstanding_count(), 1); + } + Ok(()) +} + #[test] fn replacement_end_replies_preserve_original_pending_request_and_recovery() -> Result<(), Box> { @@ -35,7 +79,11 @@ fn replacement_end_replies_preserve_original_pending_request_and_recovery() let (replacement, _) = send_end_and_read_response(response)?; assert!(matches!( WebDriverBiDiSessionEndResult::parse_and_correlate(&replacement, &mut pending), - Err(WebDriverBiDiSessionEndResponseError::Correlation { .. }) + Err(WebDriverBiDiSessionEndResponseError::Correlation { + source: WebDriverBiDiCommandCorrelationError::ResponseConnectionMismatch { + command_id: 7 + } + }) )); assert_eq!(pending.outstanding_count(), 1); let result = WebDriverBiDiSessionEndResult::parse_and_correlate(&original, &mut pending)?; @@ -92,7 +140,7 @@ fn send_end_and_read_response( response: &'static [u8], ) -> Result< ( - WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiReceivedTextMessage, WebDriverBiDiCommandCorrelation, ), Box, @@ -133,10 +181,10 @@ fn send_end_and_read_response( Duration::from_millis(500), )?; - let (_established, frame) = established.read_frame(Duration::from_millis(500))?; - let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); - let text = match assembler.push_frame(frame)? { - WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + let text = match WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + WebDriverBiDiConnectionMessageRead::Text { message, .. } => message, other => { return Err(io::Error::other(format!( "session.end response produced unexpected assembly state: {other:?}" diff --git a/docs/doctoring.md b/docs/doctoring.md index 88aca96b9..42a289dd1 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -138,6 +138,12 @@ On 7 September 2026, the session-end sender stack reproduced the inherited statu The shared routing validation, result projection order, and parent connection safeguards are unchanged. This supersedes the earlier no-received-provenance description for the status path only: the session-end sender still makes no received-acknowledgment, process-exit, profile-cleanup, browser-policy or release-acceptance claim. End-response provenance belongs to its separate consumer. Local tests and exact-head hosted checks must be evaluated independently after this adoption. +### Current session-end response connection binding + +On 7 September 2026, regression `6aaf7f3f` reproduced replacement-reply acceptance on #252. Ordinary adoption of #251 `924ad97551750d4a901ded38b89488cc5438e54f` retained the failure, confirming that parent status safeguards alone did not repair the end-response consumer. The end sender now uses the existing connection-bound registration before writing, and the response parser accepts the existing sealed received-text type and checks that connection through shared correlation before consuming pending state. The original sender preflight retirement and ambiguous-write behavior, result shape, command-id accessor and remote-error classification remain unchanged. No new authority accessor or duplicate reader is introduced. + +Real loopback tests require exact connection-mismatch rejection for replacement success and error replies, unchanged original pending state, and subsequent completion using the original received reply. They also reject unbound registrations, events and null-id errors without consuming the pending command. These fixtures use separate listener endpoints and retain receipts after their server threads finish; they do not prove same-endpoint replacement rejection or liveness of an open original connection. This supersedes the earlier unbound-response description for the current #252 branch only. A correlated acknowledgment still does not prove browser-process exit, profile deletion, resource cleanup, browser policy authority, protected integration or release acceptance. Exact-head local and hosted verification remain separate. + ## References Amazon Web Services. (n.d.). *Set up the Amazon EKS Pod Identity Agent*. Retrieved August 6, 2026, from https://docs.aws.amazon.com/eks/latest/userguide/pod-id-agent-setup.html