From 2687529751d59d5f5be14011df82ecc176898233 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 20:37:00 -0700 Subject: [PATCH 01/32] test(core): require bounded BiDi WebSocket endpoint admission --- .../webdriver_bidi_websocket_endpoint.rs | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs diff --git a/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs b/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs new file mode 100644 index 000000000..6a75f81da --- /dev/null +++ b/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs @@ -0,0 +1,168 @@ +use std::error::Error; + +use originweave_core::{ + MAX_WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_BYTES, WebDriverBiDiWebSocketEndpoint, + WebDriverBiDiWebSocketEndpointAdmissionError, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; + +#[test] +fn canonical_loopback_session_endpoints_are_admitted_without_granting_authority() { + let ipv4 = WebDriverBiDiWebSocketEndpoint::new(&format!( + "ws://127.0.0.1:9515/session/{SESSION_ID}" + )) + .unwrap(); + assert!(!ipv4.is_secure()); + assert_eq!(ipv4.host(), "127.0.0.1"); + assert_eq!(ipv4.port(), 9515); + assert_eq!(ipv4.session_id(), SESSION_ID); + assert_eq!( + ipv4.as_str(), + format!("ws://127.0.0.1:9515/session/{SESSION_ID}") + ); + + let localhost = WebDriverBiDiWebSocketEndpoint::new(&format!( + "ws://localhost:4444/session/{SESSION_ID}" + )) + .unwrap(); + assert_eq!(localhost.host(), "localhost"); + + let ipv6 = WebDriverBiDiWebSocketEndpoint::new(&format!( + "wss://[::1]:9222/session/{SESSION_ID}" + )) + .unwrap(); + assert!(ipv6.is_secure()); + assert_eq!(ipv6.host(), "::1"); + assert_eq!(ipv6.port(), 9222); +} + +#[test] +fn remote_or_ambiguous_authorities_fail_closed() { + for endpoint in [ + format!("ws://example.com:9515/session/{SESSION_ID}"), + format!("ws://192.0.2.1:9515/session/{SESSION_ID}"), + format!("ws://[2001:db8::1]:9515/session/{SESSION_ID}"), + ] { + assert_eq!( + WebDriverBiDiWebSocketEndpoint::new(&endpoint).unwrap_err(), + WebDriverBiDiWebSocketEndpointAdmissionError::NonLoopbackHost + ); + } + + for endpoint in [ + format!("ws://user@localhost:9515/session/{SESSION_ID}"), + format!("ws://localhost/session/{SESSION_ID}"), + format!("ws://::1:9515/session/{SESSION_ID}"), + format!("ws://[::1]9515/session/{SESSION_ID}"), + ] { + assert_eq!( + WebDriverBiDiWebSocketEndpoint::new(&endpoint).unwrap_err(), + WebDriverBiDiWebSocketEndpointAdmissionError::InvalidAuthority + ); + } +} + +#[test] +fn port_and_session_resource_are_canonical_and_bounded() { + for endpoint in [ + format!("ws://localhost:0/session/{SESSION_ID}"), + format!("ws://localhost:09515/session/{SESSION_ID}"), + format!("ws://localhost:65536/session/{SESSION_ID}"), + format!("ws://localhost:+9515/session/{SESSION_ID}"), + ] { + assert_eq!( + WebDriverBiDiWebSocketEndpoint::new(&endpoint).unwrap_err(), + WebDriverBiDiWebSocketEndpointAdmissionError::InvalidPort + ); + } + + for endpoint in [ + format!("ws://localhost:9515/other/{SESSION_ID}"), + format!("ws://localhost:9515/session/{SESSION_ID}/extra"), + "ws://localhost:9515/session/".to_owned(), + "ws://localhost:9515".to_owned(), + ] { + assert_eq!( + WebDriverBiDiWebSocketEndpoint::new(&endpoint).unwrap_err(), + WebDriverBiDiWebSocketEndpointAdmissionError::InvalidSessionResource + ); + } + + for session_id in [ + "01234567-89ab-cdef-0123-456789abcdeF", + "0123456789ab-cdef-0123-456789abcdef", + "01234567-89ab-cdef-0123-456789abcdeg", + ] { + assert_eq!( + WebDriverBiDiWebSocketEndpoint::new(&format!( + "ws://localhost:9515/session/{session_id}" + )) + .unwrap_err(), + WebDriverBiDiWebSocketEndpointAdmissionError::InvalidSessionId + ); + } +} + +#[test] +fn endpoint_text_rejects_noncanonical_or_unbounded_inputs_before_transport_use() { + assert_eq!( + WebDriverBiDiWebSocketEndpoint::new("").unwrap_err(), + WebDriverBiDiWebSocketEndpointAdmissionError::EmptyEndpoint + ); + assert_eq!( + WebDriverBiDiWebSocketEndpoint::new(&format!( + "http://localhost:9515/session/{SESSION_ID}" + )) + .unwrap_err(), + WebDriverBiDiWebSocketEndpointAdmissionError::InvalidScheme + ); + assert_eq!( + WebDriverBiDiWebSocketEndpoint::new(&format!( + "ws://local host:9515/session/{SESSION_ID}" + )) + .unwrap_err(), + WebDriverBiDiWebSocketEndpointAdmissionError::InvalidEndpointText + ); + assert_eq!( + WebDriverBiDiWebSocketEndpoint::new(&format!( + "ws://localhost:9515/session/{SESSION_ID}?token=secret" + )) + .unwrap_err(), + WebDriverBiDiWebSocketEndpointAdmissionError::QueryOrFragmentForbidden + ); + assert_eq!( + WebDriverBiDiWebSocketEndpoint::new(&format!( + "ws://localhost:9515/session/{SESSION_ID}#fragment" + )) + .unwrap_err(), + WebDriverBiDiWebSocketEndpointAdmissionError::QueryOrFragmentForbidden + ); + + let oversized = "x".repeat(MAX_WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_BYTES + 1); + assert_eq!( + WebDriverBiDiWebSocketEndpoint::new(&oversized).unwrap_err(), + WebDriverBiDiWebSocketEndpointAdmissionError::EndpointTooLong + ); +} + +#[test] +fn endpoint_error_contract_is_deterministic_and_source_free() { + let errors = [ + WebDriverBiDiWebSocketEndpointAdmissionError::EmptyEndpoint, + WebDriverBiDiWebSocketEndpointAdmissionError::EndpointTooLong, + WebDriverBiDiWebSocketEndpointAdmissionError::InvalidEndpointText, + WebDriverBiDiWebSocketEndpointAdmissionError::InvalidScheme, + WebDriverBiDiWebSocketEndpointAdmissionError::QueryOrFragmentForbidden, + WebDriverBiDiWebSocketEndpointAdmissionError::InvalidAuthority, + WebDriverBiDiWebSocketEndpointAdmissionError::NonLoopbackHost, + WebDriverBiDiWebSocketEndpointAdmissionError::InvalidPort, + WebDriverBiDiWebSocketEndpointAdmissionError::InvalidSessionResource, + WebDriverBiDiWebSocketEndpointAdmissionError::InvalidSessionId, + ]; + + for error in errors { + assert!(!error.to_string().is_empty()); + assert!(error.source().is_none()); + } +} From a1c4cb55609e03e76c8663b47727e302313cd08d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 20:40:16 -0700 Subject: [PATCH 02/32] style(core): canonicalize BiDi WebSocket endpoint RED --- .../webdriver_bidi_websocket_endpoint.rs | 132 ++++++++++-------- 1 file changed, 71 insertions(+), 61 deletions(-) diff --git a/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs b/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs index 6a75f81da..a5954b32d 100644 --- a/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs +++ b/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs @@ -9,10 +9,12 @@ const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; #[test] fn canonical_loopback_session_endpoints_are_admitted_without_granting_authority() { - let ipv4 = WebDriverBiDiWebSocketEndpoint::new(&format!( - "ws://127.0.0.1:9515/session/{SESSION_ID}" - )) - .unwrap(); + let ipv4_result = + WebDriverBiDiWebSocketEndpoint::new(&format!("ws://127.0.0.1:9515/session/{SESSION_ID}")); + assert!(ipv4_result.is_ok(), "{ipv4_result:?}"); + let Ok(ipv4) = ipv4_result else { + return; + }; assert!(!ipv4.is_secure()); assert_eq!(ipv4.host(), "127.0.0.1"); assert_eq!(ipv4.port(), 9515); @@ -22,16 +24,20 @@ fn canonical_loopback_session_endpoints_are_admitted_without_granting_authority( format!("ws://127.0.0.1:9515/session/{SESSION_ID}") ); - let localhost = WebDriverBiDiWebSocketEndpoint::new(&format!( - "ws://localhost:4444/session/{SESSION_ID}" - )) - .unwrap(); + let localhost_result = + WebDriverBiDiWebSocketEndpoint::new(&format!("ws://localhost:4444/session/{SESSION_ID}")); + assert!(localhost_result.is_ok(), "{localhost_result:?}"); + let Ok(localhost) = localhost_result else { + return; + }; assert_eq!(localhost.host(), "localhost"); - let ipv6 = WebDriverBiDiWebSocketEndpoint::new(&format!( - "wss://[::1]:9222/session/{SESSION_ID}" - )) - .unwrap(); + let ipv6_result = + WebDriverBiDiWebSocketEndpoint::new(&format!("wss://[::1]:9222/session/{SESSION_ID}")); + assert!(ipv6_result.is_ok(), "{ipv6_result:?}"); + let Ok(ipv6) = ipv6_result else { + return; + }; assert!(ipv6.is_secure()); assert_eq!(ipv6.host(), "::1"); assert_eq!(ipv6.port(), 9222); @@ -44,10 +50,10 @@ fn remote_or_ambiguous_authorities_fail_closed() { format!("ws://192.0.2.1:9515/session/{SESSION_ID}"), format!("ws://[2001:db8::1]:9515/session/{SESSION_ID}"), ] { - assert_eq!( - WebDriverBiDiWebSocketEndpoint::new(&endpoint).unwrap_err(), - WebDriverBiDiWebSocketEndpointAdmissionError::NonLoopbackHost - ); + assert!(matches!( + WebDriverBiDiWebSocketEndpoint::new(&endpoint), + Err(WebDriverBiDiWebSocketEndpointAdmissionError::NonLoopbackHost) + )); } for endpoint in [ @@ -55,11 +61,13 @@ fn remote_or_ambiguous_authorities_fail_closed() { format!("ws://localhost/session/{SESSION_ID}"), format!("ws://::1:9515/session/{SESSION_ID}"), format!("ws://[::1]9515/session/{SESSION_ID}"), + format!("ws://[::zz]:9515/session/{SESSION_ID}"), + format!("ws://:9515/session/{SESSION_ID}"), ] { - assert_eq!( - WebDriverBiDiWebSocketEndpoint::new(&endpoint).unwrap_err(), - WebDriverBiDiWebSocketEndpointAdmissionError::InvalidAuthority - ); + assert!(matches!( + WebDriverBiDiWebSocketEndpoint::new(&endpoint), + Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidAuthority) + )); } } @@ -71,10 +79,10 @@ fn port_and_session_resource_are_canonical_and_bounded() { format!("ws://localhost:65536/session/{SESSION_ID}"), format!("ws://localhost:+9515/session/{SESSION_ID}"), ] { - assert_eq!( - WebDriverBiDiWebSocketEndpoint::new(&endpoint).unwrap_err(), - WebDriverBiDiWebSocketEndpointAdmissionError::InvalidPort - ); + assert!(matches!( + WebDriverBiDiWebSocketEndpoint::new(&endpoint), + Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidPort) + )); } for endpoint in [ @@ -83,67 +91,69 @@ fn port_and_session_resource_are_canonical_and_bounded() { "ws://localhost:9515/session/".to_owned(), "ws://localhost:9515".to_owned(), ] { - assert_eq!( - WebDriverBiDiWebSocketEndpoint::new(&endpoint).unwrap_err(), - WebDriverBiDiWebSocketEndpointAdmissionError::InvalidSessionResource - ); + assert!(matches!( + WebDriverBiDiWebSocketEndpoint::new(&endpoint), + Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidSessionResource) + )); } for session_id in [ "01234567-89ab-cdef-0123-456789abcdeF", "0123456789ab-cdef-0123-456789abcdef", "01234567-89ab-cdef-0123-456789abcdeg", + "01234567_89ab-cdef-0123-456789abcdef", ] { - assert_eq!( + assert!(matches!( WebDriverBiDiWebSocketEndpoint::new(&format!( "ws://localhost:9515/session/{session_id}" - )) - .unwrap_err(), - WebDriverBiDiWebSocketEndpointAdmissionError::InvalidSessionId - ); + )), + Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidSessionId) + )); } } #[test] fn endpoint_text_rejects_noncanonical_or_unbounded_inputs_before_transport_use() { - assert_eq!( - WebDriverBiDiWebSocketEndpoint::new("").unwrap_err(), - WebDriverBiDiWebSocketEndpointAdmissionError::EmptyEndpoint - ); - assert_eq!( + assert!(matches!( + WebDriverBiDiWebSocketEndpoint::new(""), + Err(WebDriverBiDiWebSocketEndpointAdmissionError::EmptyEndpoint) + )); + assert!(matches!( WebDriverBiDiWebSocketEndpoint::new(&format!( "http://localhost:9515/session/{SESSION_ID}" - )) - .unwrap_err(), - WebDriverBiDiWebSocketEndpointAdmissionError::InvalidScheme - ); - assert_eq!( + )), + Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidScheme) + )); + assert!(matches!( WebDriverBiDiWebSocketEndpoint::new(&format!( "ws://local host:9515/session/{SESSION_ID}" - )) - .unwrap_err(), - WebDriverBiDiWebSocketEndpointAdmissionError::InvalidEndpointText - ); - assert_eq!( + )), + Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidEndpointText) + )); + assert!(matches!( + WebDriverBiDiWebSocketEndpoint::new(&format!( + "ws://locálhost:9515/session/{SESSION_ID}" + )), + Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidEndpointText) + )); + assert!(matches!( WebDriverBiDiWebSocketEndpoint::new(&format!( "ws://localhost:9515/session/{SESSION_ID}?token=secret" - )) - .unwrap_err(), - WebDriverBiDiWebSocketEndpointAdmissionError::QueryOrFragmentForbidden - ); - assert_eq!( + )), + Err(WebDriverBiDiWebSocketEndpointAdmissionError::QueryOrFragmentForbidden) + )); + assert!(matches!( WebDriverBiDiWebSocketEndpoint::new(&format!( "ws://localhost:9515/session/{SESSION_ID}#fragment" - )) - .unwrap_err(), - WebDriverBiDiWebSocketEndpointAdmissionError::QueryOrFragmentForbidden - ); + )), + Err(WebDriverBiDiWebSocketEndpointAdmissionError::QueryOrFragmentForbidden) + )); let oversized = "x".repeat(MAX_WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_BYTES + 1); - assert_eq!( - WebDriverBiDiWebSocketEndpoint::new(&oversized).unwrap_err(), - WebDriverBiDiWebSocketEndpointAdmissionError::EndpointTooLong - ); + assert!(matches!( + WebDriverBiDiWebSocketEndpoint::new(&oversized), + Err(WebDriverBiDiWebSocketEndpointAdmissionError::EndpointTooLong) + )); } #[test] From dbdff70a1e8a045ee2735d86a321992e9ee295c7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 20:41:25 -0700 Subject: [PATCH 03/32] style(core): apply canonical rustfmt to endpoint RED --- .../tests/webdriver_bidi_websocket_endpoint.rs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs b/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs index a5954b32d..fb47c4f3e 100644 --- a/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs +++ b/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs @@ -119,21 +119,15 @@ fn endpoint_text_rejects_noncanonical_or_unbounded_inputs_before_transport_use() Err(WebDriverBiDiWebSocketEndpointAdmissionError::EmptyEndpoint) )); assert!(matches!( - WebDriverBiDiWebSocketEndpoint::new(&format!( - "http://localhost:9515/session/{SESSION_ID}" - )), + WebDriverBiDiWebSocketEndpoint::new(&format!("http://localhost:9515/session/{SESSION_ID}")), Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidScheme) )); assert!(matches!( - WebDriverBiDiWebSocketEndpoint::new(&format!( - "ws://local host:9515/session/{SESSION_ID}" - )), + WebDriverBiDiWebSocketEndpoint::new(&format!("ws://local host:9515/session/{SESSION_ID}")), Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidEndpointText) )); assert!(matches!( - WebDriverBiDiWebSocketEndpoint::new(&format!( - "ws://locálhost:9515/session/{SESSION_ID}" - )), + WebDriverBiDiWebSocketEndpoint::new(&format!("ws://locálhost:9515/session/{SESSION_ID}")), Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidEndpointText) )); assert!(matches!( From 3d2c408aa18a5328aac283be5744d5f39633184f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 20:44:16 -0700 Subject: [PATCH 04/32] feat(core): admit bounded BiDi WebSocket endpoints --- crates/originweave-core/src/lib.rs | 5 + .../src/webdriver_bidi_websocket_endpoint.rs | 230 ++++++++++++++++++ 2 files changed, 235 insertions(+) create mode 100644 crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index c5a825a9e..67753ed6c 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -37,6 +37,7 @@ mod webdriver_bidi_response_document; mod webdriver_bidi_response_document_correlation; mod webdriver_bidi_response_envelope; mod webdriver_bidi_result; +mod webdriver_bidi_websocket_endpoint; pub use browser_authority_registry::BrowserAuthorityRegistry; pub use browser_protocol::{ @@ -87,3 +88,7 @@ pub use webdriver_bidi_response_envelope::{ pub use webdriver_bidi_result::{ ValidatedWebDriverBiDiLocateNodesResult, WebDriverBiDiLocateNodesResultAdmissionError, }; +pub use webdriver_bidi_websocket_endpoint::{ + MAX_WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_BYTES, WebDriverBiDiWebSocketEndpoint, + WebDriverBiDiWebSocketEndpointAdmissionError, +}; diff --git a/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs b/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs new file mode 100644 index 000000000..3042f97cd --- /dev/null +++ b/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs @@ -0,0 +1,230 @@ +use std::fmt; +use std::net::{Ipv4Addr, Ipv6Addr}; + +/// Maximum admitted bytes for one WebDriver BiDi WebSocket endpoint. +/// +/// This is an OriginWeave first-Chromium-fixture safety budget, not a +/// WebDriver BiDi protocol maximum. +pub const MAX_WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_BYTES: usize = 2_048; + +/// One bounded canonical WebDriver BiDi session WebSocket endpoint. +/// +/// This value is transport metadata only. Construction does not authenticate +/// Chromium, ChromeDriver, the operating-system peer, TLS, policy, or Agent +/// authority. The first real-Chromium fixture intentionally admits only +/// loopback listener identities; the connection boundary must still verify the +/// actual peer before exposing transport I/O. +#[derive(Debug, PartialEq, Eq)] +pub struct WebDriverBiDiWebSocketEndpoint { + endpoint: String, + secure: bool, + host: String, + port: u16, + session_id: String, +} + +impl WebDriverBiDiWebSocketEndpoint { + /// Admit one bounded canonical first-fixture WebDriver BiDi endpoint. + pub fn new(value: &str) -> Result { + if value.is_empty() { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::EmptyEndpoint); + } + if value.len() > MAX_WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_BYTES { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::EndpointTooLong); + } + if value.bytes().any(|byte| !byte.is_ascii_graphic()) { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidEndpointText); + } + if value.bytes().any(|byte| matches!(byte, b'?' | b'#')) { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::QueryOrFragmentForbidden); + } + + let (secure, remainder) = if let Some(remainder) = value.strip_prefix("ws://") { + (false, remainder) + } else if let Some(remainder) = value.strip_prefix("wss://") { + (true, remainder) + } else { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidScheme); + }; + + let Some(path_start) = remainder.find('/') else { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidSessionResource); + }; + let authority = &remainder[..path_start]; + let resource = &remainder[path_start..]; + if authority.is_empty() || authority.contains('@') { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidAuthority); + } + + let (host, port_text) = if let Some(bracketed) = authority.strip_prefix('[') { + let Some(close) = bracketed.find(']') else { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidAuthority); + }; + let host_text = &bracketed[..close]; + let suffix = &bracketed[close + 1..]; + let Some(port_text) = suffix.strip_prefix(':') else { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidAuthority); + }; + if port_text.is_empty() { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidAuthority); + } + let Ok(ip) = host_text.parse::() else { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidAuthority); + }; + if !ip.is_loopback() { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::NonLoopbackHost); + } + if ip.to_string() != host_text { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidAuthority); + } + (host_text.to_owned(), port_text) + } else { + let Some((host_text, port_text)) = authority.rsplit_once(':') else { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidAuthority); + }; + if host_text.is_empty() || port_text.is_empty() || host_text.contains(':') { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidAuthority); + } + if host_text == "localhost" { + (host_text.to_owned(), port_text) + } else if let Ok(ip) = host_text.parse::() { + if ip.to_string() != host_text { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidAuthority); + } + if !ip.is_loopback() { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::NonLoopbackHost); + } + (host_text.to_owned(), port_text) + } else if host_text + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'-')) + { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::NonLoopbackHost); + } else { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidAuthority); + } + }; + + let Ok(port) = port_text.parse::() else { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidPort); + }; + if port == 0 || port.to_string() != port_text { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidPort); + } + + let Some(session_id) = resource.strip_prefix("/session/") else { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidSessionResource); + }; + if session_id.is_empty() || session_id.contains('/') { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidSessionResource); + } + if !is_canonical_session_uuid(session_id) { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidSessionId); + } + + Ok(Self { + endpoint: value.to_owned(), + secure, + host, + port, + session_id: session_id.to_owned(), + }) + } + + /// Return the exact admitted endpoint text. + #[must_use] + pub fn as_str(&self) -> &str { + &self.endpoint + } + + /// Return whether the endpoint uses `wss` rather than `ws`. + #[must_use] + pub const fn is_secure(&self) -> bool { + self.secure + } + + /// Return the canonical loopback listener host without IPv6 brackets. + #[must_use] + pub fn host(&self) -> &str { + &self.host + } + + /// Return the explicit nonzero listener port. + #[must_use] + pub const fn port(&self) -> u16 { + self.port + } + + /// Return the canonical lower-case UUID session identifier. + #[must_use] + pub fn session_id(&self) -> &str { + &self.session_id + } +} + +fn is_canonical_session_uuid(value: &str) -> bool { + let bytes = value.as_bytes(); + if bytes.len() != 36 { + return false; + } + for (index, byte) in bytes.iter().copied().enumerate() { + let valid = if matches!(index, 8 | 13 | 18 | 23) { + byte == b'-' + } else { + byte.is_ascii_digit() || matches!(byte, b'a'..=b'f') + }; + if !valid { + return false; + } + } + true +} + +/// Fail-closed admission errors for WebDriver BiDi WebSocket endpoint metadata. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum WebDriverBiDiWebSocketEndpointAdmissionError { + /// The endpoint text is empty. + EmptyEndpoint, + /// The endpoint text exceeds the OriginWeave safety budget. + EndpointTooLong, + /// The endpoint contains non-ASCII, whitespace, or control text. + InvalidEndpointText, + /// The endpoint does not use the exact `ws` or `wss` scheme. + InvalidScheme, + /// Query or fragment data is present and therefore not part of the admitted session resource. + QueryOrFragmentForbidden, + /// The authority is absent, credential-bearing, ambiguous, or malformed. + InvalidAuthority, + /// The authority identifies a non-loopback host. + NonLoopbackHost, + /// The port is absent, zero, out of range, or not canonically serialized. + InvalidPort, + /// The path is not exactly one `/session/` resource. + InvalidSessionResource, + /// The session id is not one canonical lower-case UUID representation. + InvalidSessionId, +} + +impl fmt::Display for WebDriverBiDiWebSocketEndpointAdmissionError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let message = match self { + Self::EmptyEndpoint => "WebDriver BiDi WebSocket endpoint is empty", + Self::EndpointTooLong => "WebDriver BiDi WebSocket endpoint exceeds the safety budget", + Self::InvalidEndpointText => "WebDriver BiDi WebSocket endpoint text is not canonical ASCII", + Self::InvalidScheme => "WebDriver BiDi WebSocket endpoint scheme is not ws or wss", + Self::QueryOrFragmentForbidden => { + "WebDriver BiDi WebSocket endpoint query or fragment is forbidden" + } + Self::InvalidAuthority => "WebDriver BiDi WebSocket endpoint authority is invalid", + Self::NonLoopbackHost => "WebDriver BiDi WebSocket endpoint host is not loopback", + Self::InvalidPort => "WebDriver BiDi WebSocket endpoint port is invalid", + Self::InvalidSessionResource => { + "WebDriver BiDi WebSocket endpoint session resource is invalid" + } + Self::InvalidSessionId => "WebDriver BiDi WebSocket endpoint session id is invalid", + }; + f.write_str(message) + } +} + +impl std::error::Error for WebDriverBiDiWebSocketEndpointAdmissionError {} From e0ec37e97007fc6d71b3ca6956151bc5bc81b705 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 20:45:52 -0700 Subject: [PATCH 05/32] style(core): apply canonical endpoint rustfmt --- .../originweave-core/src/webdriver_bidi_websocket_endpoint.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs b/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs index 3042f97cd..2b266c679 100644 --- a/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs +++ b/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs @@ -210,7 +210,9 @@ impl fmt::Display for WebDriverBiDiWebSocketEndpointAdmissionError { let message = match self { Self::EmptyEndpoint => "WebDriver BiDi WebSocket endpoint is empty", Self::EndpointTooLong => "WebDriver BiDi WebSocket endpoint exceeds the safety budget", - Self::InvalidEndpointText => "WebDriver BiDi WebSocket endpoint text is not canonical ASCII", + Self::InvalidEndpointText => { + "WebDriver BiDi WebSocket endpoint text is not canonical ASCII" + } Self::InvalidScheme => "WebDriver BiDi WebSocket endpoint scheme is not ws or wss", Self::QueryOrFragmentForbidden => { "WebDriver BiDi WebSocket endpoint query or fragment is forbidden" From bebb1e1ad1aad847caddd9080a3888b24246c834 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 22:06:52 -0700 Subject: [PATCH 06/32] test(core): cover malformed BiDi endpoint authorities --- .../tests/webdriver_bidi_websocket_endpoint.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs b/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs index fb47c4f3e..7dca568e5 100644 --- a/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs +++ b/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs @@ -71,6 +71,22 @@ fn remote_or_ambiguous_authorities_fail_closed() { } } +#[test] +fn malformed_loopback_authority_edge_cases_fail_closed() { + for endpoint in [ + format!("ws://[::1:9515/session/{SESSION_ID}"), + format!("ws://[::1]:/session/{SESSION_ID}"), + format!("ws://[0:0:0:0:0:0:0:1]:9515/session/{SESSION_ID}"), + format!("ws://localhost:/session/{SESSION_ID}"), + format!("ws://local_host:9515/session/{SESSION_ID}"), + ] { + assert!(matches!( + WebDriverBiDiWebSocketEndpoint::new(&endpoint), + Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidAuthority) + )); + } +} + #[test] fn port_and_session_resource_are_canonical_and_bounded() { for endpoint in [ From 805412826ff7e131fcb2e433be8eefbe62778166 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 22:08:18 -0700 Subject: [PATCH 07/32] fix(core): remove unreachable IPv4 endpoint branch --- .../originweave-core/src/webdriver_bidi_websocket_endpoint.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs b/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs index 2b266c679..3a555493b 100644 --- a/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs +++ b/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs @@ -88,9 +88,6 @@ impl WebDriverBiDiWebSocketEndpoint { if host_text == "localhost" { (host_text.to_owned(), port_text) } else if let Ok(ip) = host_text.parse::() { - if ip.to_string() != host_text { - return Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidAuthority); - } if !ip.is_loopback() { return Err(WebDriverBiDiWebSocketEndpointAdmissionError::NonLoopbackHost); } From 42bfbed8a7268aaf60c5be3dc1f06b7df74107c3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 22:16:19 -0700 Subject: [PATCH 08/32] test(core): cover empty BiDi endpoint authority --- .../tests/webdriver_bidi_websocket_endpoint.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs b/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs index 7dca568e5..3549d293b 100644 --- a/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs +++ b/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs @@ -57,6 +57,7 @@ fn remote_or_ambiguous_authorities_fail_closed() { } for endpoint in [ + format!("ws:///session/{SESSION_ID}"), format!("ws://user@localhost:9515/session/{SESSION_ID}"), format!("ws://localhost/session/{SESSION_ID}"), format!("ws://::1:9515/session/{SESSION_ID}"), @@ -185,4 +186,4 @@ fn endpoint_error_contract_is_deterministic_and_source_free() { assert!(!error.to_string().is_empty()); assert!(error.source().is_none()); } -} +} \ No newline at end of file From bc042eeab9345c000012bea2c9941b3bd1d45b71 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 22:36:11 -0700 Subject: [PATCH 09/32] test(core): format BiDi WebSocket endpoint regressions --- .../originweave-core/tests/webdriver_bidi_websocket_endpoint.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs b/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs index 3549d293b..ecf5df6b6 100644 --- a/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs +++ b/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs @@ -186,4 +186,4 @@ fn endpoint_error_contract_is_deterministic_and_source_free() { assert!(!error.to_string().is_empty()); assert!(error.source().is_none()); } -} \ No newline at end of file +} From 5d7906a8bfc17ff2abbbb499d9f3be4ef77f297e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 23:11:54 -0700 Subject: [PATCH 10/32] test(core): require exact BiDi endpoint session correlation --- ...iver_bidi_websocket_session_correlation.rs | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 crates/originweave-core/tests/webdriver_bidi_websocket_session_correlation.rs diff --git a/crates/originweave-core/tests/webdriver_bidi_websocket_session_correlation.rs b/crates/originweave-core/tests/webdriver_bidi_websocket_session_correlation.rs new file mode 100644 index 000000000..7945cf629 --- /dev/null +++ b/crates/originweave-core/tests/webdriver_bidi_websocket_session_correlation.rs @@ -0,0 +1,72 @@ +use std::error::Error; + +use originweave_core::{ + WebDriverBiDiWebSocketEndpoint, WebDriverBiDiWebSocketEndpointCorrelationError, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; +const OTHER_SESSION_ID: &str = "11234567-89ab-cdef-0123-456789abcdef"; + +fn endpoint() -> WebDriverBiDiWebSocketEndpoint { + let result = WebDriverBiDiWebSocketEndpoint::new(&format!( + "ws://127.0.0.1:9515/session/{SESSION_ID}" + )); + assert!(result.is_ok(), "{result:?}"); + let Ok(endpoint) = result else { + unreachable!("asserted valid endpoint") + }; + endpoint +} + +#[test] +fn exact_session_identity_correlation_preserves_bounded_endpoint_metadata() { + let result = endpoint().correlate_session_id(SESSION_ID); + assert!(result.is_ok(), "{result:?}"); + let Ok(correlated) = result else { + return; + }; + + assert_eq!( + correlated.as_str(), + format!("ws://127.0.0.1:9515/session/{SESSION_ID}") + ); + assert!(!correlated.is_secure()); + assert_eq!(correlated.host(), "127.0.0.1"); + assert_eq!(correlated.port(), 9515); + assert_eq!(correlated.session_id(), SESSION_ID); +} + +#[test] +fn a_different_canonical_session_identity_fails_closed() { + assert!(matches!( + endpoint().correlate_session_id(OTHER_SESSION_ID), + Err(WebDriverBiDiWebSocketEndpointCorrelationError::SessionIdMismatch) + )); +} + +#[test] +fn malformed_expected_session_identity_is_rejected_before_comparison() { + for expected in [ + "", + "01234567-89ab-cdef-0123-456789abcdeF", + "0123456789ab-cdef-0123-456789abcdef", + "01234567-89ab-cdef-0123-456789abcdeg", + "01234567_89ab-cdef-0123-456789abcdef", + ] { + assert!(matches!( + endpoint().correlate_session_id(expected), + Err(WebDriverBiDiWebSocketEndpointCorrelationError::InvalidExpectedSessionId) + )); + } +} + +#[test] +fn session_correlation_errors_are_deterministic_and_source_free() { + for error in [ + WebDriverBiDiWebSocketEndpointCorrelationError::InvalidExpectedSessionId, + WebDriverBiDiWebSocketEndpointCorrelationError::SessionIdMismatch, + ] { + assert!(!error.to_string().is_empty()); + assert!(error.source().is_none()); + } +} From 96fdd9dc9eeb54a03b9392514aa4304811ed36ea Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 23:16:29 -0700 Subject: [PATCH 11/32] style(core): apply canonical BiDi session-correlation formatting --- .../tests/webdriver_bidi_websocket_session_correlation.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/originweave-core/tests/webdriver_bidi_websocket_session_correlation.rs b/crates/originweave-core/tests/webdriver_bidi_websocket_session_correlation.rs index 7945cf629..074faf57e 100644 --- a/crates/originweave-core/tests/webdriver_bidi_websocket_session_correlation.rs +++ b/crates/originweave-core/tests/webdriver_bidi_websocket_session_correlation.rs @@ -8,9 +8,8 @@ const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; const OTHER_SESSION_ID: &str = "11234567-89ab-cdef-0123-456789abcdef"; fn endpoint() -> WebDriverBiDiWebSocketEndpoint { - let result = WebDriverBiDiWebSocketEndpoint::new(&format!( - "ws://127.0.0.1:9515/session/{SESSION_ID}" - )); + let result = + WebDriverBiDiWebSocketEndpoint::new(&format!("ws://127.0.0.1:9515/session/{SESSION_ID}")); assert!(result.is_ok(), "{result:?}"); let Ok(endpoint) = result else { unreachable!("asserted valid endpoint") From c4d2e4d77d6bab17dfec6b0353f1bf8c2b28f86c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 23:20:20 -0700 Subject: [PATCH 12/32] feat(core): correlate BiDi endpoint with exact session --- .../src/webdriver_bidi_websocket_endpoint.rs | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs b/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs index 3a555493b..6b8955a58 100644 --- a/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs +++ b/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs @@ -23,6 +23,17 @@ pub struct WebDriverBiDiWebSocketEndpoint { session_id: String, } +/// One admitted WebDriver BiDi WebSocket endpoint correlated to an expected session id. +/// +/// Correlation proves only that the endpoint resource and the caller-supplied expected session id +/// contain the same canonical UUID text. The expected session id must itself come from a trusted +/// session-creation boundary. This value does not authenticate Chromium, ChromeDriver, the caller, +/// the operating-system peer, TLS, policy, or Agent authority, and it does not establish a socket. +#[derive(Debug, PartialEq, Eq)] +pub struct CorrelatedWebDriverBiDiWebSocketEndpoint { + endpoint: WebDriverBiDiWebSocketEndpoint, +} + impl WebDriverBiDiWebSocketEndpoint { /// Admit one bounded canonical first-fixture WebDriver BiDi endpoint. pub fn new(value: &str) -> Result { @@ -128,6 +139,30 @@ impl WebDriverBiDiWebSocketEndpoint { }) } + /// Correlate this endpoint resource to one exact expected WebDriver session id. + /// + /// The endpoint is consumed so downstream connection code can require the correlated type and + /// cannot accidentally retain an uncorrelated copy. This comparison does not establish that the + /// caller-supplied expected id is authentic; the caller must obtain that id from its trusted + /// session-creation boundary. + pub fn correlate_session_id( + self, + expected_session_id: &str, + ) -> Result< + CorrelatedWebDriverBiDiWebSocketEndpoint, + WebDriverBiDiWebSocketEndpointCorrelationError, + > { + if !is_canonical_session_uuid(expected_session_id) { + return Err( + WebDriverBiDiWebSocketEndpointCorrelationError::InvalidExpectedSessionId, + ); + } + if self.session_id != expected_session_id { + return Err(WebDriverBiDiWebSocketEndpointCorrelationError::SessionIdMismatch); + } + Ok(CorrelatedWebDriverBiDiWebSocketEndpoint { endpoint: self }) + } + /// Return the exact admitted endpoint text. #[must_use] pub fn as_str(&self) -> &str { @@ -159,6 +194,38 @@ impl WebDriverBiDiWebSocketEndpoint { } } +impl CorrelatedWebDriverBiDiWebSocketEndpoint { + /// Return the exact admitted endpoint text. + #[must_use] + pub fn as_str(&self) -> &str { + self.endpoint.as_str() + } + + /// Return whether the endpoint uses `wss` rather than `ws`. + #[must_use] + pub const fn is_secure(&self) -> bool { + self.endpoint.is_secure() + } + + /// Return the canonical loopback listener host without IPv6 brackets. + #[must_use] + pub fn host(&self) -> &str { + self.endpoint.host() + } + + /// Return the explicit nonzero listener port. + #[must_use] + pub const fn port(&self) -> u16 { + self.endpoint.port() + } + + /// Return the exact session id proven equal to the caller-supplied expected session id. + #[must_use] + pub fn session_id(&self) -> &str { + self.endpoint.session_id() + } +} + fn is_canonical_session_uuid(value: &str) -> bool { let bytes = value.as_bytes(); if bytes.len() != 36 { @@ -227,3 +294,28 @@ impl fmt::Display for WebDriverBiDiWebSocketEndpointAdmissionError { } impl std::error::Error for WebDriverBiDiWebSocketEndpointAdmissionError {} + +/// Fail-closed errors while correlating an admitted endpoint with an expected session id. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum WebDriverBiDiWebSocketEndpointCorrelationError { + /// The expected session id is not one canonical lower-case UUID representation. + InvalidExpectedSessionId, + /// The endpoint resource belongs to a different canonical session id. + SessionIdMismatch, +} + +impl fmt::Display for WebDriverBiDiWebSocketEndpointCorrelationError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let message = match self { + Self::InvalidExpectedSessionId => { + "expected WebDriver session id is not a canonical lower-case UUID" + } + Self::SessionIdMismatch => { + "WebDriver BiDi WebSocket endpoint session id does not match the expected session" + } + }; + f.write_str(message) + } +} + +impl std::error::Error for WebDriverBiDiWebSocketEndpointCorrelationError {} From 45935f51eadbce5d64fc480abbc3165cc7e373cc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 23:21:22 -0700 Subject: [PATCH 13/32] feat(core): export correlated BiDi endpoint contract --- crates/originweave-core/src/lib.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index 67753ed6c..27929c727 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -89,6 +89,7 @@ pub use webdriver_bidi_result::{ ValidatedWebDriverBiDiLocateNodesResult, WebDriverBiDiLocateNodesResultAdmissionError, }; pub use webdriver_bidi_websocket_endpoint::{ - MAX_WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_BYTES, WebDriverBiDiWebSocketEndpoint, - WebDriverBiDiWebSocketEndpointAdmissionError, + CorrelatedWebDriverBiDiWebSocketEndpoint, MAX_WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_BYTES, + WebDriverBiDiWebSocketEndpoint, WebDriverBiDiWebSocketEndpointAdmissionError, + WebDriverBiDiWebSocketEndpointCorrelationError, }; From 70461f197bbef8fc9f81985d2f4b9e80619a4b2f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 23:24:58 -0700 Subject: [PATCH 14/32] style(core): apply canonical session-correlation formatting --- .../originweave-core/src/webdriver_bidi_websocket_endpoint.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs b/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs index 6b8955a58..c95c309e1 100644 --- a/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs +++ b/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs @@ -153,9 +153,7 @@ impl WebDriverBiDiWebSocketEndpoint { WebDriverBiDiWebSocketEndpointCorrelationError, > { if !is_canonical_session_uuid(expected_session_id) { - return Err( - WebDriverBiDiWebSocketEndpointCorrelationError::InvalidExpectedSessionId, - ); + return Err(WebDriverBiDiWebSocketEndpointCorrelationError::InvalidExpectedSessionId); } if self.session_id != expected_session_id { return Err(WebDriverBiDiWebSocketEndpointCorrelationError::SessionIdMismatch); From 4047cef971bfdc43e352fca7b8d98c3efee31f41 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 23:29:43 -0700 Subject: [PATCH 15/32] docs(changelog): record BiDi session correlation --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f87011269..3be520bcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Typed WebDriver BiDi response-envelope correlation that preserves success/error classification, rejects success responses with absent ids, treats nullable error ids as explicitly uncorrelatable, and prevents correlated error envelopes from becoming success correlation evidence without raw JSON parsing or browser/Agent authority grants. - Bounded raw WebDriver BiDi response-document admission before JSON parsing, with a 65,536-byte product safety budget, exact wire-text retention, JSON-whitespace-aware top-level object-boundary checks, and typed fail-closed errors; this coarse boundary deliberately does not claim JSON validity, response correlation, browser authenticity, or Agent authority. - One consuming bounded WebDriver BiDi response-document correlation boundary that parses the exact admitted JSON document and feeds only its typed response kind and protocol-range id into the existing exact command-correlation gate, preserving nested parser/correlation error sources without authenticating transport or granting browser/Agent authority. +- Consuming WebDriver BiDi WebSocket endpoint/session correlation that validates one caller-supplied canonical session UUID and rejects exact session mismatches before later transport use; the correlated type preserves only bounded endpoint metadata and does not authenticate Chromium, ChromeDriver, the caller, or the socket peer. - Fail-closed WebDriver BiDi `script.NodeRemoteValue` admission that requires the exact remote type `node` and a non-empty `sharedId` within the same UTF-8 identifier budget as browser session and context identifiers, rejecting control and whitespace so an untrusted `locateNodes` item cannot be retained as a later typed-input handle without a usable shared node identity. - Same-call `locateNodes` result admission that revalidates the exact current session, browsing context, canonical origin, and document epoch, rejects an over-budget or non-node result, and translates each admitted `sharedId` through the authority registry into an `ObservedNodeHandle` without performing browser I/O. - Same-call QueryNodes admission that transfers a non-cloneable SemanticObservation protocol-use proof by ownership into `bind_current_nodes` before an untrusted `locateNodes` result can become current `ObservedNodeHandle` values, so Navigation-only or TypedInput-only proofs cannot mint observation handles. From 48ad304eb4c2a18848d5fc4fbf6e2969f5576bfb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 00:10:04 -0700 Subject: [PATCH 16/32] test(core): require explicit BiDi connect target --- ...webdriver_bidi_websocket_connect_target.rs | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 crates/originweave-core/tests/webdriver_bidi_websocket_connect_target.rs diff --git a/crates/originweave-core/tests/webdriver_bidi_websocket_connect_target.rs b/crates/originweave-core/tests/webdriver_bidi_websocket_connect_target.rs new file mode 100644 index 000000000..b71957f9d --- /dev/null +++ b/crates/originweave-core/tests/webdriver_bidi_websocket_connect_target.rs @@ -0,0 +1,69 @@ +use std::{error::Error, net::SocketAddr}; + +use originweave_core::{ + CorrelatedWebDriverBiDiWebSocketEndpoint, WebDriverBiDiWebSocketConnectTargetError, + WebDriverBiDiWebSocketEndpoint, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; + +fn correlated(endpoint: &str) -> CorrelatedWebDriverBiDiWebSocketEndpoint { + let admitted = WebDriverBiDiWebSocketEndpoint::new(endpoint); + assert!(admitted.is_ok(), "{admitted:?}"); + let Ok(admitted) = admitted else { + unreachable!("asserted valid endpoint") + }; + let correlated = admitted.correlate_session_id(SESSION_ID); + assert!(correlated.is_ok(), "{correlated:?}"); + let Ok(correlated) = correlated else { + unreachable!("asserted correlated endpoint") + }; + correlated +} + +#[test] +fn explicit_ipv4_loopback_becomes_exact_no_dns_connect_target() { + let endpoint = format!("ws://127.0.0.1:9515/session/{SESSION_ID}"); + let result = correlated(&endpoint).into_explicit_connect_target(); + assert!(result.is_ok(), "{result:?}"); + let Ok(target) = result else { + return; + }; + + assert_eq!(target.socket_addr(), SocketAddr::from(([127, 0, 0, 1], 9515))); + assert!(!target.requires_tls()); + assert_eq!(target.session_id(), SESSION_ID); +} + +#[test] +fn explicit_ipv6_loopback_preserves_exact_destination_and_tls_requirement() { + let endpoint = format!("wss://[::1]:9443/session/{SESSION_ID}"); + let result = correlated(&endpoint).into_explicit_connect_target(); + assert!(result.is_ok(), "{result:?}"); + let Ok(target) = result else { + return; + }; + + assert_eq!(target.socket_addr(), SocketAddr::from(([0, 0, 0, 0, 0, 0, 0, 1], 9443))); + assert!(target.requires_tls()); + assert_eq!(target.session_id(), SESSION_ID); +} + +#[test] +fn localhost_name_never_silently_inherits_ambient_dns_authority() { + let endpoint = format!("ws://localhost:9515/session/{SESSION_ID}"); + assert!(matches!( + correlated(&endpoint).into_explicit_connect_target(), + Err(WebDriverBiDiWebSocketConnectTargetError::NameResolutionRequired) + )); +} + +#[test] +fn connect_target_errors_are_deterministic_and_source_free() { + let error = WebDriverBiDiWebSocketConnectTargetError::NameResolutionRequired; + assert_eq!( + error.to_string(), + "WebDriver BiDi WebSocket endpoint requires explicit trusted name resolution" + ); + assert!(error.source().is_none()); +} From 3cc67111a08c5f5ae1c748bab795273a6cad4c89 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 00:16:43 -0700 Subject: [PATCH 17/32] test(core): format explicit BiDi connect target regressions --- .../tests/webdriver_bidi_websocket_connect_target.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/originweave-core/tests/webdriver_bidi_websocket_connect_target.rs b/crates/originweave-core/tests/webdriver_bidi_websocket_connect_target.rs index b71957f9d..d068b3d5a 100644 --- a/crates/originweave-core/tests/webdriver_bidi_websocket_connect_target.rs +++ b/crates/originweave-core/tests/webdriver_bidi_websocket_connect_target.rs @@ -30,7 +30,10 @@ fn explicit_ipv4_loopback_becomes_exact_no_dns_connect_target() { return; }; - assert_eq!(target.socket_addr(), SocketAddr::from(([127, 0, 0, 1], 9515))); + assert_eq!( + target.socket_addr(), + SocketAddr::from(([127, 0, 0, 1], 9515)) + ); assert!(!target.requires_tls()); assert_eq!(target.session_id(), SESSION_ID); } @@ -44,7 +47,10 @@ fn explicit_ipv6_loopback_preserves_exact_destination_and_tls_requirement() { return; }; - assert_eq!(target.socket_addr(), SocketAddr::from(([0, 0, 0, 0, 0, 0, 0, 1], 9443))); + assert_eq!( + target.socket_addr(), + SocketAddr::from(([0, 0, 0, 0, 0, 0, 0, 1], 9443)) + ); assert!(target.requires_tls()); assert_eq!(target.session_id(), SESSION_ID); } From 2e9898dea986a54f5c4cbd0adfaf8b96adae8ded Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 00:17:50 -0700 Subject: [PATCH 18/32] feat(core): derive explicit BiDi loopback connect targets --- ...webdriver_bidi_websocket_connect_target.rs | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 crates/originweave-core/src/webdriver_bidi_websocket_connect_target.rs diff --git a/crates/originweave-core/src/webdriver_bidi_websocket_connect_target.rs b/crates/originweave-core/src/webdriver_bidi_websocket_connect_target.rs new file mode 100644 index 000000000..de4bf5a69 --- /dev/null +++ b/crates/originweave-core/src/webdriver_bidi_websocket_connect_target.rs @@ -0,0 +1,89 @@ +//! Explicit no-DNS connection targets for correlated WebDriver BiDi endpoints. +//! +//! This boundary converts only literal loopback listener identities into exact socket metadata. +//! It deliberately refuses `localhost` so a later connector cannot silently inherit ambient DNS +//! authority from an admitted WebDriver endpoint. The resulting value does not open a socket, +//! authenticate a peer, negotiate TLS, perform a WebSocket handshake, or grant Agent authority. + +use std::{fmt, net::{Ipv4Addr, Ipv6Addr, SocketAddr}}; + +use crate::CorrelatedWebDriverBiDiWebSocketEndpoint; + +/// An exact loopback socket destination derived from one correlated WebDriver BiDi endpoint. +/// +/// The destination is inert connection metadata. It proves only that the already-admitted endpoint +/// named a literal loopback IP address, retained an explicit nonzero port, and was correlated to the +/// expected WebDriver session id. A runtime connector must independently enforce peer identity, +/// TLS, WebSocket, process, policy, and browser authority before using transport I/O. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WebDriverBiDiWebSocketConnectTarget { + socket_addr: SocketAddr, + requires_tls: bool, + session_id: String, +} + +impl WebDriverBiDiWebSocketConnectTarget { + /// Return the exact loopback socket destination without performing name resolution. + #[must_use] + pub const fn socket_addr(&self) -> SocketAddr { + self.socket_addr + } + + /// Return whether the admitted endpoint requires a TLS-protected WebSocket transport. + #[must_use] + pub const fn requires_tls(&self) -> bool { + self.requires_tls + } + + /// Return the exact WebDriver session id established by the preceding correlation boundary. + #[must_use] + pub fn session_id(&self) -> &str { + &self.session_id + } +} + +impl CorrelatedWebDriverBiDiWebSocketEndpoint { + /// Consume this correlated endpoint and derive one exact no-DNS loopback socket destination. + /// + /// Literal IPv4 and IPv6 loopback hosts become an exact [`SocketAddr`]. Any admitted host that + /// is not an IP literal—including `localhost`—fails closed so the caller must perform an + /// explicit, separately trusted name-resolution step rather than inheriting ambient resolver + /// authority. This method performs no DNS lookup, socket I/O, peer authentication, TLS, or + /// WebSocket handshake. + pub fn into_explicit_connect_target( + self, + ) -> Result { + let socket_addr = if let Ok(ipv4) = self.host().parse::() { + SocketAddr::from((ipv4, self.port())) + } else if let Ok(ipv6) = self.host().parse::() { + SocketAddr::from((ipv6, self.port())) + } else { + return Err(WebDriverBiDiWebSocketConnectTargetError::NameResolutionRequired); + }; + + Ok(WebDriverBiDiWebSocketConnectTarget { + socket_addr, + requires_tls: self.is_secure(), + session_id: self.session_id().to_owned(), + }) + } +} + +/// Fail-closed errors while deriving an explicit WebDriver BiDi socket destination. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum WebDriverBiDiWebSocketConnectTargetError { + /// The admitted endpoint used a host name and therefore requires explicit trusted resolution. + NameResolutionRequired, +} + +impl fmt::Display for WebDriverBiDiWebSocketConnectTargetError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::NameResolutionRequired => formatter.write_str( + "WebDriver BiDi WebSocket endpoint requires explicit trusted name resolution", + ), + } + } +} + +impl std::error::Error for WebDriverBiDiWebSocketConnectTargetError {} From aace3a8587ed7d295ff6f5dd240c3d0995f3a10f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 00:18:15 -0700 Subject: [PATCH 19/32] feat(core): export explicit BiDi connect target contract --- crates/originweave-core/src/lib.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index 27929c727..eaeecd064 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -37,6 +37,7 @@ mod webdriver_bidi_response_document; mod webdriver_bidi_response_document_correlation; mod webdriver_bidi_response_envelope; mod webdriver_bidi_result; +mod webdriver_bidi_websocket_connect_target; mod webdriver_bidi_websocket_endpoint; pub use browser_authority_registry::BrowserAuthorityRegistry; @@ -88,6 +89,9 @@ pub use webdriver_bidi_response_envelope::{ pub use webdriver_bidi_result::{ ValidatedWebDriverBiDiLocateNodesResult, WebDriverBiDiLocateNodesResultAdmissionError, }; +pub use webdriver_bidi_websocket_connect_target::{ + WebDriverBiDiWebSocketConnectTarget, WebDriverBiDiWebSocketConnectTargetError, +}; pub use webdriver_bidi_websocket_endpoint::{ CorrelatedWebDriverBiDiWebSocketEndpoint, MAX_WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_BYTES, WebDriverBiDiWebSocketEndpoint, WebDriverBiDiWebSocketEndpointAdmissionError, From 2e43ad65d57fd32a6f4a78c386e19b507e8d10a0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 00:18:46 -0700 Subject: [PATCH 20/32] style(core): format explicit BiDi connect target --- .../src/webdriver_bidi_websocket_connect_target.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/originweave-core/src/webdriver_bidi_websocket_connect_target.rs b/crates/originweave-core/src/webdriver_bidi_websocket_connect_target.rs index de4bf5a69..26e7998b7 100644 --- a/crates/originweave-core/src/webdriver_bidi_websocket_connect_target.rs +++ b/crates/originweave-core/src/webdriver_bidi_websocket_connect_target.rs @@ -5,7 +5,10 @@ //! authority from an admitted WebDriver endpoint. The resulting value does not open a socket, //! authenticate a peer, negotiate TLS, perform a WebSocket handshake, or grant Agent authority. -use std::{fmt, net::{Ipv4Addr, Ipv6Addr, SocketAddr}}; +use std::{ + fmt, + net::{Ipv4Addr, Ipv6Addr, SocketAddr}, +}; use crate::CorrelatedWebDriverBiDiWebSocketEndpoint; From 6a21bedff31217372f63e0ef1a2f5b343fee749d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 01:04:05 -0700 Subject: [PATCH 21/32] test(core): require BiDi connect target changelog evidence --- ...ebdriver_bidi_connect_target_governance.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 tests/test_webdriver_bidi_connect_target_governance.py diff --git a/tests/test_webdriver_bidi_connect_target_governance.py b/tests/test_webdriver_bidi_connect_target_governance.py new file mode 100644 index 000000000..b593727ff --- /dev/null +++ b/tests/test_webdriver_bidi_connect_target_governance.py @@ -0,0 +1,27 @@ +"""Governance regression for explicit WebDriver BiDi socket destinations.""" + +from pathlib import Path +import unittest + + +ROOT = Path(__file__).resolve().parents[1] +CHANGELOG = ROOT / "CHANGELOG.md" + + +class WebDriverBiDiConnectTargetGovernanceTests(unittest.TestCase): + """Keep the active no-DNS transport boundary visible in release evidence.""" + + def test_changelog_records_explicit_no_dns_connect_target_boundary(self) -> None: + """The production connect-target slice must have a truthful Unreleased record.""" + changelog = CHANGELOG.read_text(encoding="utf-8") + self.assertIn( + "Explicit no-DNS WebDriver BiDi loopback connection targets", + changelog, + ) + self.assertIn("localhost", changelog) + self.assertIn("no socket I/O", changelog) + self.assertIn("no Agent authority", changelog) + + +if __name__ == "__main__": + unittest.main() From 7e303600a03e7d24a5d6df3237cd63f280ca9a23 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 01:12:50 -0700 Subject: [PATCH 22/32] docs(core): record explicit BiDi connect target boundary --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3be520bcd..64946b209 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 +- Explicit no-DNS WebDriver BiDi loopback connection targets that derive exact IPv4/IPv6 loopback `SocketAddr` metadata from a session-correlated endpoint, reject `localhost` as requiring separately trusted name resolution, preserve the TLS requirement and exact session id, perform no socket I/O, and grant no Agent authority. - Rust workspace for independently reusable core, policy, destination, network, TLS, resource, and evidence modules. - Versioned browser-protocol adapter metadata that distinguishes WebDriver BiDi from pinned CDP, binds bounded adapter/browser revision tokens to an explicit duplicate-free capability set, normalizes capability-set identity independently of caller ordering, and exposes typed fail-closed capability requirements without granting browser, action, network, or secret authority by protocol kind alone. - Canonical OriginWeave protocol-version parsing for exact `originweave/.` syntax, with typed fail-closed rejection of malformed, ambiguous, overflowed, or noncanonical serialized generations; parsing does not negotiate compatibility or grant adapter authority. From dc5cfd8a55f1dee5677eedebb3e93585003d7b83 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 02:07:52 -0700 Subject: [PATCH 23/32] test(core): preserve BiDi endpoint across resolver handoff --- ...webdriver_bidi_websocket_connect_target.rs | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/crates/originweave-core/tests/webdriver_bidi_websocket_connect_target.rs b/crates/originweave-core/tests/webdriver_bidi_websocket_connect_target.rs index d068b3d5a..85f26f658 100644 --- a/crates/originweave-core/tests/webdriver_bidi_websocket_connect_target.rs +++ b/crates/originweave-core/tests/webdriver_bidi_websocket_connect_target.rs @@ -58,15 +58,39 @@ fn explicit_ipv6_loopback_preserves_exact_destination_and_tls_requirement() { #[test] fn localhost_name_never_silently_inherits_ambient_dns_authority() { let endpoint = format!("ws://localhost:9515/session/{SESSION_ID}"); + let result = correlated(&endpoint).into_explicit_connect_target(); assert!(matches!( - correlated(&endpoint).into_explicit_connect_target(), - Err(WebDriverBiDiWebSocketConnectTargetError::NameResolutionRequired) + &result, + Err(WebDriverBiDiWebSocketConnectTargetError::NameResolutionRequired { .. }) )); } +#[test] +fn name_resolution_failure_preserves_correlated_endpoint_for_trusted_resolver() { + let endpoint = format!("ws://localhost:9515/session/{SESSION_ID}"); + let result = correlated(&endpoint).into_explicit_connect_target(); + let Err(error) = result else { + return; + }; + + assert_eq!(error.correlated_endpoint().as_str(), endpoint); + assert_eq!(error.correlated_endpoint().session_id(), SESSION_ID); + assert!(!error.correlated_endpoint().is_secure()); + assert_eq!(error.correlated_endpoint().port(), 9515); + + let recovered = error.into_correlated_endpoint(); + assert_eq!(recovered.as_str(), endpoint); + assert_eq!(recovered.session_id(), SESSION_ID); +} + #[test] fn connect_target_errors_are_deterministic_and_source_free() { - let error = WebDriverBiDiWebSocketConnectTargetError::NameResolutionRequired; + let endpoint = format!("ws://localhost:9515/session/{SESSION_ID}"); + let result = correlated(&endpoint).into_explicit_connect_target(); + let Err(error) = result else { + return; + }; + assert_eq!( error.to_string(), "WebDriver BiDi WebSocket endpoint requires explicit trusted name resolution" From 3a9506c5b14113ff3fe86a84965876a549827759 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 02:09:36 -0700 Subject: [PATCH 24/32] fix(core): retain correlated BiDi endpoint on resolver handoff --- ...webdriver_bidi_websocket_connect_target.rs | 49 ++++++++++++++++--- 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/crates/originweave-core/src/webdriver_bidi_websocket_connect_target.rs b/crates/originweave-core/src/webdriver_bidi_websocket_connect_target.rs index 26e7998b7..085335f45 100644 --- a/crates/originweave-core/src/webdriver_bidi_websocket_connect_target.rs +++ b/crates/originweave-core/src/webdriver_bidi_websocket_connect_target.rs @@ -2,8 +2,10 @@ //! //! This boundary converts only literal loopback listener identities into exact socket metadata. //! It deliberately refuses `localhost` so a later connector cannot silently inherit ambient DNS -//! authority from an admitted WebDriver endpoint. The resulting value does not open a socket, -//! authenticate a peer, negotiate TLS, perform a WebSocket handshake, or grant Agent authority. +//! authority from an admitted WebDriver endpoint. When explicit trusted name resolution is needed, +//! the typed error preserves the correlated endpoint instead of discarding its session evidence. +//! The resulting value does not open a socket, authenticate a peer, negotiate TLS, perform a +//! WebSocket handshake, or grant Agent authority. use std::{ fmt, @@ -51,8 +53,10 @@ impl CorrelatedWebDriverBiDiWebSocketEndpoint { /// Literal IPv4 and IPv6 loopback hosts become an exact [`SocketAddr`]. Any admitted host that /// is not an IP literal—including `localhost`—fails closed so the caller must perform an /// explicit, separately trusted name-resolution step rather than inheriting ambient resolver - /// authority. This method performs no DNS lookup, socket I/O, peer authentication, TLS, or - /// WebSocket handshake. + /// authority. The name-resolution-required error retains this correlated endpoint so that + /// trusted resolver handoff does not require reconstructing or recorrelation of session evidence. + /// This method performs no DNS lookup, socket I/O, peer authentication, TLS, or WebSocket + /// handshake. pub fn into_explicit_connect_target( self, ) -> Result { @@ -61,7 +65,11 @@ impl CorrelatedWebDriverBiDiWebSocketEndpoint { } else if let Ok(ipv6) = self.host().parse::() { SocketAddr::from((ipv6, self.port())) } else { - return Err(WebDriverBiDiWebSocketConnectTargetError::NameResolutionRequired); + return Err( + WebDriverBiDiWebSocketConnectTargetError::NameResolutionRequired { + correlated_endpoint: self, + }, + ); }; Ok(WebDriverBiDiWebSocketConnectTarget { @@ -73,16 +81,41 @@ impl CorrelatedWebDriverBiDiWebSocketEndpoint { } /// Fail-closed errors while deriving an explicit WebDriver BiDi socket destination. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq)] pub enum WebDriverBiDiWebSocketConnectTargetError { /// The admitted endpoint used a host name and therefore requires explicit trusted resolution. - NameResolutionRequired, + NameResolutionRequired { + /// The still-correlated endpoint that must be handed to a separately trusted resolver. + correlated_endpoint: CorrelatedWebDriverBiDiWebSocketEndpoint, + }, +} + +impl WebDriverBiDiWebSocketConnectTargetError { + /// Borrow the correlated endpoint preserved for an explicit trusted resolver handoff. + #[must_use] + pub const fn correlated_endpoint(&self) -> &CorrelatedWebDriverBiDiWebSocketEndpoint { + match self { + Self::NameResolutionRequired { + correlated_endpoint, + } => correlated_endpoint, + } + } + + /// Recover the correlated endpoint for an explicit trusted resolver handoff. + #[must_use] + pub fn into_correlated_endpoint(self) -> CorrelatedWebDriverBiDiWebSocketEndpoint { + match self { + Self::NameResolutionRequired { + correlated_endpoint, + } => correlated_endpoint, + } + } } impl fmt::Display for WebDriverBiDiWebSocketConnectTargetError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::NameResolutionRequired => formatter.write_str( + Self::NameResolutionRequired { .. } => formatter.write_str( "WebDriver BiDi WebSocket endpoint requires explicit trusted name resolution", ), } From d34c528d5cbc06403c36feea97147b4f0cf2d262 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 03:44:39 -0700 Subject: [PATCH 25/32] test(core): require exact BiDi socket peer verification --- ...webdriver_bidi_socket_peer_verification.rs | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 crates/originweave-core/tests/webdriver_bidi_socket_peer_verification.rs diff --git a/crates/originweave-core/tests/webdriver_bidi_socket_peer_verification.rs b/crates/originweave-core/tests/webdriver_bidi_socket_peer_verification.rs new file mode 100644 index 000000000..dd20a4ef1 --- /dev/null +++ b/crates/originweave-core/tests/webdriver_bidi_socket_peer_verification.rs @@ -0,0 +1,107 @@ +use std::{error::Error, net::SocketAddr}; + +use originweave_core::{ + CorrelatedWebDriverBiDiWebSocketEndpoint, WebDriverBiDiSocketPeerVerificationError, + WebDriverBiDiWebSocketEndpoint, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; + +fn connect_target(endpoint: &str) -> originweave_core::WebDriverBiDiWebSocketConnectTarget { + let admitted = WebDriverBiDiWebSocketEndpoint::new(endpoint); + assert!(admitted.is_ok(), "{admitted:?}"); + let Ok(admitted) = admitted else { + unreachable!("asserted valid endpoint") + }; + + let correlated: Result = + admitted.correlate_session_id(SESSION_ID); + assert!(correlated.is_ok(), "{correlated:?}"); + let Ok(correlated) = correlated else { + unreachable!("asserted correlated endpoint") + }; + + let target = correlated.into_explicit_connect_target(); + assert!(target.is_ok(), "{target:?}"); + let Ok(target) = target else { + unreachable!("asserted literal loopback target") + }; + target +} + +#[test] +fn exact_connected_peer_becomes_verified_transport_metadata() { + let endpoint = format!("wss://127.0.0.1:9443/session/{SESSION_ID}"); + let target = connect_target(&endpoint); + let peer = SocketAddr::from(([127, 0, 0, 1], 9443)); + + let verified = target.verify_connected_peer(peer); + assert!(verified.is_ok(), "{verified:?}"); + let Ok(verified) = verified else { + return; + }; + + assert_eq!(verified.socket_addr(), peer); + assert!(verified.requires_tls()); + assert_eq!(verified.session_id(), SESSION_ID); +} + +#[test] +fn connected_peer_with_wrong_port_fails_closed() { + let endpoint = format!("ws://127.0.0.1:9515/session/{SESSION_ID}"); + let target = connect_target(&endpoint); + let actual = SocketAddr::from(([127, 0, 0, 1], 9516)); + + let result = target.verify_connected_peer(actual); + assert_eq!( + result, + Err(WebDriverBiDiSocketPeerVerificationError::PeerMismatch { + expected: SocketAddr::from(([127, 0, 0, 1], 9515)), + actual, + }) + ); +} + +#[test] +fn connected_peer_with_different_address_fails_closed() { + let endpoint = format!("ws://[::1]:9515/session/{SESSION_ID}"); + let target = connect_target(&endpoint); + let actual = SocketAddr::from(([127, 0, 0, 1], 9515)); + + let result = target.verify_connected_peer(actual); + assert!(matches!( + result, + Err(WebDriverBiDiSocketPeerVerificationError::PeerMismatch { .. }) + )); +} + +#[test] +fn non_loopback_observed_peer_cannot_inherit_approved_loopback_authority() { + let endpoint = format!("ws://127.0.0.1:9515/session/{SESSION_ID}"); + let target = connect_target(&endpoint); + let actual = SocketAddr::from(([192, 0, 2, 10], 9515)); + + let result = target.verify_connected_peer(actual); + assert!(matches!( + result, + Err(WebDriverBiDiSocketPeerVerificationError::PeerMismatch { .. }) + )); +} + +#[test] +fn peer_mismatch_error_is_deterministic_and_source_free() { + let endpoint = format!("ws://127.0.0.1:9515/session/{SESSION_ID}"); + let target = connect_target(&endpoint); + let actual = SocketAddr::from(([127, 0, 0, 1], 9516)); + + let result = target.verify_connected_peer(actual); + let Err(error) = result else { + return; + }; + + assert_eq!( + error.to_string(), + "connected WebDriver BiDi socket peer does not match the approved destination" + ); + assert!(error.source().is_none()); +} From d7cf226ba92f404262d4bab02c3fa622e676f459 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 03:49:13 -0700 Subject: [PATCH 26/32] feat(core): verify exact BiDi socket peer --- ...webdriver_bidi_websocket_connect_target.rs | 89 ++++++++++++++++++- 1 file changed, 85 insertions(+), 4 deletions(-) diff --git a/crates/originweave-core/src/webdriver_bidi_websocket_connect_target.rs b/crates/originweave-core/src/webdriver_bidi_websocket_connect_target.rs index 085335f45..5002731db 100644 --- a/crates/originweave-core/src/webdriver_bidi_websocket_connect_target.rs +++ b/crates/originweave-core/src/webdriver_bidi_websocket_connect_target.rs @@ -4,8 +4,9 @@ //! It deliberately refuses `localhost` so a later connector cannot silently inherit ambient DNS //! authority from an admitted WebDriver endpoint. When explicit trusted name resolution is needed, //! the typed error preserves the correlated endpoint instead of discarding its session evidence. -//! The resulting value does not open a socket, authenticate a peer, negotiate TLS, perform a -//! WebSocket handshake, or grant Agent authority. +//! A separately observed connected peer must also match the approved socket destination exactly +//! before it becomes verified transport metadata. These values do not open a socket, authenticate +//! a process, negotiate TLS, perform a WebSocket handshake, or grant Agent authority. use std::{ fmt, @@ -18,8 +19,9 @@ use crate::CorrelatedWebDriverBiDiWebSocketEndpoint; /// /// The destination is inert connection metadata. It proves only that the already-admitted endpoint /// named a literal loopback IP address, retained an explicit nonzero port, and was correlated to the -/// expected WebDriver session id. A runtime connector must independently enforce peer identity, -/// TLS, WebSocket, process, policy, and browser authority before using transport I/O. +/// expected WebDriver session id. A runtime connector must independently establish a connection and +/// verify its observed peer before treating that transport as the approved destination. TLS, +/// WebSocket, process, policy, and browser authority remain separate boundaries. #[derive(Debug, Clone, PartialEq, Eq)] pub struct WebDriverBiDiWebSocketConnectTarget { socket_addr: SocketAddr, @@ -45,8 +47,87 @@ impl WebDriverBiDiWebSocketConnectTarget { pub fn session_id(&self) -> &str { &self.session_id } + + /// Consume this approved destination and verify one observed connected socket peer exactly. + /// + /// Matching requires the complete [`SocketAddr`]—IP address and port—to equal the approved + /// no-DNS destination. A mismatch consumes the target and fails closed, preventing a connector + /// from accidentally reusing the same authority after observing a different peer. Success + /// produces inert verified-peer metadata only; it does not authenticate an OS process, + /// negotiate TLS, perform a WebSocket handshake, or grant browser/Agent authority. + pub fn verify_connected_peer( + self, + observed_peer: SocketAddr, + ) -> Result { + let expected = self.socket_addr; + if observed_peer != expected { + return Err(WebDriverBiDiSocketPeerVerificationError::PeerMismatch { + expected, + actual: observed_peer, + }); + } + + Ok(VerifiedWebDriverBiDiSocketPeer { + connect_target: self, + }) + } +} + +/// Inert metadata proving that a connected peer exactly matched the approved BiDi destination. +/// +/// This value carries only the destination, TLS requirement, and correlated WebDriver session id +/// already established by preceding boundaries. It does not prove process identity, TLS peer +/// identity, WebSocket protocol state, browser authenticity, policy authorization, or Agent action +/// authority. +#[derive(Debug, PartialEq, Eq)] +pub struct VerifiedWebDriverBiDiSocketPeer { + connect_target: WebDriverBiDiWebSocketConnectTarget, +} + +impl VerifiedWebDriverBiDiSocketPeer { + /// Return the exact approved and observed socket peer address. + #[must_use] + pub const fn socket_addr(&self) -> SocketAddr { + self.connect_target.socket_addr() + } + + /// Return whether the correlated endpoint still requires TLS before WebSocket use. + #[must_use] + pub const fn requires_tls(&self) -> bool { + self.connect_target.requires_tls() + } + + /// Return the exact correlated WebDriver session id. + #[must_use] + pub fn session_id(&self) -> &str { + self.connect_target.session_id() + } +} + +/// Fail-closed errors while verifying an observed BiDi socket peer. +#[derive(Debug, PartialEq, Eq)] +pub enum WebDriverBiDiSocketPeerVerificationError { + /// The connected peer differed from the exact destination approved before connection. + PeerMismatch { + /// Exact socket address that the connector was authorized to reach. + expected: SocketAddr, + /// Socket peer address observed after connection. + actual: SocketAddr, + }, } +impl fmt::Display for WebDriverBiDiSocketPeerVerificationError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::PeerMismatch { .. } => formatter.write_str( + "connected WebDriver BiDi socket peer does not match the approved destination", + ), + } + } +} + +impl std::error::Error for WebDriverBiDiSocketPeerVerificationError {} + impl CorrelatedWebDriverBiDiWebSocketEndpoint { /// Consume this correlated endpoint and derive one exact no-DNS loopback socket destination. /// From 89fc9c3d5524c2bd76ef4cb4c27398acf5146b32 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 03:49:45 -0700 Subject: [PATCH 27/32] feat(core): export verified BiDi peer contract --- crates/originweave-core/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index eaeecd064..b9d69c8b0 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -90,6 +90,7 @@ pub use webdriver_bidi_result::{ ValidatedWebDriverBiDiLocateNodesResult, WebDriverBiDiLocateNodesResultAdmissionError, }; pub use webdriver_bidi_websocket_connect_target::{ + VerifiedWebDriverBiDiSocketPeer, WebDriverBiDiSocketPeerVerificationError, WebDriverBiDiWebSocketConnectTarget, WebDriverBiDiWebSocketConnectTargetError, }; pub use webdriver_bidi_websocket_endpoint::{ From f62d20496b1d2ec4aa257ef28944b2edc2d57ff2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 04:03:34 -0700 Subject: [PATCH 28/32] test(core): require BiDi socket-peer release evidence --- tests/test_webdriver_bidi_connect_target_governance.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_webdriver_bidi_connect_target_governance.py b/tests/test_webdriver_bidi_connect_target_governance.py index b593727ff..de7949478 100644 --- a/tests/test_webdriver_bidi_connect_target_governance.py +++ b/tests/test_webdriver_bidi_connect_target_governance.py @@ -22,6 +22,14 @@ def test_changelog_records_explicit_no_dns_connect_target_boundary(self) -> None self.assertIn("no socket I/O", changelog) self.assertIn("no Agent authority", changelog) + def test_changelog_records_exact_connected_peer_verification_boundary(self) -> None: + """Verified socket-peer metadata must be visible without overstating transport trust.""" + changelog = CHANGELOG.read_text(encoding="utf-8") + self.assertIn("Exact WebDriver BiDi socket-peer verification", changelog) + self.assertIn("IP address and port", changelog) + self.assertIn("does not authenticate an OS process", changelog) + self.assertIn("does not negotiate TLS", changelog) + if __name__ == "__main__": unittest.main() From d52cdfefb8336df4edf4fb49d57ca99e1a6947e5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 04:06:09 -0700 Subject: [PATCH 29/32] docs(changelog): record BiDi socket-peer verification --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64946b209..b8a348bce 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 +- Exact WebDriver BiDi socket-peer verification that consumes an approved no-DNS connect target, requires the observed IP address and port to match exactly, preserves the TLS requirement and exact correlated session id, and remains inert metadata that does not authenticate an OS process, does not negotiate TLS, perform a WebSocket handshake, or grant Agent authority. - Explicit no-DNS WebDriver BiDi loopback connection targets that derive exact IPv4/IPv6 loopback `SocketAddr` metadata from a session-correlated endpoint, reject `localhost` as requiring separately trusted name resolution, preserve the TLS requirement and exact session id, perform no socket I/O, and grant no Agent authority. - Rust workspace for independently reusable core, policy, destination, network, TLS, resource, and evidence modules. - Versioned browser-protocol adapter metadata that distinguishes WebDriver BiDi from pinned CDP, binds bounded adapter/browser revision tokens to an explicit duplicate-free capability set, normalizes capability-set identity independently of caller ordering, and exposes typed fail-closed capability requirements without granting browser, action, network, or secret authority by protocol kind alone. @@ -77,7 +78,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - `localhost` may approve only loopback addresses, while literal IPv4 and IPv6 origins may approve only the exact canonical address encoded in the origin. - Resolver answers must remain a non-empty subset of the origin-bound approved address set; any newly introduced address fails closed as a possible DNS-rebinding event. - Every redirect rechecks target-origin authority, target-bound resolution, HTTPS downgrade, complete-target cycle state, and hop capacity before policy state changes. -- Direct TCP plans reject port zero, zero or excessive timeouts, excessive attempts, unapproved IPs, non-canonical IPv4-mapped IPv6 sockets, and IPv6 flow or scope metadata not represented in destination authority before connection I/O. +- Direct TCP plans reject port zero, zero or excessive timeouts, excessive attempts, excessive attempts, unapproved IPs, non-canonical IPv4-mapped IPv6 sockets, and IPv6 flow or scope metadata not represented in destination authority before connection I/O. - Direct connection code accepts only an explicit `SocketAddr`, never a hostname, and does not read proxy environment variables. - Established streams are discarded when peer inspection fails or the observed remote IP or port differs from the approved socket. - TLS accepts only an already verified direct stream, never a hostname or new socket, and requires the TLS origin to match the transport-authority origin exactly. From fa510a0e180b08f38f0e92920b16b874815504c4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 04:08:26 -0700 Subject: [PATCH 30/32] fix(changelog): preserve direct TCP release wording --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8a348bce..7ca383831 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -78,7 +78,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - `localhost` may approve only loopback addresses, while literal IPv4 and IPv6 origins may approve only the exact canonical address encoded in the origin. - Resolver answers must remain a non-empty subset of the origin-bound approved address set; any newly introduced address fails closed as a possible DNS-rebinding event. - Every redirect rechecks target-origin authority, target-bound resolution, HTTPS downgrade, complete-target cycle state, and hop capacity before policy state changes. -- Direct TCP plans reject port zero, zero or excessive timeouts, excessive attempts, excessive attempts, unapproved IPs, non-canonical IPv4-mapped IPv6 sockets, and IPv6 flow or scope metadata not represented in destination authority before connection I/O. +- Direct TCP plans reject port zero, zero or excessive timeouts, excessive attempts, unapproved IPs, non-canonical IPv4-mapped IPv6 sockets, and IPv6 flow or scope metadata not represented in destination authority before connection I/O. - Direct connection code accepts only an explicit `SocketAddr`, never a hostname, and does not read proxy environment variables. - Established streams are discarded when peer inspection fails or the observed remote IP or port differs from the approved socket. - TLS accepts only an already verified direct stream, never a hostname or new socket, and requires the TLS origin to match the transport-authority origin exactly. From 2495b5a6db3643d2fb030aec410fdedae0b950b8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 07:41:27 -0700 Subject: [PATCH 31/32] test(core): cover ChromeDriver session id compatibility --- .../tests/webdriver_bidi_websocket_endpoint.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs b/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs index ecf5df6b6..1e38f8ec4 100644 --- a/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs +++ b/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs @@ -6,6 +6,7 @@ use originweave_core::{ }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; +const CHROMEDRIVER_SESSION_ID: &str = "0123456789abcdef0123456789abcdef"; #[test] fn canonical_loopback_session_endpoints_are_admitted_without_granting_authority() { @@ -43,6 +44,18 @@ fn canonical_loopback_session_endpoints_are_admitted_without_granting_authority( assert_eq!(ipv6.port(), 9222); } +#[test] +fn chromedriver_generated_session_identifier_is_admitted_for_real_chromium_fixture() { + let endpoint = WebDriverBiDiWebSocketEndpoint::new(&format!( + "ws://127.0.0.1:9515/session/{CHROMEDRIVER_SESSION_ID}" + )); + assert!(endpoint.is_ok(), "{endpoint:?}"); + let Ok(endpoint) = endpoint else { + return; + }; + assert_eq!(endpoint.session_id(), CHROMEDRIVER_SESSION_ID); +} + #[test] fn remote_or_ambiguous_authorities_fail_closed() { for endpoint in [ From baf917c07cdf7187c66a3bda721970e1bc0b3657 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 07:44:51 -0700 Subject: [PATCH 32/32] fix(core): accept canonical ChromeDriver session ids --- .../src/webdriver_bidi_websocket_endpoint.rs | 39 +++++++++++-------- .../webdriver_bidi_websocket_endpoint.rs | 4 ++ 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs b/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs index 3a555493b..68ba9b061 100644 --- a/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs +++ b/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs @@ -115,7 +115,7 @@ impl WebDriverBiDiWebSocketEndpoint { if session_id.is_empty() || session_id.contains('/') { return Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidSessionResource); } - if !is_canonical_session_uuid(session_id) { + if !is_canonical_session_id(session_id) { return Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidSessionId); } @@ -152,29 +152,36 @@ impl WebDriverBiDiWebSocketEndpoint { self.port } - /// Return the canonical lower-case UUID session identifier. + /// Return the exact canonical session identifier admitted from the WebDriver endpoint. #[must_use] pub fn session_id(&self) -> &str { &self.session_id } } -fn is_canonical_session_uuid(value: &str) -> bool { +fn is_lowercase_hex(byte: u8) -> bool { + byte.is_ascii_digit() || matches!(byte, b'a'..=b'f') +} + +fn is_canonical_session_id(value: &str) -> bool { let bytes = value.as_bytes(); - if bytes.len() != 36 { - return false; - } - for (index, byte) in bytes.iter().copied().enumerate() { - let valid = if matches!(index, 8 | 13 | 18 | 23) { - byte == b'-' - } else { - byte.is_ascii_digit() || matches!(byte, b'a'..=b'f') - }; - if !valid { - return false; + match bytes.len() { + 32 => bytes.iter().copied().all(is_lowercase_hex), + 36 => { + for (index, byte) in bytes.iter().copied().enumerate() { + let valid = if matches!(index, 8 | 13 | 18 | 23) { + byte == b'-' + } else { + is_lowercase_hex(byte) + }; + if !valid { + return false; + } + } + true } + _ => false, } - true } /// Fail-closed admission errors for WebDriver BiDi WebSocket endpoint metadata. @@ -198,7 +205,7 @@ pub enum WebDriverBiDiWebSocketEndpointAdmissionError { InvalidPort, /// The path is not exactly one `/session/` resource. InvalidSessionResource, - /// The session id is not one canonical lower-case UUID representation. + /// The session id is not an admitted canonical W3C/ChromeDriver representation. InvalidSessionId, } diff --git a/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs b/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs index 1e38f8ec4..9440dff76 100644 --- a/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs +++ b/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs @@ -132,6 +132,10 @@ fn port_and_session_resource_are_canonical_and_bounded() { "0123456789ab-cdef-0123-456789abcdef", "01234567-89ab-cdef-0123-456789abcdeg", "01234567_89ab-cdef-0123-456789abcdef", + "0123456789abcdef0123456789abcdeF", + "0123456789abcdef0123456789abcdeg", + "0123456789abcdef0123456789abcde_", + "0123456789abcdef0123456789abcde", ] { assert!(matches!( WebDriverBiDiWebSocketEndpoint::new(&format!(