From 0fff9816a1534224cd30a238f2f36bc55818fdd9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 06:40:45 -0700 Subject: [PATCH 01/12] test(network): require typed BiDi transport closure evidence --- ...webdriver_bidi_transport_close_evidence.rs | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs new file mode 100644 index 000000000..e12217d33 --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs @@ -0,0 +1,129 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::WebDriverBiDiWebSocketEndpoint; +use originweave_network::{ + WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketTransportClosureError, + WebDriverBiDiWebSocketTransportClosureKind, WebDriverBiDiWebSocketTransportClosureObservation, +}; + +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_with_server_frame( + frame: Option<&'static [u8]>, +) -> Result< + ( + originweave_network::WebDriverBiDiWebSocketEstablished, + thread::JoinHandle>, + ), + Box, +> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let server = thread::spawn(move || -> io::Result<()> { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + if let Some(frame) = frame { + 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)?; + let established = WebDriverBiDiWebSocketHandshakePlan::new(connection, key)? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?; + Ok((established, server)) +} + +#[test] +fn validated_peer_close_frame_yields_nonforgeable_transport_observation() -> Result<(), Box> { + let (established, server) = established_with_server_frame(Some(&[0x88, 0x02, 0x03, 0xe8]))?; + + let observation = WebDriverBiDiWebSocketTransportClosureObservation::observe( + established, + Duration::from_millis(500), + )?; + + server + .join() + .map_err(|_| io::Error::other("transport-close test server panicked"))??; + assert_eq!( + observation.kind(), + WebDriverBiDiWebSocketTransportClosureKind::PeerCloseFrame + ); + assert_eq!(observation.peer_close_status_code(), Some(1000)); + Ok(()) +} + +#[test] +fn clean_peer_eof_yields_transport_observation_without_inventing_close_status() -> Result<(), Box> { + let (established, server) = established_with_server_frame(None)?; + + let observation = WebDriverBiDiWebSocketTransportClosureObservation::observe( + established, + Duration::from_millis(500), + )?; + + server + .join() + .map_err(|_| io::Error::other("transport-eof test server panicked"))??; + assert_eq!( + observation.kind(), + WebDriverBiDiWebSocketTransportClosureKind::PeerEof + ); + assert_eq!(observation.peer_close_status_code(), None); + Ok(()) +} + +#[test] +fn application_frame_after_teardown_does_not_become_closure_evidence() -> Result<(), Box> { + let (established, server) = established_with_server_frame(Some(&[0x81, 0x02, b'o', b'k']))?; + + let error = WebDriverBiDiWebSocketTransportClosureObservation::observe( + established, + Duration::from_millis(500), + ) + .expect_err("an application frame must not become transport-closure evidence"); + + server + .join() + .map_err(|_| io::Error::other("unexpected-frame test server panicked"))??; + assert!(matches!( + error, + WebDriverBiDiWebSocketTransportClosureError::UnexpectedFrame { opcode: 0x1 } + )); + Ok(()) +} From 4bc7c3ce22f24a9935f541ff89ae229cdc710541 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 06:43:47 -0700 Subject: [PATCH 02/12] style(network): apply canonical transport-close test formatting --- .../tests/webdriver_bidi_transport_close_evidence.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs index e12217d33..55e402aa6 100644 --- a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs +++ b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs @@ -69,7 +69,8 @@ fn established_with_server_frame( } #[test] -fn validated_peer_close_frame_yields_nonforgeable_transport_observation() -> Result<(), Box> { +fn validated_peer_close_frame_yields_nonforgeable_transport_observation() +-> Result<(), Box> { let (established, server) = established_with_server_frame(Some(&[0x88, 0x02, 0x03, 0xe8]))?; let observation = WebDriverBiDiWebSocketTransportClosureObservation::observe( @@ -89,7 +90,8 @@ fn validated_peer_close_frame_yields_nonforgeable_transport_observation() -> Res } #[test] -fn clean_peer_eof_yields_transport_observation_without_inventing_close_status() -> Result<(), Box> { +fn clean_peer_eof_yields_transport_observation_without_inventing_close_status() +-> Result<(), Box> { let (established, server) = established_with_server_frame(None)?; let observation = WebDriverBiDiWebSocketTransportClosureObservation::observe( @@ -109,7 +111,8 @@ fn clean_peer_eof_yields_transport_observation_without_inventing_close_status() } #[test] -fn application_frame_after_teardown_does_not_become_closure_evidence() -> Result<(), Box> { +fn application_frame_after_teardown_does_not_become_closure_evidence() -> Result<(), Box> +{ let (established, server) = established_with_server_frame(Some(&[0x81, 0x02, b'o', b'k']))?; let error = WebDriverBiDiWebSocketTransportClosureObservation::observe( From 916f6d4907a407e6ddcba244817d3cb64e5c7180 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 06:45:41 -0700 Subject: [PATCH 03/12] feat(network): observe typed BiDi transport closure --- ...driver_bidi_websocket_transport_closure.rs | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs b/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs new file mode 100644 index 000000000..7dfba3cc3 --- /dev/null +++ b/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs @@ -0,0 +1,112 @@ +use std::{error::Error, fmt, time::Duration}; + +use crate::{WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketFrameError}; + +/// Bounded transport-closure condition observed on one consumed WebDriver BiDi WebSocket. +/// +/// This classification proves only what the already session-correlated transport itself exposed. +/// A peer Close frame does not prove browser-process exit or profile cleanup, while peer EOF does +/// not imply that the RFC 6455 closing handshake completed. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum WebDriverBiDiWebSocketTransportClosureKind { + /// The peer sent one RFC 6455 Close frame that passed the existing strict frame validator. + PeerCloseFrame, + /// The peer ended the TCP byte stream cleanly before any new WebSocket frame byte was read. + PeerEof, +} + +/// Credential-free observation that one established WebDriver BiDi transport ceased carrying data. +/// +/// Construction consumes the established WebSocket, so this value cannot be used to regain the +/// underlying connection. It records only a validated peer Close status when one was actually +/// present, or clean TCP EOF before a new frame began. It grants no browser, process, profile, +/// policy, secret, retry, reconnect, or Agent authority and does not perform a reciprocal Close +/// handshake. +#[derive(Debug, Eq, PartialEq)] +pub struct WebDriverBiDiWebSocketTransportClosureObservation { + kind: WebDriverBiDiWebSocketTransportClosureKind, + peer_close_status_code: Option, +} + +impl WebDriverBiDiWebSocketTransportClosureObservation { + /// Consume one established connection and observe one bounded transport-closure condition. + /// + /// A validated peer Close frame and zero-byte clean EOF are the only success cases. Any data + /// frame, partial-frame EOF, timeout, malformed Close, I/O failure, or integrity failure remains + /// a typed error and is never normalized into successful teardown evidence. + pub fn observe( + established: WebDriverBiDiWebSocketEstablished, + frame_timeout: Duration, + ) -> Result { + match established.read_frame(frame_timeout) { + Ok((established, frame)) => { + if frame.opcode() != 0x8 { + return Err(WebDriverBiDiWebSocketTransportClosureError::UnexpectedFrame { + opcode: frame.opcode(), + }); + } + let peer_close_status_code = frame + .payload() + .get(..2) + .map(|bytes| u16::from_be_bytes([bytes[0], bytes[1]])); + drop(established); + Ok(Self { + kind: WebDriverBiDiWebSocketTransportClosureKind::PeerCloseFrame, + peer_close_status_code, + }) + } + Err(WebDriverBiDiWebSocketFrameError::FrameEnded { bytes_read: 0 }) => Ok(Self { + kind: WebDriverBiDiWebSocketTransportClosureKind::PeerEof, + peer_close_status_code: None, + }), + Err(source) => Err(WebDriverBiDiWebSocketTransportClosureError::Frame { source }), + } + } + + /// Return the exact transport-closure condition that produced this observation. + #[must_use] + pub const fn kind(&self) -> WebDriverBiDiWebSocketTransportClosureKind { + self.kind + } + + /// Return the validated peer Close status when a Close frame actually carried one. + #[must_use] + pub const fn peer_close_status_code(&self) -> Option { + self.peer_close_status_code + } +} + +/// Fail-closed errors while converting one established BiDi transport into closure evidence. +#[derive(Debug)] +pub enum WebDriverBiDiWebSocketTransportClosureError { + /// The peer sent a valid WebSocket frame that was not a Close frame. + UnexpectedFrame { + /// Exact validated RFC 6455 opcode observed instead of a Close frame. + opcode: u8, + }, + /// The existing bounded WebSocket frame reader failed before closure was proven. + Frame { + /// Original typed frame failure retained as the causal source. + source: WebDriverBiDiWebSocketFrameError, + }, +} + +impl fmt::Display for WebDriverBiDiWebSocketTransportClosureError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UnexpectedFrame { .. } => formatter + .write_str("WebDriver BiDi peer sent application traffic instead of closing"), + Self::Frame { .. } => formatter + .write_str("WebDriver BiDi transport closure could not be observed safely"), + } + } +} + +impl Error for WebDriverBiDiWebSocketTransportClosureError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::UnexpectedFrame { .. } => None, + Self::Frame { source } => Some(source), + } + } +} From eed38a3467ad74a02be43427cefb11e8c56e1765 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 06:46:23 -0700 Subject: [PATCH 04/12] feat(network): export bounded BiDi transport closure evidence --- crates/originweave-network/src/lib.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/originweave-network/src/lib.rs b/crates/originweave-network/src/lib.rs index ff640fac3..7400cc4d0 100644 --- a/crates/originweave-network/src/lib.rs +++ b/crates/originweave-network/src/lib.rs @@ -9,9 +9,10 @@ //! unmasked server-frame reads, assembles bounded WebDriver BiDi text messages, //! classifies complete local-end JSON envelopes, tracks bounded command-response //! correlation, sends narrowly typed `session.status` and `session.end` commands, -//! admits typed correlated status and end responses, and keeps protocol teardown -//! acknowledgment separate from explicit operational teardown observations without -//! exposing generic JSON bodies or granting browser, TLS, policy, secret, or Agent authority. +//! admits typed correlated status and end responses, observes bounded peer Close +//! or clean-EOF transport cessation, and keeps protocol/transport evidence separate +//! from explicit operational teardown observations without exposing generic JSON +//! bodies or granting browser, TLS, policy, secret, process, profile, or Agent authority. #![forbid(unsafe_code)] #![deny(missing_docs)] @@ -29,6 +30,7 @@ mod webdriver_bidi_websocket_frame; mod webdriver_bidi_websocket_handshake; mod webdriver_bidi_websocket_message; mod webdriver_bidi_websocket_opening_recovery; +mod webdriver_bidi_websocket_transport_closure; #[cfg(test)] mod webdriver_bidi_json_envelope_public_boundary_tests; @@ -86,3 +88,7 @@ pub use webdriver_bidi_websocket_message::{ WebDriverBiDiWebSocketTextMessage, }; pub use webdriver_bidi_websocket_opening_recovery::WebDriverBiDiWebSocketOpeningWriteRecoveryDisposition; +pub use webdriver_bidi_websocket_transport_closure::{ + WebDriverBiDiWebSocketTransportClosureError, WebDriverBiDiWebSocketTransportClosureKind, + WebDriverBiDiWebSocketTransportClosureObservation, +}; From 69cfe4c208db7b25ea843147510aae5b87d402b6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 06:47:59 -0700 Subject: [PATCH 05/12] style(network): apply canonical transport-closure formatting --- .../webdriver_bidi_websocket_transport_closure.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs b/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs index 7dfba3cc3..3100d773a 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs @@ -41,9 +41,11 @@ impl WebDriverBiDiWebSocketTransportClosureObservation { match established.read_frame(frame_timeout) { Ok((established, frame)) => { if frame.opcode() != 0x8 { - return Err(WebDriverBiDiWebSocketTransportClosureError::UnexpectedFrame { - opcode: frame.opcode(), - }); + return Err( + WebDriverBiDiWebSocketTransportClosureError::UnexpectedFrame { + opcode: frame.opcode(), + }, + ); } let peer_close_status_code = frame .payload() @@ -96,8 +98,9 @@ impl fmt::Display for WebDriverBiDiWebSocketTransportClosureError { match self { Self::UnexpectedFrame { .. } => formatter .write_str("WebDriver BiDi peer sent application traffic instead of closing"), - Self::Frame { .. } => formatter - .write_str("WebDriver BiDi transport closure could not be observed safely"), + Self::Frame { .. } => { + formatter.write_str("WebDriver BiDi transport closure could not be observed safely") + } } } } From 6612af70809c596301b446ab8ad6df42f40322cf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 08:34:18 -0700 Subject: [PATCH 06/12] test(network): satisfy strict transport-closure contracts --- ...webdriver_bidi_transport_close_evidence.rs | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs index 55e402aa6..306ed780e 100644 --- a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs +++ b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs @@ -17,6 +17,11 @@ const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; +type EstablishedWithServer = ( + originweave_network::WebDriverBiDiWebSocketEstablished, + thread::JoinHandle>, +); + fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { stream.set_read_timeout(Some(Duration::from_secs(2)))?; let mut request = Vec::new(); @@ -36,13 +41,7 @@ fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { fn established_with_server_frame( frame: Option<&'static [u8]>, -) -> Result< - ( - originweave_network::WebDriverBiDiWebSocketEstablished, - thread::JoinHandle>, - ), - Box, -> { +) -> Result> { let listener = TcpListener::bind(("127.0.0.1", 0))?; let local_addr = listener.local_addr()?; let server = thread::spawn(move || -> io::Result<()> { @@ -115,11 +114,15 @@ fn application_frame_after_teardown_does_not_become_closure_evidence() -> Result { let (established, server) = established_with_server_frame(Some(&[0x81, 0x02, b'o', b'k']))?; - let error = WebDriverBiDiWebSocketTransportClosureObservation::observe( + let Err(error) = WebDriverBiDiWebSocketTransportClosureObservation::observe( established, Duration::from_millis(500), - ) - .expect_err("an application frame must not become transport-closure evidence"); + ) else { + return Err(io::Error::other( + "application frame unexpectedly became transport-closure evidence", + ) + .into()); + }; server .join() From fa68bb9b10bfd888b620131c396494da022fe472 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 08:35:39 -0700 Subject: [PATCH 07/12] test(network): cover fail-closed transport closure errors --- ...webdriver_bidi_transport_close_evidence.rs | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs index 306ed780e..76a8aa35f 100644 --- a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs +++ b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs @@ -128,8 +128,42 @@ fn application_frame_after_teardown_does_not_become_closure_evidence() -> Result .join() .map_err(|_| io::Error::other("unexpected-frame test server panicked"))??; assert!(matches!( - error, + &error, WebDriverBiDiWebSocketTransportClosureError::UnexpectedFrame { opcode: 0x1 } )); + assert_eq!( + error.to_string(), + "WebDriver BiDi peer sent application traffic instead of closing" + ); + assert!(error.source().is_none()); + Ok(()) +} + +#[test] +fn malformed_close_frame_remains_a_typed_frame_failure() -> Result<(), Box> { + let (established, server) = established_with_server_frame(Some(&[0x88, 0x01, 0x00]))?; + + let Err(error) = WebDriverBiDiWebSocketTransportClosureObservation::observe( + established, + Duration::from_millis(500), + ) else { + return Err(io::Error::other( + "malformed close frame unexpectedly became transport-closure evidence", + ) + .into()); + }; + + server + .join() + .map_err(|_| io::Error::other("malformed-close test server panicked"))??; + assert!(matches!( + &error, + WebDriverBiDiWebSocketTransportClosureError::Frame { .. } + )); + assert_eq!( + error.to_string(), + "WebDriver BiDi transport closure could not be observed safely" + ); + assert!(error.source().is_some()); Ok(()) } From f956341f3c036b4b85c845b7632b36f7008046e2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 09:02:03 -0700 Subject: [PATCH 08/12] test(network): require pong-tolerant BiDi transport close --- ...webdriver_bidi_transport_close_evidence.rs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs index 76a8aa35f..cd7b14790 100644 --- a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs +++ b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs @@ -88,6 +88,28 @@ fn validated_peer_close_frame_yields_nonforgeable_transport_observation() Ok(()) } +#[test] +fn one_unsolicited_pong_before_close_does_not_block_closure_observation() +-> Result<(), Box> { + let (established, server) = + established_with_server_frame(Some(&[0x8a, 0x00, 0x88, 0x02, 0x03, 0xe8]))?; + + let observation = WebDriverBiDiWebSocketTransportClosureObservation::observe( + established, + Duration::from_millis(500), + )?; + + server + .join() + .map_err(|_| io::Error::other("pong-before-close test server panicked"))??; + assert_eq!( + observation.kind(), + WebDriverBiDiWebSocketTransportClosureKind::PeerCloseFrame + ); + assert_eq!(observation.peer_close_status_code(), Some(1000)); + Ok(()) +} + #[test] fn clean_peer_eof_yields_transport_observation_without_inventing_close_status() -> Result<(), Box> { From 80f50008041ae81f770752e4381861aba6089ee9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 09:04:06 -0700 Subject: [PATCH 09/12] fix(network): tolerate bounded pong before BiDi transport close --- ...driver_bidi_websocket_transport_closure.rs | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs b/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs index 3100d773a..bea19f905 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs @@ -31,14 +31,28 @@ pub struct WebDriverBiDiWebSocketTransportClosureObservation { impl WebDriverBiDiWebSocketTransportClosureObservation { /// Consume one established connection and observe one bounded transport-closure condition. /// - /// A validated peer Close frame and zero-byte clean EOF are the only success cases. Any data - /// frame, partial-frame EOF, timeout, malformed Close, I/O failure, or integrity failure remains - /// a typed error and is never normalized into successful teardown evidence. + /// A validated peer Close frame and zero-byte clean EOF are the only success cases. One + /// unsolicited Pong may be ignored before that closure signal because Pong requires no client + /// response; a second Pong, Ping, data frame, partial-frame EOF, timeout, malformed Close, I/O + /// failure, or integrity failure remains a typed error. This fixed two-read envelope prevents + /// peer control traffic from extending teardown observation indefinitely and does not invent + /// masking entropy or outbound authority to answer Ping frames. pub fn observe( established: WebDriverBiDiWebSocketEstablished, frame_timeout: Duration, + ) -> Result { + Self::observe_frame(established, frame_timeout, true) + } + + fn observe_frame( + established: WebDriverBiDiWebSocketEstablished, + frame_timeout: Duration, + allow_pong: bool, ) -> Result { match established.read_frame(frame_timeout) { + Ok((established, frame)) if frame.opcode() == 0xa && allow_pong => { + Self::observe_frame(established, frame_timeout, false) + } Ok((established, frame)) => { if frame.opcode() != 0x8 { return Err( @@ -81,9 +95,9 @@ impl WebDriverBiDiWebSocketTransportClosureObservation { /// Fail-closed errors while converting one established BiDi transport into closure evidence. #[derive(Debug)] pub enum WebDriverBiDiWebSocketTransportClosureError { - /// The peer sent a valid WebSocket frame that was not a Close frame. + /// The peer sent a valid WebSocket frame that was not an admissible closure signal. UnexpectedFrame { - /// Exact validated RFC 6455 opcode observed instead of a Close frame. + /// Exact validated RFC 6455 opcode observed instead of an admissible closure signal. opcode: u8, }, /// The existing bounded WebSocket frame reader failed before closure was proven. @@ -97,7 +111,7 @@ impl fmt::Display for WebDriverBiDiWebSocketTransportClosureError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::UnexpectedFrame { .. } => formatter - .write_str("WebDriver BiDi peer sent application traffic instead of closing"), + .write_str("WebDriver BiDi peer sent non-closure traffic instead of closing"), Self::Frame { .. } => { formatter.write_str("WebDriver BiDi transport closure could not be observed safely") } From 01cfce2c2c1c89c31cac72d6a53979e78b6c24d9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 09:04:37 -0700 Subject: [PATCH 10/12] test(network): bound pong handling during BiDi transport close --- ...webdriver_bidi_transport_close_evidence.rs | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs index cd7b14790..895e54b10 100644 --- a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs +++ b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs @@ -110,6 +110,36 @@ fn one_unsolicited_pong_before_close_does_not_block_closure_observation() Ok(()) } +#[test] +fn repeated_pong_frames_remain_fail_closed_under_fixed_read_budget() -> Result<(), Box> { + let (established, server) = + established_with_server_frame(Some(&[0x8a, 0x00, 0x8a, 0x00]))?; + + let Err(error) = WebDriverBiDiWebSocketTransportClosureObservation::observe( + established, + Duration::from_millis(500), + ) else { + return Err(io::Error::other( + "repeated Pong frames unexpectedly became transport-closure evidence", + ) + .into()); + }; + + server + .join() + .map_err(|_| io::Error::other("repeated-pong test server panicked"))??; + assert!(matches!( + &error, + WebDriverBiDiWebSocketTransportClosureError::UnexpectedFrame { opcode: 0xa } + )); + assert_eq!( + error.to_string(), + "WebDriver BiDi peer sent non-closure traffic instead of closing" + ); + assert!(error.source().is_none()); + Ok(()) +} + #[test] fn clean_peer_eof_yields_transport_observation_without_inventing_close_status() -> Result<(), Box> { @@ -155,7 +185,7 @@ fn application_frame_after_teardown_does_not_become_closure_evidence() -> Result )); assert_eq!( error.to_string(), - "WebDriver BiDi peer sent application traffic instead of closing" + "WebDriver BiDi peer sent non-closure traffic instead of closing" ); assert!(error.source().is_none()); Ok(()) From a45812dcbb742ac134764b3e4c3de3429b434575 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 09:11:38 -0700 Subject: [PATCH 11/12] style(network): apply canonical rustfmt to transport close tests --- .../tests/webdriver_bidi_transport_close_evidence.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs index 895e54b10..073ecb790 100644 --- a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs +++ b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs @@ -112,8 +112,7 @@ fn one_unsolicited_pong_before_close_does_not_block_closure_observation() #[test] fn repeated_pong_frames_remain_fail_closed_under_fixed_read_budget() -> Result<(), Box> { - let (established, server) = - established_with_server_frame(Some(&[0x8a, 0x00, 0x8a, 0x00]))?; + let (established, server) = established_with_server_frame(Some(&[0x8a, 0x00, 0x8a, 0x00]))?; let Err(error) = WebDriverBiDiWebSocketTransportClosureObservation::observe( established, From 870cb39d76748e9a5e632345d81e8e0ce00bc4c2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 00:36:25 +0900 Subject: [PATCH 12/12] fix(network): make BiDi response routing structurally valid Repair exact-head coverage at the earliest executing teardown-stack owner: a validated success response now carries a structurally present command id, nullable ids remain exclusive to protocol errors, and events are id-less. Refresh browser-protocol doctoring to the W3C WebDriver BiDi 18 August 2026 Working Draft. Preserve public envelope behavior and avoid synthetic coverage or authority widening. --- crates/originweave-network/src/lib.rs | 1 + .../src/webdriver_bidi_command_correlation.rs | 41 ++++++++-------- .../src/webdriver_bidi_json_envelope.rs | 48 ++++++++++++++----- docs/doctoring/browser-agent-protocols.md | 18 +++---- 4 files changed, 65 insertions(+), 43 deletions(-) diff --git a/crates/originweave-network/src/lib.rs b/crates/originweave-network/src/lib.rs index 1d3fc2d24..1ff23ce4d 100644 --- a/crates/originweave-network/src/lib.rs +++ b/crates/originweave-network/src/lib.rs @@ -48,6 +48,7 @@ pub use webdriver_bidi_connection::{ WebDriverBiDiTcpConnection, WebDriverBiDiTcpConnectionError, WebDriverBiDiTcpConnectionEvidence, WebDriverBiDiTcpConnectionPlan, }; +pub(crate) use webdriver_bidi_json_envelope::WebDriverBiDiJsonEnvelopeRouting; pub use webdriver_bidi_json_envelope::{ MAX_WEBDRIVER_BIDI_JS_UINT, MAX_WEBDRIVER_BIDI_JSON_DEPTH, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeError, WebDriverBiDiJsonEnvelopeKind, diff --git a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs index 4dd9bcd78..43d5e911e 100644 --- a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs +++ b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs @@ -1,6 +1,8 @@ use std::{collections::BTreeMap, error::Error, fmt}; -use crate::{MAX_WEBDRIVER_BIDI_JS_UINT, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeKind}; +use crate::{ + MAX_WEBDRIVER_BIDI_JS_UINT, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeRouting, +}; /// Maximum number of local WebDriver BiDi commands retained as outstanding at once. /// @@ -183,30 +185,25 @@ impl WebDriverBiDiCommandCorrelation { envelope: &WebDriverBiDiJsonEnvelope, expected_kind: WebDriverBiDiCommandKind, ) -> Result { - match envelope.kind() { - WebDriverBiDiJsonEnvelopeKind::Event => { + match envelope.routing() { + WebDriverBiDiJsonEnvelopeRouting::Event => { Err(WebDriverBiDiCommandCorrelationError::EventIsNotResponse) } - WebDriverBiDiJsonEnvelopeKind::Error => { - let Some(command_id) = envelope.command_id() else { - return Err(WebDriverBiDiCommandCorrelationError::UncorrelatableErrorResponse); - }; - self.complete( - command_id, - expected_kind, - WebDriverBiDiCorrelatedResponseOutcome::Error, - ) - } - WebDriverBiDiJsonEnvelopeKind::Success => { - let Some(command_id) = envelope.command_id() else { - return Err(WebDriverBiDiCommandCorrelationError::CommandNotOutstanding); - }; - self.complete( - command_id, - expected_kind, - WebDriverBiDiCorrelatedResponseOutcome::Success, - ) + WebDriverBiDiJsonEnvelopeRouting::CommandError { command_id: None } => { + Err(WebDriverBiDiCommandCorrelationError::UncorrelatableErrorResponse) } + WebDriverBiDiJsonEnvelopeRouting::CommandError { + command_id: Some(command_id), + } => self.complete( + command_id, + expected_kind, + WebDriverBiDiCorrelatedResponseOutcome::Error, + ), + WebDriverBiDiJsonEnvelopeRouting::CommandSuccess { command_id } => self.complete( + command_id, + expected_kind, + WebDriverBiDiCorrelatedResponseOutcome::Success, + ), } } diff --git a/crates/originweave-network/src/webdriver_bidi_json_envelope.rs b/crates/originweave-network/src/webdriver_bidi_json_envelope.rs index e99adf2c9..7e878980f 100644 --- a/crates/originweave-network/src/webdriver_bidi_json_envelope.rs +++ b/crates/originweave-network/src/webdriver_bidi_json_envelope.rs @@ -23,6 +23,18 @@ pub enum WebDriverBiDiJsonEnvelopeKind { Event, } +/// Structurally valid command/event routing retained after common-envelope validation. +/// +/// Keeping success ids inside the success variant prevents an impossible `success` + missing-id +/// state from leaking into downstream command correlation. Error ids remain optional because the +/// WebDriver BiDi protocol explicitly permits `null` there, while events carry no command id. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum WebDriverBiDiJsonEnvelopeRouting { + CommandSuccess { command_id: u64 }, + CommandError { command_id: Option }, + Event, +} + /// Credential-minimal classification of one complete WebDriver BiDi local-end JSON envelope. /// /// Result and parameter bodies are deliberately validated and discarded at this boundary. They @@ -30,8 +42,7 @@ pub enum WebDriverBiDiJsonEnvelopeKind { /// as generic JSON values that could become ambient browser or Agent authority. #[derive(Eq, PartialEq)] pub struct WebDriverBiDiJsonEnvelope { - kind: WebDriverBiDiJsonEnvelopeKind, - command_id: Option, + routing: WebDriverBiDiJsonEnvelopeRouting, method: Option, error_code: Option, } @@ -40,8 +51,8 @@ impl fmt::Debug for WebDriverBiDiJsonEnvelope { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter .debug_struct("WebDriverBiDiJsonEnvelope") - .field("kind", &self.kind) - .field("command_id", &self.command_id) + .field("kind", &self.kind()) + .field("command_id", &self.command_id()) .field("has_method", &self.method.is_some()) .field("has_error_code", &self.error_code.is_some()) .finish() @@ -73,7 +84,15 @@ impl WebDriverBiDiJsonEnvelope { /// Return the classified local-end envelope kind. #[must_use] pub const fn kind(&self) -> WebDriverBiDiJsonEnvelopeKind { - self.kind + match self.routing { + WebDriverBiDiJsonEnvelopeRouting::CommandSuccess { .. } => { + WebDriverBiDiJsonEnvelopeKind::Success + } + WebDriverBiDiJsonEnvelopeRouting::CommandError { .. } => { + WebDriverBiDiJsonEnvelopeKind::Error + } + WebDriverBiDiJsonEnvelopeRouting::Event => WebDriverBiDiJsonEnvelopeKind::Event, + } } /// Return the command identifier for success and correlatable error responses. @@ -81,7 +100,15 @@ impl WebDriverBiDiJsonEnvelope { /// Events and error responses whose protocol `id` is `null` return `None`. #[must_use] pub const fn command_id(&self) -> Option { - self.command_id + match self.routing { + WebDriverBiDiJsonEnvelopeRouting::CommandSuccess { command_id } => Some(command_id), + WebDriverBiDiJsonEnvelopeRouting::CommandError { command_id } => command_id, + WebDriverBiDiJsonEnvelopeRouting::Event => None, + } + } + + pub(crate) const fn routing(&self) -> WebDriverBiDiJsonEnvelopeRouting { + self.routing } /// Borrow the event method when this is an event envelope. @@ -208,8 +235,7 @@ impl TopLevelFields { let command_id = required_js_uint(self.id, "id")?; require_object(self.result, "result")?; Ok(WebDriverBiDiJsonEnvelope { - kind: WebDriverBiDiJsonEnvelopeKind::Success, - command_id: Some(command_id), + routing: WebDriverBiDiJsonEnvelopeRouting::CommandSuccess { command_id }, method: None, error_code: None, }) @@ -223,8 +249,7 @@ impl TopLevelFields { require_text_value(stacktrace, "stacktrace")?; } Ok(WebDriverBiDiJsonEnvelope { - kind: WebDriverBiDiJsonEnvelopeKind::Error, - command_id, + routing: WebDriverBiDiJsonEnvelopeRouting::CommandError { command_id }, method: None, error_code: Some(error_code), }) @@ -234,8 +259,7 @@ impl TopLevelFields { let method = required_text(self.method, "method")?; require_object(self.params, "params")?; Ok(WebDriverBiDiJsonEnvelope { - kind: WebDriverBiDiJsonEnvelopeKind::Event, - command_id: None, + routing: WebDriverBiDiJsonEnvelopeRouting::Event, method: Some(method), error_code: None, }) diff --git a/docs/doctoring/browser-agent-protocols.md b/docs/doctoring/browser-agent-protocols.md index dbf3ef731..4cc19b210 100644 --- a/docs/doctoring/browser-agent-protocols.md +++ b/docs/doctoring/browser-agent-protocols.md @@ -1,6 +1,6 @@ # Browser and Agent Protocol Standards Evidence -- **Reviewed:** 2026-08-18 +- **Reviewed:** 2026-09-03 - **Purpose:** primary-source evidence for OriginWeave browser compatibility and adapter boundaries - **Canonical research index:** [`../doctoring.md`](../doctoring.md) @@ -8,15 +8,15 @@ This addendum complements the main doctoring record. The main record already car ## WebDriver BiDi -The latest published W3C technical-report baseline reviewed here remains the 1 June 2026 **Working Draft**, not a Recommendation. The current Editor’s Draft reviewed on 18 August 2026 identifies itself as the 20 July 2026 draft. OriginWeave therefore treats BiDi as a versioned browser-automation adapter rather than product-internal authority. Raw BiDi session/context/node identifiers do not become durable OriginWeave identities. +The latest published W3C technical-report baseline reviewed here is the 18 August 2026 **Working Draft**, not a Recommendation. The current Editor’s Draft is consulted only as moving supplemental evidence; the dated Working Draft is the reproducible standards baseline for this review. OriginWeave therefore treats BiDi as a versioned browser-automation adapter rather than product-internal authority. Raw BiDi session/context/node identifiers do not become durable OriginWeave identities. -For the bounded `browsingContext.locateNodes` command-serialization boundary, the reviewed Editor’s Draft defines a command envelope with `id: js-uint`, defines `js-uint` as `0..9007199254740991`, and defines `browsingContext.locateNodes` parameters containing a browsing context, locator, optional positive `maxNodeCount`, optional `serializationOptions`, and optional `startNodes`. OriginWeave serializes only its separately reviewed accessibility-locator subset and fixed minimal serialization options; this deterministic JSON value is not transport authentication or browser/Agent authority. +For the bounded `browsingContext.locateNodes` command-serialization boundary, the reviewed specification defines a command envelope with `id: js-uint`, defines `js-uint` as `0..9007199254740991`, and defines `browsingContext.locateNodes` parameters containing a browsing context, locator, optional positive `maxNodeCount`, optional `serializationOptions`, and optional `startNodes`. OriginWeave serializes only its separately reviewed accessibility-locator subset and fixed minimal serialization options; this deterministic JSON value is not transport authentication or browser/Agent authority. -WebDriver BiDi commands may execute concurrently and finish out of order. The Editor’s Draft defines the command id as the local end’s correlation identifier and sets a successful `CommandResponse.id` to that exact command id; an `ErrorResponse.id` may be `null` when no valid command id can be recovered. OriginWeave therefore fails closed unless a non-null protocol-range response id exactly matches the consumed command before later payload admission. Parsing success/error envelopes, handling nullable malformed-command errors, and authenticating the browser transport remain separate adapter boundaries. +WebDriver BiDi commands may execute concurrently and finish out of order. The 18 August 2026 Working Draft defines the command id as the local end’s correlation identifier; its local-end `CommandResponse` production requires `id: js-uint`, while `ErrorResponse.id` is `js-uint / null`. OriginWeave therefore represents a validated success response with a structurally present command id, retains nullable ids only for protocol errors, and fails closed unless a non-null protocol-range response id exactly matches the consumed command before later payload admission. Parsing success/error envelopes, handling nullable malformed-command errors, and authenticating the browser transport remain separate adapter boundaries. -The same reviewed Editor’s Draft defines a closed `ErrorCode` vocabulary that currently includes `no such client window`. OriginWeave admits only the reviewed vocabulary at its bounded response-envelope parser and rejects unknown error-code text fail closed; adding a newly reviewed protocol code changes compatibility only and grants no browser, transport, node, policy, or Agent authority. +The same reviewed Working Draft defines a closed `ErrorCode` vocabulary that currently includes `no such client window`. OriginWeave admits only the reviewed vocabulary at its bounded response-envelope parser and rejects unknown error-code text fail closed; adding a newly reviewed protocol code changes compatibility only and grants no browser, transport, node, policy, or Agent authority. -Primary sources: World Wide Web Consortium, *WebDriver BiDi* (published Working Draft and current Editor’s Draft). +Primary sources: World Wide Web Consortium, *WebDriver BiDi* (18 August 2026 Working Draft and current Editor’s Draft). ## Chrome Manifest V3 @@ -83,10 +83,10 @@ World Wide Web Consortium. (2013). *PROV-O: The PROV ontology*. https://www.w3.o World Wide Web Consortium. (2023, June 6). *Accessible Rich Internet Applications (WAI-ARIA) 1.2*. https://www.w3.org/TR/2023/REC-wai-aria-1.2-20230606/ -World Wide Web Consortium. (2026, June 1). *WebDriver BiDi* (W3C Working Draft). https://www.w3.org/TR/2026/WD-webdriver-bidi-20260601/ +World Wide Web Consortium. (2026, August 18). *WebDriver BiDi* (W3C Working Draft). https://www.w3.org/TR/2026/WD-webdriver-bidi-20260818/ -World Wide Web Consortium. (2026, July 20). *WebDriver BiDi* (Editor’s Draft). https://w3c.github.io/webdriver-bidi/ +World Wide Web Consortium. (2026). *WebDriver BiDi* (Editor’s Draft). Retrieved September 3, 2026, from https://w3c.github.io/webdriver-bidi/ World Wide Web Consortium. (2026, August 5). *Accessible name and description computation 1.2* (W3C Working Draft). https://www.w3.org/TR/2026/WD-accname-1.2-20260805/ -International Organization for Standardization. (2017). *Information and documentation—WARC file format* (ISO Standard No. 28500:2017). https://www.iso.org/standard/68004.html +International Organization for Standardization. (2017). *Information and documentation—WARC file format* (ISO Standard No. 28500:2017). https://www.iso.org/standard/68004.html \ No newline at end of file