From 3211b013c9555d58a957223189e754c537e9bb54 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 19:22:43 -0700 Subject: [PATCH 01/17] test(network): require bounded BiDi command correlation --- .../webdriver_bidi_command_correlation.rs | 169 ++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_command_correlation.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_command_correlation.rs b/crates/originweave-network/tests/webdriver_bidi_command_correlation.rs new file mode 100644 index 000000000..7867e3e6b --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_command_correlation.rs @@ -0,0 +1,169 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::WebDriverBiDiWebSocketEndpoint; +use originweave_network::{ + MAX_WEBDRIVER_BIDI_JS_UINT, MAX_WEBDRIVER_BIDI_OUTSTANDING_COMMANDS, + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, + WebDriverBiDiCorrelatedResponseOutcome, WebDriverBiDiJsonEnvelope, + WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMessageAssembler, + WebDriverBiDiWebSocketMessageAssembly, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; +const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; +const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; + +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 parse_over_loopback(document: &'static [u8]) -> Result> { + if document.len() > 125 { + return Err(io::Error::other("test JSON document exceeded one-byte frame length").into()); + } + 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)?; + stream.write_all(&[0x81, document.len() as u8])?; + stream.write_all(document) + }); + + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? + .correlate_session_id(SESSION_ID)? + .into_explicit_connect_target()?; + let connection = + WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; + let key = WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?; + let established = WebDriverBiDiWebSocketHandshakePlan::new(connection, key)? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?; + let (_established, frame) = established.read_frame(Duration::from_millis(500))?; + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); + let text = match assembler.push_frame(frame)? { + WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + other => { + return Err(io::Error::other(format!( + "validated text frame produced unexpected assembly state: {other:?}" + )) + .into()); + } + }; + let envelope = WebDriverBiDiJsonEnvelope::parse(&text)?; + server + .join() + .map_err(|_| io::Error::other("command-correlation test server panicked"))??; + Ok(envelope) +} + +#[test] +fn responses_correlate_out_of_order_and_ids_can_be_reused_after_completion() -> Result<(), Box> { + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + correlation.register_command(7)?; + correlation.register_command(8)?; + assert_eq!(correlation.outstanding_count(), 2); + + let success = parse_over_loopback(br#"{"type":"success","id":8,"result":{}}"#)?; + let completed = correlation.correlate_response(&success)?; + assert_eq!(completed.command_id(), 8); + assert_eq!(completed.outcome(), WebDriverBiDiCorrelatedResponseOutcome::Success); + assert_eq!(correlation.outstanding_count(), 1); + + let error = parse_over_loopback( + br#"{"type":"error","id":7,"error":"invalid argument","message":"redacted by parser"}"#, + )?; + let completed = correlation.correlate_response(&error)?; + assert_eq!(completed.command_id(), 7); + assert_eq!(completed.outcome(), WebDriverBiDiCorrelatedResponseOutcome::Error); + assert_eq!(correlation.outstanding_count(), 0); + + correlation.register_command(8)?; + assert_eq!(correlation.outstanding_count(), 1); + Ok(()) +} + +#[test] +fn correlation_fails_closed_without_consuming_unrelated_outstanding_commands() -> Result<(), Box> { + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + correlation.register_command(7)?; + + let unknown = parse_over_loopback(br#"{"type":"success","id":8,"result":{}}"#)?; + assert_eq!( + correlation.correlate_response(&unknown), + Err(WebDriverBiDiCommandCorrelationError::CommandNotOutstanding) + ); + assert_eq!(correlation.outstanding_count(), 1); + + let event = parse_over_loopback(br#"{"type":"event","method":"log.entryAdded","params":{}}"#)?; + assert_eq!( + correlation.correlate_response(&event), + Err(WebDriverBiDiCommandCorrelationError::EventIsNotResponse) + ); + assert_eq!(correlation.outstanding_count(), 1); + + let uncorrelatable = parse_over_loopback( + br#"{"type":"error","id":null,"error":"invalid argument","message":"no command id"}"#, + )?; + assert_eq!( + correlation.correlate_response(&uncorrelatable), + Err(WebDriverBiDiCommandCorrelationError::UncorrelatableErrorResponse) + ); + assert_eq!(correlation.outstanding_count(), 1); + Ok(()) +} + +#[test] +fn outstanding_command_budget_and_retirement_are_bounded() -> Result<(), Box> { + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + assert_eq!( + correlation.register_command(MAX_WEBDRIVER_BIDI_JS_UINT + 1), + Err(WebDriverBiDiCommandCorrelationError::CommandIdOutOfRange) + ); + correlation.register_command(1)?; + assert_eq!( + correlation.register_command(1), + Err(WebDriverBiDiCommandCorrelationError::CommandAlreadyOutstanding) + ); + correlation.retire_command(1)?; + assert_eq!(correlation.outstanding_count(), 0); + assert_eq!( + correlation.retire_command(1), + Err(WebDriverBiDiCommandCorrelationError::CommandNotOutstanding) + ); + + for command_id in 0..MAX_WEBDRIVER_BIDI_OUTSTANDING_COMMANDS as u64 { + correlation.register_command(command_id)?; + } + assert_eq!(correlation.outstanding_count(), MAX_WEBDRIVER_BIDI_OUTSTANDING_COMMANDS); + assert_eq!( + correlation.register_command(MAX_WEBDRIVER_BIDI_OUTSTANDING_COMMANDS as u64), + Err(WebDriverBiDiCommandCorrelationError::OutstandingCommandLimit) + ); + correlation.retire_command(0)?; + correlation.register_command(MAX_WEBDRIVER_BIDI_OUTSTANDING_COMMANDS as u64)?; + assert_eq!(correlation.outstanding_count(), MAX_WEBDRIVER_BIDI_OUTSTANDING_COMMANDS); + Ok(()) +} From ceacd54f1900aa2c1b5c549292dda8219dc58c8b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 19:24:31 -0700 Subject: [PATCH 02/17] test(network): canonicalize BiDi correlation regression --- .../webdriver_bidi_command_correlation.rs | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_command_correlation.rs b/crates/originweave-network/tests/webdriver_bidi_command_correlation.rs index 7867e3e6b..cc9dfce48 100644 --- a/crates/originweave-network/tests/webdriver_bidi_command_correlation.rs +++ b/crates/originweave-network/tests/webdriver_bidi_command_correlation.rs @@ -37,7 +37,9 @@ fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { Ok(()) } -fn parse_over_loopback(document: &'static [u8]) -> Result> { +fn parse_over_loopback( + document: &'static [u8], +) -> Result> { if document.len() > 125 { return Err(io::Error::other("test JSON document exceeded one-byte frame length").into()); } @@ -80,7 +82,8 @@ fn parse_over_loopback(document: &'static [u8]) -> Result Result<(), Box> { +fn responses_correlate_out_of_order_and_ids_can_be_reused_after_completion() +-> Result<(), Box> { let mut correlation = WebDriverBiDiCommandCorrelation::new(); correlation.register_command(7)?; correlation.register_command(8)?; @@ -89,7 +92,10 @@ fn responses_correlate_out_of_order_and_ids_can_be_reused_after_completion() -> let success = parse_over_loopback(br#"{"type":"success","id":8,"result":{}}"#)?; let completed = correlation.correlate_response(&success)?; assert_eq!(completed.command_id(), 8); - assert_eq!(completed.outcome(), WebDriverBiDiCorrelatedResponseOutcome::Success); + assert_eq!( + completed.outcome(), + WebDriverBiDiCorrelatedResponseOutcome::Success + ); assert_eq!(correlation.outstanding_count(), 1); let error = parse_over_loopback( @@ -97,7 +103,10 @@ fn responses_correlate_out_of_order_and_ids_can_be_reused_after_completion() -> )?; let completed = correlation.correlate_response(&error)?; assert_eq!(completed.command_id(), 7); - assert_eq!(completed.outcome(), WebDriverBiDiCorrelatedResponseOutcome::Error); + assert_eq!( + completed.outcome(), + WebDriverBiDiCorrelatedResponseOutcome::Error + ); assert_eq!(correlation.outstanding_count(), 0); correlation.register_command(8)?; @@ -106,7 +115,8 @@ fn responses_correlate_out_of_order_and_ids_can_be_reused_after_completion() -> } #[test] -fn correlation_fails_closed_without_consuming_unrelated_outstanding_commands() -> Result<(), Box> { +fn correlation_fails_closed_without_consuming_unrelated_outstanding_commands() +-> Result<(), Box> { let mut correlation = WebDriverBiDiCommandCorrelation::new(); correlation.register_command(7)?; @@ -157,13 +167,19 @@ fn outstanding_command_budget_and_retirement_are_bounded() -> Result<(), Box Date: Sat, 29 Aug 2026 19:33:28 -0700 Subject: [PATCH 03/17] feat(network): correlate bounded BiDi responses --- crates/originweave-network/src/lib.rs | 13 +- .../src/webdriver_bidi_command_correlation.rs | 222 ++++++++++++++++++ 2 files changed, 232 insertions(+), 3 deletions(-) create mode 100644 crates/originweave-network/src/webdriver_bidi_command_correlation.rs diff --git a/crates/originweave-network/src/lib.rs b/crates/originweave-network/src/lib.rs index 79dd97492..5e077b3a8 100644 --- a/crates/originweave-network/src/lib.rs +++ b/crates/originweave-network/src/lib.rs @@ -6,14 +6,16 @@ //! It also bridges a session-correlated WebDriver BiDi loopback target from //! `originweave-core` into one bounded exact TCP connection, binds and validates //! the RFC 6455 opening exchange, provides bounded masked client writes and -//! unmasked server-frame reads, assembles bounded WebDriver BiDi text messages, and -//! classifies complete local-end JSON envelopes without exposing generic JSON bodies -//! or granting browser, TLS, policy, secret, or Agent authority. +//! unmasked server-frame reads, assembles bounded WebDriver BiDi text messages, +//! classifies complete local-end JSON envelopes, and tracks bounded command-response +//! correlation without exposing generic JSON bodies or granting browser, TLS, +//! policy, secret, or Agent authority. #![forbid(unsafe_code)] #![deny(missing_docs)] mod connection; +mod webdriver_bidi_command_correlation; mod webdriver_bidi_connection; mod webdriver_bidi_json_envelope; mod webdriver_bidi_websocket_frame; @@ -28,6 +30,11 @@ pub use connection::{ ConnectionPlan, DirectTcpConnection, MAX_CONNECT_TIMEOUT, MAX_CONNECTION_ATTEMPTS, NetworkError, SocketConnectionEvidence, }; +pub use webdriver_bidi_command_correlation::{ + MAX_WEBDRIVER_BIDI_OUTSTANDING_COMMANDS, WebDriverBiDiCommandCorrelation, + WebDriverBiDiCommandCorrelationError, WebDriverBiDiCorrelatedResponse, + WebDriverBiDiCorrelatedResponseOutcome, +}; pub use webdriver_bidi_connection::{ WebDriverBiDiTcpConnection, WebDriverBiDiTcpConnectionError, WebDriverBiDiTcpConnectionEvidence, WebDriverBiDiTcpConnectionPlan, diff --git a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs new file mode 100644 index 000000000..72f03370f --- /dev/null +++ b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs @@ -0,0 +1,222 @@ +use std::{collections::BTreeSet, error::Error, fmt}; + +use crate::{ + MAX_WEBDRIVER_BIDI_JS_UINT, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeKind, +}; + +/// Maximum number of local WebDriver BiDi commands retained as outstanding at once. +/// +/// WebDriver BiDi permits commands to complete out of order. OriginWeave therefore keeps a +/// bounded local correlation set instead of assuming response order, while this resource ceiling +/// prevents an unbounded remote-control session from growing local correlation state indefinitely. +pub const MAX_WEBDRIVER_BIDI_OUTSTANDING_COMMANDS: usize = 256; + +/// Outcome of a response after it has consumed the matching outstanding command identifier. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum WebDriverBiDiCorrelatedResponseOutcome { + /// The remote end returned a successful command response. + Success, + /// The remote end returned a protocol error for the command. + Error, +} + +/// Credential-free evidence that one parsed response consumed one outstanding local command. +/// +/// This value carries only the matched command identifier and success/error classification. It +/// does not retain result bodies, error text, browser authority, transport authority, or secrets. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct WebDriverBiDiCorrelatedResponse { + command_id: u64, + outcome: WebDriverBiDiCorrelatedResponseOutcome, +} + +impl WebDriverBiDiCorrelatedResponse { + /// Return the local command identifier consumed by this response. + #[must_use] + pub const fn command_id(&self) -> u64 { + self.command_id + } + + /// Return whether the correlated response was successful or a protocol error. + #[must_use] + pub const fn outcome(&self) -> WebDriverBiDiCorrelatedResponseOutcome { + self.outcome + } +} + +/// Fail-closed command-correlation failures at the local WebDriver BiDi response boundary. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum WebDriverBiDiCommandCorrelationError { + /// A caller attempted to register an identifier outside WebDriver BiDi's `js-uint` range. + CommandIdOutOfRange, + /// The identifier is already outstanding and cannot become ambiguous. + CommandAlreadyOutstanding, + /// The reviewed outstanding-command resource budget has been reached. + OutstandingCommandLimit, + /// No currently outstanding command matches the requested or returned identifier. + CommandNotOutstanding, + /// An event is not a command response and cannot consume correlation state. + EventIsNotResponse, + /// A protocol error with a `null` id cannot be attributed to one outstanding command. + UncorrelatableErrorResponse, +} + +impl fmt::Display for WebDriverBiDiCommandCorrelationError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + let message = match self { + Self::CommandIdOutOfRange => "WebDriver BiDi command id is outside the js-uint range", + Self::CommandAlreadyOutstanding => "WebDriver BiDi command id is already outstanding", + Self::OutstandingCommandLimit => "WebDriver BiDi outstanding-command limit reached", + Self::CommandNotOutstanding => "WebDriver BiDi command id is not outstanding", + Self::EventIsNotResponse => "WebDriver BiDi event cannot be correlated as a response", + Self::UncorrelatableErrorResponse => { + "WebDriver BiDi error response has no correlatable command id" + } + }; + formatter.write_str(message) + } +} + +impl Error for WebDriverBiDiCommandCorrelationError {} + +/// Bounded local WebDriver BiDi command-response correlation state. +/// +/// Register an id only after the caller has committed to one outbound command. A success or +/// correlatable error response consumes the id exactly once. Events and null-id errors leave all +/// outstanding state untouched. This type performs no I/O, retry, command serialization, browser +/// authentication, or authority grant. +#[derive(Debug, Default)] +pub struct WebDriverBiDiCommandCorrelation { + outstanding: BTreeSet, +} + +impl WebDriverBiDiCommandCorrelation { + /// Create empty correlation state. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Return the number of commands currently awaiting a correlatable response. + #[must_use] + pub fn outstanding_count(&self) -> usize { + self.outstanding.len() + } + + /// Register one local command id before its response can be accepted. + /// + /// Identifiers are unique only while outstanding. A completed or explicitly retired id may be + /// reused later, matching WebDriver BiDi's local-end correlation semantics. + pub fn register_command( + &mut self, + command_id: u64, + ) -> Result<(), WebDriverBiDiCommandCorrelationError> { + if command_id > MAX_WEBDRIVER_BIDI_JS_UINT { + return Err(WebDriverBiDiCommandCorrelationError::CommandIdOutOfRange); + } + if self.outstanding.contains(&command_id) { + return Err(WebDriverBiDiCommandCorrelationError::CommandAlreadyOutstanding); + } + if self.outstanding.len() >= MAX_WEBDRIVER_BIDI_OUTSTANDING_COMMANDS { + return Err(WebDriverBiDiCommandCorrelationError::OutstandingCommandLimit); + } + self.outstanding.insert(command_id); + Ok(()) + } + + /// Explicitly retire one outstanding command without accepting a response for it. + /// + /// This supports caller-owned cancellation or session teardown without retaining stale ids. + pub fn retire_command( + &mut self, + command_id: u64, + ) -> Result<(), WebDriverBiDiCommandCorrelationError> { + if self.outstanding.remove(&command_id) { + Ok(()) + } else { + Err(WebDriverBiDiCommandCorrelationError::CommandNotOutstanding) + } + } + + /// Correlate one already parsed local-end envelope with the outstanding command set. + /// + /// Successful responses and error responses with ids consume exactly one matching command. + /// Unknown ids fail without consuming unrelated state. Events and null-id errors fail before + /// touching the set. + pub fn correlate_response( + &mut self, + envelope: &WebDriverBiDiJsonEnvelope, + ) -> Result { + match envelope.kind() { + WebDriverBiDiJsonEnvelopeKind::Event => { + Err(WebDriverBiDiCommandCorrelationError::EventIsNotResponse) + } + WebDriverBiDiJsonEnvelopeKind::Error => { + let Some(command_id) = envelope.command_id() else { + return Err( + WebDriverBiDiCommandCorrelationError::UncorrelatableErrorResponse, + ); + }; + self.complete(command_id, WebDriverBiDiCorrelatedResponseOutcome::Error) + } + WebDriverBiDiJsonEnvelopeKind::Success => self.complete( + envelope + .command_id() + .unwrap_or(MAX_WEBDRIVER_BIDI_JS_UINT.saturating_add(1)), + WebDriverBiDiCorrelatedResponseOutcome::Success, + ), + } + } + + fn complete( + &mut self, + command_id: u64, + outcome: WebDriverBiDiCorrelatedResponseOutcome, + ) -> Result { + if !self.outstanding.remove(&command_id) { + return Err(WebDriverBiDiCommandCorrelationError::CommandNotOutstanding); + } + Ok(WebDriverBiDiCorrelatedResponse { + command_id, + outcome, + }) + } +} + +#[cfg(test)] +mod tests { + use super::WebDriverBiDiCommandCorrelationError; + + #[test] + fn correlation_errors_have_stable_nonempty_operator_messages() { + let cases = [ + ( + WebDriverBiDiCommandCorrelationError::CommandIdOutOfRange, + "WebDriver BiDi command id is outside the js-uint range", + ), + ( + WebDriverBiDiCommandCorrelationError::CommandAlreadyOutstanding, + "WebDriver BiDi command id is already outstanding", + ), + ( + WebDriverBiDiCommandCorrelationError::OutstandingCommandLimit, + "WebDriver BiDi outstanding-command limit reached", + ), + ( + WebDriverBiDiCommandCorrelationError::CommandNotOutstanding, + "WebDriver BiDi command id is not outstanding", + ), + ( + WebDriverBiDiCommandCorrelationError::EventIsNotResponse, + "WebDriver BiDi event cannot be correlated as a response", + ), + ( + WebDriverBiDiCommandCorrelationError::UncorrelatableErrorResponse, + "WebDriver BiDi error response has no correlatable command id", + ), + ]; + for (error, expected) in cases { + assert_eq!(error.to_string(), expected); + } + } +} From a2ee2b48a334f05a4aed1e5592f5262a9508d27e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 19:35:09 -0700 Subject: [PATCH 04/17] style(network): apply canonical BiDi correlation format --- .../src/webdriver_bidi_command_correlation.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs index 72f03370f..7961c758a 100644 --- a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs +++ b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs @@ -1,8 +1,6 @@ use std::{collections::BTreeSet, error::Error, fmt}; -use crate::{ - MAX_WEBDRIVER_BIDI_JS_UINT, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeKind, -}; +use crate::{MAX_WEBDRIVER_BIDI_JS_UINT, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeKind}; /// Maximum number of local WebDriver BiDi commands retained as outstanding at once. /// @@ -153,9 +151,7 @@ impl WebDriverBiDiCommandCorrelation { } WebDriverBiDiJsonEnvelopeKind::Error => { let Some(command_id) = envelope.command_id() else { - return Err( - WebDriverBiDiCommandCorrelationError::UncorrelatableErrorResponse, - ); + return Err(WebDriverBiDiCommandCorrelationError::UncorrelatableErrorResponse); }; self.complete(command_id, WebDriverBiDiCorrelatedResponseOutcome::Error) } From adc54f24407e12130b5fc3214a8b307d47148b93 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 20:01:07 -0700 Subject: [PATCH 05/17] test(network): require redacted BiDi correlation diagnostics --- .../tests/webdriver_bidi_command_correlation.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/crates/originweave-network/tests/webdriver_bidi_command_correlation.rs b/crates/originweave-network/tests/webdriver_bidi_command_correlation.rs index cc9dfce48..9ef3375c0 100644 --- a/crates/originweave-network/tests/webdriver_bidi_command_correlation.rs +++ b/crates/originweave-network/tests/webdriver_bidi_command_correlation.rs @@ -183,3 +183,14 @@ fn outstanding_command_budget_and_retirement_are_bounded() -> Result<(), Box Result<(), Box> { + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + correlation.register_command(123_456_789)?; + + let debug = format!("{correlation:?}"); + assert!(debug.contains("outstanding_count")); + assert!(!debug.contains("123456789")); + Ok(()) +} From 53b944bd7b48ad3a7f203173bd31c15a604fd589 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 20:03:35 -0700 Subject: [PATCH 06/17] fix(network): redact BiDi correlation debug state --- .../src/webdriver_bidi_command_correlation.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs index 7961c758a..6adc3d43d 100644 --- a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs +++ b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs @@ -82,12 +82,22 @@ impl Error for WebDriverBiDiCommandCorrelationError {} /// Register an id only after the caller has committed to one outbound command. A success or /// correlatable error response consumes the id exactly once. Events and null-id errors leave all /// outstanding state untouched. This type performs no I/O, retry, command serialization, browser -/// authentication, or authority grant. -#[derive(Debug, Default)] +/// authentication, or authority grant. Debug output reports only the outstanding-count summary; +/// command identifiers remain private correlation state. +#[derive(Default)] pub struct WebDriverBiDiCommandCorrelation { outstanding: BTreeSet, } +impl fmt::Debug for WebDriverBiDiCommandCorrelation { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("WebDriverBiDiCommandCorrelation") + .field("outstanding_count", &self.outstanding.len()) + .finish() + } +} + impl WebDriverBiDiCommandCorrelation { /// Create empty correlation state. #[must_use] From afef787758bac47e693b513cc3331db3525816bd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 21:45:29 +0900 Subject: [PATCH 07/17] test(network): reject cross-command BiDi response correlation --- ...webdriver_bidi_command_kind_correlation.rs | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_command_kind_correlation.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_command_kind_correlation.rs b/crates/originweave-network/tests/webdriver_bidi_command_kind_correlation.rs new file mode 100644 index 000000000..228485591 --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_command_kind_correlation.rs @@ -0,0 +1,103 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::WebDriverBiDiWebSocketEndpoint; +use originweave_network::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, + WebDriverBiDiCommandKind, WebDriverBiDiCorrelatedResponseOutcome, + WebDriverBiDiJsonEnvelope, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; +const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; +const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; + +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 parse_success_over_loopback() -> Result> { + const DOCUMENT: &[u8] = br#"{"type":"success","id":42,"result":{}}"#; + 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)?; + stream.write_all(&[0x81, DOCUMENT.len() as u8])?; + stream.write_all(DOCUMENT) + }); + + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? + .correlate_session_id(SESSION_ID)? + .into_explicit_connect_target()?; + let connection = + WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; + let key = WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?; + let established = WebDriverBiDiWebSocketHandshakePlan::new(connection, key)? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?; + let (_established, frame) = established.read_frame(Duration::from_millis(500))?; + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); + let text = match assembler.push_frame(frame)? { + WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + other => { + return Err(io::Error::other(format!( + "validated text frame produced unexpected assembly state: {other:?}" + )) + .into()); + } + }; + let envelope = WebDriverBiDiJsonEnvelope::parse(&text)?; + server + .join() + .map_err(|_| io::Error::other("command-kind correlation test server panicked"))??; + Ok(envelope) +} + +#[test] +fn response_cannot_consume_a_different_outstanding_command_kind() -> Result<(), Box> { + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + correlation.register_command_for(42, WebDriverBiDiCommandKind::SessionStatus)?; + let response = parse_success_over_loopback()?; + + assert_eq!( + correlation.correlate_response_for(&response, WebDriverBiDiCommandKind::SessionEnd), + Err(WebDriverBiDiCommandCorrelationError::CommandKindMismatch { + expected: WebDriverBiDiCommandKind::SessionEnd, + actual: WebDriverBiDiCommandKind::SessionStatus, + }) + ); + assert_eq!(correlation.outstanding_count(), 1); + + let completed = correlation + .correlate_response_for(&response, WebDriverBiDiCommandKind::SessionStatus)?; + assert_eq!(completed.command_id(), 42); + assert_eq!( + completed.outcome(), + WebDriverBiDiCorrelatedResponseOutcome::Success + ); + assert_eq!(correlation.outstanding_count(), 0); + Ok(()) +} From 3170911e66ce65cb47713042079301d4efbb601c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 21:49:53 +0900 Subject: [PATCH 08/17] style(network): format command-kind correlation regression --- .../tests/webdriver_bidi_command_kind_correlation.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_command_kind_correlation.rs b/crates/originweave-network/tests/webdriver_bidi_command_kind_correlation.rs index 228485591..2321aa7fe 100644 --- a/crates/originweave-network/tests/webdriver_bidi_command_kind_correlation.rs +++ b/crates/originweave-network/tests/webdriver_bidi_command_kind_correlation.rs @@ -9,10 +9,10 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use originweave_network::{ WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, - WebDriverBiDiCommandKind, WebDriverBiDiCorrelatedResponseOutcome, - WebDriverBiDiJsonEnvelope, WebDriverBiDiTcpConnectionPlan, - WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, + WebDriverBiDiCommandKind, WebDriverBiDiCorrelatedResponseOutcome, WebDriverBiDiJsonEnvelope, + WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMessageAssembler, + WebDriverBiDiWebSocketMessageAssembly, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -91,8 +91,8 @@ fn response_cannot_consume_a_different_outstanding_command_kind() -> Result<(), ); assert_eq!(correlation.outstanding_count(), 1); - let completed = correlation - .correlate_response_for(&response, WebDriverBiDiCommandKind::SessionStatus)?; + let completed = + correlation.correlate_response_for(&response, WebDriverBiDiCommandKind::SessionStatus)?; assert_eq!(completed.command_id(), 42); assert_eq!( completed.outcome(), From 974b68b60523eb804cfae7353d82643aa2a0facb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 21:54:53 +0900 Subject: [PATCH 09/17] fix(network): bind BiDi responses to command kind --- crates/originweave-network/src/lib.rs | 2 +- .../src/webdriver_bidi_command_correlation.rs | 130 +++++++++++++----- .../webdriver_bidi_command_correlation.rs | 63 ++++++--- 3 files changed, 140 insertions(+), 55 deletions(-) diff --git a/crates/originweave-network/src/lib.rs b/crates/originweave-network/src/lib.rs index 5e077b3a8..ba2f2ee88 100644 --- a/crates/originweave-network/src/lib.rs +++ b/crates/originweave-network/src/lib.rs @@ -32,7 +32,7 @@ pub use connection::{ }; pub use webdriver_bidi_command_correlation::{ MAX_WEBDRIVER_BIDI_OUTSTANDING_COMMANDS, WebDriverBiDiCommandCorrelation, - WebDriverBiDiCommandCorrelationError, WebDriverBiDiCorrelatedResponse, + WebDriverBiDiCommandCorrelationError, WebDriverBiDiCommandKind, WebDriverBiDiCorrelatedResponse, WebDriverBiDiCorrelatedResponseOutcome, }; pub use webdriver_bidi_connection::{ diff --git a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs index 6adc3d43d..4dd9bcd78 100644 --- a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs +++ b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs @@ -1,14 +1,28 @@ -use std::{collections::BTreeSet, error::Error, fmt}; +use std::{collections::BTreeMap, error::Error, fmt}; use crate::{MAX_WEBDRIVER_BIDI_JS_UINT, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeKind}; /// Maximum number of local WebDriver BiDi commands retained as outstanding at once. /// /// WebDriver BiDi permits commands to complete out of order. OriginWeave therefore keeps a -/// bounded local correlation set instead of assuming response order, while this resource ceiling +/// bounded local correlation map instead of assuming response order, while this resource ceiling /// prevents an unbounded remote-control session from growing local correlation state indefinitely. pub const MAX_WEBDRIVER_BIDI_OUTSTANDING_COMMANDS: usize = 256; +/// Exact WebDriver BiDi command family bound to one outstanding local correlation identifier. +/// +/// Command identifiers are local-end routing values rather than command-type provenance. Keeping +/// the reviewed command family beside each outstanding id prevents a success or protocol error for +/// one command from being consumed by a different typed response boundary that happens to receive +/// the same id. Additional command families are introduced by their owning typed command slices. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum WebDriverBiDiCommandKind { + /// WebDriver BiDi `session.status`. + SessionStatus, + /// WebDriver BiDi `session.end`. + SessionEnd, +} + /// Outcome of a response after it has consumed the matching outstanding command identifier. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum WebDriverBiDiCorrelatedResponseOutcome { @@ -53,6 +67,13 @@ pub enum WebDriverBiDiCommandCorrelationError { OutstandingCommandLimit, /// No currently outstanding command matches the requested or returned identifier. CommandNotOutstanding, + /// The typed consumer does not match the command family registered for this identifier. + CommandKindMismatch { + /// Command family required by the typed consumer. + expected: WebDriverBiDiCommandKind, + /// Command family actually registered for the outstanding identifier. + actual: WebDriverBiDiCommandKind, + }, /// An event is not a command response and cannot consume correlation state. EventIsNotResponse, /// A protocol error with a `null` id cannot be attributed to one outstanding command. @@ -66,6 +87,9 @@ impl fmt::Display for WebDriverBiDiCommandCorrelationError { Self::CommandAlreadyOutstanding => "WebDriver BiDi command id is already outstanding", Self::OutstandingCommandLimit => "WebDriver BiDi outstanding-command limit reached", Self::CommandNotOutstanding => "WebDriver BiDi command id is not outstanding", + Self::CommandKindMismatch { .. } => { + "WebDriver BiDi response command kind does not match the outstanding command" + } Self::EventIsNotResponse => "WebDriver BiDi event cannot be correlated as a response", Self::UncorrelatableErrorResponse => { "WebDriver BiDi error response has no correlatable command id" @@ -79,14 +103,15 @@ impl Error for WebDriverBiDiCommandCorrelationError {} /// Bounded local WebDriver BiDi command-response correlation state. /// -/// Register an id only after the caller has committed to one outbound command. A success or -/// correlatable error response consumes the id exactly once. Events and null-id errors leave all +/// Register an id together with its exact typed command family only after the caller has committed +/// to that outbound command. A success or correlatable error response consumes the id exactly once +/// only through a matching typed consumer. Events, null-id errors, and command-kind mismatches leave /// outstanding state untouched. This type performs no I/O, retry, command serialization, browser /// authentication, or authority grant. Debug output reports only the outstanding-count summary; -/// command identifiers remain private correlation state. +/// command identifiers and command families remain private correlation state. #[derive(Default)] pub struct WebDriverBiDiCommandCorrelation { - outstanding: BTreeSet, + outstanding: BTreeMap, } impl fmt::Debug for WebDriverBiDiCommandCorrelation { @@ -111,49 +136,52 @@ impl WebDriverBiDiCommandCorrelation { self.outstanding.len() } - /// Register one local command id before its response can be accepted. + /// Register one local command id and its exact command family before its response can be accepted. /// /// Identifiers are unique only while outstanding. A completed or explicitly retired id may be - /// reused later, matching WebDriver BiDi's local-end correlation semantics. - pub fn register_command( + /// reused later, matching WebDriver BiDi's local-end correlation semantics. Reusing an id while + /// any command family is still outstanding fails before replacing its provenance. + pub fn register_command_for( &mut self, command_id: u64, + command_kind: WebDriverBiDiCommandKind, ) -> Result<(), WebDriverBiDiCommandCorrelationError> { if command_id > MAX_WEBDRIVER_BIDI_JS_UINT { return Err(WebDriverBiDiCommandCorrelationError::CommandIdOutOfRange); } - if self.outstanding.contains(&command_id) { + if self.outstanding.contains_key(&command_id) { return Err(WebDriverBiDiCommandCorrelationError::CommandAlreadyOutstanding); } if self.outstanding.len() >= MAX_WEBDRIVER_BIDI_OUTSTANDING_COMMANDS { return Err(WebDriverBiDiCommandCorrelationError::OutstandingCommandLimit); } - self.outstanding.insert(command_id); + let _previous = self.outstanding.insert(command_id, command_kind); Ok(()) } - /// Explicitly retire one outstanding command without accepting a response for it. + /// Explicitly retire one exact outstanding command without accepting a response for it. /// - /// This supports caller-owned cancellation or session teardown without retaining stale ids. - pub fn retire_command( + /// The expected command family must match the registered provenance. A mismatched caller cannot + /// retire another typed command merely by knowing or reusing its local correlation identifier. + pub fn retire_command_for( &mut self, command_id: u64, + expected_kind: WebDriverBiDiCommandKind, ) -> Result<(), WebDriverBiDiCommandCorrelationError> { - if self.outstanding.remove(&command_id) { - Ok(()) - } else { - Err(WebDriverBiDiCommandCorrelationError::CommandNotOutstanding) - } + self.require_command_kind(command_id, expected_kind)?; + let _removed = self.outstanding.remove(&command_id); + Ok(()) } - /// Correlate one already parsed local-end envelope with the outstanding command set. + /// Correlate one parsed local-end envelope with an exact outstanding command family. /// /// Successful responses and error responses with ids consume exactly one matching command. - /// Unknown ids fail without consuming unrelated state. Events and null-id errors fail before - /// touching the set. - pub fn correlate_response( + /// Unknown ids and command-kind mismatches fail without consuming state. Events and null-id + /// errors fail before touching the map. + pub fn correlate_response_for( &mut self, envelope: &WebDriverBiDiJsonEnvelope, + expected_kind: WebDriverBiDiCommandKind, ) -> Result { match envelope.kind() { WebDriverBiDiJsonEnvelopeKind::Event => { @@ -163,25 +191,52 @@ impl WebDriverBiDiCommandCorrelation { let Some(command_id) = envelope.command_id() else { return Err(WebDriverBiDiCommandCorrelationError::UncorrelatableErrorResponse); }; - self.complete(command_id, WebDriverBiDiCorrelatedResponseOutcome::Error) + self.complete( + command_id, + expected_kind, + WebDriverBiDiCorrelatedResponseOutcome::Error, + ) } - WebDriverBiDiJsonEnvelopeKind::Success => self.complete( - envelope - .command_id() - .unwrap_or(MAX_WEBDRIVER_BIDI_JS_UINT.saturating_add(1)), - WebDriverBiDiCorrelatedResponseOutcome::Success, - ), + WebDriverBiDiJsonEnvelopeKind::Success => { + let Some(command_id) = envelope.command_id() else { + return Err(WebDriverBiDiCommandCorrelationError::CommandNotOutstanding); + }; + self.complete( + command_id, + expected_kind, + WebDriverBiDiCorrelatedResponseOutcome::Success, + ) + } + } + } + + fn require_command_kind( + &self, + command_id: u64, + expected_kind: WebDriverBiDiCommandKind, + ) -> Result<(), WebDriverBiDiCommandCorrelationError> { + let actual = self + .outstanding + .get(&command_id) + .copied() + .ok_or(WebDriverBiDiCommandCorrelationError::CommandNotOutstanding)?; + if actual != expected_kind { + return Err(WebDriverBiDiCommandCorrelationError::CommandKindMismatch { + expected: expected_kind, + actual, + }); } + Ok(()) } fn complete( &mut self, command_id: u64, + expected_kind: WebDriverBiDiCommandKind, outcome: WebDriverBiDiCorrelatedResponseOutcome, ) -> Result { - if !self.outstanding.remove(&command_id) { - return Err(WebDriverBiDiCommandCorrelationError::CommandNotOutstanding); - } + self.require_command_kind(command_id, expected_kind)?; + let _removed = self.outstanding.remove(&command_id); Ok(WebDriverBiDiCorrelatedResponse { command_id, outcome, @@ -191,7 +246,7 @@ impl WebDriverBiDiCommandCorrelation { #[cfg(test)] mod tests { - use super::WebDriverBiDiCommandCorrelationError; + use super::{WebDriverBiDiCommandCorrelationError, WebDriverBiDiCommandKind}; #[test] fn correlation_errors_have_stable_nonempty_operator_messages() { @@ -212,6 +267,13 @@ mod tests { WebDriverBiDiCommandCorrelationError::CommandNotOutstanding, "WebDriver BiDi command id is not outstanding", ), + ( + WebDriverBiDiCommandCorrelationError::CommandKindMismatch { + expected: WebDriverBiDiCommandKind::SessionEnd, + actual: WebDriverBiDiCommandKind::SessionStatus, + }, + "WebDriver BiDi response command kind does not match the outstanding command", + ), ( WebDriverBiDiCommandCorrelationError::EventIsNotResponse, "WebDriver BiDi event cannot be correlated as a response", diff --git a/crates/originweave-network/tests/webdriver_bidi_command_correlation.rs b/crates/originweave-network/tests/webdriver_bidi_command_correlation.rs index 9ef3375c0..f08dd0a6f 100644 --- a/crates/originweave-network/tests/webdriver_bidi_command_correlation.rs +++ b/crates/originweave-network/tests/webdriver_bidi_command_correlation.rs @@ -10,7 +10,7 @@ use originweave_core::WebDriverBiDiWebSocketEndpoint; use originweave_network::{ MAX_WEBDRIVER_BIDI_JS_UINT, MAX_WEBDRIVER_BIDI_OUTSTANDING_COMMANDS, WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, - WebDriverBiDiCorrelatedResponseOutcome, WebDriverBiDiJsonEnvelope, + WebDriverBiDiCommandKind, WebDriverBiDiCorrelatedResponseOutcome, WebDriverBiDiJsonEnvelope, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, @@ -85,12 +85,13 @@ fn parse_over_loopback( fn responses_correlate_out_of_order_and_ids_can_be_reused_after_completion() -> Result<(), Box> { let mut correlation = WebDriverBiDiCommandCorrelation::new(); - correlation.register_command(7)?; - correlation.register_command(8)?; + correlation.register_command_for(7, WebDriverBiDiCommandKind::SessionStatus)?; + correlation.register_command_for(8, WebDriverBiDiCommandKind::SessionStatus)?; assert_eq!(correlation.outstanding_count(), 2); let success = parse_over_loopback(br#"{"type":"success","id":8,"result":{}}"#)?; - let completed = correlation.correlate_response(&success)?; + let completed = + correlation.correlate_response_for(&success, WebDriverBiDiCommandKind::SessionStatus)?; assert_eq!(completed.command_id(), 8); assert_eq!( completed.outcome(), @@ -101,7 +102,8 @@ fn responses_correlate_out_of_order_and_ids_can_be_reused_after_completion() let error = parse_over_loopback( br#"{"type":"error","id":7,"error":"invalid argument","message":"redacted by parser"}"#, )?; - let completed = correlation.correlate_response(&error)?; + let completed = + correlation.correlate_response_for(&error, WebDriverBiDiCommandKind::SessionStatus)?; assert_eq!(completed.command_id(), 7); assert_eq!( completed.outcome(), @@ -109,7 +111,7 @@ fn responses_correlate_out_of_order_and_ids_can_be_reused_after_completion() ); assert_eq!(correlation.outstanding_count(), 0); - correlation.register_command(8)?; + correlation.register_command_for(8, WebDriverBiDiCommandKind::SessionStatus)?; assert_eq!(correlation.outstanding_count(), 1); Ok(()) } @@ -118,18 +120,18 @@ fn responses_correlate_out_of_order_and_ids_can_be_reused_after_completion() fn correlation_fails_closed_without_consuming_unrelated_outstanding_commands() -> Result<(), Box> { let mut correlation = WebDriverBiDiCommandCorrelation::new(); - correlation.register_command(7)?; + correlation.register_command_for(7, WebDriverBiDiCommandKind::SessionStatus)?; let unknown = parse_over_loopback(br#"{"type":"success","id":8,"result":{}}"#)?; assert_eq!( - correlation.correlate_response(&unknown), + correlation.correlate_response_for(&unknown, WebDriverBiDiCommandKind::SessionStatus), Err(WebDriverBiDiCommandCorrelationError::CommandNotOutstanding) ); assert_eq!(correlation.outstanding_count(), 1); let event = parse_over_loopback(br#"{"type":"event","method":"log.entryAdded","params":{}}"#)?; assert_eq!( - correlation.correlate_response(&event), + correlation.correlate_response_for(&event, WebDriverBiDiCommandKind::SessionStatus), Err(WebDriverBiDiCommandCorrelationError::EventIsNotResponse) ); assert_eq!(correlation.outstanding_count(), 1); @@ -138,7 +140,10 @@ fn correlation_fails_closed_without_consuming_unrelated_outstanding_commands() br#"{"type":"error","id":null,"error":"invalid argument","message":"no command id"}"#, )?; assert_eq!( - correlation.correlate_response(&uncorrelatable), + correlation.correlate_response_for( + &uncorrelatable, + WebDriverBiDiCommandKind::SessionStatus, + ), Err(WebDriverBiDiCommandCorrelationError::UncorrelatableErrorResponse) ); assert_eq!(correlation.outstanding_count(), 1); @@ -149,34 +154,51 @@ fn correlation_fails_closed_without_consuming_unrelated_outstanding_commands() fn outstanding_command_budget_and_retirement_are_bounded() -> Result<(), Box> { let mut correlation = WebDriverBiDiCommandCorrelation::new(); assert_eq!( - correlation.register_command(MAX_WEBDRIVER_BIDI_JS_UINT + 1), + correlation.register_command_for( + MAX_WEBDRIVER_BIDI_JS_UINT + 1, + WebDriverBiDiCommandKind::SessionStatus, + ), Err(WebDriverBiDiCommandCorrelationError::CommandIdOutOfRange) ); - correlation.register_command(1)?; + correlation.register_command_for(1, WebDriverBiDiCommandKind::SessionStatus)?; assert_eq!( - correlation.register_command(1), + correlation.register_command_for(1, WebDriverBiDiCommandKind::SessionEnd), Err(WebDriverBiDiCommandCorrelationError::CommandAlreadyOutstanding) ); - correlation.retire_command(1)?; + assert_eq!( + correlation.retire_command_for(1, WebDriverBiDiCommandKind::SessionEnd), + Err(WebDriverBiDiCommandCorrelationError::CommandKindMismatch { + expected: WebDriverBiDiCommandKind::SessionEnd, + actual: WebDriverBiDiCommandKind::SessionStatus, + }) + ); + assert_eq!(correlation.outstanding_count(), 1); + correlation.retire_command_for(1, WebDriverBiDiCommandKind::SessionStatus)?; assert_eq!(correlation.outstanding_count(), 0); assert_eq!( - correlation.retire_command(1), + correlation.retire_command_for(1, WebDriverBiDiCommandKind::SessionStatus), Err(WebDriverBiDiCommandCorrelationError::CommandNotOutstanding) ); for command_id in 0..MAX_WEBDRIVER_BIDI_OUTSTANDING_COMMANDS as u64 { - correlation.register_command(command_id)?; + correlation.register_command_for(command_id, WebDriverBiDiCommandKind::SessionStatus)?; } assert_eq!( correlation.outstanding_count(), MAX_WEBDRIVER_BIDI_OUTSTANDING_COMMANDS ); assert_eq!( - correlation.register_command(MAX_WEBDRIVER_BIDI_OUTSTANDING_COMMANDS as u64), + correlation.register_command_for( + MAX_WEBDRIVER_BIDI_OUTSTANDING_COMMANDS as u64, + WebDriverBiDiCommandKind::SessionStatus, + ), Err(WebDriverBiDiCommandCorrelationError::OutstandingCommandLimit) ); - correlation.retire_command(0)?; - correlation.register_command(MAX_WEBDRIVER_BIDI_OUTSTANDING_COMMANDS as u64)?; + correlation.retire_command_for(0, WebDriverBiDiCommandKind::SessionStatus)?; + correlation.register_command_for( + MAX_WEBDRIVER_BIDI_OUTSTANDING_COMMANDS as u64, + WebDriverBiDiCommandKind::SessionStatus, + )?; assert_eq!( correlation.outstanding_count(), MAX_WEBDRIVER_BIDI_OUTSTANDING_COMMANDS @@ -187,10 +209,11 @@ fn outstanding_command_budget_and_retirement_are_bounded() -> Result<(), Box Result<(), Box> { let mut correlation = WebDriverBiDiCommandCorrelation::new(); - correlation.register_command(123_456_789)?; + correlation.register_command_for(123_456_789, WebDriverBiDiCommandKind::SessionStatus)?; let debug = format!("{correlation:?}"); assert!(debug.contains("outstanding_count")); assert!(!debug.contains("123456789")); + assert!(!debug.contains("SessionStatus")); Ok(()) } From ac3dfa8a5a05f16fbb3989efb183b2b5364a8e7a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 21:58:15 +0900 Subject: [PATCH 10/17] style(network): apply command-kind rustfmt output --- crates/originweave-network/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/originweave-network/src/lib.rs b/crates/originweave-network/src/lib.rs index ba2f2ee88..7b2115f06 100644 --- a/crates/originweave-network/src/lib.rs +++ b/crates/originweave-network/src/lib.rs @@ -32,8 +32,8 @@ pub use connection::{ }; pub use webdriver_bidi_command_correlation::{ MAX_WEBDRIVER_BIDI_OUTSTANDING_COMMANDS, WebDriverBiDiCommandCorrelation, - WebDriverBiDiCommandCorrelationError, WebDriverBiDiCommandKind, WebDriverBiDiCorrelatedResponse, - WebDriverBiDiCorrelatedResponseOutcome, + WebDriverBiDiCommandCorrelationError, WebDriverBiDiCommandKind, + WebDriverBiDiCorrelatedResponse, WebDriverBiDiCorrelatedResponseOutcome, }; pub use webdriver_bidi_connection::{ WebDriverBiDiTcpConnection, WebDriverBiDiTcpConnectionError, From a5bd8ac0ed8975bdf19a2ff67aee6dc10ad1c954 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 21:58:59 +0900 Subject: [PATCH 11/17] style(network): finish command-kind rustfmt output --- .../tests/webdriver_bidi_command_correlation.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_command_correlation.rs b/crates/originweave-network/tests/webdriver_bidi_command_correlation.rs index f08dd0a6f..bb30ff988 100644 --- a/crates/originweave-network/tests/webdriver_bidi_command_correlation.rs +++ b/crates/originweave-network/tests/webdriver_bidi_command_correlation.rs @@ -140,10 +140,8 @@ fn correlation_fails_closed_without_consuming_unrelated_outstanding_commands() br#"{"type":"error","id":null,"error":"invalid argument","message":"no command id"}"#, )?; assert_eq!( - correlation.correlate_response_for( - &uncorrelatable, - WebDriverBiDiCommandKind::SessionStatus, - ), + correlation + .correlate_response_for(&uncorrelatable, WebDriverBiDiCommandKind::SessionStatus,), Err(WebDriverBiDiCommandCorrelationError::UncorrelatableErrorResponse) ); assert_eq!(correlation.outstanding_count(), 1); From 4b9a04e2dd161115c1ca7894bd16f22638e8b6ef Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 02:51:25 +0900 Subject: [PATCH 12/17] fix(network): remove unreachable success correlation branch --- .../src/webdriver_bidi_command_correlation.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs index 4dd9bcd78..e9c043b76 100644 --- a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs +++ b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs @@ -198,9 +198,12 @@ impl WebDriverBiDiCommandCorrelation { ) } WebDriverBiDiJsonEnvelopeKind::Success => { - let Some(command_id) = envelope.command_id() else { - return Err(WebDriverBiDiCommandCorrelationError::CommandNotOutstanding); - }; + // Success envelopes are privately constructed only after the JSON boundary admits + // a required js-uint id. Keep an internal invariant violation fail-closed without an + // unreachable branch: this sentinel can never be registered as an outstanding id. + let command_id = envelope + .command_id() + .unwrap_or(MAX_WEBDRIVER_BIDI_JS_UINT + 1); self.complete( command_id, expected_kind, From d835c35b0f75c6b95311a99a2270942f120a48df Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 00:42:20 +0900 Subject: [PATCH 13/17] fix(network): encode validated BiDi routing at correlation root Replace the out-of-range sentinel workaround with a structurally valid common-envelope representation at the canonical command-correlation owner. Successful responses carry a required js-uint id, protocol errors alone retain nullable ids, and events are id-less. Preserve the public envelope API and refresh WebDriver BiDi doctoring to the 18 August 2026 W3C Working Draft. --- crates/originweave-network/src/lib.rs | 1 + .../src/webdriver_bidi_command_correlation.rs | 44 ++++++++--------- .../src/webdriver_bidi_json_envelope.rs | 48 ++++++++++++++----- docs/doctoring/browser-agent-protocols.md | 18 +++---- 4 files changed, 65 insertions(+), 46 deletions(-) diff --git a/crates/originweave-network/src/lib.rs b/crates/originweave-network/src/lib.rs index 7b2115f06..9115025a1 100644 --- a/crates/originweave-network/src/lib.rs +++ b/crates/originweave-network/src/lib.rs @@ -39,6 +39,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 e9c043b76..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,33 +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 => { - // Success envelopes are privately constructed only after the JSON boundary admits - // a required js-uint id. Keep an internal invariant violation fail-closed without an - // unreachable branch: this sentinel can never be registered as an outstanding id. - let command_id = envelope - .command_id() - .unwrap_or(MAX_WEBDRIVER_BIDI_JS_UINT + 1); - 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 From e522826b654bfd4637307e1766b426501cfcbcc3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 12:59:28 +0900 Subject: [PATCH 14/17] test(docs): require BiDi correlation release record --- ...ommand_correlation_documentation_contract.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 tests/test_webdriver_bidi_command_correlation_documentation_contract.py diff --git a/tests/test_webdriver_bidi_command_correlation_documentation_contract.py b/tests/test_webdriver_bidi_command_correlation_documentation_contract.py new file mode 100644 index 000000000..1ba936ff5 --- /dev/null +++ b/tests/test_webdriver_bidi_command_correlation_documentation_contract.py @@ -0,0 +1,17 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +CHANGELOG = ROOT / "CHANGELOG.md" +SOURCE = ROOT / "crates/originweave-network/src/webdriver_bidi_command_correlation.rs" + + +def test_command_correlation_release_record_matches_public_boundary() -> None: + changelog = CHANGELOG.read_text(encoding="utf-8") + source = SOURCE.read_text(encoding="utf-8") + + assert "Bounded WebDriver BiDi outstanding-command correlation" in changelog + assert "MAX_WEBDRIVER_BIDI_OUTSTANDING_COMMANDS: usize = 256" in source + assert "CommandKindMismatch" in source + assert "UncorrelatableErrorResponse" in source + assert "protocol ACK" not in changelog From c5187585964c9f622cce675c334059ede49145d9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 13:00:36 +0900 Subject: [PATCH 15/17] docs(changelog): record BiDi command correlation boundary --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a137ee95..a05c016e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Deterministic WebDriver BiDi `browsingContext.locateNodes` command serialization that accepts only protocol-range `js-uint` identifiers and bounded injection-safe browsing-context identifiers, JSON-escapes reviewed query text, and emits the exact accessibility locator, finite node budget, and minimal serialization options without performing transport I/O or granting browser or Agent authority. - Fail-closed WebDriver BiDi `locateNodes` response correlation that consumes the exact serialized command, rejects out-of-range or mismatched response `id` values, and returns non-cloneable correlation evidence carrying only the matched command identifier and browsing context without parsing the response or granting browser/Agent authority. - 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 WebDriver BiDi outstanding-command correlation that retains at most 256 local ids with exact typed command-family provenance, consumes only matching success or correlatable error responses exactly once, leaves events, null-id errors, and kind mismatches unable to retire unrelated state, and performs no transport I/O or browser, policy, secret, or Agent authority grant. - 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. @@ -99,4 +100,4 @@ All notable changes to OriginWeave are documented in this file. The format follo - The hourly product agent has no Git metadata or repository authority. A separate post-verification publisher opens one PR and cannot approve or merge it. - The unprivileged OpenCode user is restricted to loopback egress during model execution, preventing runner-wide allow-listed endpoints from becoming direct source-exfiltration channels. -[Unreleased]: https://github.com/ContextualWisdomLab/OriginWeave/compare/main...HEAD +[Unreleased]: https://github.com/ContextualWisdomLab/OriginWeave/compare/main...HEAD \ No newline at end of file From 77cf9176396341aed52acb513a04fa927bc0eb22 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 13:01:10 +0900 Subject: [PATCH 16/17] style(changelog): preserve trailing newline --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a05c016e1..e8341a9bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,4 +100,4 @@ All notable changes to OriginWeave are documented in this file. The format follo - The hourly product agent has no Git metadata or repository authority. A separate post-verification publisher opens one PR and cannot approve or merge it. - The unprivileged OpenCode user is restricted to loopback egress during model execution, preventing runner-wide allow-listed endpoints from becoming direct source-exfiltration channels. -[Unreleased]: https://github.com/ContextualWisdomLab/OriginWeave/compare/main...HEAD \ No newline at end of file +[Unreleased]: https://github.com/ContextualWisdomLab/OriginWeave/compare/main...HEAD From 7d6db16b2ead201fcec320854923f90d3ad0d8bc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 13:03:30 +0900 Subject: [PATCH 17/17] test(docs): scope BiDi release contract to owned record --- ...i_command_correlation_documentation_contract.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/test_webdriver_bidi_command_correlation_documentation_contract.py b/tests/test_webdriver_bidi_command_correlation_documentation_contract.py index 1ba936ff5..664b06540 100644 --- a/tests/test_webdriver_bidi_command_correlation_documentation_contract.py +++ b/tests/test_webdriver_bidi_command_correlation_documentation_contract.py @@ -10,8 +10,18 @@ def test_command_correlation_release_record_matches_public_boundary() -> None: changelog = CHANGELOG.read_text(encoding="utf-8") source = SOURCE.read_text(encoding="utf-8") - assert "Bounded WebDriver BiDi outstanding-command correlation" in changelog + release_records = [ + line + for line in changelog.splitlines() + if line.startswith("- Bounded WebDriver BiDi outstanding-command correlation") + ] + assert len(release_records) == 1 + release_record = release_records[0] + assert "at most 256 local ids" in release_record + assert "exact typed command-family provenance" in release_record + assert "events, null-id errors, and kind mismatches" in release_record + assert "performs no transport I/O or browser, policy, secret, or Agent authority grant" in release_record + assert "MAX_WEBDRIVER_BIDI_OUTSTANDING_COMMANDS: usize = 256" in source assert "CommandKindMismatch" in source assert "UncorrelatableErrorResponse" in source - assert "protocol ACK" not in changelog