From 424d048fba244489cf42084f9e7433cb7f634df4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 18:13:08 +0900 Subject: [PATCH 01/76] test(network): require active subscription for navigation admission --- ...gation_committed_subscription_admission.rs | 199 ++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs new file mode 100644 index 000000000..ac8d4782c --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs @@ -0,0 +1,199 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::{BrowserAuthorityRegistry, WebDriverBiDiWebSocketEndpoint}; +use originweave_network::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiNavigationCommittedSubscriptionAdmission, + WebDriverBiDiNavigationCommittedSubscriptionCommand, + WebDriverBiDiNavigationCommittedSubscriptionResult, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler, + WebDriverBiDiWebSocketMessageAssembly, advance_webdriver_bidi_navigation_document_epoch, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; +const CONTEXT_ID: &str = "context-a"; +const EXPECTED_URL: &str = "https://example.test/after"; +const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; +const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; +const SUBSCRIBE_RESPONSE: &[u8] = + br#"{"type":"success","id":7,"result":{"subscription":"subscription-a"}}"#; +const NAVIGATION_EVENT: &[u8] = br#"{"type":"event","method":"browsingContext.navigationCommitted","params":{"context":"context-a","navigation":"nav-8","timestamp":1234,"url":"https://example.test/after"}}"#; + +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, + "opening request ended before the header terminator", + )); + } + request.extend_from_slice(&buffer[..count]); + } + Ok(()) +} + +fn read_masked_text_frame(stream: &mut TcpStream) -> io::Result> { + let mut header = [0_u8; 2]; + stream.read_exact(&mut header)?; + if header[0] != 0x81 || header[1] & 0x80 == 0 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "expected one final masked client text frame", + )); + } + let length = match header[1] & 0x7f { + length @ 0..=125 => usize::from(length), + 126 => { + let mut extended = [0_u8; 2]; + stream.read_exact(&mut extended)?; + usize::from(u16::from_be_bytes(extended)) + } + _ => { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "test command unexpectedly required 64-bit framing", + )); + } + }; + let mut mask = [0_u8; 4]; + stream.read_exact(&mut mask)?; + let mut payload = vec![0_u8; length]; + stream.read_exact(&mut payload)?; + for (index, byte) in payload.iter_mut().enumerate() { + *byte ^= mask[index % mask.len()]; + } + Ok(payload) +} + +fn write_text_frame(stream: &mut TcpStream, payload: &[u8]) -> io::Result<()> { + stream.write_all(&[0x81])?; + match payload.len() { + 0..=125 => stream.write_all(&[payload.len() as u8])?, + 126..=65_535 => { + stream.write_all(&[126])?; + stream.write_all(&(payload.len() as u16).to_be_bytes())?; + } + _ => { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "test payload unexpectedly required 64-bit framing", + )); + } + } + stream.write_all(payload) +} + +fn next_text( + established: originweave_network::WebDriverBiDiWebSocketEstablished, + assembler: &mut WebDriverBiDiWebSocketMessageAssembler, +) -> Result< + ( + originweave_network::WebDriverBiDiWebSocketEstablished, + originweave_network::WebDriverBiDiWebSocketTextMessage, + ), + Box, +> { + let (established, frame) = established.read_frame(Duration::from_millis(500))?; + match assembler.push_frame(frame)? { + WebDriverBiDiWebSocketMessageAssembly::Text(text) => Ok((established, text)), + other => Err(io::Error::other(format!( + "expected a complete WebDriver BiDi text message, got {other:?}" + )) + .into()), + } +} + +#[test] +fn committed_navigation_requires_the_exact_active_subscription_before_document_mutation() +-> Result<(), Box> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let server = thread::spawn(move || -> io::Result<()> { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + let command = read_masked_text_frame(&mut stream)?; + if command + != br#"{"id":7,"method":"session.subscribe","params":{"events":["browsingContext.navigationCommitted"],"contexts":["context-a"]}}"# + { + return Err(io::Error::other("unexpected session.subscribe command")); + } + write_text_frame(&mut stream, SUBSCRIBE_RESPONSE)?; + write_text_frame(&mut stream, NAVIGATION_EVENT) + }); + + let mut registry = BrowserAuthorityRegistry::new(); + let session = registry.register_session(SESSION_ID)?; + let context = registry.register_context(session, CONTEXT_ID)?; + let pre_navigation_epoch = registry.current_context_epoch(session, context)?; + + 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 established = WebDriverBiDiWebSocketHandshakePlan::new( + connection, + WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?, + )? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?; + + let command = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( + 7, ®istry, session, context, CONTEXT_ID, + )?; + let binding = command.admission_binding(); + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + let established = command.send( + ®istry, + established, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + Duration::from_millis(500), + )?; + + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); + let (established, response) = next_text(established, &mut assembler)?; + let subscription = WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( + &response, + &mut correlation, + )?; + let admission = WebDriverBiDiNavigationCommittedSubscriptionAdmission::new( + subscription, + binding, + ®istry, + )?; + + let (_established, event) = next_text(established, &mut assembler)?; + let observation = admission.admit(&event, ®istry, EXPECTED_URL)?; + assert_eq!(observation.browser_session(), session); + assert_eq!(observation.browsing_context(), context); + assert_eq!(observation.navigation_id(), Some("nav-8")); + + let advanced = advance_webdriver_bidi_navigation_document_epoch( + observation, + &mut registry, + pre_navigation_epoch, + )?; + assert_eq!(advanced.browser_session(), session); + assert_eq!(advanced.browsing_context(), context); + + let unsubscribe = admission.into_unsubscribe(8)?; + assert_eq!(unsubscribe.command_id(), 8); + + server + .join() + .map_err(|_| io::Error::other("subscription admission test server panicked"))??; + Ok(()) +} From 1dfdf3d54c6b7be279fc8634834c5c9dbd520a86 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 18:17:43 +0900 Subject: [PATCH 02/76] feat(network): bind navigation event admission to subscription --- ...gation_committed_subscription_admission.rs | 284 ++++++++++++++++++ 1 file changed, 284 insertions(+) create mode 100644 crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_admission.rs diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_admission.rs b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_admission.rs new file mode 100644 index 000000000..458a996f5 --- /dev/null +++ b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_admission.rs @@ -0,0 +1,284 @@ +use std::{error::Error, fmt}; + +use originweave_core::{ + BrowserAuthorityRegistry, BrowserRegistryError, BrowserSessionId, BrowsingContextId, +}; + +use crate::{ + WebDriverBiDiNavigationCommittedObservation, WebDriverBiDiNavigationCommittedObservationError, + WebDriverBiDiNavigationCommittedSubscriptionResult, + WebDriverBiDiNavigationCommittedUnsubscribeCommand, + WebDriverBiDiNavigationCommittedUnsubscribeCommandError, WebDriverBiDiWebSocketTextMessage, +}; + +/// Immutable command-side binding retained before a committed-navigation subscription is sent. +/// +/// The binding carries only the exact local command identifier and the already-registered +/// OriginWeave session/context association used to serialize that command. The external BiDi +/// context identifier is retained privately for immediate registry revalidation and is not exposed +/// as durable OriginWeave authority. +pub struct WebDriverBiDiNavigationCommittedSubscriptionBinding { + command_id: u64, + browser_session: BrowserSessionId, + browsing_context: BrowsingContextId, + external_context: String, +} + +impl fmt::Debug for WebDriverBiDiNavigationCommittedSubscriptionBinding { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("WebDriverBiDiNavigationCommittedSubscriptionBinding") + .field("command_id", &self.command_id) + .field("browser_session", &self.browser_session.value()) + .field("browsing_context", &self.browsing_context.value()) + .field("external_context_bytes", &self.external_context.len()) + .finish() + } +} + +impl WebDriverBiDiNavigationCommittedSubscriptionBinding { + pub(crate) fn new( + command_id: u64, + browser_session: BrowserSessionId, + browsing_context: BrowsingContextId, + external_context: &str, + ) -> Self { + Self { + command_id, + browser_session, + browsing_context, + external_context: external_context.to_owned(), + } + } + + /// Return the exact local command identifier this binding was captured from. + #[must_use] + pub const fn command_id(&self) -> u64 { + self.command_id + } + + /// Return the exact registered OriginWeave browser session bound to the subscription command. + #[must_use] + pub const fn browser_session(&self) -> BrowserSessionId { + self.browser_session + } + + /// Return the exact registered OriginWeave browsing context bound to the subscription command. + #[must_use] + pub const fn browsing_context(&self) -> BrowsingContextId { + self.browsing_context + } +} + +/// Active local admission capability for one exact committed-navigation BiDi subscription. +/// +/// Construction requires both the correlated remote subscription receipt and the immutable binding +/// captured from the exact command that requested it. The command identifiers must match and the +/// original external context mapping must still resolve to the exact OriginWeave session/context. +/// Holding this value is therefore narrower than holding an opaque protocol subscription string. +/// It grants only admission of the matching committed-navigation event through the existing bounded +/// parser; it grants no navigation, destination, origin, policy, secret, node, or Agent authority. +pub struct WebDriverBiDiNavigationCommittedSubscriptionAdmission { + subscription: WebDriverBiDiNavigationCommittedSubscriptionResult, + binding: WebDriverBiDiNavigationCommittedSubscriptionBinding, +} + +impl fmt::Debug for WebDriverBiDiNavigationCommittedSubscriptionAdmission { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("WebDriverBiDiNavigationCommittedSubscriptionAdmission") + .field("command_id", &self.binding.command_id) + .field("browser_session", &self.binding.browser_session.value()) + .field("browsing_context", &self.binding.browsing_context.value()) + .field( + "subscription_id_bytes", + &self.subscription.subscription_id().len(), + ) + .finish() + } +} + +impl WebDriverBiDiNavigationCommittedSubscriptionAdmission { + /// Bind one correlated subscription receipt to the exact command-side session/context intent. + /// + /// A response correlated to a different command cannot be rebound to this capability. The + /// original external BiDi context is revalidated before the capability exists, so a retired or + /// replaced registry mapping fails closed without creating active event-admission state. + pub fn new( + subscription: WebDriverBiDiNavigationCommittedSubscriptionResult, + binding: WebDriverBiDiNavigationCommittedSubscriptionBinding, + registry: &BrowserAuthorityRegistry, + ) -> Result { + if subscription.command_id() != binding.command_id { + return Err( + WebDriverBiDiNavigationCommittedSubscriptionAdmissionError::CommandIdMismatch { + subscription_command_id: subscription.command_id(), + binding_command_id: binding.command_id, + }, + ); + } + require_current_binding(registry, &binding).map_err(|source| { + WebDriverBiDiNavigationCommittedSubscriptionAdmissionError::ContextBinding { source } + })?; + Ok(Self { + subscription, + binding, + }) + } + + /// Return the exact registered OriginWeave browser session admitted by this capability. + #[must_use] + pub const fn browser_session(&self) -> BrowserSessionId { + self.binding.browser_session + } + + /// Return the exact registered OriginWeave browsing context admitted by this capability. + #[must_use] + pub const fn browsing_context(&self) -> BrowsingContextId { + self.binding.browsing_context + } + + /// Admit one exact committed-navigation event while this subscription capability remains active. + /// + /// The original command-side external-context mapping is revalidated immediately before parsing + /// the event. The event must then independently carry that same registered context and the exact + /// declared URL. The returned subscribed observation is the only navigation observation type + /// accepted by the state-changing document-advance boundary. + pub fn admit( + &self, + message: &WebDriverBiDiWebSocketTextMessage, + registry: &BrowserAuthorityRegistry, + expected_url: &str, + ) -> Result< + WebDriverBiDiNavigationCommittedSubscribedObservation, + WebDriverBiDiNavigationCommittedObservationError, + > { + require_current_binding(registry, &self.binding).map_err(|source| { + WebDriverBiDiNavigationCommittedObservationError::ContextBinding { source } + })?; + WebDriverBiDiNavigationCommittedObservation::parse_and_match( + message, + registry, + self.binding.browser_session, + self.binding.browsing_context, + expected_url, + ) + .map(WebDriverBiDiNavigationCommittedSubscribedObservation) + } + + /// Consume active event admission and construct teardown for this exact subscription receipt. + /// + /// Consumption deliberately ends local event admission before the unsubscribe command can be + /// emitted. If later transport or remote teardown fails, callers must explicitly establish a new + /// typed subscription before admitting more events; ambiguous teardown never restores authority. + pub fn into_unsubscribe( + self, + command_id: u64, + ) -> Result< + WebDriverBiDiNavigationCommittedUnsubscribeCommand, + WebDriverBiDiNavigationCommittedUnsubscribeCommandError, + > { + WebDriverBiDiNavigationCommittedUnsubscribeCommand::new(command_id, &self.subscription) + } +} + +fn require_current_binding( + registry: &BrowserAuthorityRegistry, + binding: &WebDriverBiDiNavigationCommittedSubscriptionBinding, +) -> Result<(), BrowserRegistryError> { + registry.require_registered_context_external_identifier( + binding.browser_session, + binding.browsing_context, + &binding.external_context, + ) +} + +/// One committed-navigation observation admitted through an active exact subscription capability. +/// +/// Unlike the lower-level protocol observation, this value proves that local admission was bound to +/// the exact typed `session.subscribe` command/receipt pair for the same registered context at the +/// time the event was admitted. It still does not prove action causality or grant destination, +/// origin, policy, node, secret, process, profile, or reusable Agent authority. +pub struct WebDriverBiDiNavigationCommittedSubscribedObservation( + WebDriverBiDiNavigationCommittedObservation, +); + +impl fmt::Debug for WebDriverBiDiNavigationCommittedSubscribedObservation { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_tuple("WebDriverBiDiNavigationCommittedSubscribedObservation") + .field(&self.0) + .finish() + } +} + +impl WebDriverBiDiNavigationCommittedSubscribedObservation { + /// Return the exact OriginWeave browser session whose active subscription admitted the event. + #[must_use] + pub const fn browser_session(&self) -> BrowserSessionId { + self.0.browser_session() + } + + /// Return the exact OriginWeave browsing context whose active subscription admitted the event. + #[must_use] + pub const fn browsing_context(&self) -> BrowsingContextId { + self.0.browsing_context() + } + + /// Borrow the optional opaque WebDriver BiDi navigation identifier. + #[must_use] + pub fn navigation_id(&self) -> Option<&str> { + self.0.navigation_id() + } + + /// Return the admitted WebDriver BiDi monotonic event timestamp. + #[must_use] + pub const fn timestamp(&self) -> u64 { + self.0.timestamp() + } + + /// Borrow the exact bounded serialized URL admitted through the active subscription. + #[must_use] + pub fn url(&self) -> &str { + self.0.url() + } +} + +/// Fail-closed failures while binding a correlated subscription receipt to command-side authority. +#[derive(Debug)] +pub enum WebDriverBiDiNavigationCommittedSubscriptionAdmissionError { + /// The correlated response belongs to a different local command than the supplied binding. + CommandIdMismatch { + /// Exact command identifier carried by the correlated subscription receipt. + subscription_command_id: u64, + /// Exact command identifier captured from the intended subscription command. + binding_command_id: u64, + }, + /// The original external context no longer maps to the exact registered session/context pair. + ContextBinding { + /// Exact browser-registry authority failure. + source: BrowserRegistryError, + }, +} + +impl fmt::Display for WebDriverBiDiNavigationCommittedSubscriptionAdmissionError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::CommandIdMismatch { .. } => formatter.write_str( + "WebDriver BiDi navigation subscription response does not match its command binding", + ), + Self::ContextBinding { .. } => formatter.write_str( + "WebDriver BiDi navigation subscription context is no longer registered authority", + ), + } + } +} + +impl Error for WebDriverBiDiNavigationCommittedSubscriptionAdmissionError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::CommandIdMismatch { .. } => None, + Self::ContextBinding { source } => Some(source), + } + } +} From 0612b56ecd7f46742c0f3c703c25349e19584a71 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 18:18:35 +0900 Subject: [PATCH 03/76] feat(network): retain subscription command admission binding --- ..._bidi_navigation_committed_subscription.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs index fae6b9bd0..fc09d8d3f 100644 --- a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs +++ b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs @@ -7,8 +7,8 @@ use originweave_core::{ use crate::{ MAX_WEBDRIVER_BIDI_JS_UINT, WEBDRIVER_BIDI_NAVIGATION_COMMITTED_METHOD, WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, - WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketFrameError, - WebDriverBiDiWebSocketMaskKey, + WebDriverBiDiNavigationCommittedSubscriptionBinding, WebDriverBiDiWebSocketEstablished, + WebDriverBiDiWebSocketFrameError, WebDriverBiDiWebSocketMaskKey, }; const SESSION_SUBSCRIBE_METHOD: &str = "session.subscribe"; @@ -87,6 +87,21 @@ impl WebDriverBiDiNavigationCommittedSubscriptionCommand { &self.external_context } + /// Capture the exact command-side binding required to admit the correlated subscription later. + /// + /// The binding is intentionally captured before this single-use command is consumed by + /// [`Self::send`]. It contains no remote subscription identifier and grants no event authority by + /// itself; the exact correlated `session.subscribe` result must be bound to it separately. + #[must_use] + pub fn admission_binding(&self) -> WebDriverBiDiNavigationCommittedSubscriptionBinding { + WebDriverBiDiNavigationCommittedSubscriptionBinding::new( + self.command_id, + self.browser_session, + self.browsing_context, + &self.external_context, + ) + } + /// Revalidate, register, and write this exact subscription on an established verified BiDi stream. /// /// Context binding is revalidated immediately before command correlation and network I/O so a From 8419b0833e9af540b33ca1fa148dd81c60e55217 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 18:19:09 +0900 Subject: [PATCH 04/76] feat(network): export subscription-bound navigation admission --- crates/originweave-network/src/lib.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/originweave-network/src/lib.rs b/crates/originweave-network/src/lib.rs index 2dd949f68..5af6f1038 100644 --- a/crates/originweave-network/src/lib.rs +++ b/crates/originweave-network/src/lib.rs @@ -11,7 +11,8 @@ //! correlation, transports a narrowly typed pointer click, admits its typed //! correlated protocol acknowledgment, sends a context-bound subscription for //! committed-navigation events, retains its typed bounded correlated subscription -//! identifier, explicitly tears down that exact subscription by identifier, admits +//! identifier, binds navigation-event admission to that exact active command/receipt +//! lifecycle, explicitly tears down that exact subscription by identifier, admits //! its typed correlated unsubscribe acknowledgment, admits a bounded //! navigation-committed post-condition observation for one exact registered context //! and URL, rotates the matched context's document epoch only from an exact @@ -32,6 +33,7 @@ mod webdriver_bidi_connection; mod webdriver_bidi_json_envelope; mod webdriver_bidi_navigation_committed_postcondition; mod webdriver_bidi_navigation_committed_subscription; +mod webdriver_bidi_navigation_committed_subscription_admission; mod webdriver_bidi_navigation_committed_subscription_response; mod webdriver_bidi_navigation_committed_unsubscribe; mod webdriver_bidi_navigation_committed_unsubscribe_response; @@ -80,6 +82,12 @@ pub use webdriver_bidi_navigation_committed_subscription::{ WebDriverBiDiNavigationCommittedSubscriptionCommand, WebDriverBiDiNavigationCommittedSubscriptionCommandError, }; +pub use webdriver_bidi_navigation_committed_subscription_admission::{ + WebDriverBiDiNavigationCommittedSubscribedObservation, + WebDriverBiDiNavigationCommittedSubscriptionAdmission, + WebDriverBiDiNavigationCommittedSubscriptionAdmissionError, + WebDriverBiDiNavigationCommittedSubscriptionBinding, +}; pub use webdriver_bidi_navigation_committed_subscription_response::{ MAX_WEBDRIVER_BIDI_SUBSCRIPTION_IDENTIFIER_BYTES, WebDriverBiDiNavigationCommittedSubscriptionResponseError, From 3083843ab5761b91dd18a5fade3492bed874c30c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 18:21:20 +0900 Subject: [PATCH 05/76] fix(network): require subscribed observation for document advance --- ...bdriver_bidi_navigation_document_advance.rs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_document_advance.rs b/crates/originweave-network/src/webdriver_bidi_navigation_document_advance.rs index 9af15216f..d0ec1f521 100644 --- a/crates/originweave-network/src/webdriver_bidi_navigation_document_advance.rs +++ b/crates/originweave-network/src/webdriver_bidi_navigation_document_advance.rs @@ -5,9 +5,9 @@ use originweave_core::{ DocumentEpoch, }; -use crate::WebDriverBiDiNavigationCommittedObservation; +use crate::WebDriverBiDiNavigationCommittedSubscribedObservation; -/// Immutable evidence that one accepted navigation observation rotated one exact document epoch. +/// Immutable evidence that one subscribed navigation observation rotated one exact document epoch. /// /// The value records only the registry-local session/context transition. It does not bind the new /// document origin, authenticate the browser adapter, prove which action caused the navigation, or @@ -46,7 +46,7 @@ impl WebDriverBiDiNavigationCommittedDocumentAdvance { } } -/// Fail-closed failures while rotating document authority from an accepted navigation observation. +/// Fail-closed failures while rotating document authority from a subscribed navigation observation. #[derive(Debug)] pub enum WebDriverBiDiNavigationCommittedDocumentAdvanceError { /// Registered session/context state could not be revalidated or advanced. @@ -93,12 +93,14 @@ fn advance_registered_document_if_expected( registry.advance_document(browsing_context).map(Some) } -/// Consume one exact accepted navigation observation and rotate that context's document authority. +/// Consume one exact subscription-admitted navigation and rotate that context's document authority. /// /// The caller must supply the document epoch captured before dispatching the action whose -/// post-condition is being evaluated. The observation is consumed so one admitted event cannot be -/// reused to rotate the registry twice. The exact session/context pair and caller-captured epoch -/// are revalidated immediately before mutation, and stale state fails closed without mutation. +/// post-condition is being evaluated. The subscribed observation is consumed so one admitted event +/// cannot be reused to rotate the registry twice, and raw protocol observations cannot cross this +/// state-changing boundary without first being bound to an active exact `session.subscribe` +/// command/receipt. The exact session/context pair and caller-captured epoch are revalidated +/// immediately before mutation, and stale state fails closed without mutation. /// /// A successful advance delegates to [`BrowserAuthorityRegistry::advance_document`], which clears /// the previous canonical-origin binding and all node bindings owned by the context. The new @@ -107,7 +109,7 @@ fn advance_registered_document_if_expected( /// exhaustion, remain available as the typed [`BrowserRegistryError`] source instead of being /// converted into a panic or successful authority transition. pub fn advance_webdriver_bidi_navigation_document_epoch( - observation: WebDriverBiDiNavigationCommittedObservation, + observation: WebDriverBiDiNavigationCommittedSubscribedObservation, registry: &mut BrowserAuthorityRegistry, expected_previous_epoch: DocumentEpoch, ) -> Result< From cf1cd711f1fb199a72f181dde1334a962dc353a2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 18:22:06 +0900 Subject: [PATCH 06/76] fix(network): require subscribed observation for origin binding --- ...bdriver_bidi_navigation_document_origin.rs | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_document_origin.rs b/crates/originweave-network/src/webdriver_bidi_navigation_document_origin.rs index 1ef7a6fcb..8eac490e1 100644 --- a/crates/originweave-network/src/webdriver_bidi_navigation_document_origin.rs +++ b/crates/originweave-network/src/webdriver_bidi_navigation_document_origin.rs @@ -7,16 +7,17 @@ use originweave_core::{ use crate::{ WebDriverBiDiNavigationCommittedDocumentAdvanceError, - WebDriverBiDiNavigationCommittedObservation, advance_webdriver_bidi_navigation_document_epoch, + WebDriverBiDiNavigationCommittedSubscribedObservation, + advance_webdriver_bidi_navigation_document_epoch, }; -/// Immutable evidence that one accepted navigation rotated its document and bound its observed origin. +/// Immutable evidence that one subscribed navigation rotated its document and bound its observed origin. /// /// The value records only the registry-local session/context/document transition and the canonical -/// origin derived from the exact serialized URL carried by the accepted WebDriver BiDi navigation -/// observation. It does not authenticate the browser adapter, prove which action caused the -/// navigation, authorize the destination, or grant browser, policy, node, credential, process, or -/// reusable Agent authority. +/// origin derived from the exact serialized URL carried by the subscription-admitted WebDriver BiDi +/// navigation observation. It does not authenticate the browser adapter, prove which action caused +/// the navigation, authorize the destination, or grant browser, policy, node, credential, process, +/// or reusable Agent authority. #[derive(Debug, Clone, PartialEq, Eq)] pub struct WebDriverBiDiNavigationCommittedDocumentOrigin { browser_session: BrowserSessionId, @@ -121,19 +122,21 @@ fn bind_advanced_document_origin( ) } -/// Consume one accepted navigation, rotate the exact expected document, and bind its canonical origin. +/// Consume one subscription-admitted navigation, rotate its document, and bind its canonical origin. /// /// Origin derivation is completed before any registry mutation. Only serialized HTTP(S) URLs whose /// authority can enter [`Origin`] are accepted; credential-bearing, opaque, malformed, insecure /// remote HTTP, and otherwise unsupported authorities therefore fail before document rotation. -/// The accepted observation is then consumed by the existing exact-epoch document-advance boundary, -/// which clears stale origin and node authority. Finally, the derived canonical origin is bound to -/// that newly advanced document through the canonical browser registry lifecycle. +/// The observation must already have crossed the exact active-subscription admission boundary; raw +/// protocol observations cannot reach this state-changing API. The subscribed observation is then +/// consumed by the exact-epoch document-advance boundary, which clears stale origin and node +/// authority. Finally, the derived canonical origin is bound to that newly advanced document +/// through the canonical browser registry lifecycle. /// /// The caller must still treat the returned value as immediate-use registry evidence rather than as /// proof of action causality, browser authenticity, destination authorization, or reusable authority. pub fn advance_and_bind_webdriver_bidi_navigation_document_origin( - observation: WebDriverBiDiNavigationCommittedObservation, + observation: WebDriverBiDiNavigationCommittedSubscribedObservation, registry: &mut BrowserAuthorityRegistry, expected_previous_epoch: DocumentEpoch, ) -> Result< From 4dbbb966e609ac62e7ed3789822e9ee13fccea17 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 18:23:46 +0900 Subject: [PATCH 07/76] test(network): share subscribed navigation loopback fixture --- .../originweave-network/tests/support/mod.rs | 190 ++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 crates/originweave-network/tests/support/mod.rs diff --git a/crates/originweave-network/tests/support/mod.rs b/crates/originweave-network/tests/support/mod.rs new file mode 100644 index 000000000..393d3322f --- /dev/null +++ b/crates/originweave-network/tests/support/mod.rs @@ -0,0 +1,190 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::{ + BrowserAuthorityRegistry, BrowserSessionId, BrowsingContextId, WebDriverBiDiWebSocketEndpoint, +}; +use originweave_network::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiNavigationCommittedSubscribedObservation, + WebDriverBiDiNavigationCommittedSubscriptionAdmission, + WebDriverBiDiNavigationCommittedSubscriptionCommand, + WebDriverBiDiNavigationCommittedSubscriptionResult, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler, + WebDriverBiDiWebSocketMessageAssembly, +}; + +const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; +const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; +const SUBSCRIBE_RESPONSE: &[u8] = + br#"{"type":"success","id":7,"result":{"subscription":"subscription-a"}}"#; + +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, + "opening request ended before the header terminator", + )); + } + request.extend_from_slice(&buffer[..count]); + } + Ok(()) +} + +fn read_masked_text_frame(stream: &mut TcpStream) -> io::Result> { + let mut header = [0_u8; 2]; + stream.read_exact(&mut header)?; + if header[0] != 0x81 || header[1] & 0x80 == 0 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "expected one final masked client text frame", + )); + } + let length = match header[1] & 0x7f { + length @ 0..=125 => usize::from(length), + 126 => { + let mut extended = [0_u8; 2]; + stream.read_exact(&mut extended)?; + usize::from(u16::from_be_bytes(extended)) + } + _ => { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "fixture subscribe command unexpectedly required 64-bit framing", + )); + } + }; + let mut mask = [0_u8; 4]; + stream.read_exact(&mut mask)?; + let mut payload = vec![0_u8; length]; + stream.read_exact(&mut payload)?; + for (index, byte) in payload.iter_mut().enumerate() { + *byte ^= mask[index % mask.len()]; + } + Ok(payload) +} + +fn write_text_frame(stream: &mut TcpStream, payload: &[u8]) -> io::Result<()> { + stream.write_all(&[0x81])?; + match payload.len() { + 0..=125 => stream.write_all(&[payload.len() as u8])?, + 126..=65_535 => { + stream.write_all(&[126])?; + stream.write_all(&(payload.len() as u16).to_be_bytes())?; + } + _ => { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "fixture event unexpectedly required 64-bit framing", + )); + } + } + stream.write_all(payload) +} + +fn next_text( + established: originweave_network::WebDriverBiDiWebSocketEstablished, + assembler: &mut WebDriverBiDiWebSocketMessageAssembler, +) -> Result< + ( + originweave_network::WebDriverBiDiWebSocketEstablished, + originweave_network::WebDriverBiDiWebSocketTextMessage, + ), + Box, +> { + let (established, frame) = established.read_frame(Duration::from_millis(500))?; + match assembler.push_frame(frame)? { + WebDriverBiDiWebSocketMessageAssembly::Text(text) => Ok((established, text)), + other => Err(io::Error::other(format!( + "expected a complete WebDriver BiDi text message, got {other:?}" + )) + .into()), + } +} + +pub fn receive_subscribed_navigation_event( + registry: &BrowserAuthorityRegistry, + browser_session: BrowserSessionId, + browsing_context: BrowsingContextId, + session_id: &str, + external_context: &str, + expected_url: &str, +) -> Result> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let expected_command = format!( + "{{\"id\":7,\"method\":\"session.subscribe\",\"params\":{{\"events\":[\"browsingContext.navigationCommitted\"],\"contexts\":[\"{external_context}\"]}}}}" + ) + .into_bytes(); + let event = format!( + "{{\"type\":\"event\",\"method\":\"browsingContext.navigationCommitted\",\"params\":{{\"context\":\"{external_context}\",\"navigation\":\"navigation-a\",\"timestamp\":17,\"url\":\"{expected_url}\"}}}}" + ) + .into_bytes(); + let server = thread::spawn(move || -> io::Result<()> { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + let command = read_masked_text_frame(&mut stream)?; + if command != expected_command { + return Err(io::Error::other("unexpected session.subscribe fixture command")); + } + write_text_frame(&mut stream, SUBSCRIBE_RESPONSE)?; + write_text_frame(&mut stream, &event) + }); + + 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 established = WebDriverBiDiWebSocketHandshakePlan::new( + connection, + WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?, + )? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?; + + let command = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( + 7, + registry, + browser_session, + browsing_context, + external_context, + )?; + let binding = command.admission_binding(); + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + let established = command.send( + registry, + established, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + Duration::from_millis(500), + )?; + + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); + let (established, response) = next_text(established, &mut assembler)?; + let result = WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( + &response, + &mut correlation, + )?; + let admission = + WebDriverBiDiNavigationCommittedSubscriptionAdmission::new(result, binding, registry)?; + let (_established, event) = next_text(established, &mut assembler)?; + let observation = admission.admit(&event, registry, expected_url)?; + + server + .join() + .map_err(|_| io::Error::other("subscribed navigation fixture server panicked"))??; + Ok(observation) +} From b9c01dd26ac810a518c095318a3fdbe552f4d894 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 18:24:12 +0900 Subject: [PATCH 08/76] test(network): exercise document advance through active subscription --- ...driver_bidi_navigation_document_advance.rs | 116 +++--------------- 1 file changed, 18 insertions(+), 98 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_document_advance.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_document_advance.rs index fe086fb8d..6f531fbf1 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_document_advance.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_document_advance.rs @@ -1,115 +1,35 @@ -use std::{ - error::Error, - io::{self, Read, Write}, - net::{TcpListener, TcpStream}, - thread, - time::Duration, -}; +mod support; -use originweave_core::{ - BrowserAuthorityRegistry, BrowserRegistryError, Origin, WebDriverBiDiWebSocketEndpoint, -}; +use std::{error::Error, io}; + +use originweave_core::{BrowserAuthorityRegistry, BrowserRegistryError, Origin}; use originweave_network::{ WebDriverBiDiNavigationCommittedDocumentAdvanceError, - WebDriverBiDiNavigationCommittedObservation, WebDriverBiDiTcpConnectionPlan, - WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, - WebDriverBiDiWebSocketTextMessage, advance_webdriver_bidi_navigation_document_epoch, + advance_webdriver_bidi_navigation_document_epoch, }; +use support::receive_subscribed_navigation_event; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; -const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; +const CONTEXT_ID: &str = "context-a"; const EXPECTED_URL: &str = "https://example.test/after"; -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 write_unmasked_text_frame(stream: &mut TcpStream, payload: &[u8]) -> io::Result<()> { - stream.write_all(&[0x81])?; - match payload.len() { - 0..=125 => stream.write_all(&[payload.len() as u8])?, - 126..=65_535 => { - stream.write_all(&[126])?; - stream.write_all(&(payload.len() as u16).to_be_bytes())?; - } - _ => { - stream.write_all(&[127])?; - stream.write_all(&(payload.len() as u64).to_be_bytes())?; - } - } - stream.write_all(payload) -} - -fn receive_navigation_event() -> Result> { - let listener = TcpListener::bind(("127.0.0.1", 0))?; - let local_addr = listener.local_addr()?; - let payload = br#"{"type":"event","method":"browsingContext.navigationCommitted","params":{"context":"context-a","navigation":"navigation-a","timestamp":17,"url":"https://example.test/after"}}"#.to_vec(); - let server = thread::spawn(move || -> io::Result<()> { - let (mut stream, _) = listener.accept()?; - read_opening_request(&mut stream)?; - stream.write_all(OPENING_RESPONSE)?; - write_unmasked_text_frame(&mut stream, &payload) - }); - - 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!( - "navigation document-advance event produced unexpected assembly state: {other:?}" - )) - .into()); - } - }; - server - .join() - .map_err(|_| io::Error::other("navigation document-advance server panicked"))??; - Ok(text) -} #[test] fn accepted_navigation_advances_only_the_exact_pre_action_document_epoch() -> Result<(), Box> { let mut registry = BrowserAuthorityRegistry::new(); let session = registry.register_session(SESSION_ID)?; - let context = registry.register_context(session, "context-a")?; + let context = registry.register_context(session, CONTEXT_ID)?; let before = registry.current_context_epoch(session, context)?; let previous_origin = Origin::parse("https://example.test") .map_err(|error| io::Error::other(format!("fixture origin parse failed: {error:?}")))?; registry.bind_context_origin(session, context, &previous_origin)?; - let event = receive_navigation_event()?; - let observation = WebDriverBiDiNavigationCommittedObservation::parse_and_match( - &event, + let observation = receive_subscribed_navigation_event( ®istry, session, context, + SESSION_ID, + CONTEXT_ID, EXPECTED_URL, )?; let advance = @@ -128,12 +48,12 @@ fn accepted_navigation_advances_only_the_exact_pre_action_document_epoch() Err(BrowserRegistryError::ContextOriginNotBound) ); - let replay_event = receive_navigation_event()?; - let replay = WebDriverBiDiNavigationCommittedObservation::parse_and_match( - &replay_event, + let replay = receive_subscribed_navigation_event( ®istry, session, context, + SESSION_ID, + CONTEXT_ID, EXPECTED_URL, )?; let replay_error = @@ -161,14 +81,14 @@ fn retired_context_between_observation_and_advance_fails_closed_with_typed_sourc -> Result<(), Box> { let mut registry = BrowserAuthorityRegistry::new(); let session = registry.register_session(SESSION_ID)?; - let context = registry.register_context(session, "context-a")?; + let context = registry.register_context(session, CONTEXT_ID)?; let before = registry.current_context_epoch(session, context)?; - let event = receive_navigation_event()?; - let observation = WebDriverBiDiNavigationCommittedObservation::parse_and_match( - &event, + let observation = receive_subscribed_navigation_event( ®istry, session, context, + SESSION_ID, + CONTEXT_ID, EXPECTED_URL, )?; From 20fc200156cb0e58ed58ff4f8112973dae3e942e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 18:24:43 +0900 Subject: [PATCH 09/76] test(network): bind navigation origin through active subscription --- ...ebdriver_bidi_navigation_origin_binding.rs | 121 +++--------------- 1 file changed, 19 insertions(+), 102 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_origin_binding.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_origin_binding.rs index 6ac899ca5..081d89b46 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_origin_binding.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_origin_binding.rs @@ -1,99 +1,16 @@ -use std::{ - error::Error, - io::{self, Read, Write}, - net::{TcpListener, TcpStream}, - thread, - time::Duration, -}; +mod support; + +use std::{error::Error, io}; -use originweave_core::{BrowserAuthorityRegistry, Origin, WebDriverBiDiWebSocketEndpoint}; +use originweave_core::{BrowserAuthorityRegistry, Origin}; use originweave_network::{ WebDriverBiDiNavigationCommittedDocumentAdvanceError, - WebDriverBiDiNavigationCommittedObservation, WebDriverBiDiTcpConnectionPlan, - WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, - WebDriverBiDiWebSocketTextMessage, advance_and_bind_webdriver_bidi_navigation_document_origin, + advance_and_bind_webdriver_bidi_navigation_document_origin, }; +use support::receive_subscribed_navigation_event; 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 write_unmasked_text_frame(stream: &mut TcpStream, payload: &[u8]) -> io::Result<()> { - stream.write_all(&[0x81])?; - match payload.len() { - 0..=125 => stream.write_all(&[payload.len() as u8])?, - 126..=65_535 => { - stream.write_all(&[126])?; - stream.write_all(&(payload.len() as u16).to_be_bytes())?; - } - _ => { - stream.write_all(&[127])?; - stream.write_all(&(payload.len() as u64).to_be_bytes())?; - } - } - stream.write_all(payload) -} - -fn receive_navigation_event( - url: &str, -) -> Result> { - let listener = TcpListener::bind(("127.0.0.1", 0))?; - let local_addr = listener.local_addr()?; - let payload = format!( - "{{\"type\":\"event\",\"method\":\"browsingContext.navigationCommitted\",\"params\":{{\"context\":\"context-a\",\"navigation\":\"navigation-a\",\"timestamp\":17,\"url\":\"{url}\"}}}}" - ) - .into_bytes(); - let server = thread::spawn(move || -> io::Result<()> { - let (mut stream, _) = listener.accept()?; - read_opening_request(&mut stream)?; - stream.write_all(OPENING_RESPONSE)?; - write_unmasked_text_frame(&mut stream, &payload) - }); - - 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!( - "navigation origin-binding event produced unexpected assembly state: {other:?}" - )) - .into()); - } - }; - server - .join() - .map_err(|_| io::Error::other("navigation origin-binding server panicked"))??; - Ok(text) -} +const CONTEXT_ID: &str = "context-a"; fn fixture_origin(value: &str) -> Result> { Origin::parse(value) @@ -105,18 +22,18 @@ fn committed_navigation_rotates_document_and_binds_canonical_observed_origin() -> Result<(), Box> { let mut registry = BrowserAuthorityRegistry::new(); let session = registry.register_session(SESSION_ID)?; - let context = registry.register_context(session, "context-a")?; + let context = registry.register_context(session, CONTEXT_ID)?; let before = registry.current_context_epoch(session, context)?; let previous_origin = fixture_origin("https://before.example")?; registry.bind_context_origin(session, context, &previous_origin)?; let observed_url = "https://EXAMPLE.TEST:443/after?from=originweave#done"; - let event = receive_navigation_event(observed_url)?; - let observation = WebDriverBiDiNavigationCommittedObservation::parse_and_match( - &event, + let observation = receive_subscribed_navigation_event( ®istry, session, context, + SESSION_ID, + CONTEXT_ID, observed_url, )?; let binding = advance_and_bind_webdriver_bidi_navigation_document_origin( @@ -143,18 +60,18 @@ fn invalid_observed_origin_fails_before_document_authority_is_rotated() -> Resul { let mut registry = BrowserAuthorityRegistry::new(); let session = registry.register_session(SESSION_ID)?; - let context = registry.register_context(session, "context-a")?; + let context = registry.register_context(session, CONTEXT_ID)?; let before = registry.current_context_epoch(session, context)?; let previous_origin = fixture_origin("https://before.example")?; registry.bind_context_origin(session, context, &previous_origin)?; let observed_url = "https://user@example.test/after"; - let event = receive_navigation_event(observed_url)?; - let observation = WebDriverBiDiNavigationCommittedObservation::parse_and_match( - &event, + let observation = receive_subscribed_navigation_event( ®istry, session, context, + SESSION_ID, + CONTEXT_ID, observed_url, )?; let error = advance_and_bind_webdriver_bidi_navigation_document_origin( @@ -182,18 +99,18 @@ fn invalid_observed_origin_fails_before_document_authority_is_rotated() -> Resul fn stale_pre_action_epoch_fails_before_observed_origin_is_bound() -> Result<(), Box> { let mut registry = BrowserAuthorityRegistry::new(); let session = registry.register_session(SESSION_ID)?; - let context = registry.register_context(session, "context-a")?; + let context = registry.register_context(session, CONTEXT_ID)?; let before = registry.current_context_epoch(session, context)?; let previous_origin = fixture_origin("https://before.example")?; registry.bind_context_origin(session, context, &previous_origin)?; let observed_url = "https://example.test/after"; - let event = receive_navigation_event(observed_url)?; - let observation = WebDriverBiDiNavigationCommittedObservation::parse_and_match( - &event, + let observation = receive_subscribed_navigation_event( ®istry, session, context, + SESSION_ID, + CONTEXT_ID, observed_url, )?; From 31b5c2aab3656b33b75ba88f9af158534725952a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 19:12:23 +0900 Subject: [PATCH 10/76] test(network): cover subscription admission failure contracts --- .../originweave-network/tests/support/mod.rs | 4 +- ...gation_committed_subscription_admission.rs | 148 +++++++++++++++++- 2 files changed, 150 insertions(+), 2 deletions(-) diff --git a/crates/originweave-network/tests/support/mod.rs b/crates/originweave-network/tests/support/mod.rs index 393d3322f..54b337415 100644 --- a/crates/originweave-network/tests/support/mod.rs +++ b/crates/originweave-network/tests/support/mod.rs @@ -136,7 +136,9 @@ pub fn receive_subscribed_navigation_event( stream.write_all(OPENING_RESPONSE)?; let command = read_masked_text_frame(&mut stream)?; if command != expected_command { - return Err(io::Error::other("unexpected session.subscribe fixture command")); + return Err(io::Error::other( + "unexpected session.subscribe fixture command", + )); } write_text_frame(&mut stream, SUBSCRIBE_RESPONSE)?; write_text_frame(&mut stream, &event) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs index ac8d4782c..fef0c3950 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs @@ -6,9 +6,12 @@ use std::{ time::Duration, }; -use originweave_core::{BrowserAuthorityRegistry, WebDriverBiDiWebSocketEndpoint}; +use originweave_core::{ + BrowserAuthorityRegistry, BrowserSessionId, BrowsingContextId, WebDriverBiDiWebSocketEndpoint, +}; use originweave_network::{ WebDriverBiDiCommandCorrelation, WebDriverBiDiNavigationCommittedSubscriptionAdmission, + WebDriverBiDiNavigationCommittedSubscriptionBinding, WebDriverBiDiNavigationCommittedSubscriptionCommand, WebDriverBiDiNavigationCommittedSubscriptionResult, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, @@ -113,6 +116,83 @@ fn next_text( } } +fn receive_subscription_result( + registry: &BrowserAuthorityRegistry, + browser_session: BrowserSessionId, + browsing_context: BrowsingContextId, + command_id: u64, +) -> Result< + ( + WebDriverBiDiNavigationCommittedSubscriptionResult, + WebDriverBiDiNavigationCommittedSubscriptionBinding, + ), + Box, +> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let expected_command = format!( + "{{\"id\":{command_id},\"method\":\"session.subscribe\",\"params\":{{\"events\":[\"browsingContext.navigationCommitted\"],\"contexts\":[\"{CONTEXT_ID}\"]}}}}" + ) + .into_bytes(); + let response = format!( + "{{\"type\":\"success\",\"id\":{command_id},\"result\":{{\"subscription\":\"subscription-{command_id}\"}}}}" + ) + .into_bytes(); + let server = thread::spawn(move || -> io::Result<()> { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + let command = read_masked_text_frame(&mut stream)?; + if command != expected_command { + return Err(io::Error::other( + "unexpected session.subscribe failure-contract command", + )); + } + write_text_frame(&mut stream, &response) + }); + + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? + .correlate_session_id(SESSION_ID)? + .into_explicit_connect_target()?; + let connection = + WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; + let established = WebDriverBiDiWebSocketHandshakePlan::new( + connection, + WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?, + )? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?; + + let command = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( + command_id, + registry, + browser_session, + browsing_context, + CONTEXT_ID, + )?; + let binding = command.admission_binding(); + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + let established = command.send( + registry, + established, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([9, 8, 7, 6]), + Duration::from_millis(500), + )?; + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); + let (_established, response) = next_text(established, &mut assembler)?; + let result = WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( + &response, + &mut correlation, + )?; + + server + .join() + .map_err(|_| io::Error::other("subscription result test server panicked"))??; + Ok((result, binding)) +} + #[test] fn committed_navigation_requires_the_exact_active_subscription_before_document_mutation() -> Result<(), Box> { @@ -154,6 +234,13 @@ fn committed_navigation_requires_the_exact_active_subscription_before_document_m 7, ®istry, session, context, CONTEXT_ID, )?; let binding = command.admission_binding(); + assert_eq!(binding.command_id(), 7); + assert_eq!(binding.browser_session(), session); + assert_eq!(binding.browsing_context(), context); + let binding_debug = format!("{binding:?}"); + assert!(binding_debug.contains("command_id: 7")); + assert!(!binding_debug.contains(CONTEXT_ID)); + let mut correlation = WebDriverBiDiCommandCorrelation::new(); let established = command.send( ®istry, @@ -174,12 +261,22 @@ fn committed_navigation_requires_the_exact_active_subscription_before_document_m binding, ®istry, )?; + assert_eq!(admission.browser_session(), session); + assert_eq!(admission.browsing_context(), context); + let admission_debug = format!("{admission:?}"); + assert!(admission_debug.contains("command_id: 7")); + assert!(!admission_debug.contains("subscription-a")); let (_established, event) = next_text(established, &mut assembler)?; let observation = admission.admit(&event, ®istry, EXPECTED_URL)?; assert_eq!(observation.browser_session(), session); assert_eq!(observation.browsing_context(), context); assert_eq!(observation.navigation_id(), Some("nav-8")); + assert_eq!(observation.timestamp(), 1234); + assert!( + format!("{observation:?}") + .contains("WebDriverBiDiNavigationCommittedSubscribedObservation") + ); let advanced = advance_webdriver_bidi_navigation_document_epoch( observation, @@ -189,6 +286,13 @@ fn committed_navigation_requires_the_exact_active_subscription_before_document_m assert_eq!(advanced.browser_session(), session); assert_eq!(advanced.browsing_context(), context); + registry.remove_context(context)?; + let stale_error = admission + .admit(&event, ®istry, EXPECTED_URL) + .err() + .ok_or_else(|| io::Error::other("retired context unexpectedly admitted navigation event"))?; + assert!(stale_error.source().is_some()); + let unsubscribe = admission.into_unsubscribe(8)?; assert_eq!(unsubscribe.command_id(), 8); @@ -197,3 +301,45 @@ fn committed_navigation_requires_the_exact_active_subscription_before_document_m .map_err(|_| io::Error::other("subscription admission test server panicked"))??; Ok(()) } + +#[test] +fn subscription_admission_rejects_mismatched_command_and_retired_context() +-> Result<(), Box> { + let mut registry = BrowserAuthorityRegistry::new(); + let session = registry.register_session(SESSION_ID)?; + let context = registry.register_context(session, CONTEXT_ID)?; + + let (subscription, _) = receive_subscription_result(®istry, session, context, 8)?; + let wrong_binding = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( + 9, ®istry, session, context, CONTEXT_ID, + )? + .admission_binding(); + let mismatch = WebDriverBiDiNavigationCommittedSubscriptionAdmission::new( + subscription, + wrong_binding, + ®istry, + ) + .err() + .ok_or_else(|| io::Error::other("mismatched subscription command unexpectedly admitted"))?; + assert_eq!( + mismatch.to_string(), + "WebDriver BiDi navigation subscription response does not match its command binding" + ); + assert!(mismatch.source().is_none()); + + let (subscription, binding) = receive_subscription_result(®istry, session, context, 10)?; + registry.remove_context(context)?; + let retired = WebDriverBiDiNavigationCommittedSubscriptionAdmission::new( + subscription, + binding, + ®istry, + ) + .err() + .ok_or_else(|| io::Error::other("retired subscription context unexpectedly admitted"))?; + assert_eq!( + retired.to_string(), + "WebDriver BiDi navigation subscription context is no longer registered authority" + ); + assert!(retired.source().is_some()); + Ok(()) +} From 9e151d690ed0bcb5231e157070d6f3a84e243699 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 19:18:37 +0900 Subject: [PATCH 11/76] style(network): apply canonical subscription admission format --- ...driver_bidi_navigation_committed_subscription_admission.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs index fef0c3950..a26c16713 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs @@ -290,7 +290,9 @@ fn committed_navigation_requires_the_exact_active_subscription_before_document_m let stale_error = admission .admit(&event, ®istry, EXPECTED_URL) .err() - .ok_or_else(|| io::Error::other("retired context unexpectedly admitted navigation event"))?; + .ok_or_else(|| { + io::Error::other("retired context unexpectedly admitted navigation event") + })?; assert!(stale_error.source().is_some()); let unsubscribe = admission.into_unsubscribe(8)?; From df9d64392ad099dca21f2dbc8b005ddfe42cbf26 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 19:35:15 +0900 Subject: [PATCH 12/76] test(network): reject replayed subscribed navigation event --- ...igation_committed_subscription_admission.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs index a26c16713..88613a52e 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs @@ -256,7 +256,7 @@ fn committed_navigation_requires_the_exact_active_subscription_before_document_m &response, &mut correlation, )?; - let admission = WebDriverBiDiNavigationCommittedSubscriptionAdmission::new( + let mut admission = WebDriverBiDiNavigationCommittedSubscriptionAdmission::new( subscription, binding, ®istry, @@ -286,6 +286,22 @@ fn committed_navigation_requires_the_exact_active_subscription_before_document_m assert_eq!(advanced.browser_session(), session); assert_eq!(advanced.browsing_context(), context); + let replay_error = admission + .admit(&event, ®istry, EXPECTED_URL) + .err() + .ok_or_else(|| { + io::Error::other("replayed navigation event unexpectedly readmitted") + })?; + assert_eq!( + replay_error.to_string(), + "WebDriver BiDi navigation-committed event was already admitted by this active subscription" + ); + assert!(replay_error.source().is_none()); + assert_eq!( + registry.current_context_epoch(session, context)?, + advanced.current_epoch() + ); + registry.remove_context(context)?; let stale_error = admission .admit(&event, ®istry, EXPECTED_URL) From 3461bdcb8008d1e898f0a3f55e41ba70a483e7da Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 19:38:24 +0900 Subject: [PATCH 13/76] style(network): apply canonical replay regression format --- ...driver_bidi_navigation_committed_subscription_admission.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs index 88613a52e..1e1749704 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs @@ -289,9 +289,7 @@ fn committed_navigation_requires_the_exact_active_subscription_before_document_m let replay_error = admission .admit(&event, ®istry, EXPECTED_URL) .err() - .ok_or_else(|| { - io::Error::other("replayed navigation event unexpectedly readmitted") - })?; + .ok_or_else(|| io::Error::other("replayed navigation event unexpectedly readmitted"))?; assert_eq!( replay_error.to_string(), "WebDriver BiDi navigation-committed event was already admitted by this active subscription" From f88521a0ff3634185a13d08d4b03d4ab92eab085 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 19:43:10 +0900 Subject: [PATCH 14/76] fix(network): prevent subscribed navigation replay --- ...gation_committed_subscription_admission.rs | 117 ++++++++++++++++-- 1 file changed, 110 insertions(+), 7 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_admission.rs b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_admission.rs index 458a996f5..0f94c853a 100644 --- a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_admission.rs +++ b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_admission.rs @@ -11,6 +11,13 @@ use crate::{ WebDriverBiDiNavigationCommittedUnsubscribeCommandError, WebDriverBiDiWebSocketTextMessage, }; +/// Maximum distinct committed-navigation identifiers retained by one active subscription admission. +/// +/// Exhaustion fails closed instead of evicting old identifiers because eviction would permit an old +/// protocol event to become fresh state-changing evidence again. Callers can explicitly unsubscribe +/// and establish a new typed subscription when this reviewed per-subscription resource bound is met. +pub const MAX_WEBDRIVER_BIDI_NAVIGATION_COMMITTED_ADMISSIONS: usize = 256; + /// Immutable command-side binding retained before a committed-navigation subscription is sent. /// /// The binding carries only the exact local command identifier and the already-registered @@ -78,9 +85,12 @@ impl WebDriverBiDiNavigationCommittedSubscriptionBinding { /// Holding this value is therefore narrower than holding an opaque protocol subscription string. /// It grants only admission of the matching committed-navigation event through the existing bounded /// parser; it grants no navigation, destination, origin, policy, secret, node, or Agent authority. +/// Each admitted non-null WebDriver BiDi navigation identifier is retained until unsubscribe so a +/// replayed remote event cannot mint a second state-changing observation from the same navigation. pub struct WebDriverBiDiNavigationCommittedSubscriptionAdmission { subscription: WebDriverBiDiNavigationCommittedSubscriptionResult, binding: WebDriverBiDiNavigationCommittedSubscriptionBinding, + admitted_navigation_ids: Vec, } impl fmt::Debug for WebDriverBiDiNavigationCommittedSubscriptionAdmission { @@ -94,6 +104,7 @@ impl fmt::Debug for WebDriverBiDiNavigationCommittedSubscriptionAdmission { "subscription_id_bytes", &self.subscription.subscription_id().len(), ) + .field("admitted_navigation_count", &self.admitted_navigation_ids.len()) .finish() } } @@ -123,6 +134,7 @@ impl WebDriverBiDiNavigationCommittedSubscriptionAdmission { Ok(Self { subscription, binding, + admitted_navigation_ids: Vec::new(), }) } @@ -142,28 +154,60 @@ impl WebDriverBiDiNavigationCommittedSubscriptionAdmission { /// /// The original command-side external-context mapping is revalidated immediately before parsing /// the event. The event must then independently carry that same registered context and the exact - /// declared URL. The returned subscribed observation is the only navigation observation type - /// accepted by the state-changing document-advance boundary. + /// declared URL. State-changing admission additionally requires the WebDriver BiDi navigation + /// identifier to be present and unique within this active subscription. The specification defines + /// non-null navigation identifiers as unique identifiers for ongoing navigations; retaining them + /// prevents replay of an already-admitted event. The history is resource bounded and fails closed + /// at capacity rather than evicting evidence that would make an older replay admissible again. + /// The returned subscribed observation is the only navigation observation type accepted by the + /// state-changing document-advance boundary. pub fn admit( - &self, + &mut self, message: &WebDriverBiDiWebSocketTextMessage, registry: &BrowserAuthorityRegistry, expected_url: &str, ) -> Result< WebDriverBiDiNavigationCommittedSubscribedObservation, - WebDriverBiDiNavigationCommittedObservationError, + WebDriverBiDiNavigationCommittedSubscriptionEventError, > { require_current_binding(registry, &self.binding).map_err(|source| { - WebDriverBiDiNavigationCommittedObservationError::ContextBinding { source } + WebDriverBiDiNavigationCommittedSubscriptionEventError::ContextBinding { source } })?; - WebDriverBiDiNavigationCommittedObservation::parse_and_match( + let observation = WebDriverBiDiNavigationCommittedObservation::parse_and_match( message, registry, self.binding.browser_session, self.binding.browsing_context, expected_url, ) - .map(WebDriverBiDiNavigationCommittedSubscribedObservation) + .map_err(|source| WebDriverBiDiNavigationCommittedSubscriptionEventError::Observation { + source, + })?; + let navigation_id = observation.navigation_id().ok_or( + WebDriverBiDiNavigationCommittedSubscriptionEventError::MissingNavigationIdentity, + )?; + if self + .admitted_navigation_ids + .iter() + .any(|admitted| admitted == navigation_id) + { + return Err( + WebDriverBiDiNavigationCommittedSubscriptionEventError::ReplayedNavigation, + ); + } + if self.admitted_navigation_ids.len() + >= MAX_WEBDRIVER_BIDI_NAVIGATION_COMMITTED_ADMISSIONS + { + return Err( + WebDriverBiDiNavigationCommittedSubscriptionEventError::ReplayHistoryExhausted { + maximum_events: MAX_WEBDRIVER_BIDI_NAVIGATION_COMMITTED_ADMISSIONS, + }, + ); + } + self.admitted_navigation_ids.push(navigation_id.to_owned()); + Ok(WebDriverBiDiNavigationCommittedSubscribedObservation( + observation, + )) } /// Consume active event admission and construct teardown for this exact subscription receipt. @@ -282,3 +326,62 @@ impl Error for WebDriverBiDiNavigationCommittedSubscriptionAdmissionError { } } } + +/// Fail-closed failures while admitting an event through one active subscription capability. +#[derive(Debug)] +pub enum WebDriverBiDiNavigationCommittedSubscriptionEventError { + /// The original external context no longer maps to the exact registered session/context pair. + ContextBinding { + /// Exact browser-registry authority failure. + source: BrowserRegistryError, + }, + /// The bounded committed-navigation observation itself could not be admitted. + Observation { + /// Underlying typed observation failure. + source: WebDriverBiDiNavigationCommittedObservationError, + }, + /// The event did not carry a non-null navigation identity suitable for state-changing evidence. + MissingNavigationIdentity, + /// The same WebDriver BiDi navigation identity was already admitted by this active subscription. + ReplayedNavigation, + /// The bounded replay-prevention history is full and must not evict older evidence. + ReplayHistoryExhausted { + /// Maximum distinct navigation identities retained by one active subscription admission. + maximum_events: usize, + }, +} + +impl fmt::Display for WebDriverBiDiNavigationCommittedSubscriptionEventError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::ContextBinding { .. } => formatter.write_str( + "WebDriver BiDi navigation subscription context is no longer registered authority", + ), + Self::Observation { .. } => { + formatter.write_str("WebDriver BiDi navigation-committed event is not admissible") + } + Self::MissingNavigationIdentity => formatter.write_str( + "WebDriver BiDi navigation-committed event has no reusable-safe navigation identity", + ), + Self::ReplayedNavigation => formatter.write_str( + "WebDriver BiDi navigation-committed event was already admitted by this active subscription", + ), + Self::ReplayHistoryExhausted { maximum_events } => write!( + formatter, + "WebDriver BiDi navigation subscription reached its {maximum_events}-event replay-history limit" + ), + } + } +} + +impl Error for WebDriverBiDiNavigationCommittedSubscriptionEventError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::ContextBinding { source } => Some(source), + Self::Observation { source } => Some(source), + Self::MissingNavigationIdentity + | Self::ReplayedNavigation + | Self::ReplayHistoryExhausted { .. } => None, + } + } +} From 1434d248b6fe098b7553f8c40056cc64020014cd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 19:44:08 +0900 Subject: [PATCH 15/76] docs(network): expose replay-bounded admission contract --- crates/originweave-network/src/lib.rs | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/crates/originweave-network/src/lib.rs b/crates/originweave-network/src/lib.rs index 5af6f1038..9517f6592 100644 --- a/crates/originweave-network/src/lib.rs +++ b/crates/originweave-network/src/lib.rs @@ -12,17 +12,17 @@ //! correlated protocol acknowledgment, sends a context-bound subscription for //! committed-navigation events, retains its typed bounded correlated subscription //! identifier, binds navigation-event admission to that exact active command/receipt -//! lifecycle, explicitly tears down that exact subscription by identifier, admits -//! its typed correlated unsubscribe acknowledgment, admits a bounded -//! navigation-committed post-condition observation for one exact registered context -//! and URL, rotates the matched context's document epoch only from an exact -//! caller-captured pre-action epoch, derives and binds the committed HTTP(S) URL's -//! canonical origin to that newly advanced document, sends narrowly typed -//! `session.status` and `session.end` commands, admits typed correlated status and -//! end responses, observes bounded peer Close or clean-EOF transport cessation, and -//! keeps protocol/transport evidence separate from explicit operational teardown -//! observations without exposing generic JSON bodies or granting browser, TLS, -//! policy, secret, process, profile, or Agent authority. +//! lifecycle with bounded fail-closed navigation replay prevention, explicitly tears +//! down that exact subscription by identifier, admits its typed correlated unsubscribe +//! acknowledgment, admits a bounded navigation-committed post-condition observation +//! for one exact registered context and URL, rotates the matched context's document +//! epoch only from an exact caller-captured pre-action epoch, derives and binds the +//! committed HTTP(S) URL's canonical origin to that newly advanced document, sends +//! narrowly typed `session.status` and `session.end` commands, admits typed correlated +//! status and end responses, observes bounded peer Close or clean-EOF transport +//! cessation, and keeps protocol/transport evidence separate from explicit operational +//! teardown observations without exposing generic JSON bodies or granting browser, +//! TLS, policy, secret, process, profile, or Agent authority. #![forbid(unsafe_code)] #![deny(missing_docs)] @@ -83,10 +83,12 @@ pub use webdriver_bidi_navigation_committed_subscription::{ WebDriverBiDiNavigationCommittedSubscriptionCommandError, }; pub use webdriver_bidi_navigation_committed_subscription_admission::{ + MAX_WEBDRIVER_BIDI_NAVIGATION_COMMITTED_ADMISSIONS, WebDriverBiDiNavigationCommittedSubscribedObservation, WebDriverBiDiNavigationCommittedSubscriptionAdmission, WebDriverBiDiNavigationCommittedSubscriptionAdmissionError, WebDriverBiDiNavigationCommittedSubscriptionBinding, + WebDriverBiDiNavigationCommittedSubscriptionEventError, }; pub use webdriver_bidi_navigation_committed_subscription_response::{ MAX_WEBDRIVER_BIDI_SUBSCRIPTION_IDENTIFIER_BYTES, From 4dc8c5cf2bc728b110e28ab67ab7dd03915322a2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 19:48:25 +0900 Subject: [PATCH 16/76] style(network): apply canonical replay guard formatting --- ...avigation_committed_subscription_admission.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_admission.rs b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_admission.rs index 0f94c853a..af042517b 100644 --- a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_admission.rs +++ b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_admission.rs @@ -104,7 +104,10 @@ impl fmt::Debug for WebDriverBiDiNavigationCommittedSubscriptionAdmission { "subscription_id_bytes", &self.subscription.subscription_id().len(), ) - .field("admitted_navigation_count", &self.admitted_navigation_ids.len()) + .field( + "admitted_navigation_count", + &self.admitted_navigation_ids.len(), + ) .finish() } } @@ -180,8 +183,8 @@ impl WebDriverBiDiNavigationCommittedSubscriptionAdmission { self.binding.browsing_context, expected_url, ) - .map_err(|source| WebDriverBiDiNavigationCommittedSubscriptionEventError::Observation { - source, + .map_err(|source| { + WebDriverBiDiNavigationCommittedSubscriptionEventError::Observation { source } })?; let navigation_id = observation.navigation_id().ok_or( WebDriverBiDiNavigationCommittedSubscriptionEventError::MissingNavigationIdentity, @@ -191,12 +194,9 @@ impl WebDriverBiDiNavigationCommittedSubscriptionAdmission { .iter() .any(|admitted| admitted == navigation_id) { - return Err( - WebDriverBiDiNavigationCommittedSubscriptionEventError::ReplayedNavigation, - ); + return Err(WebDriverBiDiNavigationCommittedSubscriptionEventError::ReplayedNavigation); } - if self.admitted_navigation_ids.len() - >= MAX_WEBDRIVER_BIDI_NAVIGATION_COMMITTED_ADMISSIONS + if self.admitted_navigation_ids.len() >= MAX_WEBDRIVER_BIDI_NAVIGATION_COMMITTED_ADMISSIONS { return Err( WebDriverBiDiNavigationCommittedSubscriptionEventError::ReplayHistoryExhausted { From 24281e2a59d2e92787c2703081c7e6b9e06120ea Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 19:59:56 +0900 Subject: [PATCH 17/76] test(network): update navigation fixture for mutable admission --- crates/originweave-network/tests/support/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/originweave-network/tests/support/mod.rs b/crates/originweave-network/tests/support/mod.rs index 54b337415..45dae6ccb 100644 --- a/crates/originweave-network/tests/support/mod.rs +++ b/crates/originweave-network/tests/support/mod.rs @@ -180,7 +180,7 @@ pub fn receive_subscribed_navigation_event( &response, &mut correlation, )?; - let admission = + let mut admission = WebDriverBiDiNavigationCommittedSubscriptionAdmission::new(result, binding, registry)?; let (_established, event) = next_text(established, &mut assembler)?; let observation = admission.admit(&event, registry, expected_url)?; From f6f6dcc72ae3cac5687dbd341f5b08633434798a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 20:16:21 +0900 Subject: [PATCH 18/76] test(network): cover navigation admission failure bounds --- ...gation_committed_subscription_admission.rs | 48 +++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs index 1e1749704..15379be3b 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs @@ -10,7 +10,8 @@ use originweave_core::{ BrowserAuthorityRegistry, BrowserSessionId, BrowsingContextId, WebDriverBiDiWebSocketEndpoint, }; use originweave_network::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiNavigationCommittedSubscriptionAdmission, + MAX_WEBDRIVER_BIDI_NAVIGATION_COMMITTED_ADMISSIONS, WebDriverBiDiCommandCorrelation, + WebDriverBiDiNavigationCommittedSubscriptionAdmission, WebDriverBiDiNavigationCommittedSubscriptionBinding, WebDriverBiDiNavigationCommittedSubscriptionCommand, WebDriverBiDiNavigationCommittedSubscriptionResult, WebDriverBiDiTcpConnectionPlan, @@ -209,7 +210,18 @@ fn committed_navigation_requires_the_exact_active_subscription_before_document_m return Err(io::Error::other("unexpected session.subscribe command")); } write_text_frame(&mut stream, SUBSCRIBE_RESPONSE)?; - write_text_frame(&mut stream, NAVIGATION_EVENT) + write_text_frame(&mut stream, NAVIGATION_EVENT)?; + for index in 1..MAX_WEBDRIVER_BIDI_NAVIGATION_COMMITTED_ADMISSIONS { + let event = format!( + "{{\"type\":\"event\",\"method\":\"browsingContext.navigationCommitted\",\"params\":{{\"context\":\"{CONTEXT_ID}\",\"navigation\":\"nav-fill-{index}\",\"timestamp\":{},\"url\":\"{EXPECTED_URL}\"}}}}", + 2_000 + index + ); + write_text_frame(&mut stream, event.as_bytes())?; + } + let overflow_event = format!( + "{{\"type\":\"event\",\"method\":\"browsingContext.navigationCommitted\",\"params\":{{\"context\":\"{CONTEXT_ID}\",\"navigation\":\"nav-overflow\",\"timestamp\":9999,\"url\":\"{EXPECTED_URL}\"}}}}" + ); + write_text_frame(&mut stream, overflow_event.as_bytes()) }); let mut registry = BrowserAuthorityRegistry::new(); @@ -267,7 +279,17 @@ fn committed_navigation_requires_the_exact_active_subscription_before_document_m assert!(admission_debug.contains("command_id: 7")); assert!(!admission_debug.contains("subscription-a")); - let (_established, event) = next_text(established, &mut assembler)?; + let (mut established, event) = next_text(established, &mut assembler)?; + let observation_error = admission + .admit(&event, ®istry, "https://example.test/unexpected") + .err() + .ok_or_else(|| io::Error::other("mismatched navigation URL unexpectedly admitted"))?; + assert_eq!( + observation_error.to_string(), + "WebDriver BiDi navigation-committed event is not admissible" + ); + assert!(observation_error.source().is_some()); + let observation = admission.admit(&event, ®istry, EXPECTED_URL)?; assert_eq!(observation.browser_session(), session); assert_eq!(observation.browsing_context(), context); @@ -300,6 +322,26 @@ fn committed_navigation_requires_the_exact_active_subscription_before_document_m advanced.current_epoch() ); + for _ in 1..MAX_WEBDRIVER_BIDI_NAVIGATION_COMMITTED_ADMISSIONS { + let (next_established, fill_event) = next_text(established, &mut assembler)?; + established = next_established; + admission.admit(&fill_event, ®istry, EXPECTED_URL)?; + } + let (_established, overflow_event) = next_text(established, &mut assembler)?; + let exhausted = admission + .admit(&overflow_event, ®istry, EXPECTED_URL) + .err() + .ok_or_else(|| { + io::Error::other("full navigation replay history unexpectedly admitted another event") + })?; + assert_eq!( + exhausted.to_string(), + format!( + "WebDriver BiDi navigation subscription reached its {MAX_WEBDRIVER_BIDI_NAVIGATION_COMMITTED_ADMISSIONS}-event replay-history limit" + ) + ); + assert!(exhausted.source().is_none()); + registry.remove_context(context)?; let stale_error = admission .admit(&event, ®istry, EXPECTED_URL) From 45b73a6b9357636e362ab5de53005b95c6c1a21b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 20:34:58 +0900 Subject: [PATCH 19/76] test(network): exercise subscription admission diagnostics --- ...mmitted_subscription_admission_failures.rs | 238 ++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission_failures.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission_failures.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission_failures.rs new file mode 100644 index 000000000..a50047c9a --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission_failures.rs @@ -0,0 +1,238 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::{ + BrowserAuthorityRegistry, BrowserSessionId, BrowsingContextId, WebDriverBiDiWebSocketEndpoint, +}; +use originweave_network::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiNavigationCommittedSubscriptionAdmission, + WebDriverBiDiNavigationCommittedSubscriptionBinding, + WebDriverBiDiNavigationCommittedSubscriptionCommand, + WebDriverBiDiNavigationCommittedSubscriptionResult, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler, + WebDriverBiDiWebSocketMessageAssembly, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; +const CONTEXT_ID: &str = "context-a"; +const EXPECTED_URL: &str = "https://example.test/after"; +const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; +const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; +const MISSING_NAVIGATION_EVENT: &[u8] = br#"{"type":"event","method":"browsingContext.navigationCommitted","params":{"context":"context-a","navigation":null,"timestamp":1234,"url":"https://example.test/after"}}"#; + +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, + "opening request ended before the header terminator", + )); + } + request.extend_from_slice(&buffer[..count]); + } + Ok(()) +} + +fn read_masked_text_frame(stream: &mut TcpStream) -> io::Result> { + let mut header = [0_u8; 2]; + stream.read_exact(&mut header)?; + if header[0] != 0x81 || header[1] & 0x80 == 0 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "expected one final masked client text frame", + )); + } + let length = match header[1] & 0x7f { + length @ 0..=125 => usize::from(length), + 126 => { + let mut extended = [0_u8; 2]; + stream.read_exact(&mut extended)?; + usize::from(u16::from_be_bytes(extended)) + } + _ => { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "test command unexpectedly required 64-bit framing", + )); + } + }; + let mut mask = [0_u8; 4]; + stream.read_exact(&mut mask)?; + let mut payload = vec![0_u8; length]; + stream.read_exact(&mut payload)?; + for (index, byte) in payload.iter_mut().enumerate() { + *byte ^= mask[index % mask.len()]; + } + Ok(payload) +} + +fn write_text_frame(stream: &mut TcpStream, payload: &[u8]) -> io::Result<()> { + stream.write_all(&[0x81])?; + match payload.len() { + 0..=125 => stream.write_all(&[payload.len() as u8])?, + 126..=65_535 => { + stream.write_all(&[126])?; + stream.write_all(&(payload.len() as u16).to_be_bytes())?; + } + _ => { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "test payload unexpectedly required 64-bit framing", + )); + } + } + stream.write_all(payload) +} + +fn next_text( + established: originweave_network::WebDriverBiDiWebSocketEstablished, +) -> Result> { + let (_established, frame) = established.read_frame(Duration::from_millis(500))?; + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); + match assembler.push_frame(frame)? { + WebDriverBiDiWebSocketMessageAssembly::Text(text) => Ok(text), + other => Err(io::Error::other(format!( + "expected a complete WebDriver BiDi text message, got {other:?}" + )) + .into()), + } +} + +fn establish(local_addr: std::net::SocketAddr) -> Result> { + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? + .correlate_session_id(SESSION_ID)? + .into_explicit_connect_target()?; + let connection = + WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; + Ok(WebDriverBiDiWebSocketHandshakePlan::new( + connection, + WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?, + )? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?) +} + +fn receive_subscription_result( + registry: &BrowserAuthorityRegistry, + browser_session: BrowserSessionId, + browsing_context: BrowsingContextId, +) -> Result< + ( + WebDriverBiDiNavigationCommittedSubscriptionResult, + WebDriverBiDiNavigationCommittedSubscriptionBinding, + ), + Box, +> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let server = thread::spawn(move || -> io::Result<()> { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + let command = read_masked_text_frame(&mut stream)?; + if command + != br#"{"id":7,"method":"session.subscribe","params":{"events":["browsingContext.navigationCommitted"],"contexts":["context-a"]}}"# + { + return Err(io::Error::other("unexpected session.subscribe command")); + } + write_text_frame( + &mut stream, + br#"{"type":"success","id":7,"result":{"subscription":"subscription-a"}}"#, + ) + }); + + let established = establish(local_addr)?; + let command = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( + 7, + registry, + browser_session, + browsing_context, + CONTEXT_ID, + )?; + let binding = command.admission_binding(); + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + let established = command.send( + registry, + established, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + Duration::from_millis(500), + )?; + let response = next_text(established)?; + let result = WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( + &response, + &mut correlation, + )?; + + server + .join() + .map_err(|_| io::Error::other("subscription failure-contract server panicked"))??; + Ok((result, binding)) +} + +fn receive_event( + payload: &'static [u8], +) -> 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)?; + write_text_frame(&mut stream, payload) + }); + + let event = next_text(establish(local_addr)?)?; + server + .join() + .map_err(|_| io::Error::other("event failure-contract server panicked"))??; + Ok(event) +} + +#[test] +fn subscription_event_failures_keep_specific_public_diagnostics() -> Result<(), Box> { + let mut registry = BrowserAuthorityRegistry::new(); + let session = registry.register_session(SESSION_ID)?; + let context = registry.register_context(session, CONTEXT_ID)?; + let (subscription, binding) = receive_subscription_result(®istry, session, context)?; + let mut admission = WebDriverBiDiNavigationCommittedSubscriptionAdmission::new( + subscription, + binding, + ®istry, + )?; + let missing_navigation_event = receive_event(MISSING_NAVIGATION_EVENT)?; + + let missing_navigation = admission + .admit(&missing_navigation_event, ®istry, EXPECTED_URL) + .err() + .ok_or_else(|| io::Error::other("null navigation identity unexpectedly admitted"))?; + assert_eq!( + missing_navigation.to_string(), + "WebDriver BiDi navigation-committed event has no reusable-safe navigation identity" + ); + assert!(missing_navigation.source().is_none()); + + registry.remove_context(context)?; + let stale_context = admission + .admit(&missing_navigation_event, ®istry, EXPECTED_URL) + .err() + .ok_or_else(|| io::Error::other("retired context unexpectedly admitted an event"))?; + assert_eq!( + stale_context.to_string(), + "WebDriver BiDi navigation subscription context is no longer registered authority" + ); + assert!(stale_context.source().is_some()); + + Ok(()) +} From 86dce3eb1e29228b3f1d0277c0a9c49629bfa90a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 20:38:50 +0900 Subject: [PATCH 20/76] style(network): apply canonical rustfmt --- ...di_navigation_committed_subscription_admission_failures.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission_failures.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission_failures.rs index a50047c9a..d88ca83b3 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission_failures.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission_failures.rs @@ -108,7 +108,9 @@ fn next_text( } } -fn establish(local_addr: std::net::SocketAddr) -> Result> { +fn establish( + local_addr: std::net::SocketAddr, +) -> Result> { let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? .correlate_session_id(SESSION_ID)? From 44c44b46dd4eabf8f09adc4e1da7c608f3bbf1be Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 16:19:20 +0900 Subject: [PATCH 21/76] test: replay received-response provenance before parent adoption The unchanged current-parent loopback regression cannot compile on the admission predecessor: its connection-bound reader types and transport-mismatch rejection variant are absent. Record the inherited RED contract before ordinary adoption; do not replace it with raw assembled response evidence. Commit-Message-Assisted-by: Codex (via Codex) Signed-off-by: Seongho Bae --- ...sion_end_response_connection_provenance.rs | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_session_end_response_connection_provenance.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_session_end_response_connection_provenance.rs b/crates/originweave-network/tests/webdriver_bidi_session_end_response_connection_provenance.rs new file mode 100644 index 000000000..2f3c69fa0 --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_session_end_response_connection_provenance.rs @@ -0,0 +1,160 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{SocketAddr, TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::WebDriverBiDiWebSocketEndpoint; +use originweave_network::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiConnectionMessageRead, + WebDriverBiDiReceivedTextMessage, WebDriverBiDiSessionEndCommand, + WebDriverBiDiSessionEndResponseError, WebDriverBiDiSessionEndResult, + WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, + WebDriverBiDiWebSocketMessageReader, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; +const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; +const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; +const END_SUCCESS_RESPONSE: &[u8] = br#"{"type":"success","id":7,"result":{}}"#; + +fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { + stream.set_read_timeout(Some(Duration::from_secs(2)))?; + let mut request = Vec::new(); + let mut buffer = [0_u8; 512]; + while !request.ends_with(b"\r\n\r\n") { + let count = stream.read(&mut buffer)?; + if count == 0 { + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "client opening request ended before the header terminator", + )); + } + request.extend_from_slice(&buffer[..count]); + } + Ok(()) +} + +fn read_masked_text_frame(stream: &mut TcpStream) -> io::Result> { + let mut header = [0_u8; 2]; + stream.read_exact(&mut header)?; + if header[0] != 0x81 || header[1] & 0x80 == 0 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "expected one final masked client text frame", + )); + } + let length = usize::from(header[1] & 0x7f); + if length > 125 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "session.end command unexpectedly required extended framing", + )); + } + let mut mask = [0_u8; 4]; + stream.read_exact(&mut mask)?; + let mut payload = vec![0_u8; length]; + stream.read_exact(&mut payload)?; + for (index, byte) in payload.iter_mut().enumerate() { + *byte ^= mask[index % mask.len()]; + } + Ok(payload) +} + +fn establish( + local_addr: SocketAddr, +) -> Result> { + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? + .correlate_session_id(SESSION_ID)? + .into_explicit_connect_target()?; + let connection = + WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; + let key = WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?; + Ok(WebDriverBiDiWebSocketHandshakePlan::new(connection, key)? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?) +} + +fn read_one_connection_bound_text( + established: originweave_network::WebDriverBiDiWebSocketEstablished, +) -> Result> { + let mut reader = WebDriverBiDiWebSocketMessageReader::new(established); + loop { + match reader.read_next(Duration::from_millis(500))? { + WebDriverBiDiConnectionMessageRead::Pending(next) => reader = next, + WebDriverBiDiConnectionMessageRead::Text { message, .. } => return Ok(message), + WebDriverBiDiConnectionMessageRead::Control { message, .. } => { + return Err(io::Error::other(format!( + "foreign response produced unexpected control message: {message:?}" + )) + .into()); + } + } + } +} + +#[test] +fn reconnected_response_cannot_consume_prior_connection_command() -> Result<(), Box> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let server = thread::spawn(move || -> io::Result<()> { + let (mut first, _) = listener.accept()?; + read_opening_request(&mut first)?; + first.write_all(OPENING_RESPONSE)?; + let command = read_masked_text_frame(&mut first)?; + if command != br#"{"id":7,"method":"session.end","params":{}}"# { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "unexpected session.end command on first connection", + )); + } + + let (mut second, _) = listener.accept()?; + read_opening_request(&mut second)?; + second.write_all(OPENING_RESPONSE)?; + second.write_all(&[0x81, END_SUCCESS_RESPONSE.len() as u8])?; + second.write_all(END_SUCCESS_RESPONSE)?; + Ok(()) + }); + + let first = establish(local_addr)?; + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + let _first = WebDriverBiDiSessionEndCommand::new(7)?.send( + first, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + Duration::from_millis(500), + )?; + assert_eq!(correlation.outstanding_count(), 1); + + let second = establish(local_addr)?; + let foreign_response = read_one_connection_bound_text(second)?; + let parsed = + WebDriverBiDiSessionEndResult::parse_and_correlate(&foreign_response, &mut correlation); + let error = parsed + .err() + .ok_or_else(|| io::Error::other("foreign response acknowledged prior connection"))?; + + assert!(matches!( + error, + WebDriverBiDiSessionEndResponseError::TransportConnectionMismatch { command_id: 7 } + )); + assert_eq!( + error.to_string(), + "WebDriver BiDi session.end response arrived on a different connection" + ); + assert_eq!( + correlation.outstanding_count(), + 1, + "foreign response must not consume connection A correlation" + ); + + server + .join() + .map_err(|_| io::Error::other("connection-provenance test server panicked"))??; + Ok(()) +} From 73f11de2232060ac7680e188db88ef7609123296 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 16:38:22 +0900 Subject: [PATCH 22/76] test(network): reject fabricated subscription admission provenance Reproduce same-id context substitution and receipt reconstruction after public correlation re-registration over real loopback transport. Commit-Message-Assisted-by: Codex (via Codex) Signed-off-by: Seongho Bae --- ...gation_committed_subscription_admission.rs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs index 15379be3b..aa00c2025 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs @@ -357,6 +357,44 @@ fn committed_navigation_requires_the_exact_active_subscription_before_document_m server .join() .map_err(|_| io::Error::other("subscription admission test server panicked"))??; + correlation.register_command_for( + 7, + originweave_network::WebDriverBiDiCommandKind::NavigationCommittedSubscription, + )?; + assert!( + WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( + &response, + &mut correlation, + ) + .is_err(), + "re-registering an id without another send must not reconstruct a consumed receipt" + ); + assert_eq!(correlation.outstanding_count(), 1); + Ok(()) +} + +#[test] +fn sent_subscription_cannot_be_rebound_to_an_unsent_same_id_context() +-> Result<(), Box> { + let mut registry = BrowserAuthorityRegistry::new(); + let session = registry.register_session(SESSION_ID)?; + let context = registry.register_context(session, CONTEXT_ID)?; + let other_context = registry.register_context(session, "context-b")?; + let (subscription, _) = receive_subscription_result(®istry, session, context, 7)?; + let unsent_binding = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( + 7, ®istry, session, other_context, "context-b", + )? + .admission_binding(); + let rejected = WebDriverBiDiNavigationCommittedSubscriptionAdmission::new( + subscription, unsent_binding, ®istry, + ) + .err() + .ok_or_else(|| io::Error::other("sent context-a receipt admitted an unsent context-b binding"))?; + assert!(rejected.source().is_none()); + assert_eq!( + rejected.to_string(), + "WebDriver BiDi navigation subscription binding differs from its sent context" + ); Ok(()) } From 43d3b5a3a2b5ce4f51a93d1152a0ee82620f4f3e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 16:46:38 +0900 Subject: [PATCH 23/76] fix(network): bind subscription receipts to the original sent command Keep private command-instance identity in existing bounded correlation state. Reject unsent same-id bindings, cross-registry numeric collisions and receipt reconstruction through generic re-registration; preserve frame-failure and typed-response semantics. Commit-Message-Assisted-by: Codex (via Codex) Signed-off-by: Seongho Bae --- CHANGELOG.md | 1 + .../src/webdriver_bidi_command_correlation.rs | 62 +++++++++++++-- ..._bidi_navigation_committed_subscription.rs | 10 +-- ...gation_committed_subscription_admission.rs | 26 ++++++- ...igation_committed_subscription_response.rs | 32 ++++---- ...gation_committed_subscription_admission.rs | 76 +++++++++++++++++-- docs/doctoring.md | 10 +++ ...igation_subscription_doctoring_contract.py | 2 +- 8 files changed, 182 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8fef2611..c07eb2986 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Changed +- Prevent an unsent navigation subscription from borrowing another request's successful response, including when separate sessions reuse the same local numbers. Re-registering a completed request number without sending a new request cannot recreate its consumed subscription. - Reject invalid navigation-subscription deadlines before reserving a pending request, preserving existing requests and leaving the rejected identifier reusable without sending subscription bytes. - Retire only the exact committed-navigation subscription correlation when frame preparation fails locally as `MalformedFrame` before any command bytes can be emitted, while preserving unrelated requests and retaining correlation after ambiguous frame-write failures. - Integrated current origin-binding prerequisites into context-scoped navigation subscriptions, preserving typed command isolation, response bounds, and the original subscription tests while restoring the inherited executable release contract. diff --git a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs index b70a8feba..b6428af42 100644 --- a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs +++ b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs @@ -1,4 +1,4 @@ -use std::{collections::BTreeMap, error::Error, fmt}; +use std::{collections::BTreeMap, error::Error, fmt, sync::Arc}; use crate::{ MAX_WEBDRIVER_BIDI_JS_UINT, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeRouting, @@ -32,10 +32,11 @@ pub enum WebDriverBiDiCommandKind { NavigationCommittedUnsubscribe, } -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] struct OutstandingCommand { kind: WebDriverBiDiCommandKind, connection_generation: Option, + subscription_intent: Option>, } /// Outcome of a response after it has consumed the matching outstanding command identifier. @@ -97,6 +98,11 @@ pub enum WebDriverBiDiCommandCorrelationError { /// Exact outstanding local command identifier. command_id: u64, }, + /// No subscription sender bound its private command-instance identity to this command. + CommandSubscriptionProvenanceMissing { + /// Exact outstanding local command identifier left untouched after rejection. + command_id: u64, + }, /// The response was received on a different verified connection from the outstanding command. ResponseConnectionMismatch { /// Exact outstanding local command identifier left untouched after rejection. @@ -121,6 +127,9 @@ impl fmt::Display for WebDriverBiDiCommandCorrelationError { Self::CommandConnectionProvenanceMissing { .. } => { "WebDriver BiDi outstanding command lacks connection provenance" } + Self::CommandSubscriptionProvenanceMissing { .. } => { + "WebDriver BiDi outstanding subscription lacks sent-context provenance" + } Self::ResponseConnectionMismatch { .. } => { "WebDriver BiDi response arrived on a different connection" } @@ -139,7 +148,8 @@ impl Error for WebDriverBiDiCommandCorrelationError {} /// /// Register an id together with its exact typed command family only after the caller has committed /// to that outbound command. Connection-owning command adapters may additionally bind the private -/// generation of the exact established transport before I/O. A success or correlatable error +/// generation of the exact established transport before I/O. Subscription senders retain a private +/// command-instance identity which generic registration cannot supply. A success or correlatable error /// response consumes the id exactly once only through a matching typed consumer. Events, null-id /// errors, command-kind mismatches, missing connection provenance, and responses received on a /// different verified connection leave outstanding state untouched. This type performs no I/O, @@ -183,7 +193,7 @@ impl WebDriverBiDiCommandCorrelation { command_id: u64, command_kind: WebDriverBiDiCommandKind, ) -> Result<(), WebDriverBiDiCommandCorrelationError> { - self.register(command_id, command_kind, None) + self.register(command_id, command_kind, None, None) } pub(crate) fn register_command_for_connection( @@ -192,7 +202,39 @@ impl WebDriverBiDiCommandCorrelation { command_kind: WebDriverBiDiCommandKind, connection_generation: WebDriverBiDiConnectionGeneration, ) -> Result<(), WebDriverBiDiCommandCorrelationError> { - self.register(command_id, command_kind, Some(connection_generation)) + self.register(command_id, command_kind, Some(connection_generation), None) + } + + pub(crate) fn register_subscription_command( + &mut self, + command_id: u64, + subscription_intent: Arc<()>, + ) -> Result<(), WebDriverBiDiCommandCorrelationError> { + self.register( + command_id, + WebDriverBiDiCommandKind::NavigationCommittedSubscription, + None, + Some(subscription_intent), + ) + } + + pub(crate) fn complete_subscription_command( + &mut self, + command_id: u64, + ) -> Result, WebDriverBiDiCommandCorrelationError> { + let subscription_intent = self + .require_command_kind( + command_id, + WebDriverBiDiCommandKind::NavigationCommittedSubscription, + )? + .subscription_intent + .ok_or( + WebDriverBiDiCommandCorrelationError::CommandSubscriptionProvenanceMissing { + command_id, + }, + )?; + let _removed = self.outstanding.remove(&command_id); + Ok(subscription_intent) } fn register( @@ -200,6 +242,7 @@ impl WebDriverBiDiCommandCorrelation { command_id: u64, command_kind: WebDriverBiDiCommandKind, connection_generation: Option, + subscription_intent: Option>, ) -> Result<(), WebDriverBiDiCommandCorrelationError> { if command_id > MAX_WEBDRIVER_BIDI_JS_UINT { return Err(WebDriverBiDiCommandCorrelationError::CommandIdOutOfRange); @@ -215,6 +258,7 @@ impl WebDriverBiDiCommandCorrelation { OutstandingCommand { kind: command_kind, connection_generation, + subscription_intent, }, ); Ok(()) @@ -306,7 +350,7 @@ impl WebDriverBiDiCommandCorrelation { let actual = self .outstanding .get(&command_id) - .copied() + .cloned() .ok_or(WebDriverBiDiCommandCorrelationError::CommandNotOutstanding)?; if actual.kind != expected_kind { return Err(WebDriverBiDiCommandCorrelationError::CommandKindMismatch { @@ -393,6 +437,12 @@ mod tests { }, "WebDriver BiDi outstanding command lacks connection provenance", ), + ( + WebDriverBiDiCommandCorrelationError::CommandSubscriptionProvenanceMissing { + command_id: 7, + }, + "WebDriver BiDi outstanding subscription lacks sent-context provenance", + ), ( WebDriverBiDiCommandCorrelationError::ResponseConnectionMismatch { command_id: 7 }, "WebDriver BiDi response arrived on a different connection", diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs index ea8a880f7..a0fb3dc11 100644 --- a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs +++ b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs @@ -1,4 +1,4 @@ -use std::{error::Error, fmt, time::Duration}; +use std::{error::Error, fmt, sync::Arc, time::Duration}; use originweave_core::{ BrowserAuthorityRegistry, BrowserRegistryError, BrowserSessionId, BrowsingContextId, @@ -29,6 +29,7 @@ pub struct WebDriverBiDiNavigationCommittedSubscriptionCommand { browser_session: BrowserSessionId, browsing_context: BrowsingContextId, external_context: String, + subscription_intent: Arc<()>, } impl WebDriverBiDiNavigationCommittedSubscriptionCommand { @@ -62,6 +63,7 @@ impl WebDriverBiDiNavigationCommittedSubscriptionCommand { browser_session, browsing_context, external_context: external_context.to_owned(), + subscription_intent: Arc::new(()), }) } @@ -101,6 +103,7 @@ impl WebDriverBiDiNavigationCommittedSubscriptionCommand { self.browser_session, self.browsing_context, &self.external_context, + Arc::clone(&self.subscription_intent), ) } @@ -134,10 +137,7 @@ impl WebDriverBiDiNavigationCommittedSubscriptionCommand { WebDriverBiDiNavigationCommittedSubscriptionCommandError::FrameWrite { source } })?; correlation - .register_command_for( - self.command_id, - WebDriverBiDiCommandKind::NavigationCommittedSubscription, - ) + .register_subscription_command(self.command_id, Arc::clone(&self.subscription_intent)) .map_err(|source| { WebDriverBiDiNavigationCommittedSubscriptionCommandError::Correlation { source } })?; diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_admission.rs b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_admission.rs index af042517b..e14312b35 100644 --- a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_admission.rs +++ b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_admission.rs @@ -1,4 +1,4 @@ -use std::{error::Error, fmt}; +use std::{error::Error, fmt, sync::Arc}; use originweave_core::{ BrowserAuthorityRegistry, BrowserRegistryError, BrowserSessionId, BrowsingContextId, @@ -23,12 +23,14 @@ pub const MAX_WEBDRIVER_BIDI_NAVIGATION_COMMITTED_ADMISSIONS: usize = 256; /// The binding carries only the exact local command identifier and the already-registered /// OriginWeave session/context association used to serialize that command. The external BiDi /// context identifier is retained privately for immediate registry revalidation and is not exposed -/// as durable OriginWeave authority. +/// as durable OriginWeave authority. A private allocation identity binds this value to the exact +/// command instance; matching caller-supplied numbers cannot recreate that identity. pub struct WebDriverBiDiNavigationCommittedSubscriptionBinding { command_id: u64, browser_session: BrowserSessionId, browsing_context: BrowsingContextId, external_context: String, + subscription_intent: Arc<()>, } impl fmt::Debug for WebDriverBiDiNavigationCommittedSubscriptionBinding { @@ -49,12 +51,14 @@ impl WebDriverBiDiNavigationCommittedSubscriptionBinding { browser_session: BrowserSessionId, browsing_context: BrowsingContextId, external_context: &str, + subscription_intent: Arc<()>, ) -> Self { Self { command_id, browser_session, browsing_context, external_context: external_context.to_owned(), + subscription_intent, } } @@ -115,7 +119,8 @@ impl fmt::Debug for WebDriverBiDiNavigationCommittedSubscriptionAdmission { impl WebDriverBiDiNavigationCommittedSubscriptionAdmission { /// Bind one correlated subscription receipt to the exact command-side session/context intent. /// - /// A response correlated to a different command cannot be rebound to this capability. The + /// A response correlated to a different command instance cannot be rebound to this capability, + /// even when its numeric command, session and context identifiers match. The /// original external BiDi context is revalidated before the capability exists, so a retired or /// replaced registry mapping fails closed without creating active event-admission state. pub fn new( @@ -131,6 +136,14 @@ impl WebDriverBiDiNavigationCommittedSubscriptionAdmission { }, ); } + if !Arc::ptr_eq( + &subscription.subscription_intent, + &binding.subscription_intent, + ) { + return Err( + WebDriverBiDiNavigationCommittedSubscriptionAdmissionError::CommandIntentMismatch, + ); + } require_current_binding(registry, &binding).map_err(|source| { WebDriverBiDiNavigationCommittedSubscriptionAdmissionError::ContextBinding { source } })?; @@ -291,6 +304,8 @@ impl WebDriverBiDiNavigationCommittedSubscribedObservation { /// Fail-closed failures while binding a correlated subscription receipt to command-side authority. #[derive(Debug)] pub enum WebDriverBiDiNavigationCommittedSubscriptionAdmissionError { + /// The supplied binding was captured from a different command instance than the actual sender. + CommandIntentMismatch, /// The correlated response belongs to a different local command than the supplied binding. CommandIdMismatch { /// Exact command identifier carried by the correlated subscription receipt. @@ -308,6 +323,9 @@ pub enum WebDriverBiDiNavigationCommittedSubscriptionAdmissionError { impl fmt::Display for WebDriverBiDiNavigationCommittedSubscriptionAdmissionError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + Self::CommandIntentMismatch => formatter.write_str( + "WebDriver BiDi navigation subscription binding differs from its sent command", + ), Self::CommandIdMismatch { .. } => formatter.write_str( "WebDriver BiDi navigation subscription response does not match its command binding", ), @@ -321,7 +339,7 @@ impl fmt::Display for WebDriverBiDiNavigationCommittedSubscriptionAdmissionError impl Error for WebDriverBiDiNavigationCommittedSubscriptionAdmissionError { fn source(&self) -> Option<&(dyn Error + 'static)> { match self { - Self::CommandIdMismatch { .. } => None, + Self::CommandIdMismatch { .. } | Self::CommandIntentMismatch => None, Self::ContextBinding { source } => Some(source), } } diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_response.rs b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_response.rs index 9a2f66394..eeccd12cf 100644 --- a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_response.rs @@ -1,9 +1,9 @@ -use std::{error::Error, fmt}; +use std::{error::Error, fmt, sync::Arc}; use crate::{ WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, WebDriverBiDiCommandKind, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeError, - WebDriverBiDiJsonEnvelopeKind, WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiJsonEnvelopeRouting, WebDriverBiDiWebSocketTextMessage, }; /// Maximum decoded UTF-8 bytes retained from a WebDriver BiDi `session.Subscription` identifier. @@ -15,13 +15,14 @@ pub const MAX_WEBDRIVER_BIDI_SUBSCRIPTION_IDENTIFIER_BYTES: usize = 4_096; /// Typed, correlated successful result of one context-scoped WebDriver BiDi `session.subscribe`. /// -/// This value retains only the exact correlated command id and the bounded opaque subscription -/// identifier returned by the remote end. It does not expose a generic JSON result, grant event, +/// This value retains the exact correlated command id, private original-command identity and the +/// bounded opaque subscription identifier returned by the remote end. It does not expose a generic JSON result, grant event, /// browser, policy, origin, secret, or Agent authority, or prove that any subscribed event has fired. #[derive(Eq, PartialEq)] pub struct WebDriverBiDiNavigationCommittedSubscriptionResult { command_id: u64, subscription_id: String, + pub(crate) subscription_intent: Arc<()>, } impl fmt::Debug for WebDriverBiDiNavigationCommittedSubscriptionResult { @@ -39,7 +40,9 @@ impl WebDriverBiDiNavigationCommittedSubscriptionResult { /// /// Common WebDriver BiDi envelope validation runs first. A successful envelope then undergoes /// command-specific projection of the required `result.subscription` text before correlation is - /// consumed, so malformed or ambiguous success bodies cannot silently retire a command id. A + /// consumed, so malformed or ambiguous success bodies cannot silently retire a command id. + /// Success also requires private command-instance provenance registered by the typed sender; + /// public correlation registration cannot mint a subscription receipt. A /// correlatable protocol-error response consumes its matching id and returns a typed remote /// failure retaining only the protocol error code. Events, null-id errors, malformed envelopes, /// and unknown ids fail closed without consuming unrelated outstanding correlation state. @@ -51,25 +54,23 @@ impl WebDriverBiDiNavigationCommittedSubscriptionResult { WebDriverBiDiNavigationCommittedSubscriptionResponseError::Envelope { source } })?; - match envelope.kind() { - WebDriverBiDiJsonEnvelopeKind::Success => { + match envelope.routing() { + WebDriverBiDiJsonEnvelopeRouting::CommandSuccess { command_id } => { let projected = SubscriptionProjection::parse(message.as_str())?; - let completed = correlation - .correlate_response_for( - &envelope, - WebDriverBiDiCommandKind::NavigationCommittedSubscription, - ) + let subscription_intent = correlation + .complete_subscription_command(command_id) .map_err(|source| { WebDriverBiDiNavigationCommittedSubscriptionResponseError::Correlation { source, } })?; Ok(Self { - command_id: completed.command_id(), + command_id, subscription_id: projected.subscription_id, + subscription_intent, }) } - WebDriverBiDiJsonEnvelopeKind::Error => { + WebDriverBiDiJsonEnvelopeRouting::CommandError { .. } => { retain_validated_error_code(envelope.error_code()).and_then(|error_code| { let completed = correlation .correlate_response_for( @@ -89,7 +90,7 @@ impl WebDriverBiDiNavigationCommittedSubscriptionResult { ) }) } - WebDriverBiDiJsonEnvelopeKind::Event => Err( + WebDriverBiDiJsonEnvelopeRouting::Event => Err( WebDriverBiDiNavigationCommittedSubscriptionResponseError::Correlation { source: WebDriverBiDiCommandCorrelationError::EventIsNotResponse, }, @@ -744,6 +745,7 @@ mod tests { let result = WebDriverBiDiNavigationCommittedSubscriptionResult { command_id: 7, subscription_id: "sensitive-subscription".to_owned(), + subscription_intent: std::sync::Arc::new(()), }; let debug = format!("{result:?}"); assert!(debug.contains("command_id")); diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs index aa00c2025..2fd6322ab 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs @@ -374,26 +374,90 @@ fn committed_navigation_requires_the_exact_active_subscription_before_document_m } #[test] -fn sent_subscription_cannot_be_rebound_to_an_unsent_same_id_context() --> Result<(), Box> { +fn sent_subscription_cannot_be_rebound_to_an_unsent_same_id_context() -> Result<(), Box> +{ let mut registry = BrowserAuthorityRegistry::new(); let session = registry.register_session(SESSION_ID)?; let context = registry.register_context(session, CONTEXT_ID)?; let other_context = registry.register_context(session, "context-b")?; let (subscription, _) = receive_subscription_result(®istry, session, context, 7)?; let unsent_binding = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( - 7, ®istry, session, other_context, "context-b", + 7, + ®istry, + session, + other_context, + "context-b", )? .admission_binding(); let rejected = WebDriverBiDiNavigationCommittedSubscriptionAdmission::new( - subscription, unsent_binding, ®istry, + subscription, + unsent_binding, + ®istry, ) .err() - .ok_or_else(|| io::Error::other("sent context-a receipt admitted an unsent context-b binding"))?; + .ok_or_else(|| { + io::Error::other("sent context-a receipt admitted an unsent context-b binding") + })?; assert!(rejected.source().is_none()); assert_eq!( rejected.to_string(), - "WebDriver BiDi navigation subscription binding differs from its sent context" + "WebDriver BiDi navigation subscription binding differs from its sent command" + ); + Ok(()) +} + +#[test] +fn identical_unsent_command_fields_do_not_recreate_sent_command_identity() +-> Result<(), Box> { + let mut registry = BrowserAuthorityRegistry::new(); + let session = registry.register_session(SESSION_ID)?; + let context = registry.register_context(session, CONTEXT_ID)?; + let (subscription, _) = receive_subscription_result(®istry, session, context, 7)?; + let unsent_binding = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( + 7, ®istry, session, context, CONTEXT_ID, + )? + .admission_binding(); + assert!( + WebDriverBiDiNavigationCommittedSubscriptionAdmission::new( + subscription, + unsent_binding, + ®istry, + ) + .is_err(), + "identical fields must not recreate the original command instance" + ); + Ok(()) +} + +#[test] +fn subscription_identity_does_not_collide_across_registries() -> Result<(), Box> { + let mut original_registry = BrowserAuthorityRegistry::new(); + let original_session = original_registry.register_session(SESSION_ID)?; + let original_context = original_registry.register_context(original_session, CONTEXT_ID)?; + let (subscription, _) = + receive_subscription_result(&original_registry, original_session, original_context, 7)?; + let mut replacement_registry = BrowserAuthorityRegistry::new(); + let replacement_session = replacement_registry.register_session("replacement-session")?; + let replacement_context = + replacement_registry.register_context(replacement_session, "context-b")?; + assert_eq!(original_session, replacement_session); + assert_eq!(original_context, replacement_context); + let unsent_binding = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( + 7, + &replacement_registry, + replacement_session, + replacement_context, + "context-b", + )? + .admission_binding(); + assert!( + WebDriverBiDiNavigationCommittedSubscriptionAdmission::new( + subscription, + unsent_binding, + &replacement_registry, + ) + .is_err(), + "registry-local identifiers must not substitute for the actual sent command" ); Ok(()) } diff --git a/docs/doctoring.md b/docs/doctoring.md index 8663aa056..5803f4238 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -4,6 +4,16 @@ This document records external evidence that changes OriginWeave architecture, t ## Decision trace +### Subscription receipt and command-instance integrity + +On September 6, 2026, real-loopback regressions against #264 `cf0f2452ea0612106f1076dcb2df58c7d6428943` reproduced two existing admission defects. A genuinely sent context-A subscription receipt accepted an unsent same-id context-B binding. After one completed subscription was consumed into unsubscribe, public typed correlation re-registration also allowed the retained response text to create another receipt without another send. Test-first commit `73f11de2232060ac7680e188db88ef7609123296` records both failures; these were not parent-adoption regressions. + +The first candidate retained numeric session/context identifiers in the existing correlation entry. A second realistic regression rejected that candidate: independent registries allocate the same local numbers, so an unsent binding in another registry still matched. The final repair instead retains one private standard-library allocation identity from command construction through its captured binding, typed sender registration and successful receipt. Admission requires the exact same command instance before the existing current-context check. Generic registration cannot provide the private identity. Successful receipt parsing validates command kind and private provenance before consuming correlation; malformed, unknown, wrong-kind and missing-provenance responses leave outstanding state untouched. Remote protocol-error retirement, local no-write retirement and ambiguous-write retention retain their previous semantics. There is no second registry, dependency or global counter. + +This is an implementation repair of the existing exact-command admission invariant, not a new browser authority or accepted architectural decision. The retained identity has a small allocation/reference-count cost and lives only as long as its command, captured bindings, outstanding entry or consumed receipt. It does not authenticate a received response or event connection, prevent a stale response being matched after an actual new send reuses the same protocol id, establish click causality, or bind a caller-supplied registry instance to a transport. Those broader provenance requirements remain unreleased work. #264 remains Draft behind #195/#279; local regressions do not prove protected-main or real-browser acceptance. + +Fresh final-tree verification passes five admission loopback tests, all 144 Python contracts without skips, the complete locked Rust 1.97.1 workspace checks/tests, strict all-feature Clippy, warning-denying rustdoc, formatting, compileall and CodeGraph sync. Pinned nightly coverage is exactly 1273/1273 functions, 13294/13294 lines, 16963/16963 regions and 1430/1430 branches; the unstable branch-instrumentation warning is retained. Independent read-only review found no production defect in the bounded identity repair and requested the now-added identical-fields/different-command regression. This review is not a counted GitHub approval. + ### Active-subscription admission parent adoption On September 6, 2026, #264 predecessor `9c4116b23e5b35e50bb66fff9f72d52bba3adbd0` still inherited #263's older `24fc763f0c4ae4e0dd2c62b9dca4b5bc0d23a94b` tree. Replaying the unchanged current-parent reconnect regression produced compiler RED: the connection-bound received-message reader types and `TransportConnectionMismatch` rejection variant were absent. Test-first commit `44c44b46` records that missing contract before ordinary adoption of #263 `3f22de94b63da83eaa8b5b1270912b21a3ecd006`. diff --git a/tests/test_navigation_subscription_doctoring_contract.py b/tests/test_navigation_subscription_doctoring_contract.py index 407fa2df2..5aae71666 100644 --- a/tests/test_navigation_subscription_doctoring_contract.py +++ b/tests/test_navigation_subscription_doctoring_contract.py @@ -21,7 +21,7 @@ def test_doctoring_matches_provably_local_subscription_failure_retirement(self) self.assertLess( source.index("validate_frame_timeout(frame_timeout)"), - source.index(".register_command_for("), + source.index(".register_subscription_command("), ) self.assertIn("WebDriverBiDiWebSocketFrameError::MalformedFrame", source) self.assertIn("correlation.retire_command_for(", source) From 918c4ebeb27e1eb7e03567eb52e6e547dfd499df Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 16:56:12 +0900 Subject: [PATCH 24/76] test(network): reject crossed subscription transport evidence Require a foreign connection to preserve outstanding subscription commands and prevent foreign event text from creating state-changing observations. Commit-Message-Assisted-by: Codex (via Codex) Signed-off-by: Seongho Bae --- ...gation_committed_subscription_admission.rs | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs index 2fd6322ab..c053726f7 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs @@ -194,6 +194,128 @@ fn receive_subscription_result( Ok((result, binding)) } +fn establish_connection( + local_addr: std::net::SocketAddr, +) -> Result> { + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? + .correlate_session_id(SESSION_ID)? + .into_explicit_connect_target()?; + let connection = + WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; + Ok(WebDriverBiDiWebSocketHandshakePlan::new( + connection, + WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?, + )? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?) +} + +#[test] +fn foreign_connection_cannot_complete_a_subscription_command() -> Result<(), Box> { + for foreign_payload in [ + SUBSCRIBE_RESPONSE, + br#"{"type":"error","id":7,"error":"invalid argument","message":"rejected"}"#, + ] { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let server = thread::spawn(move || -> io::Result<()> { + let (mut original, _) = listener.accept()?; + read_opening_request(&mut original)?; + original.write_all(OPENING_RESPONSE)?; + assert_eq!(read_masked_text_frame(&mut original)?, + br#"{"id":7,"method":"session.subscribe","params":{"events":["browsingContext.navigationCommitted"],"contexts":["context-a"]}}"#); + let (mut foreign, _) = listener.accept()?; + read_opening_request(&mut foreign)?; + foreign.write_all(OPENING_RESPONSE)?; + write_text_frame(&mut foreign, foreign_payload)?; + write_text_frame(&mut original, SUBSCRIBE_RESPONSE) + }); + let mut registry = BrowserAuthorityRegistry::new(); + let session = registry.register_session(SESSION_ID)?; + let context = registry.register_context(session, CONTEXT_ID)?; + let command = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( + 7, ®istry, session, context, CONTEXT_ID, + )?; + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + let original = command.send( + ®istry, + establish_connection(local_addr)?, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + Duration::from_millis(500), + )?; + let foreign = establish_connection(local_addr)?; + let (_, foreign_message) = + next_text(foreign, &mut WebDriverBiDiWebSocketMessageAssembler::new())?; + let foreign_result = + WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( + &foreign_message, + &mut correlation, + ); + server + .join() + .map_err(|_| io::Error::other("crossed response server panicked"))??; + assert!( + foreign_result.is_err(), + "foreign success must not mint a subscription receipt" + ); + assert_eq!( + correlation.outstanding_count(), + 1, + "foreign success or error must not consume the original command" + ); + let (_, original_message) = + next_text(original, &mut WebDriverBiDiWebSocketMessageAssembler::new())?; + WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( + &original_message, + &mut correlation, + )?; + assert_eq!(correlation.outstanding_count(), 0); + } + Ok(()) +} + +#[test] +fn foreign_connection_event_cannot_mutate_a_subscribed_document() -> Result<(), Box> { + let mut registry = BrowserAuthorityRegistry::new(); + let session = registry.register_session(SESSION_ID)?; + let context = registry.register_context(session, CONTEXT_ID)?; + let original_epoch = registry.current_context_epoch(session, context)?; + let (subscription, binding) = receive_subscription_result(®istry, session, context, 7)?; + let mut admission = WebDriverBiDiNavigationCommittedSubscriptionAdmission::new( + subscription, + binding, + ®istry, + )?; + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let server = thread::spawn(move || -> io::Result<()> { + let (mut foreign, _) = listener.accept()?; + read_opening_request(&mut foreign)?; + foreign.write_all(OPENING_RESPONSE)?; + write_text_frame(&mut foreign, NAVIGATION_EVENT) + }); + let (_, foreign_event) = next_text( + establish_connection(local_addr)?, + &mut WebDriverBiDiWebSocketMessageAssembler::new(), + )?; + server + .join() + .map_err(|_| io::Error::other("crossed event server panicked"))??; + assert!( + admission + .admit(&foreign_event, ®istry, EXPECTED_URL) + .is_err(), + "same session/context text on another connection must not create a state-changing observation" + ); + assert_eq!( + registry.current_context_epoch(session, context)?, + original_epoch + ); + Ok(()) +} + #[test] fn committed_navigation_requires_the_exact_active_subscription_before_document_mutation() -> Result<(), Box> { From bc2e69d52c0d95b1abc3ff69dd0433c0349254b2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 17:04:33 +0900 Subject: [PATCH 25/76] test(network): reproduce crossed-connection subscription provenance --- ...ation_subscription_transport_provenance.rs | 254 ++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_navigation_subscription_transport_provenance.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_subscription_transport_provenance.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_subscription_transport_provenance.rs new file mode 100644 index 000000000..683f376be --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_subscription_transport_provenance.rs @@ -0,0 +1,254 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{SocketAddr, TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::{BrowserAuthorityRegistry, WebDriverBiDiWebSocketEndpoint}; +use originweave_network::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiNavigationCommittedSubscriptionAdmission, + WebDriverBiDiNavigationCommittedSubscriptionCommand, + WebDriverBiDiNavigationCommittedSubscriptionResult, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketEstablished, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, + WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, + WebDriverBiDiWebSocketTextMessage, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; +const CONTEXT_ID: &str = "context-a"; +const EXPECTED_URL: &str = "https://example.test/after"; +const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; +const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; +const SUBSCRIBE_RESPONSE: &[u8] = + br#"{"type":"success","id":7,"result":{"subscription":"subscription-a"}}"#; +const NAVIGATION_EVENT: &[u8] = br#"{"type":"event","method":"browsingContext.navigationCommitted","params":{"context":"context-a","navigation":"nav-crossed","timestamp":33,"url":"https://example.test/after"}}"#; + +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, + "opening request ended before the header terminator", + )); + } + request.extend_from_slice(&buffer[..count]); + } + Ok(()) +} + +fn read_masked_text_frame(stream: &mut TcpStream) -> io::Result> { + let mut header = [0_u8; 2]; + stream.read_exact(&mut header)?; + if header[0] != 0x81 || header[1] & 0x80 == 0 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "expected one final masked client text frame", + )); + } + let length = match header[1] & 0x7f { + length @ 0..=125 => usize::from(length), + 126 => { + let mut extended = [0_u8; 2]; + stream.read_exact(&mut extended)?; + usize::from(u16::from_be_bytes(extended)) + } + _ => { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "fixture command unexpectedly required 64-bit framing", + )); + } + }; + let mut mask = [0_u8; 4]; + stream.read_exact(&mut mask)?; + let mut payload = vec![0_u8; length]; + stream.read_exact(&mut payload)?; + for (index, byte) in payload.iter_mut().enumerate() { + *byte ^= mask[index % mask.len()]; + } + Ok(payload) +} + +fn write_text_frame(stream: &mut TcpStream, payload: &[u8]) -> io::Result<()> { + stream.write_all(&[0x81])?; + match payload.len() { + 0..=125 => stream.write_all(&[payload.len() as u8])?, + 126..=65_535 => { + stream.write_all(&[126])?; + stream.write_all(&(payload.len() as u16).to_be_bytes())?; + } + _ => { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "fixture payload unexpectedly required 64-bit framing", + )); + } + } + stream.write_all(payload) +} + +fn establish(local_addr: SocketAddr) -> Result> { + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? + .correlate_session_id(SESSION_ID)? + .into_explicit_connect_target()?; + let connection = + WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; + Ok(WebDriverBiDiWebSocketHandshakePlan::new( + connection, + WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?, + )? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?) +} + +fn next_text( + established: WebDriverBiDiWebSocketEstablished, +) -> Result> { + let (_established, frame) = established.read_frame(Duration::from_millis(500))?; + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); + match assembler.push_frame(frame)? { + WebDriverBiDiWebSocketMessageAssembly::Text(text) => Ok(text), + other => Err(io::Error::other(format!( + "expected a complete WebDriver BiDi text message, got {other:?}" + )) + .into()), + } +} + +fn spawn_subscription_sender( + listener: TcpListener, + response: Option<&'static [u8]>, +) -> thread::JoinHandle> { + thread::spawn(move || { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + let command = read_masked_text_frame(&mut stream)?; + if command + != br#"{"id":7,"method":"session.subscribe","params":{"events":["browsingContext.navigationCommitted"],"contexts":["context-a"]}}"# + { + return Err(io::Error::other("unexpected session.subscribe command")); + } + if let Some(response) = response { + write_text_frame(&mut stream, response)?; + } + Ok(()) + }) +} + +fn spawn_unsolicited_message_sender( + listener: TcpListener, + payload: &'static [u8], +) -> thread::JoinHandle> { + thread::spawn(move || { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + write_text_frame(&mut stream, payload) + }) +} + +#[test] +fn subscription_receipt_from_another_verified_connection_is_rejected() +-> Result<(), Box> { + let mut registry = BrowserAuthorityRegistry::new(); + let session = registry.register_session(SESSION_ID)?; + let context = registry.register_context(session, CONTEXT_ID)?; + + let sent_listener = TcpListener::bind(("127.0.0.1", 0))?; + let sent_addr = sent_listener.local_addr()?; + let sent_server = spawn_subscription_sender(sent_listener, None); + let command = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( + 7, ®istry, session, context, CONTEXT_ID, + )?; + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + let sent_established = command.send( + ®istry, + establish(sent_addr)?, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + Duration::from_millis(500), + )?; + drop(sent_established); + sent_server + .join() + .map_err(|_| io::Error::other("sent-connection fixture server panicked"))??; + assert_eq!(correlation.outstanding_count(), 1); + + let foreign_listener = TcpListener::bind(("127.0.0.1", 0))?; + let foreign_addr = foreign_listener.local_addr()?; + let foreign_server = spawn_unsolicited_message_sender(foreign_listener, SUBSCRIBE_RESPONSE); + let response = next_text(establish(foreign_addr)?)?; + let result = WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( + &response, + &mut correlation, + ); + assert!( + result.is_err(), + "a session.subscribe receipt read on another verified connection must not consume the sent command" + ); + assert_eq!(correlation.outstanding_count(), 1); + foreign_server + .join() + .map_err(|_| io::Error::other("foreign-receipt fixture server panicked"))??; + Ok(()) +} + +#[test] +fn subscription_event_from_another_verified_connection_is_rejected() +-> Result<(), Box> { + let mut registry = BrowserAuthorityRegistry::new(); + let session = registry.register_session(SESSION_ID)?; + let context = registry.register_context(session, CONTEXT_ID)?; + + let sent_listener = TcpListener::bind(("127.0.0.1", 0))?; + let sent_addr = sent_listener.local_addr()?; + let sent_server = spawn_subscription_sender(sent_listener, Some(SUBSCRIBE_RESPONSE)); + let command = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( + 7, ®istry, session, context, CONTEXT_ID, + )?; + let binding = command.admission_binding(); + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + let established = command.send( + ®istry, + establish(sent_addr)?, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([4, 3, 2, 1]), + Duration::from_millis(500), + )?; + let response = next_text(established)?; + let subscription = WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( + &response, + &mut correlation, + )?; + let mut admission = WebDriverBiDiNavigationCommittedSubscriptionAdmission::new( + subscription, + binding, + ®istry, + )?; + sent_server + .join() + .map_err(|_| io::Error::other("subscription fixture server panicked"))??; + + let foreign_listener = TcpListener::bind(("127.0.0.1", 0))?; + let foreign_addr = foreign_listener.local_addr()?; + let foreign_server = spawn_unsolicited_message_sender(foreign_listener, NAVIGATION_EVENT); + let event = next_text(establish(foreign_addr)?)?; + let result = admission.admit(&event, ®istry, EXPECTED_URL); + assert!( + result.is_err(), + "a navigation event read on another verified connection must not become subscription-backed evidence" + ); + foreign_server + .join() + .map_err(|_| io::Error::other("foreign-event fixture server panicked"))??; + Ok(()) +} From f80a17fd8f1eb9de284f162fc0beae7917961635 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 17:09:46 +0900 Subject: [PATCH 26/76] fix(network): bind subscription correlation to connection --- .../src/webdriver_bidi_command_correlation.rs | 56 +++++++++++-------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs index b6428af42..11f5130a2 100644 --- a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs +++ b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs @@ -148,14 +148,15 @@ impl Error for WebDriverBiDiCommandCorrelationError {} /// /// Register an id together with its exact typed command family only after the caller has committed /// to that outbound command. Connection-owning command adapters may additionally bind the private -/// generation of the exact established transport before I/O. Subscription senders retain a private -/// command-instance identity which generic registration cannot supply. A success or correlatable error -/// response consumes the id exactly once only through a matching typed consumer. Events, null-id -/// errors, command-kind mismatches, missing connection provenance, and responses received on a -/// different verified connection leave outstanding state untouched. This type performs no I/O, -/// retry, command serialization, browser authentication, or authority grant. Debug output reports -/// only the outstanding-count summary; command identifiers, families, and generations remain -/// private correlation state. +/// generation of the exact established transport before I/O. Subscription senders retain both a +/// private command-instance identity and the exact verified connection generation; generic +/// registration cannot supply either. A success or correlatable error response consumes the id +/// exactly once only through a matching typed consumer. Events, null-id errors, command-kind +/// mismatches, missing connection provenance, and responses received on a different verified +/// connection leave outstanding state untouched. This type performs no I/O, retry, command +/// serialization, browser authentication, or authority grant. Debug output reports only the +/// outstanding-count summary; command identifiers, families, and generations remain private +/// correlation state. #[derive(Default)] pub struct WebDriverBiDiCommandCorrelation { outstanding: BTreeMap, @@ -205,36 +206,45 @@ impl WebDriverBiDiCommandCorrelation { self.register(command_id, command_kind, Some(connection_generation), None) } - pub(crate) fn register_subscription_command( + pub(crate) fn register_subscription_command_for_connection( &mut self, command_id: u64, + connection_generation: WebDriverBiDiConnectionGeneration, subscription_intent: Arc<()>, ) -> Result<(), WebDriverBiDiCommandCorrelationError> { self.register( command_id, WebDriverBiDiCommandKind::NavigationCommittedSubscription, - None, + Some(connection_generation), Some(subscription_intent), ) } - pub(crate) fn complete_subscription_command( + pub(crate) fn complete_subscription_command_on_connection( &mut self, command_id: u64, - ) -> Result, WebDriverBiDiCommandCorrelationError> { - let subscription_intent = self - .require_command_kind( + received_connection_generation: WebDriverBiDiConnectionGeneration, + ) -> Result<(Arc<()>, WebDriverBiDiConnectionGeneration), WebDriverBiDiCommandCorrelationError> + { + let outstanding = self.require_command_kind( + command_id, + WebDriverBiDiCommandKind::NavigationCommittedSubscription, + )?; + let subscription_intent = outstanding.subscription_intent.ok_or( + WebDriverBiDiCommandCorrelationError::CommandSubscriptionProvenanceMissing { command_id, - WebDriverBiDiCommandKind::NavigationCommittedSubscription, - )? - .subscription_intent - .ok_or( - WebDriverBiDiCommandCorrelationError::CommandSubscriptionProvenanceMissing { - command_id, - }, - )?; + }, + )?; + let expected_connection_generation = outstanding.connection_generation.ok_or( + WebDriverBiDiCommandCorrelationError::CommandConnectionProvenanceMissing { command_id }, + )?; + if expected_connection_generation != received_connection_generation { + return Err( + WebDriverBiDiCommandCorrelationError::ResponseConnectionMismatch { command_id }, + ); + } let _removed = self.outstanding.remove(&command_id); - Ok(subscription_intent) + Ok((subscription_intent, expected_connection_generation)) } fn register( From a3dfd190769c28a67fc6838da03630346ffd43c8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 17:10:21 +0900 Subject: [PATCH 27/76] fix(network): retain subscription send connection provenance --- ...r_bidi_navigation_committed_subscription.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs index a0fb3dc11..ed33d8faf 100644 --- a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs +++ b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs @@ -111,11 +111,12 @@ impl WebDriverBiDiNavigationCommittedSubscriptionCommand { /// /// Context binding is revalidated immediately before command correlation and network I/O so a /// command retained across registry retirement cannot subscribe a stale or replacement context. - /// Invalid frame deadlines fail before correlation registration. Registration then occurs before - /// the first possible remote side effect. A frame-owner preflight rejection that proves no write - /// began retires this exact subscription again; currently that covers adjacent client masking-key - /// reuse. Once frame emission can have begun, later failures conservatively leave the identifier - /// outstanding because partial or full emission is ambiguous. + /// Invalid frame deadlines fail before correlation registration. Registration then binds both the + /// private command-instance identity and this established connection's process-local generation + /// before the first possible remote side effect. A frame-owner preflight rejection that proves no + /// write began retires this exact subscription again; currently that covers adjacent client + /// masking-key reuse. Once frame emission can have begun, later failures conservatively leave the + /// identifier outstanding because partial or full emission is ambiguous. pub fn send( self, registry: &BrowserAuthorityRegistry, @@ -136,8 +137,13 @@ impl WebDriverBiDiNavigationCommittedSubscriptionCommand { validate_frame_timeout(frame_timeout).map_err(|source| { WebDriverBiDiNavigationCommittedSubscriptionCommandError::FrameWrite { source } })?; + let connection_generation = established.transport_evidence().connection_generation(); correlation - .register_subscription_command(self.command_id, Arc::clone(&self.subscription_intent)) + .register_subscription_command_for_connection( + self.command_id, + connection_generation, + Arc::clone(&self.subscription_intent), + ) .map_err(|source| { WebDriverBiDiNavigationCommittedSubscriptionCommandError::Correlation { source } })?; From 3e66c447e236762768c9a43abbb6ae27d3ae7140 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 17:11:41 +0900 Subject: [PATCH 28/76] fix(network): require connection-bound subscription receipts --- ...igation_committed_subscription_response.rs | 46 +++++++++++++------ 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_response.rs b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_response.rs index eeccd12cf..be3d3d982 100644 --- a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_response.rs @@ -3,7 +3,8 @@ use std::{error::Error, fmt, sync::Arc}; use crate::{ WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, WebDriverBiDiCommandKind, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeError, - WebDriverBiDiJsonEnvelopeRouting, WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiJsonEnvelopeRouting, WebDriverBiDiReceivedTextMessage, + webdriver_bidi_connection::WebDriverBiDiConnectionGeneration, }; /// Maximum decoded UTF-8 bytes retained from a WebDriver BiDi `session.Subscription` identifier. @@ -15,14 +16,16 @@ pub const MAX_WEBDRIVER_BIDI_SUBSCRIPTION_IDENTIFIER_BYTES: usize = 4_096; /// Typed, correlated successful result of one context-scoped WebDriver BiDi `session.subscribe`. /// -/// This value retains the exact correlated command id, private original-command identity and the -/// bounded opaque subscription identifier returned by the remote end. It does not expose a generic JSON result, grant event, -/// browser, policy, origin, secret, or Agent authority, or prove that any subscribed event has fired. +/// This value retains the exact correlated command id, private original-command identity, exact +/// verified receive-connection generation, and bounded opaque subscription identifier returned by +/// the remote end. It does not expose a generic JSON result, grant event, browser, policy, origin, +/// secret, or Agent authority, or prove that any subscribed event has fired. #[derive(Eq, PartialEq)] pub struct WebDriverBiDiNavigationCommittedSubscriptionResult { command_id: u64, subscription_id: String, pub(crate) subscription_intent: Arc<()>, + connection_generation: Option, } impl fmt::Debug for WebDriverBiDiNavigationCommittedSubscriptionResult { @@ -31,25 +34,28 @@ impl fmt::Debug for WebDriverBiDiNavigationCommittedSubscriptionResult { .debug_struct("WebDriverBiDiNavigationCommittedSubscriptionResult") .field("command_id", &self.command_id) .field("subscription_id_len", &self.subscription_id.len()) + .field("connection_bound", &self.connection_generation.is_some()) .finish() } } impl WebDriverBiDiNavigationCommittedSubscriptionResult { - /// Parse one bounded local-end message and consume its exact outstanding command on success. + /// Parse one connection-bound local-end message and consume its exact outstanding command. /// /// Common WebDriver BiDi envelope validation runs first. A successful envelope then undergoes /// command-specific projection of the required `result.subscription` text before correlation is /// consumed, so malformed or ambiguous success bodies cannot silently retire a command id. - /// Success also requires private command-instance provenance registered by the typed sender; - /// public correlation registration cannot mint a subscription receipt. A - /// correlatable protocol-error response consumes its matching id and returns a typed remote - /// failure retaining only the protocol error code. Events, null-id errors, malformed envelopes, - /// and unknown ids fail closed without consuming unrelated outstanding correlation state. + /// Success requires both the private command-instance provenance registered by the typed sender + /// and equality between the sender's private connection generation and the exact connection that + /// assembled this received message. Public correlation registration cannot mint a subscription + /// receipt. A correlatable protocol-error response is subject to the same connection check before + /// consuming its matching id. Events, null-id errors, malformed envelopes, unknown ids, and + /// crossed-connection responses fail closed without consuming unrelated outstanding state. pub fn parse_and_correlate( - message: &WebDriverBiDiWebSocketTextMessage, + received: &WebDriverBiDiReceivedTextMessage, correlation: &mut WebDriverBiDiCommandCorrelation, ) -> Result { + let message = received.message(); let envelope = WebDriverBiDiJsonEnvelope::parse(message).map_err(|source| { WebDriverBiDiNavigationCommittedSubscriptionResponseError::Envelope { source } })?; @@ -57,8 +63,11 @@ impl WebDriverBiDiNavigationCommittedSubscriptionResult { match envelope.routing() { WebDriverBiDiJsonEnvelopeRouting::CommandSuccess { command_id } => { let projected = SubscriptionProjection::parse(message.as_str())?; - let subscription_intent = correlation - .complete_subscription_command(command_id) + let (subscription_intent, connection_generation) = correlation + .complete_subscription_command_on_connection( + command_id, + received.connection_generation(), + ) .map_err(|source| { WebDriverBiDiNavigationCommittedSubscriptionResponseError::Correlation { source, @@ -68,14 +77,16 @@ impl WebDriverBiDiNavigationCommittedSubscriptionResult { command_id, subscription_id: projected.subscription_id, subscription_intent, + connection_generation: Some(connection_generation), }) } WebDriverBiDiJsonEnvelopeRouting::CommandError { .. } => { retain_validated_error_code(envelope.error_code()).and_then(|error_code| { let completed = correlation - .correlate_response_for( + .correlate_response_for_connection( &envelope, WebDriverBiDiCommandKind::NavigationCommittedSubscription, + received.connection_generation(), ) .map_err(|source| { WebDriverBiDiNavigationCommittedSubscriptionResponseError::Correlation { @@ -109,6 +120,10 @@ impl WebDriverBiDiNavigationCommittedSubscriptionResult { pub fn subscription_id(&self) -> &str { &self.subscription_id } + + pub(crate) const fn connection_generation(&self) -> Option { + self.connection_generation + } } /// Fail-closed failures while admitting one typed WebDriver BiDi `session.subscribe` response. @@ -746,13 +761,16 @@ mod tests { command_id: 7, subscription_id: "sensitive-subscription".to_owned(), subscription_intent: std::sync::Arc::new(()), + connection_generation: None, }; let debug = format!("{result:?}"); assert!(debug.contains("command_id")); assert!(debug.contains("subscription_id_len")); + assert!(debug.contains("connection_bound")); assert!(!debug.contains("sensitive-subscription")); assert_eq!(result.command_id(), 7); assert_eq!(result.subscription_id(), "sensitive-subscription"); + assert!(result.connection_generation().is_none()); } #[test] From d9b9013647bf14da3b417dd8e76cac38eeb83d56 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 17:12:35 +0900 Subject: [PATCH 29/76] fix(network): bind navigation event admission to receipt connection --- ...gation_committed_subscription_admission.rs | 63 +++++++++++++------ 1 file changed, 44 insertions(+), 19 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_admission.rs b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_admission.rs index e14312b35..5e76e6283 100644 --- a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_admission.rs +++ b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_admission.rs @@ -8,7 +8,7 @@ use crate::{ WebDriverBiDiNavigationCommittedObservation, WebDriverBiDiNavigationCommittedObservationError, WebDriverBiDiNavigationCommittedSubscriptionResult, WebDriverBiDiNavigationCommittedUnsubscribeCommand, - WebDriverBiDiNavigationCommittedUnsubscribeCommandError, WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiNavigationCommittedUnsubscribeCommandError, WebDriverBiDiReceivedTextMessage, }; /// Maximum distinct committed-navigation identifiers retained by one active subscription admission. @@ -84,13 +84,16 @@ impl WebDriverBiDiNavigationCommittedSubscriptionBinding { /// Active local admission capability for one exact committed-navigation BiDi subscription. /// /// Construction requires both the correlated remote subscription receipt and the immutable binding -/// captured from the exact command that requested it. The command identifiers must match and the -/// original external context mapping must still resolve to the exact OriginWeave session/context. -/// Holding this value is therefore narrower than holding an opaque protocol subscription string. -/// It grants only admission of the matching committed-navigation event through the existing bounded -/// parser; it grants no navigation, destination, origin, policy, secret, node, or Agent authority. -/// Each admitted non-null WebDriver BiDi navigation identifier is retained until unsubscribe so a -/// replayed remote event cannot mint a second state-changing observation from the same navigation. +/// captured from the exact command that requested it. The command identifiers and private command +/// allocation identity must match, and the original external context mapping must still resolve to +/// the exact OriginWeave session/context. Event admission additionally requires the event message to +/// have been assembled on the same verified connection generation that carried the subscription +/// command and receipt. Holding this value is therefore narrower than holding an opaque protocol +/// subscription string. It grants only admission of the matching committed-navigation event through +/// the existing bounded parser; it grants no navigation, destination, origin, policy, secret, node, +/// or Agent authority. Each admitted non-null WebDriver BiDi navigation identifier is retained until +/// unsubscribe so a replayed remote event cannot mint a second state-changing observation from the +/// same navigation. pub struct WebDriverBiDiNavigationCommittedSubscriptionAdmission { subscription: WebDriverBiDiNavigationCommittedSubscriptionResult, binding: WebDriverBiDiNavigationCommittedSubscriptionBinding, @@ -108,6 +111,10 @@ impl fmt::Debug for WebDriverBiDiNavigationCommittedSubscriptionAdmission { "subscription_id_bytes", &self.subscription.subscription_id().len(), ) + .field( + "connection_bound", + &self.subscription.connection_generation().is_some(), + ) .field( "admitted_navigation_count", &self.admitted_navigation_ids.len(), @@ -120,9 +127,10 @@ impl WebDriverBiDiNavigationCommittedSubscriptionAdmission { /// Bind one correlated subscription receipt to the exact command-side session/context intent. /// /// A response correlated to a different command instance cannot be rebound to this capability, - /// even when its numeric command, session and context identifiers match. The - /// original external BiDi context is revalidated before the capability exists, so a retired or - /// replaced registry mapping fails closed without creating active event-admission state. + /// even when its numeric command, session and context identifiers match. The original external + /// BiDi context is revalidated before the capability exists, so a retired or replaced registry + /// mapping fails closed without creating active event-admission state. Connection provenance is + /// retained inside the correlated receipt and cannot be supplied by this caller. pub fn new( subscription: WebDriverBiDiNavigationCommittedSubscriptionResult, binding: WebDriverBiDiNavigationCommittedSubscriptionBinding, @@ -168,8 +176,11 @@ impl WebDriverBiDiNavigationCommittedSubscriptionAdmission { /// Admit one exact committed-navigation event while this subscription capability remains active. /// - /// The original command-side external-context mapping is revalidated immediately before parsing - /// the event. The event must then independently carry that same registered context and the exact + /// The event must first have been assembled by the connection-bound reader on the same private + /// connection generation that carried the exact typed subscription command and receipt. A message + /// from another verified connection fails before registry revalidation or event parsing. The + /// original command-side external-context mapping is then revalidated immediately before parsing + /// the event. The event must independently carry that same registered context and the exact /// declared URL. State-changing admission additionally requires the WebDriver BiDi navigation /// identifier to be present and unique within this active subscription. The specification defines /// non-null navigation identifiers as unique identifiers for ongoing navigations; retaining them @@ -179,18 +190,23 @@ impl WebDriverBiDiNavigationCommittedSubscriptionAdmission { /// state-changing document-advance boundary. pub fn admit( &mut self, - message: &WebDriverBiDiWebSocketTextMessage, + received: &WebDriverBiDiReceivedTextMessage, registry: &BrowserAuthorityRegistry, expected_url: &str, ) -> Result< WebDriverBiDiNavigationCommittedSubscribedObservation, WebDriverBiDiNavigationCommittedSubscriptionEventError, > { + if self.subscription.connection_generation() != Some(received.connection_generation()) { + return Err( + WebDriverBiDiNavigationCommittedSubscriptionEventError::EventConnectionMismatch, + ); + } require_current_binding(registry, &self.binding).map_err(|source| { WebDriverBiDiNavigationCommittedSubscriptionEventError::ContextBinding { source } })?; let observation = WebDriverBiDiNavigationCommittedObservation::parse_and_match( - message, + received.message(), registry, self.binding.browser_session, self.binding.browsing_context, @@ -228,6 +244,8 @@ impl WebDriverBiDiNavigationCommittedSubscriptionAdmission { /// Consumption deliberately ends local event admission before the unsubscribe command can be /// emitted. If later transport or remote teardown fails, callers must explicitly establish a new /// typed subscription before admitting more events; ambiguous teardown never restores authority. + /// This handoff does not claim that the existing unsubscribe command or its receipt is bound to + /// the subscription connection; unsubscribe transport provenance remains a separate boundary. pub fn into_unsubscribe( self, command_id: u64, @@ -253,9 +271,10 @@ fn require_current_binding( /// One committed-navigation observation admitted through an active exact subscription capability. /// /// Unlike the lower-level protocol observation, this value proves that local admission was bound to -/// the exact typed `session.subscribe` command/receipt pair for the same registered context at the -/// time the event was admitted. It still does not prove action causality or grant destination, -/// origin, policy, node, secret, process, profile, or reusable Agent authority. +/// the exact typed `session.subscribe` command/receipt pair and the same verified transport +/// generation for the registered context at the time the event was admitted. It still does not prove +/// action causality or grant destination, origin, policy, node, secret, process, profile, or reusable +/// Agent authority. pub struct WebDriverBiDiNavigationCommittedSubscribedObservation( WebDriverBiDiNavigationCommittedObservation, ); @@ -348,6 +367,8 @@ impl Error for WebDriverBiDiNavigationCommittedSubscriptionAdmissionError { /// Fail-closed failures while admitting an event through one active subscription capability. #[derive(Debug)] pub enum WebDriverBiDiNavigationCommittedSubscriptionEventError { + /// The event message was assembled on a different verified connection from the subscription. + EventConnectionMismatch, /// The original external context no longer maps to the exact registered session/context pair. ContextBinding { /// Exact browser-registry authority failure. @@ -372,6 +393,9 @@ pub enum WebDriverBiDiNavigationCommittedSubscriptionEventError { impl fmt::Display for WebDriverBiDiNavigationCommittedSubscriptionEventError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + Self::EventConnectionMismatch => formatter.write_str( + "WebDriver BiDi navigation event arrived on a different subscription connection", + ), Self::ContextBinding { .. } => formatter.write_str( "WebDriver BiDi navigation subscription context is no longer registered authority", ), @@ -397,7 +421,8 @@ impl Error for WebDriverBiDiNavigationCommittedSubscriptionEventError { match self { Self::ContextBinding { source } => Some(source), Self::Observation { source } => Some(source), - Self::MissingNavigationIdentity + Self::EventConnectionMismatch + | Self::MissingNavigationIdentity | Self::ReplayedNavigation | Self::ReplayHistoryExhausted { .. } => None, } From c46642e276cc8eec68a95624750757f9214edd34 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 17:13:20 +0900 Subject: [PATCH 30/76] test(network): consume subscription messages through connection reader --- .../originweave-network/tests/support/mod.rs | 39 +++++++++---------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/crates/originweave-network/tests/support/mod.rs b/crates/originweave-network/tests/support/mod.rs index 45dae6ccb..0f282158c 100644 --- a/crates/originweave-network/tests/support/mod.rs +++ b/crates/originweave-network/tests/support/mod.rs @@ -10,13 +10,14 @@ use originweave_core::{ BrowserAuthorityRegistry, BrowserSessionId, BrowsingContextId, WebDriverBiDiWebSocketEndpoint, }; use originweave_network::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiNavigationCommittedSubscribedObservation, + WebDriverBiDiCommandCorrelation, WebDriverBiDiConnectionMessageRead, + WebDriverBiDiNavigationCommittedSubscribedObservation, WebDriverBiDiNavigationCommittedSubscriptionAdmission, WebDriverBiDiNavigationCommittedSubscriptionCommand, - WebDriverBiDiNavigationCommittedSubscriptionResult, WebDriverBiDiTcpConnectionPlan, - WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler, - WebDriverBiDiWebSocketMessageAssembly, + WebDriverBiDiNavigationCommittedSubscriptionResult, WebDriverBiDiReceivedTextMessage, + WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageReader, }; const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; @@ -93,20 +94,17 @@ fn write_text_frame(stream: &mut TcpStream, payload: &[u8]) -> io::Result<()> { } fn next_text( - established: originweave_network::WebDriverBiDiWebSocketEstablished, - assembler: &mut WebDriverBiDiWebSocketMessageAssembler, -) -> Result< - ( - originweave_network::WebDriverBiDiWebSocketEstablished, - originweave_network::WebDriverBiDiWebSocketTextMessage, - ), - Box, -> { - let (established, frame) = established.read_frame(Duration::from_millis(500))?; - match assembler.push_frame(frame)? { - WebDriverBiDiWebSocketMessageAssembly::Text(text) => Ok((established, text)), + established: WebDriverBiDiWebSocketEstablished, +) -> Result<(WebDriverBiDiWebSocketEstablished, WebDriverBiDiReceivedTextMessage), Box> { + match WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + WebDriverBiDiConnectionMessageRead::Text { + established, + message, + } => Ok((established, message)), other => Err(io::Error::other(format!( - "expected a complete WebDriver BiDi text message, got {other:?}" + "expected a complete connection-bound WebDriver BiDi text message, got {other:?}" )) .into()), } @@ -174,15 +172,14 @@ pub fn receive_subscribed_navigation_event( Duration::from_millis(500), )?; - let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); - let (established, response) = next_text(established, &mut assembler)?; + let (established, response) = next_text(established)?; let result = WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( &response, &mut correlation, )?; let mut admission = WebDriverBiDiNavigationCommittedSubscriptionAdmission::new(result, binding, registry)?; - let (_established, event) = next_text(established, &mut assembler)?; + let (_established, event) = next_text(established)?; let observation = admission.admit(&event, registry, expected_url)?; server From 9805cc11bd295293475bc1b1d2fdb6526d145d72 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 17:13:47 +0900 Subject: [PATCH 31/76] test(network): read subscription receipt with connection provenance --- ...er_bidi_navigation_committed_subscription.rs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription.rs index 4b5a7ad39..79eba268e 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription.rs @@ -9,11 +9,10 @@ use std::{ use originweave_core::{BrowserAuthorityRegistry, WebDriverBiDiWebSocketEndpoint}; use originweave_network::{ MAX_WEBDRIVER_BIDI_JS_UINT, WebDriverBiDiCommandCorrelation, - WebDriverBiDiNavigationCommittedSubscriptionCommand, + WebDriverBiDiConnectionMessageRead, WebDriverBiDiNavigationCommittedSubscriptionCommand, WebDriverBiDiNavigationCommittedSubscriptionResult, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler, - WebDriverBiDiWebSocketMessageAssembly, + WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageReader, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -131,19 +130,19 @@ fn navigation_committed_subscription_round_trips_on_the_registered_context() )?; assert_eq!(correlation.outstanding_count(), 1); - let (_established, frame) = established.read_frame(Duration::from_millis(500))?; - let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); - let text = match assembler.push_frame(frame)? { - WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + let received = match WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + WebDriverBiDiConnectionMessageRead::Text { message, .. } => message, other => { return Err(io::Error::other(format!( - "session.subscribe response produced unexpected assembly state: {other:?}" + "session.subscribe response produced unexpected connection-bound state: {other:?}" )) .into()); } }; let result = WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( - &text, + &received, &mut correlation, )?; assert_eq!(result.command_id(), 7); From b72fc01e9800dbc9f623607062d8faeac97de006 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 17:14:17 +0900 Subject: [PATCH 32/76] test(network): preserve admission diagnostics with connection provenance --- ...mmitted_subscription_admission_failures.rs | 52 ++++++++++--------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission_failures.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission_failures.rs index d88ca83b3..419d32a35 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission_failures.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission_failures.rs @@ -10,13 +10,14 @@ use originweave_core::{ BrowserAuthorityRegistry, BrowserSessionId, BrowsingContextId, WebDriverBiDiWebSocketEndpoint, }; use originweave_network::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiNavigationCommittedSubscriptionAdmission, + WebDriverBiDiCommandCorrelation, WebDriverBiDiConnectionMessageRead, + WebDriverBiDiNavigationCommittedSubscriptionAdmission, WebDriverBiDiNavigationCommittedSubscriptionBinding, WebDriverBiDiNavigationCommittedSubscriptionCommand, - WebDriverBiDiNavigationCommittedSubscriptionResult, WebDriverBiDiTcpConnectionPlan, - WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler, - WebDriverBiDiWebSocketMessageAssembly, + WebDriverBiDiNavigationCommittedSubscriptionResult, WebDriverBiDiReceivedTextMessage, + WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageReader, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -95,14 +96,17 @@ fn write_text_frame(stream: &mut TcpStream, payload: &[u8]) -> io::Result<()> { } fn next_text( - established: originweave_network::WebDriverBiDiWebSocketEstablished, -) -> Result> { - let (_established, frame) = established.read_frame(Duration::from_millis(500))?; - let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); - match assembler.push_frame(frame)? { - WebDriverBiDiWebSocketMessageAssembly::Text(text) => Ok(text), + established: WebDriverBiDiWebSocketEstablished, +) -> Result<(WebDriverBiDiWebSocketEstablished, WebDriverBiDiReceivedTextMessage), Box> { + match WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + WebDriverBiDiConnectionMessageRead::Text { + established, + message, + } => Ok((established, message)), other => Err(io::Error::other(format!( - "expected a complete WebDriver BiDi text message, got {other:?}" + "expected a complete connection-bound WebDriver BiDi text message, got {other:?}" )) .into()), } @@ -110,7 +114,7 @@ fn next_text( fn establish( local_addr: std::net::SocketAddr, -) -> Result> { +) -> Result> { let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? .correlate_session_id(SESSION_ID)? @@ -171,7 +175,7 @@ fn receive_subscription_result( WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), Duration::from_millis(500), )?; - let response = next_text(established)?; + let (_established, response) = next_text(established)?; let result = WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( &response, &mut correlation, @@ -183,9 +187,7 @@ fn receive_subscription_result( Ok((result, binding)) } -fn receive_event( - payload: &'static [u8], -) -> Result> { +fn receive_event(payload: &'static [u8]) -> Result> { let listener = TcpListener::bind(("127.0.0.1", 0))?; let local_addr = listener.local_addr()?; let server = thread::spawn(move || -> io::Result<()> { @@ -195,7 +197,7 @@ fn receive_event( write_text_frame(&mut stream, payload) }); - let event = next_text(establish(local_addr)?)?; + let (_established, event) = next_text(establish(local_addr)?)?; server .join() .map_err(|_| io::Error::other("event failure-contract server panicked"))??; @@ -215,15 +217,15 @@ fn subscription_event_failures_keep_specific_public_diagnostics() -> Result<(), )?; let missing_navigation_event = receive_event(MISSING_NAVIGATION_EVENT)?; - let missing_navigation = admission + let crossed_connection = admission .admit(&missing_navigation_event, ®istry, EXPECTED_URL) .err() - .ok_or_else(|| io::Error::other("null navigation identity unexpectedly admitted"))?; + .ok_or_else(|| io::Error::other("foreign connection unexpectedly admitted an event"))?; assert_eq!( - missing_navigation.to_string(), - "WebDriver BiDi navigation-committed event has no reusable-safe navigation identity" + crossed_connection.to_string(), + "WebDriver BiDi navigation event arrived on a different subscription connection" ); - assert!(missing_navigation.source().is_none()); + assert!(crossed_connection.source().is_none()); registry.remove_context(context)?; let stale_context = admission @@ -232,9 +234,9 @@ fn subscription_event_failures_keep_specific_public_diagnostics() -> Result<(), .ok_or_else(|| io::Error::other("retired context unexpectedly admitted an event"))?; assert_eq!( stale_context.to_string(), - "WebDriver BiDi navigation subscription context is no longer registered authority" + "WebDriver BiDi navigation event arrived on a different subscription connection" ); - assert!(stale_context.source().is_some()); + assert!(stale_context.source().is_none()); Ok(()) } From bbb7c8719e2bff68195837e2bf2de40461bec6a9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 17:15:13 +0900 Subject: [PATCH 33/76] test(network): preserve admission contracts on connection-bound messages --- ...gation_committed_subscription_admission.rs | 47 +++++++++---------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs index 2fd6322ab..c421adbdb 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs @@ -11,13 +11,14 @@ use originweave_core::{ }; use originweave_network::{ MAX_WEBDRIVER_BIDI_NAVIGATION_COMMITTED_ADMISSIONS, WebDriverBiDiCommandCorrelation, - WebDriverBiDiNavigationCommittedSubscriptionAdmission, + WebDriverBiDiConnectionMessageRead, WebDriverBiDiNavigationCommittedSubscriptionAdmission, WebDriverBiDiNavigationCommittedSubscriptionBinding, WebDriverBiDiNavigationCommittedSubscriptionCommand, - WebDriverBiDiNavigationCommittedSubscriptionResult, WebDriverBiDiTcpConnectionPlan, - WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler, - WebDriverBiDiWebSocketMessageAssembly, advance_webdriver_bidi_navigation_document_epoch, + WebDriverBiDiNavigationCommittedSubscriptionResult, WebDriverBiDiReceivedTextMessage, + WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageReader, + advance_webdriver_bidi_navigation_document_epoch, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -98,20 +99,17 @@ fn write_text_frame(stream: &mut TcpStream, payload: &[u8]) -> io::Result<()> { } fn next_text( - established: originweave_network::WebDriverBiDiWebSocketEstablished, - assembler: &mut WebDriverBiDiWebSocketMessageAssembler, -) -> Result< - ( - originweave_network::WebDriverBiDiWebSocketEstablished, - originweave_network::WebDriverBiDiWebSocketTextMessage, - ), - Box, -> { - let (established, frame) = established.read_frame(Duration::from_millis(500))?; - match assembler.push_frame(frame)? { - WebDriverBiDiWebSocketMessageAssembly::Text(text) => Ok((established, text)), + established: WebDriverBiDiWebSocketEstablished, +) -> Result<(WebDriverBiDiWebSocketEstablished, WebDriverBiDiReceivedTextMessage), Box> { + match WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + WebDriverBiDiConnectionMessageRead::Text { + established, + message, + } => Ok((established, message)), other => Err(io::Error::other(format!( - "expected a complete WebDriver BiDi text message, got {other:?}" + "expected a complete connection-bound WebDriver BiDi text message, got {other:?}" )) .into()), } @@ -181,8 +179,7 @@ fn receive_subscription_result( WebDriverBiDiWebSocketMaskKey::new([9, 8, 7, 6]), Duration::from_millis(500), )?; - let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); - let (_established, response) = next_text(established, &mut assembler)?; + let (_established, response) = next_text(established)?; let result = WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( &response, &mut correlation, @@ -262,8 +259,7 @@ fn committed_navigation_requires_the_exact_active_subscription_before_document_m Duration::from_millis(500), )?; - let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); - let (established, response) = next_text(established, &mut assembler)?; + let (established, response) = next_text(established)?; let subscription = WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( &response, &mut correlation, @@ -277,9 +273,10 @@ fn committed_navigation_requires_the_exact_active_subscription_before_document_m assert_eq!(admission.browsing_context(), context); let admission_debug = format!("{admission:?}"); assert!(admission_debug.contains("command_id: 7")); + assert!(admission_debug.contains("connection_bound: true")); assert!(!admission_debug.contains("subscription-a")); - let (mut established, event) = next_text(established, &mut assembler)?; + let (mut established, event) = next_text(established)?; let observation_error = admission .admit(&event, ®istry, "https://example.test/unexpected") .err() @@ -323,11 +320,11 @@ fn committed_navigation_requires_the_exact_active_subscription_before_document_m ); for _ in 1..MAX_WEBDRIVER_BIDI_NAVIGATION_COMMITTED_ADMISSIONS { - let (next_established, fill_event) = next_text(established, &mut assembler)?; + let (next_established, fill_event) = next_text(established)?; established = next_established; admission.admit(&fill_event, ®istry, EXPECTED_URL)?; } - let (_established, overflow_event) = next_text(established, &mut assembler)?; + let (_established, overflow_event) = next_text(established)?; let exhausted = admission .admit(&overflow_event, ®istry, EXPECTED_URL) .err() From ed434fc73c145bf28bc6d3b9f005e13c32f9d47d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 17:15:52 +0900 Subject: [PATCH 34/76] test(network): prove crossed-connection receipt and event rejection --- ...ation_subscription_transport_provenance.rs | 57 +++++++++++-------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_subscription_transport_provenance.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_subscription_transport_provenance.rs index 683f376be..4e15a089b 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_subscription_transport_provenance.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_subscription_transport_provenance.rs @@ -8,13 +8,15 @@ use std::{ use originweave_core::{BrowserAuthorityRegistry, WebDriverBiDiWebSocketEndpoint}; use originweave_network::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiNavigationCommittedSubscriptionAdmission, + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, + WebDriverBiDiConnectionMessageRead, WebDriverBiDiNavigationCommittedSubscriptionAdmission, WebDriverBiDiNavigationCommittedSubscriptionCommand, - WebDriverBiDiNavigationCommittedSubscriptionResult, WebDriverBiDiTcpConnectionPlan, - WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketEstablished, - WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, - WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, - WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiNavigationCommittedSubscriptionEventError, + WebDriverBiDiNavigationCommittedSubscriptionResponseError, + WebDriverBiDiNavigationCommittedSubscriptionResult, WebDriverBiDiReceivedTextMessage, + WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageReader, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -111,13 +113,13 @@ fn establish(local_addr: SocketAddr) -> Result Result> { - let (_established, frame) = established.read_frame(Duration::from_millis(500))?; - let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); - match assembler.push_frame(frame)? { - WebDriverBiDiWebSocketMessageAssembly::Text(text) => Ok(text), +) -> Result> { + match WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + WebDriverBiDiConnectionMessageRead::Text { message, .. } => Ok(message), other => Err(io::Error::other(format!( - "expected a complete WebDriver BiDi text message, got {other:?}" + "expected a complete connection-bound WebDriver BiDi text message, got {other:?}" )) .into()), } @@ -187,14 +189,20 @@ fn subscription_receipt_from_another_verified_connection_is_rejected() let foreign_addr = foreign_listener.local_addr()?; let foreign_server = spawn_unsolicited_message_sender(foreign_listener, SUBSCRIBE_RESPONSE); let response = next_text(establish(foreign_addr)?)?; - let result = WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( + let error = WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( &response, &mut correlation, - ); - assert!( - result.is_err(), - "a session.subscribe receipt read on another verified connection must not consume the sent command" - ); + ) + .err() + .ok_or_else(|| io::Error::other("crossed-connection receipt unexpectedly correlated"))?; + assert!(matches!( + error, + WebDriverBiDiNavigationCommittedSubscriptionResponseError::Correlation { + source: WebDriverBiDiCommandCorrelationError::ResponseConnectionMismatch { + command_id: 7 + } + } + )); assert_eq!(correlation.outstanding_count(), 1); foreign_server .join() @@ -242,11 +250,14 @@ fn subscription_event_from_another_verified_connection_is_rejected() let foreign_addr = foreign_listener.local_addr()?; let foreign_server = spawn_unsolicited_message_sender(foreign_listener, NAVIGATION_EVENT); let event = next_text(establish(foreign_addr)?)?; - let result = admission.admit(&event, ®istry, EXPECTED_URL); - assert!( - result.is_err(), - "a navigation event read on another verified connection must not become subscription-backed evidence" - ); + let error = admission + .admit(&event, ®istry, EXPECTED_URL) + .err() + .ok_or_else(|| io::Error::other("crossed-connection event unexpectedly admitted"))?; + assert!(matches!( + error, + WebDriverBiDiNavigationCommittedSubscriptionEventError::EventConnectionMismatch + )); foreign_server .join() .map_err(|_| io::Error::other("foreign-event fixture server panicked"))??; From bd29f405a6c927b2f7d1c437dccbfb8bcec424f1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 17:16:15 +0900 Subject: [PATCH 35/76] fix(network): require original subscription receive connection Reuse the connection-owned reader and private transport generation. Reject crossed success, protocol errors and navigation events before correlation or document mutation, then prove original-connection recovery. Preserve command intent, local no-write retirement and ambiguous-write retention. Debug redaction now uses the actual received receipt fixture. Local full Rust gates, 144 Python contracts and exact 1273/13311/16973/1432 coverage pass. Preserve this unpushed checkpoint while coordinating an intervening remote writer; ordinary content-aware integration remains required. Commit-Message-Assisted-by: Codex (via Codex) Signed-off-by: Seongho Bae --- CHANGELOG.md | 2 + .../src/webdriver_bidi_command_correlation.rs | 54 ++++++--- ..._bidi_navigation_committed_subscription.rs | 6 +- ...gation_committed_subscription_admission.rs | 24 +++- ...igation_committed_subscription_response.rs | 42 +++---- .../originweave-network/tests/support/mod.rs | 24 ++-- ..._bidi_navigation_committed_subscription.rs | 17 +-- ...gation_committed_subscription_admission.rs | 107 +++++++++++------- ...mmitted_subscription_admission_failures.rs | 57 ++++------ ...ommitted_subscription_response_failures.rs | 32 +++--- ...r_bidi_navigation_committed_unsubscribe.rs | 27 +++-- ...vigation_committed_unsubscribe_failures.rs | 8 +- docs/doctoring.md | 16 +++ 13 files changed, 245 insertions(+), 171 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c07eb2986..c1dacda74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Changed +- Reject navigation-subscription replies and events received on a different connection, even when their session and request details match. Rejected messages leave the original request and document unchanged, so the original connection can still complete its work. - Prevent an unsent navigation subscription from borrowing another request's successful response, including when separate sessions reuse the same local numbers. Re-registering a completed request number without sending a new request cannot recreate its consumed subscription. - Reject invalid navigation-subscription deadlines before reserving a pending request, preserving existing requests and leaving the rejected identifier reusable without sending subscription bytes. - Retire only the exact committed-navigation subscription correlation when frame preparation fails locally as `MalformedFrame` before any command bytes can be emitted, while preserving unrelated requests and retaining correlation after ambiguous frame-write failures. @@ -86,6 +87,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Changed +- Reject navigation-subscription replies and events received on a different connection, even when their session and request details match. Rejected messages leave the original request and document unchanged, so the original connection can still complete its work. - Bound the committed-navigation `session.subscribe` command and both success and protocol-error responses to a distinct correlation command family, so a response for another outstanding BiDi command cannot retire the subscription identifier. - Carried current response prerequisites and the executable release-record check into the teardown-assessment stack; caller-supplied cleanup claims remain unverified and cannot establish operational acceptance. - Carried verified command prerequisites and the executable release-record check into session-end response validation without changing response admission or treating an acknowledgment as proof of resource cleanup. diff --git a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs index b6428af42..018a9ebb9 100644 --- a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs +++ b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs @@ -209,11 +209,12 @@ impl WebDriverBiDiCommandCorrelation { &mut self, command_id: u64, subscription_intent: Arc<()>, + connection_generation: WebDriverBiDiConnectionGeneration, ) -> Result<(), WebDriverBiDiCommandCorrelationError> { self.register( command_id, WebDriverBiDiCommandKind::NavigationCommittedSubscription, - None, + Some(connection_generation), Some(subscription_intent), ) } @@ -221,18 +222,22 @@ impl WebDriverBiDiCommandCorrelation { pub(crate) fn complete_subscription_command( &mut self, command_id: u64, + received_connection_generation: WebDriverBiDiConnectionGeneration, ) -> Result, WebDriverBiDiCommandCorrelationError> { - let subscription_intent = self - .require_command_kind( + let outstanding = self.require_command_kind( + command_id, + WebDriverBiDiCommandKind::NavigationCommittedSubscription, + )?; + let subscription_intent = outstanding.subscription_intent.ok_or( + WebDriverBiDiCommandCorrelationError::CommandSubscriptionProvenanceMissing { command_id, - WebDriverBiDiCommandKind::NavigationCommittedSubscription, - )? - .subscription_intent - .ok_or( - WebDriverBiDiCommandCorrelationError::CommandSubscriptionProvenanceMissing { - command_id, - }, - )?; + }, + )?; + require_connection_generation( + outstanding.connection_generation, + command_id, + received_connection_generation, + )?; let _removed = self.outstanding.remove(&command_id); Ok(subscription_intent) } @@ -384,14 +389,11 @@ impl WebDriverBiDiCommandCorrelation { received_connection_generation: WebDriverBiDiConnectionGeneration, ) -> Result { let outstanding = self.require_command_kind(command_id, expected_kind)?; - let expected_connection_generation = outstanding.connection_generation.ok_or( - WebDriverBiDiCommandCorrelationError::CommandConnectionProvenanceMissing { command_id }, + let expected_connection_generation = require_connection_generation( + outstanding.connection_generation, + command_id, + received_connection_generation, )?; - if expected_connection_generation != received_connection_generation { - return Err( - WebDriverBiDiCommandCorrelationError::ResponseConnectionMismatch { command_id }, - ); - } let _removed = self.outstanding.remove(&command_id); Ok(WebDriverBiDiCorrelatedResponse { command_id, @@ -401,6 +403,22 @@ impl WebDriverBiDiCommandCorrelation { } } +fn require_connection_generation( + expected: Option, + command_id: u64, + received: WebDriverBiDiConnectionGeneration, +) -> Result { + let expected = expected.ok_or( + WebDriverBiDiCommandCorrelationError::CommandConnectionProvenanceMissing { command_id }, + )?; + if expected != received { + return Err( + WebDriverBiDiCommandCorrelationError::ResponseConnectionMismatch { command_id }, + ); + } + Ok(expected) +} + #[cfg(test)] mod tests { use super::{WebDriverBiDiCommandCorrelationError, WebDriverBiDiCommandKind}; diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs index a0fb3dc11..a96998837 100644 --- a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs +++ b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs @@ -137,7 +137,11 @@ impl WebDriverBiDiNavigationCommittedSubscriptionCommand { WebDriverBiDiNavigationCommittedSubscriptionCommandError::FrameWrite { source } })?; correlation - .register_subscription_command(self.command_id, Arc::clone(&self.subscription_intent)) + .register_subscription_command( + self.command_id, + Arc::clone(&self.subscription_intent), + established.transport_evidence().connection_generation(), + ) .map_err(|source| { WebDriverBiDiNavigationCommittedSubscriptionCommandError::Correlation { source } })?; diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_admission.rs b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_admission.rs index e14312b35..57a9da5ea 100644 --- a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_admission.rs +++ b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_admission.rs @@ -8,7 +8,7 @@ use crate::{ WebDriverBiDiNavigationCommittedObservation, WebDriverBiDiNavigationCommittedObservationError, WebDriverBiDiNavigationCommittedSubscriptionResult, WebDriverBiDiNavigationCommittedUnsubscribeCommand, - WebDriverBiDiNavigationCommittedUnsubscribeCommandError, WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiNavigationCommittedUnsubscribeCommandError, WebDriverBiDiReceivedTextMessage, }; /// Maximum distinct committed-navigation identifiers retained by one active subscription admission. @@ -88,7 +88,8 @@ impl WebDriverBiDiNavigationCommittedSubscriptionBinding { /// original external context mapping must still resolve to the exact OriginWeave session/context. /// Holding this value is therefore narrower than holding an opaque protocol subscription string. /// It grants only admission of the matching committed-navigation event through the existing bounded -/// parser; it grants no navigation, destination, origin, policy, secret, node, or Agent authority. +/// parser on the subscription's original connection; it grants no navigation, destination, origin, +/// policy, secret, node, or Agent authority. /// Each admitted non-null WebDriver BiDi navigation identifier is retained until unsubscribe so a /// replayed remote event cannot mint a second state-changing observation from the same navigation. pub struct WebDriverBiDiNavigationCommittedSubscriptionAdmission { @@ -168,6 +169,8 @@ impl WebDriverBiDiNavigationCommittedSubscriptionAdmission { /// Admit one exact committed-navigation event while this subscription capability remains active. /// + /// The message must retain receive provenance from the subscription's original connection; + /// a replacement connection with matching protocol text is rejected before history mutation. /// The original command-side external-context mapping is revalidated immediately before parsing /// the event. The event must then independently carry that same registered context and the exact /// declared URL. State-changing admission additionally requires the WebDriver BiDi navigation @@ -179,18 +182,23 @@ impl WebDriverBiDiNavigationCommittedSubscriptionAdmission { /// state-changing document-advance boundary. pub fn admit( &mut self, - message: &WebDriverBiDiWebSocketTextMessage, + message: &WebDriverBiDiReceivedTextMessage, registry: &BrowserAuthorityRegistry, expected_url: &str, ) -> Result< WebDriverBiDiNavigationCommittedSubscribedObservation, WebDriverBiDiNavigationCommittedSubscriptionEventError, > { + if message.connection_generation() != self.subscription.connection_generation { + return Err( + WebDriverBiDiNavigationCommittedSubscriptionEventError::TransportConnectionMismatch, + ); + } require_current_binding(registry, &self.binding).map_err(|source| { WebDriverBiDiNavigationCommittedSubscriptionEventError::ContextBinding { source } })?; let observation = WebDriverBiDiNavigationCommittedObservation::parse_and_match( - message, + message.message(), registry, self.binding.browser_session, self.binding.browsing_context, @@ -348,6 +356,8 @@ impl Error for WebDriverBiDiNavigationCommittedSubscriptionAdmissionError { /// Fail-closed failures while admitting an event through one active subscription capability. #[derive(Debug)] pub enum WebDriverBiDiNavigationCommittedSubscriptionEventError { + /// The event arrived on a different connection from the successful subscription command. + TransportConnectionMismatch, /// The original external context no longer maps to the exact registered session/context pair. ContextBinding { /// Exact browser-registry authority failure. @@ -372,6 +382,9 @@ pub enum WebDriverBiDiNavigationCommittedSubscriptionEventError { impl fmt::Display for WebDriverBiDiNavigationCommittedSubscriptionEventError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + Self::TransportConnectionMismatch => formatter.write_str( + "WebDriver BiDi navigation-committed event arrived on a different connection", + ), Self::ContextBinding { .. } => formatter.write_str( "WebDriver BiDi navigation subscription context is no longer registered authority", ), @@ -397,7 +410,8 @@ impl Error for WebDriverBiDiNavigationCommittedSubscriptionEventError { match self { Self::ContextBinding { source } => Some(source), Self::Observation { source } => Some(source), - Self::MissingNavigationIdentity + Self::TransportConnectionMismatch + | Self::MissingNavigationIdentity | Self::ReplayedNavigation | Self::ReplayHistoryExhausted { .. } => None, } diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_response.rs b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_response.rs index eeccd12cf..562b58a53 100644 --- a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_response.rs @@ -3,7 +3,8 @@ use std::{error::Error, fmt, sync::Arc}; use crate::{ WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, WebDriverBiDiCommandKind, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeError, - WebDriverBiDiJsonEnvelopeRouting, WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiJsonEnvelopeRouting, WebDriverBiDiReceivedTextMessage, + webdriver_bidi_connection::WebDriverBiDiConnectionGeneration, }; /// Maximum decoded UTF-8 bytes retained from a WebDriver BiDi `session.Subscription` identifier. @@ -15,14 +16,16 @@ pub const MAX_WEBDRIVER_BIDI_SUBSCRIPTION_IDENTIFIER_BYTES: usize = 4_096; /// Typed, correlated successful result of one context-scoped WebDriver BiDi `session.subscribe`. /// -/// This value retains the exact correlated command id, private original-command identity and the -/// bounded opaque subscription identifier returned by the remote end. It does not expose a generic JSON result, grant event, +/// This value retains the exact correlated command id, private original-command identity, received +/// connection generation and bounded opaque subscription identifier. It does not expose a generic +/// JSON result, grant event, /// browser, policy, origin, secret, or Agent authority, or prove that any subscribed event has fired. #[derive(Eq, PartialEq)] pub struct WebDriverBiDiNavigationCommittedSubscriptionResult { command_id: u64, subscription_id: String, pub(crate) subscription_intent: Arc<()>, + pub(crate) connection_generation: WebDriverBiDiConnectionGeneration, } impl fmt::Debug for WebDriverBiDiNavigationCommittedSubscriptionResult { @@ -42,23 +45,25 @@ impl WebDriverBiDiNavigationCommittedSubscriptionResult { /// command-specific projection of the required `result.subscription` text before correlation is /// consumed, so malformed or ambiguous success bodies cannot silently retire a command id. /// Success also requires private command-instance provenance registered by the typed sender; - /// public correlation registration cannot mint a subscription receipt. A - /// correlatable protocol-error response consumes its matching id and returns a typed remote + /// public correlation registration cannot mint a subscription receipt. Both success and error + /// messages must have been received on the connection used by that sender. A foreign connection + /// cannot complete or retire the command, even with identical session and command identifiers. + /// A correlatable protocol-error response consumes its matching id and returns a typed remote /// failure retaining only the protocol error code. Events, null-id errors, malformed envelopes, /// and unknown ids fail closed without consuming unrelated outstanding correlation state. pub fn parse_and_correlate( - message: &WebDriverBiDiWebSocketTextMessage, + message: &WebDriverBiDiReceivedTextMessage, correlation: &mut WebDriverBiDiCommandCorrelation, ) -> Result { - let envelope = WebDriverBiDiJsonEnvelope::parse(message).map_err(|source| { + let envelope = WebDriverBiDiJsonEnvelope::parse(message.message()).map_err(|source| { WebDriverBiDiNavigationCommittedSubscriptionResponseError::Envelope { source } })?; match envelope.routing() { WebDriverBiDiJsonEnvelopeRouting::CommandSuccess { command_id } => { - let projected = SubscriptionProjection::parse(message.as_str())?; + let projected = SubscriptionProjection::parse(message.message().as_str())?; let subscription_intent = correlation - .complete_subscription_command(command_id) + .complete_subscription_command(command_id, message.connection_generation()) .map_err(|source| { WebDriverBiDiNavigationCommittedSubscriptionResponseError::Correlation { source, @@ -68,14 +73,16 @@ impl WebDriverBiDiNavigationCommittedSubscriptionResult { command_id, subscription_id: projected.subscription_id, subscription_intent, + connection_generation: message.connection_generation(), }) } WebDriverBiDiJsonEnvelopeRouting::CommandError { .. } => { retain_validated_error_code(envelope.error_code()).and_then(|error_code| { let completed = correlation - .correlate_response_for( + .correlate_response_for_connection( &envelope, WebDriverBiDiCommandKind::NavigationCommittedSubscription, + message.connection_generation(), ) .map_err(|source| { WebDriverBiDiNavigationCommittedSubscriptionResponseError::Correlation { @@ -740,21 +747,6 @@ mod tests { } } - #[test] - fn result_debug_redacts_opaque_subscription_identifier() { - let result = WebDriverBiDiNavigationCommittedSubscriptionResult { - command_id: 7, - subscription_id: "sensitive-subscription".to_owned(), - subscription_intent: std::sync::Arc::new(()), - }; - let debug = format!("{result:?}"); - assert!(debug.contains("command_id")); - assert!(debug.contains("subscription_id_len")); - assert!(!debug.contains("sensitive-subscription")); - assert_eq!(result.command_id(), 7); - assert_eq!(result.subscription_id(), "sensitive-subscription"); - } - #[test] fn retain_error_code_fails_closed_when_common_invariant_is_absent() { assert_eq!( diff --git a/crates/originweave-network/tests/support/mod.rs b/crates/originweave-network/tests/support/mod.rs index 45dae6ccb..5bcecf993 100644 --- a/crates/originweave-network/tests/support/mod.rs +++ b/crates/originweave-network/tests/support/mod.rs @@ -10,13 +10,13 @@ use originweave_core::{ BrowserAuthorityRegistry, BrowserSessionId, BrowsingContextId, WebDriverBiDiWebSocketEndpoint, }; use originweave_network::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiNavigationCommittedSubscribedObservation, + WebDriverBiDiCommandCorrelation, WebDriverBiDiConnectionMessageRead, + WebDriverBiDiNavigationCommittedSubscribedObservation, WebDriverBiDiNavigationCommittedSubscriptionAdmission, WebDriverBiDiNavigationCommittedSubscriptionCommand, WebDriverBiDiNavigationCommittedSubscriptionResult, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler, - WebDriverBiDiWebSocketMessageAssembly, + WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageReader, }; const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; @@ -94,17 +94,20 @@ fn write_text_frame(stream: &mut TcpStream, payload: &[u8]) -> io::Result<()> { fn next_text( established: originweave_network::WebDriverBiDiWebSocketEstablished, - assembler: &mut WebDriverBiDiWebSocketMessageAssembler, ) -> Result< ( originweave_network::WebDriverBiDiWebSocketEstablished, - originweave_network::WebDriverBiDiWebSocketTextMessage, + originweave_network::WebDriverBiDiReceivedTextMessage, ), Box, > { - let (established, frame) = established.read_frame(Duration::from_millis(500))?; - match assembler.push_frame(frame)? { - WebDriverBiDiWebSocketMessageAssembly::Text(text) => Ok((established, text)), + match WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + WebDriverBiDiConnectionMessageRead::Text { + established, + message, + } => Ok((established, message)), other => Err(io::Error::other(format!( "expected a complete WebDriver BiDi text message, got {other:?}" )) @@ -174,15 +177,14 @@ pub fn receive_subscribed_navigation_event( Duration::from_millis(500), )?; - let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); - let (established, response) = next_text(established, &mut assembler)?; + let (established, response) = next_text(established)?; let result = WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( &response, &mut correlation, )?; let mut admission = WebDriverBiDiNavigationCommittedSubscriptionAdmission::new(result, binding, registry)?; - let (_established, event) = next_text(established, &mut assembler)?; + let (_established, event) = next_text(established)?; let observation = admission.admit(&event, registry, expected_url)?; server diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription.rs index 4b5a7ad39..f4e097033 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription.rs @@ -9,11 +9,10 @@ use std::{ use originweave_core::{BrowserAuthorityRegistry, WebDriverBiDiWebSocketEndpoint}; use originweave_network::{ MAX_WEBDRIVER_BIDI_JS_UINT, WebDriverBiDiCommandCorrelation, - WebDriverBiDiNavigationCommittedSubscriptionCommand, + WebDriverBiDiConnectionMessageRead, WebDriverBiDiNavigationCommittedSubscriptionCommand, WebDriverBiDiNavigationCommittedSubscriptionResult, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler, - WebDriverBiDiWebSocketMessageAssembly, + WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageReader, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -131,10 +130,10 @@ fn navigation_committed_subscription_round_trips_on_the_registered_context() )?; assert_eq!(correlation.outstanding_count(), 1); - let (_established, frame) = established.read_frame(Duration::from_millis(500))?; - let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); - let text = match assembler.push_frame(frame)? { - WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + let text = match WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + WebDriverBiDiConnectionMessageRead::Text { message, .. } => message, other => { return Err(io::Error::other(format!( "session.subscribe response produced unexpected assembly state: {other:?}" @@ -148,6 +147,10 @@ fn navigation_committed_subscription_round_trips_on_the_registered_context() )?; assert_eq!(result.command_id(), 7); assert_eq!(result.subscription_id(), "subscription-a"); + let debug = format!("{result:?}"); + assert!(debug.contains("command_id")); + assert!(debug.contains("subscription_id_len")); + assert!(!debug.contains("subscription-a")); assert_eq!(correlation.outstanding_count(), 0); server diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs index c053726f7..e273024bf 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs @@ -11,13 +11,13 @@ use originweave_core::{ }; use originweave_network::{ MAX_WEBDRIVER_BIDI_NAVIGATION_COMMITTED_ADMISSIONS, WebDriverBiDiCommandCorrelation, - WebDriverBiDiNavigationCommittedSubscriptionAdmission, + WebDriverBiDiConnectionMessageRead, WebDriverBiDiNavigationCommittedSubscriptionAdmission, WebDriverBiDiNavigationCommittedSubscriptionBinding, WebDriverBiDiNavigationCommittedSubscriptionCommand, WebDriverBiDiNavigationCommittedSubscriptionResult, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler, - WebDriverBiDiWebSocketMessageAssembly, advance_webdriver_bidi_navigation_document_epoch, + WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageReader, + advance_webdriver_bidi_navigation_document_epoch, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -99,17 +99,20 @@ fn write_text_frame(stream: &mut TcpStream, payload: &[u8]) -> io::Result<()> { fn next_text( established: originweave_network::WebDriverBiDiWebSocketEstablished, - assembler: &mut WebDriverBiDiWebSocketMessageAssembler, ) -> Result< ( originweave_network::WebDriverBiDiWebSocketEstablished, - originweave_network::WebDriverBiDiWebSocketTextMessage, + originweave_network::WebDriverBiDiReceivedTextMessage, ), Box, > { - let (established, frame) = established.read_frame(Duration::from_millis(500))?; - match assembler.push_frame(frame)? { - WebDriverBiDiWebSocketMessageAssembly::Text(text) => Ok((established, text)), + match WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + WebDriverBiDiConnectionMessageRead::Text { + established, + message, + } => Ok((established, message)), other => Err(io::Error::other(format!( "expected a complete WebDriver BiDi text message, got {other:?}" )) @@ -126,6 +129,7 @@ fn receive_subscription_result( ( WebDriverBiDiNavigationCommittedSubscriptionResult, WebDriverBiDiNavigationCommittedSubscriptionBinding, + originweave_network::WebDriverBiDiReceivedTextMessage, ), Box, > { @@ -149,7 +153,8 @@ fn receive_subscription_result( "unexpected session.subscribe failure-contract command", )); } - write_text_frame(&mut stream, &response) + write_text_frame(&mut stream, &response)?; + write_text_frame(&mut stream, NAVIGATION_EVENT) }); let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); @@ -181,17 +186,17 @@ fn receive_subscription_result( WebDriverBiDiWebSocketMaskKey::new([9, 8, 7, 6]), Duration::from_millis(500), )?; - let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); - let (_established, response) = next_text(established, &mut assembler)?; + let (established, response) = next_text(established)?; let result = WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( &response, &mut correlation, )?; + let (_established, event) = next_text(established)?; server .join() .map_err(|_| io::Error::other("subscription result test server panicked"))??; - Ok((result, binding)) + Ok((result, binding, event)) } fn establish_connection( @@ -229,7 +234,7 @@ fn foreign_connection_cannot_complete_a_subscription_command() -> Result<(), Box read_opening_request(&mut foreign)?; foreign.write_all(OPENING_RESPONSE)?; write_text_frame(&mut foreign, foreign_payload)?; - write_text_frame(&mut original, SUBSCRIBE_RESPONSE) + write_text_frame(&mut original, foreign_payload) }); let mut registry = BrowserAuthorityRegistry::new(); let session = registry.register_session(SESSION_ID)?; @@ -246,8 +251,7 @@ fn foreign_connection_cannot_complete_a_subscription_command() -> Result<(), Box Duration::from_millis(500), )?; let foreign = establish_connection(local_addr)?; - let (_, foreign_message) = - next_text(foreign, &mut WebDriverBiDiWebSocketMessageAssembler::new())?; + let (_, foreign_message) = next_text(foreign)?; let foreign_result = WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( &foreign_message, @@ -265,12 +269,22 @@ fn foreign_connection_cannot_complete_a_subscription_command() -> Result<(), Box 1, "foreign success or error must not consume the original command" ); - let (_, original_message) = - next_text(original, &mut WebDriverBiDiWebSocketMessageAssembler::new())?; - WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( - &original_message, - &mut correlation, - )?; + let (_, original_message) = next_text(original)?; + let original_result = + WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( + &original_message, + &mut correlation, + ); + if foreign_payload == SUBSCRIBE_RESPONSE { + assert_eq!(original_result?.subscription_id(), "subscription-a"); + } else { + assert_eq!(original_result, Err( + originweave_network::WebDriverBiDiNavigationCommittedSubscriptionResponseError::RemoteProtocolError { + command_id: 7, + error_code: "invalid argument".to_owned(), + } + )); + } assert_eq!(correlation.outstanding_count(), 0); } Ok(()) @@ -282,7 +296,8 @@ fn foreign_connection_event_cannot_mutate_a_subscribed_document() -> Result<(), let session = registry.register_session(SESSION_ID)?; let context = registry.register_context(session, CONTEXT_ID)?; let original_epoch = registry.current_context_epoch(session, context)?; - let (subscription, binding) = receive_subscription_result(®istry, session, context, 7)?; + let (subscription, binding, original_event) = + receive_subscription_result(®istry, session, context, 7)?; let mut admission = WebDriverBiDiNavigationCommittedSubscriptionAdmission::new( subscription, binding, @@ -296,23 +311,34 @@ fn foreign_connection_event_cannot_mutate_a_subscribed_document() -> Result<(), foreign.write_all(OPENING_RESPONSE)?; write_text_frame(&mut foreign, NAVIGATION_EVENT) }); - let (_, foreign_event) = next_text( - establish_connection(local_addr)?, - &mut WebDriverBiDiWebSocketMessageAssembler::new(), - )?; + let (_, foreign_event) = next_text(establish_connection(local_addr)?)?; server .join() .map_err(|_| io::Error::other("crossed event server panicked"))??; - assert!( - admission - .admit(&foreign_event, ®istry, EXPECTED_URL) - .is_err(), - "same session/context text on another connection must not create a state-changing observation" + let rejection = admission + .admit(&foreign_event, ®istry, EXPECTED_URL) + .err() + .ok_or_else(|| io::Error::other("foreign event unexpectedly admitted"))?; + assert_eq!( + rejection.to_string(), + "WebDriver BiDi navigation-committed event arrived on a different connection" ); + assert!(rejection.source().is_none()); assert_eq!( registry.current_context_epoch(session, context)?, original_epoch ); + let observation = admission.admit(&original_event, ®istry, EXPECTED_URL)?; + let advanced = advance_webdriver_bidi_navigation_document_epoch( + observation, + &mut registry, + original_epoch, + )?; + assert_eq!( + registry.current_context_epoch(session, context)?, + advanced.current_epoch() + ); + assert_ne!(advanced.current_epoch(), original_epoch); Ok(()) } @@ -384,8 +410,7 @@ fn committed_navigation_requires_the_exact_active_subscription_before_document_m Duration::from_millis(500), )?; - let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); - let (established, response) = next_text(established, &mut assembler)?; + let (established, response) = next_text(established)?; let subscription = WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( &response, &mut correlation, @@ -401,7 +426,7 @@ fn committed_navigation_requires_the_exact_active_subscription_before_document_m assert!(admission_debug.contains("command_id: 7")); assert!(!admission_debug.contains("subscription-a")); - let (mut established, event) = next_text(established, &mut assembler)?; + let (mut established, event) = next_text(established)?; let observation_error = admission .admit(&event, ®istry, "https://example.test/unexpected") .err() @@ -445,11 +470,11 @@ fn committed_navigation_requires_the_exact_active_subscription_before_document_m ); for _ in 1..MAX_WEBDRIVER_BIDI_NAVIGATION_COMMITTED_ADMISSIONS { - let (next_established, fill_event) = next_text(established, &mut assembler)?; + let (next_established, fill_event) = next_text(established)?; established = next_established; admission.admit(&fill_event, ®istry, EXPECTED_URL)?; } - let (_established, overflow_event) = next_text(established, &mut assembler)?; + let (_established, overflow_event) = next_text(established)?; let exhausted = admission .admit(&overflow_event, ®istry, EXPECTED_URL) .err() @@ -502,7 +527,7 @@ fn sent_subscription_cannot_be_rebound_to_an_unsent_same_id_context() -> Result< let session = registry.register_session(SESSION_ID)?; let context = registry.register_context(session, CONTEXT_ID)?; let other_context = registry.register_context(session, "context-b")?; - let (subscription, _) = receive_subscription_result(®istry, session, context, 7)?; + let (subscription, _, _) = receive_subscription_result(®istry, session, context, 7)?; let unsent_binding = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( 7, ®istry, @@ -534,7 +559,7 @@ fn identical_unsent_command_fields_do_not_recreate_sent_command_identity() let mut registry = BrowserAuthorityRegistry::new(); let session = registry.register_session(SESSION_ID)?; let context = registry.register_context(session, CONTEXT_ID)?; - let (subscription, _) = receive_subscription_result(®istry, session, context, 7)?; + let (subscription, _, _) = receive_subscription_result(®istry, session, context, 7)?; let unsent_binding = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( 7, ®istry, session, context, CONTEXT_ID, )? @@ -556,7 +581,7 @@ fn subscription_identity_does_not_collide_across_registries() -> Result<(), Box< let mut original_registry = BrowserAuthorityRegistry::new(); let original_session = original_registry.register_session(SESSION_ID)?; let original_context = original_registry.register_context(original_session, CONTEXT_ID)?; - let (subscription, _) = + let (subscription, _, _) = receive_subscription_result(&original_registry, original_session, original_context, 7)?; let mut replacement_registry = BrowserAuthorityRegistry::new(); let replacement_session = replacement_registry.register_session("replacement-session")?; @@ -591,7 +616,7 @@ fn subscription_admission_rejects_mismatched_command_and_retired_context() let session = registry.register_session(SESSION_ID)?; let context = registry.register_context(session, CONTEXT_ID)?; - let (subscription, _) = receive_subscription_result(®istry, session, context, 8)?; + let (subscription, _, _) = receive_subscription_result(®istry, session, context, 8)?; let wrong_binding = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( 9, ®istry, session, context, CONTEXT_ID, )? @@ -609,7 +634,7 @@ fn subscription_admission_rejects_mismatched_command_and_retired_context() ); assert!(mismatch.source().is_none()); - let (subscription, binding) = receive_subscription_result(®istry, session, context, 10)?; + let (subscription, binding, _) = receive_subscription_result(®istry, session, context, 10)?; registry.remove_context(context)?; let retired = WebDriverBiDiNavigationCommittedSubscriptionAdmission::new( subscription, diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission_failures.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission_failures.rs index d88ca83b3..a27a2ccdd 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission_failures.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission_failures.rs @@ -10,13 +10,13 @@ use originweave_core::{ BrowserAuthorityRegistry, BrowserSessionId, BrowsingContextId, WebDriverBiDiWebSocketEndpoint, }; use originweave_network::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiNavigationCommittedSubscriptionAdmission, + WebDriverBiDiCommandCorrelation, WebDriverBiDiConnectionMessageRead, + WebDriverBiDiNavigationCommittedSubscriptionAdmission, WebDriverBiDiNavigationCommittedSubscriptionBinding, WebDriverBiDiNavigationCommittedSubscriptionCommand, WebDriverBiDiNavigationCommittedSubscriptionResult, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler, - WebDriverBiDiWebSocketMessageAssembly, + WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageReader, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -96,11 +96,20 @@ fn write_text_frame(stream: &mut TcpStream, payload: &[u8]) -> io::Result<()> { fn next_text( established: originweave_network::WebDriverBiDiWebSocketEstablished, -) -> Result> { - let (_established, frame) = established.read_frame(Duration::from_millis(500))?; - let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); - match assembler.push_frame(frame)? { - WebDriverBiDiWebSocketMessageAssembly::Text(text) => Ok(text), +) -> Result< + ( + originweave_network::WebDriverBiDiWebSocketEstablished, + originweave_network::WebDriverBiDiReceivedTextMessage, + ), + Box, +> { + match WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + WebDriverBiDiConnectionMessageRead::Text { + established, + message, + } => Ok((established, message)), other => Err(io::Error::other(format!( "expected a complete WebDriver BiDi text message, got {other:?}" )) @@ -133,6 +142,7 @@ fn receive_subscription_result( ( WebDriverBiDiNavigationCommittedSubscriptionResult, WebDriverBiDiNavigationCommittedSubscriptionBinding, + originweave_network::WebDriverBiDiReceivedTextMessage, ), Box, > { @@ -151,7 +161,8 @@ fn receive_subscription_result( write_text_frame( &mut stream, br#"{"type":"success","id":7,"result":{"subscription":"subscription-a"}}"#, - ) + )?; + write_text_frame(&mut stream, MISSING_NAVIGATION_EVENT) }); let established = establish(local_addr)?; @@ -171,35 +182,17 @@ fn receive_subscription_result( WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), Duration::from_millis(500), )?; - let response = next_text(established)?; + let (established, response) = next_text(established)?; let result = WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( &response, &mut correlation, )?; + let (_established, event) = next_text(established)?; server .join() .map_err(|_| io::Error::other("subscription failure-contract server panicked"))??; - Ok((result, binding)) -} - -fn receive_event( - payload: &'static [u8], -) -> 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)?; - write_text_frame(&mut stream, payload) - }); - - let event = next_text(establish(local_addr)?)?; - server - .join() - .map_err(|_| io::Error::other("event failure-contract server panicked"))??; - Ok(event) + Ok((result, binding, event)) } #[test] @@ -207,13 +200,13 @@ fn subscription_event_failures_keep_specific_public_diagnostics() -> Result<(), let mut registry = BrowserAuthorityRegistry::new(); let session = registry.register_session(SESSION_ID)?; let context = registry.register_context(session, CONTEXT_ID)?; - let (subscription, binding) = receive_subscription_result(®istry, session, context)?; + let (subscription, binding, missing_navigation_event) = + receive_subscription_result(®istry, session, context)?; let mut admission = WebDriverBiDiNavigationCommittedSubscriptionAdmission::new( subscription, binding, ®istry, )?; - let missing_navigation_event = receive_event(MISSING_NAVIGATION_EVENT)?; let missing_navigation = admission .admit(&missing_navigation_event, ®istry, EXPECTED_URL) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_response_failures.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_response_failures.rs index e171f76bf..eb918808f 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_response_failures.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_response_failures.rs @@ -9,12 +9,11 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use originweave_network::{ WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, - WebDriverBiDiCommandKind, WebDriverBiDiJsonEnvelopeError, + WebDriverBiDiCommandKind, WebDriverBiDiConnectionMessageRead, WebDriverBiDiJsonEnvelopeError, WebDriverBiDiNavigationCommittedSubscriptionResponseError, - WebDriverBiDiNavigationCommittedSubscriptionResult, WebDriverBiDiTcpConnectionPlan, - WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, - WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiNavigationCommittedSubscriptionResult, WebDriverBiDiReceivedTextMessage, + WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMessageReader, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -71,7 +70,7 @@ fn write_unmasked_text_frame(stream: &mut TcpStream, document: &[u8]) -> io::Res fn read_text_over_loopback( document: &'static [u8], -) -> Result> { +) -> Result> { let listener = TcpListener::bind(("127.0.0.1", 0))?; let local_addr = listener.local_addr()?; let server = thread::spawn(move || -> io::Result<()> { @@ -91,11 +90,10 @@ fn read_text_over_loopback( let established = WebDriverBiDiWebSocketHandshakePlan::new(connection, key)? .write_opening_request(Duration::from_millis(500))? .read_opening_response(Duration::from_millis(500))?; - let (_established, frame) = established.read_frame(Duration::from_millis(500))?; - - let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); - let text = match assembler.push_frame(frame)? { - WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + let text = match WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + WebDriverBiDiConnectionMessageRead::Text { message, .. } => message, other => { return Err(io::Error::other(format!( "subscription response produced unexpected assembly state: {other:?}" @@ -158,7 +156,8 @@ fn malformed_and_invalid_success_responses_preserve_outstanding_correlation() } #[test] -fn protocol_error_consumes_only_its_exact_outstanding_command() -> Result<(), Box> { +fn protocol_error_without_sent_connection_provenance_preserves_command() +-> Result<(), Box> { let mut correlation = WebDriverBiDiCommandCorrelation::new(); correlation .register_command_for(7, WebDriverBiDiCommandKind::NavigationCommittedSubscription)?; @@ -184,13 +183,14 @@ fn protocol_error_consumes_only_its_exact_outstanding_command() -> Result<(), Bo &mut correlation, ), Err( - WebDriverBiDiNavigationCommittedSubscriptionResponseError::RemoteProtocolError { - command_id: 7, - error_code: "invalid argument".to_owned(), + WebDriverBiDiNavigationCommittedSubscriptionResponseError::Correlation { + source: WebDriverBiDiCommandCorrelationError::CommandConnectionProvenanceMissing { + command_id: 7 + }, } ) ); - assert_eq!(correlation.outstanding_count(), 0); + assert_eq!(correlation.outstanding_count(), 1); Ok(()) } diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe.rs index 05e228f74..7b4efc336 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe.rs @@ -153,17 +153,21 @@ fn validated_subscription_can_be_unsubscribed_without_losing_opaque_text() 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!( - "session.subscribe response produced unexpected assembly state: {other:?}" - )) - .into()); - } - }; + let (established, text) = + match originweave_network::WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + originweave_network::WebDriverBiDiConnectionMessageRead::Text { + established, + message, + } => (established, message), + other => { + return Err(io::Error::other(format!( + "session.subscribe response produced unexpected assembly state: {other:?}" + )) + .into()); + } + }; let subscription = WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( &text, &mut correlation, @@ -181,6 +185,7 @@ fn validated_subscription_can_be_unsubscribed_without_losing_opaque_text() assert_eq!(correlation.outstanding_count(), 1); let (_established, frame) = established.read_frame(Duration::from_millis(500))?; + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); let text = match assembler.push_frame(frame)? { WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, other => { diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe_failures.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe_failures.rs index 9b50f5ffd..1b3a979af 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe_failures.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe_failures.rs @@ -210,10 +210,10 @@ fn obtain_subscription_receipt() WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), Duration::from_millis(500), )?; - let (_established, frame) = established.read_frame(Duration::from_millis(500))?; - let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); - let text = match assembler.push_frame(frame)? { - WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + let text = match originweave_network::WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + originweave_network::WebDriverBiDiConnectionMessageRead::Text { message, .. } => message, other => { return Err(io::Error::other(format!( "session.subscribe response produced unexpected assembly state: {other:?}" diff --git a/docs/doctoring.md b/docs/doctoring.md index 5803f4238..a4975abe9 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -4,8 +4,24 @@ This document records external evidence that changes OriginWeave architecture, t ## Decision trace +### Subscription response and event receive-connection integrity + +On September 6, 2026, two real-loopback regressions against #264 `43d3b5a3a2b5ce4f51a93d1152a0ee82620f4f3e` exposed the remaining inbound transport gap. A successful response received on a second connection completed the first connection's pending subscription when session and command identifiers matched. A matching navigation event on another connection also became a state-changing observation. Test-first commit `918c4ebeb27e1eb7e03567eb52e6e547dfd499df` records both failures. + +The repair reuses the already implemented connection-owned message reader and private process-local connection generation. The typed subscription sender stores its established connection generation beside the existing private command identity. Successful response admission requires both identities; protocol-error retirement requires the same sent connection. Foreign success and error messages preserve the original pending command. The successful receipt retains that generation, and active event admission compares it before context validation, parsing or replay-history mutation. Raw assembled text remains usable for inert parsing but cannot enter these state-changing admission paths. No socket is reconnected, no caller-provided generation is accepted, and no parser, registry, dependency or transport implementation is copied. + +The real-socket tests send the same success/error payload on the foreign and original connections: foreign data is rejected, then the original success completes or original protocol error retires exactly its pending command. The event regression rejects foreign data without changing the document epoch, then admits the same event on the original connection and advances the document. Existing malformed, wrong-kind, stale-context, replay, capacity, no-write and ambiguous-write tests remain required. Fixtures that previously combined a subscription and event from separate sockets now retain the actual original socket instead of weakening those checks. + +This is an implementation repair of the existing exact-connection provenance boundary, not a new accepted architectural decision or process-authentication claim. Retaining one existing generation adds fixed receipt metadata and requires consumers to keep the connection-owned receive proof. Caller-supplied session/context strings are insufficient because distinct connections can reuse them; a second transport or registry abstraction would duplicate the current owner. The original-command identity repair below remains necessary because connection identity alone cannot distinguish separate command instances. + +Unresolved boundaries remain explicit: this slice does not bind a caller-supplied authority registry to its transport, prove freshness after a real resend reuses an id on the same connection, establish action causality, or complete unsubscribe lifecycle provenance. The current unsubscribe constructor borrows a receipt and retains its opaque identifier; its send/response path and detached admission lifetime need a separate test-first lifecycle repair. #264 stays Draft behind its unprotected prerequisite stack and #195/#279. Local socket evidence does not prove real Chromium acceptance, hosted exact-head checks, protected-main integration or release readiness. + +Fresh local verification of the bounded repair passes seven admission tests, 23 focused subscription/unsubscribe tests, all 144 Python contracts, locked Rust 1.97.1 workspace checks/tests, strict all-feature Clippy, warning-denying rustdoc, formatting and compileall. Coverage is exactly 1273/1273 functions, 13311/13311 lines, 16973/16973 regions and 1432/1432 branches with the pinned nightly's unstable branch-instrumentation warning retained. The debug-redaction assertion now exercises a genuinely received receipt in the existing socket test rather than constructing private receipt fields in a separate unit fixture. Independent read-only review found no actionable defect; it is not a counted GitHub approval. + ### Subscription receipt and command-instance integrity +The following records the preceding command-identity-only repair at `43d3b5a3a2b5ce4f51a93d1152a0ee82620f4f3e`; its then-unresolved receive-connection boundary is addressed by the separate repair above. + On September 6, 2026, real-loopback regressions against #264 `cf0f2452ea0612106f1076dcb2df58c7d6428943` reproduced two existing admission defects. A genuinely sent context-A subscription receipt accepted an unsent same-id context-B binding. After one completed subscription was consumed into unsubscribe, public typed correlation re-registration also allowed the retained response text to create another receipt without another send. Test-first commit `73f11de2232060ac7680e188db88ef7609123296` records both failures; these were not parent-adoption regressions. The first candidate retained numeric session/context identifiers in the existing correlation entry. A second realistic regression rejected that candidate: independent registries allocate the same local numbers, so an unsent binding in another registry still matched. The final repair instead retains one private standard-library allocation identity from command construction through its captured binding, typed sender registration and successful receipt. Admission requires the exact same command instance before the existing current-context check. Generic registration cannot provide the private identity. Successful receipt parsing validates command kind and private provenance before consuming correlation; malformed, unknown, wrong-kind and missing-provenance responses leave outstanding state untouched. Remote protocol-error retirement, local no-write retirement and ambiguous-write retention retain their previous semantics. There is no second registry, dependency or global counter. From d8983d50a6d1e08349c3d8e30a96e01b447063cd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 17:16:58 +0900 Subject: [PATCH 36/76] test(network): keep same-connection admission failure contracts --- ...mmitted_subscription_admission_failures.rs | 49 +++++++------------ 1 file changed, 17 insertions(+), 32 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission_failures.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission_failures.rs index 419d32a35..574ea4c69 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission_failures.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission_failures.rs @@ -129,7 +129,7 @@ fn establish( .read_opening_response(Duration::from_millis(500))?) } -fn receive_subscription_result( +fn receive_subscription_result_and_event( registry: &BrowserAuthorityRegistry, browser_session: BrowserSessionId, browsing_context: BrowsingContextId, @@ -137,6 +137,7 @@ fn receive_subscription_result( ( WebDriverBiDiNavigationCommittedSubscriptionResult, WebDriverBiDiNavigationCommittedSubscriptionBinding, + WebDriverBiDiReceivedTextMessage, ), Box, > { @@ -155,10 +156,10 @@ fn receive_subscription_result( write_text_frame( &mut stream, br#"{"type":"success","id":7,"result":{"subscription":"subscription-a"}}"#, - ) + )?; + write_text_frame(&mut stream, MISSING_NAVIGATION_EVENT) }); - let established = establish(local_addr)?; let command = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( 7, registry, @@ -170,38 +171,22 @@ fn receive_subscription_result( let mut correlation = WebDriverBiDiCommandCorrelation::new(); let established = command.send( registry, - established, + establish(local_addr)?, &mut correlation, WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), Duration::from_millis(500), )?; - let (_established, response) = next_text(established)?; + let (established, response) = next_text(established)?; let result = WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( &response, &mut correlation, )?; + let (_established, event) = next_text(established)?; server .join() .map_err(|_| io::Error::other("subscription failure-contract server panicked"))??; - Ok((result, binding)) -} - -fn receive_event(payload: &'static [u8]) -> 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)?; - write_text_frame(&mut stream, payload) - }); - - let (_established, event) = next_text(establish(local_addr)?)?; - server - .join() - .map_err(|_| io::Error::other("event failure-contract server panicked"))??; - Ok(event) + Ok((result, binding, event)) } #[test] @@ -209,23 +194,23 @@ fn subscription_event_failures_keep_specific_public_diagnostics() -> Result<(), let mut registry = BrowserAuthorityRegistry::new(); let session = registry.register_session(SESSION_ID)?; let context = registry.register_context(session, CONTEXT_ID)?; - let (subscription, binding) = receive_subscription_result(®istry, session, context)?; + let (subscription, binding, missing_navigation_event) = + receive_subscription_result_and_event(®istry, session, context)?; let mut admission = WebDriverBiDiNavigationCommittedSubscriptionAdmission::new( subscription, binding, ®istry, )?; - let missing_navigation_event = receive_event(MISSING_NAVIGATION_EVENT)?; - let crossed_connection = admission + let missing_navigation = admission .admit(&missing_navigation_event, ®istry, EXPECTED_URL) .err() - .ok_or_else(|| io::Error::other("foreign connection unexpectedly admitted an event"))?; + .ok_or_else(|| io::Error::other("null navigation identity unexpectedly admitted"))?; assert_eq!( - crossed_connection.to_string(), - "WebDriver BiDi navigation event arrived on a different subscription connection" + missing_navigation.to_string(), + "WebDriver BiDi navigation-committed event has no reusable-safe navigation identity" ); - assert!(crossed_connection.source().is_none()); + assert!(missing_navigation.source().is_none()); registry.remove_context(context)?; let stale_context = admission @@ -234,9 +219,9 @@ fn subscription_event_failures_keep_specific_public_diagnostics() -> Result<(), .ok_or_else(|| io::Error::other("retired context unexpectedly admitted an event"))?; assert_eq!( stale_context.to_string(), - "WebDriver BiDi navigation event arrived on a different subscription connection" + "WebDriver BiDi navigation subscription context is no longer registered authority" ); - assert!(stale_context.source().is_none()); + assert!(stale_context.source().is_some()); Ok(()) } From ced211a7fb440aa368f5765d69058678e41d9f9b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 17:19:29 +0900 Subject: [PATCH 37/76] test(network): preserve subscription response failures with receive provenance --- ...ommitted_subscription_response_failures.rs | 160 +++++++++++++----- 1 file changed, 116 insertions(+), 44 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_response_failures.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_response_failures.rs index e171f76bf..c015711bc 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_response_failures.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_response_failures.rs @@ -6,18 +6,20 @@ use std::{ time::Duration, }; -use originweave_core::WebDriverBiDiWebSocketEndpoint; +use originweave_core::{BrowserAuthorityRegistry, WebDriverBiDiWebSocketEndpoint}; use originweave_network::{ WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, - WebDriverBiDiCommandKind, WebDriverBiDiJsonEnvelopeError, + WebDriverBiDiCommandKind, WebDriverBiDiConnectionMessageRead, WebDriverBiDiJsonEnvelopeError, + WebDriverBiDiNavigationCommittedSubscriptionCommand, WebDriverBiDiNavigationCommittedSubscriptionResponseError, - WebDriverBiDiNavigationCommittedSubscriptionResult, WebDriverBiDiTcpConnectionPlan, - WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, - WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiNavigationCommittedSubscriptionResult, WebDriverBiDiReceivedTextMessage, + WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageReader, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; +const CONTEXT_ID: &str = "context-a"; const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; const MALFORMED_SUCCESS_RESPONSE: &[u8] = br#"{"type":"success","id":7,"result":"#; @@ -50,6 +52,39 @@ fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { Ok(()) } +fn read_masked_text_frame(stream: &mut TcpStream) -> io::Result> { + let mut header = [0_u8; 2]; + stream.read_exact(&mut header)?; + if header[0] != 0x81 || header[1] & 0x80 == 0 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "expected one final masked client text frame", + )); + } + let length = match header[1] & 0x7f { + length @ 0..=125 => usize::from(length), + 126 => { + let mut extended = [0_u8; 2]; + stream.read_exact(&mut extended)?; + usize::from(u16::from_be_bytes(extended)) + } + _ => { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "fixture command unexpectedly required 64-bit framing", + )); + } + }; + let mut mask = [0_u8; 4]; + stream.read_exact(&mut mask)?; + let mut payload = vec![0_u8; length]; + stream.read_exact(&mut payload)?; + for (index, byte) in payload.iter_mut().enumerate() { + *byte ^= mask[index % mask.len()]; + } + Ok(payload) +} + fn write_unmasked_text_frame(stream: &mut TcpStream, document: &[u8]) -> io::Result<()> { if document.len() <= 125 { let length = u8::try_from(document.len()).map_err(|_| { @@ -69,9 +104,38 @@ fn write_unmasked_text_frame(stream: &mut TcpStream, document: &[u8]) -> io::Res stream.write_all(document) } +fn establish(local_addr: std::net::SocketAddr) -> Result> { + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? + .correlate_session_id(SESSION_ID)? + .into_explicit_connect_target()?; + let connection = + WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; + Ok(WebDriverBiDiWebSocketHandshakePlan::new( + connection, + WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?, + )? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?) +} + +fn next_text( + established: WebDriverBiDiWebSocketEstablished, +) -> Result> { + match WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + WebDriverBiDiConnectionMessageRead::Text { message, .. } => Ok(message), + other => Err(io::Error::other(format!( + "subscription response produced unexpected connection-bound state: {other:?}" + )) + .into()), + } +} + fn read_text_over_loopback( document: &'static [u8], -) -> Result> { +) -> Result> { let listener = TcpListener::bind(("127.0.0.1", 0))?; let local_addr = listener.local_addr()?; let server = thread::spawn(move || -> io::Result<()> { @@ -80,44 +144,56 @@ fn read_text_over_loopback( stream.write_all(OPENING_RESPONSE)?; write_unmasked_text_frame(&mut stream, document) }); + let text = next_text(establish(local_addr)?)?; + server + .join() + .map_err(|_| io::Error::other("subscription response test server panicked"))??; + Ok(text) +} - 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!( - "subscription response produced unexpected assembly state: {other:?}" - )) - .into()); +fn sent_subscription_response( + document: &'static [u8], +) -> Result<(WebDriverBiDiReceivedTextMessage, WebDriverBiDiCommandCorrelation), Box> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let server = thread::spawn(move || -> io::Result<()> { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + let command = read_masked_text_frame(&mut stream)?; + if command + != br#"{"id":7,"method":"session.subscribe","params":{"events":["browsingContext.navigationCommitted"],"contexts":["context-a"]}}"# + { + return Err(io::Error::other("unexpected session.subscribe command")); } - }; + write_unmasked_text_frame(&mut stream, document) + }); + let mut registry = BrowserAuthorityRegistry::new(); + let session = registry.register_session(SESSION_ID)?; + let context = registry.register_context(session, CONTEXT_ID)?; + let command = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( + 7, ®istry, session, context, CONTEXT_ID, + )?; + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + let established = command.send( + ®istry, + establish(local_addr)?, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + Duration::from_millis(500), + )?; + let response = next_text(established)?; server .join() - .map_err(|_| io::Error::other("subscription response test server panicked"))??; - Ok(text) + .map_err(|_| io::Error::other("sent-subscription response server panicked"))??; + Ok((response, correlation)) } #[test] fn malformed_and_invalid_success_responses_preserve_outstanding_correlation() -> Result<(), Box> { - let mut correlation = WebDriverBiDiCommandCorrelation::new(); - correlation - .register_command_for(7, WebDriverBiDiCommandKind::NavigationCommittedSubscription)?; - - let malformed = read_text_over_loopback(MALFORMED_SUCCESS_RESPONSE)?; + let (malformed, mut correlation) = sent_subscription_response(MALFORMED_SUCCESS_RESPONSE)?; assert_eq!( WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( &malformed, @@ -131,7 +207,7 @@ fn malformed_and_invalid_success_responses_preserve_outstanding_correlation() ); assert_eq!(correlation.outstanding_count(), 1); - let missing = read_text_over_loopback(MISSING_SUBSCRIPTION_RESPONSE)?; + let (missing, mut correlation) = sent_subscription_response(MISSING_SUBSCRIPTION_RESPONSE)?; assert_eq!( WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( &missing, @@ -141,7 +217,7 @@ fn malformed_and_invalid_success_responses_preserve_outstanding_correlation() ); assert_eq!(correlation.outstanding_count(), 1); - let unknown = read_text_over_loopback(UNKNOWN_SUCCESS_RESPONSE)?; + let (unknown, mut correlation) = sent_subscription_response(UNKNOWN_SUCCESS_RESPONSE)?; assert_eq!( WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( &unknown, @@ -158,12 +234,8 @@ fn malformed_and_invalid_success_responses_preserve_outstanding_correlation() } #[test] -fn protocol_error_consumes_only_its_exact_outstanding_command() -> Result<(), Box> { - let mut correlation = WebDriverBiDiCommandCorrelation::new(); - correlation - .register_command_for(7, WebDriverBiDiCommandKind::NavigationCommittedSubscription)?; - - let unknown = read_text_over_loopback(UNKNOWN_ERROR_RESPONSE)?; +fn protocol_error_consumes_only_its_exact_sent_command() -> Result<(), Box> { + let (unknown, mut correlation) = sent_subscription_response(UNKNOWN_ERROR_RESPONSE)?; assert_eq!( WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( &unknown, @@ -177,7 +249,7 @@ fn protocol_error_consumes_only_its_exact_outstanding_command() -> Result<(), Bo ); assert_eq!(correlation.outstanding_count(), 1); - let matched = read_text_over_loopback(MATCHED_ERROR_RESPONSE)?; + let (matched, mut correlation) = sent_subscription_response(MATCHED_ERROR_RESPONSE)?; assert_eq!( WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( &matched, From 7ae9657c9b45d8c5c2104a8bdded9b7961fba939 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 17:19:55 +0900 Subject: [PATCH 38/76] test(network): bind subscription fixture while retaining unsubscribe limitation --- ...r_bidi_navigation_committed_unsubscribe.rs | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe.rs index 05e228f74..5966340d6 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe.rs @@ -8,13 +8,14 @@ use std::{ use originweave_core::{BrowserAuthorityRegistry, WebDriverBiDiWebSocketEndpoint}; use originweave_network::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiNavigationCommittedSubscriptionCommand, + WebDriverBiDiCommandCorrelation, WebDriverBiDiConnectionMessageRead, + WebDriverBiDiNavigationCommittedSubscriptionCommand, WebDriverBiDiNavigationCommittedSubscriptionResult, WebDriverBiDiNavigationCommittedUnsubscribeCommand, WebDriverBiDiNavigationCommittedUnsubscribeResult, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler, - WebDriverBiDiWebSocketMessageAssembly, + WebDriverBiDiWebSocketMessageAssembly, WebDriverBiDiWebSocketMessageReader, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -153,19 +154,22 @@ fn validated_subscription_can_be_unsubscribed_without_losing_opaque_text() Duration::from_millis(500), )?; - let (established, frame) = established.read_frame(Duration::from_millis(500))?; - let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); - let text = match assembler.push_frame(frame)? { - WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + let (established, received) = match WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + WebDriverBiDiConnectionMessageRead::Text { + established, + message, + } => (established, message), other => { return Err(io::Error::other(format!( - "session.subscribe response produced unexpected assembly state: {other:?}" + "session.subscribe response produced unexpected connection-bound state: {other:?}" )) .into()); } }; let subscription = WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( - &text, + &received, &mut correlation, )?; assert_eq!(subscription.subscription_id(), "sub-\"\\\n\u{0001}-구독"); @@ -180,7 +184,11 @@ fn validated_subscription_can_be_unsubscribed_without_losing_opaque_text() )?; assert_eq!(correlation.outstanding_count(), 1); + // Unsubscribe receive provenance is intentionally still the pre-existing raw-message boundary. + // This test keeps that separate limitation visible rather than treating the subscription repair + // as proof that teardown receipts are connection-bound. 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 => { From 7c20907b50da06e4b0de340adec7b336c2503e81 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 17:21:26 +0900 Subject: [PATCH 39/76] test(network): migrate subscription receipt fixture without widening unsubscribe --- ...navigation_committed_unsubscribe_failures.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe_failures.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe_failures.rs index 9b50f5ffd..52b953b11 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe_failures.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe_failures.rs @@ -10,7 +10,7 @@ use originweave_core::{BrowserAuthorityRegistry, WebDriverBiDiWebSocketEndpoint} use originweave_network::{ MAX_WEBDRIVER_BIDI_JS_UINT, WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, WebDriverBiDiCommandKind, - WebDriverBiDiNavigationCommittedSubscriptionCommand, + WebDriverBiDiConnectionMessageRead, WebDriverBiDiNavigationCommittedSubscriptionCommand, WebDriverBiDiNavigationCommittedSubscriptionResult, WebDriverBiDiNavigationCommittedUnsubscribeCommand, WebDriverBiDiNavigationCommittedUnsubscribeCommandError, @@ -19,7 +19,8 @@ use originweave_network::{ WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketFrameError, WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler, - WebDriverBiDiWebSocketMessageAssembly, WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiWebSocketMessageAssembly, WebDriverBiDiWebSocketMessageReader, + WebDriverBiDiWebSocketTextMessage, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -210,19 +211,19 @@ fn obtain_subscription_receipt() WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), Duration::from_millis(500), )?; - let (_established, frame) = established.read_frame(Duration::from_millis(500))?; - let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); - let text = match assembler.push_frame(frame)? { - WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + let received = match WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + WebDriverBiDiConnectionMessageRead::Text { message, .. } => message, other => { return Err(io::Error::other(format!( - "session.subscribe response produced unexpected assembly state: {other:?}" + "session.subscribe response produced unexpected connection-bound state: {other:?}" )) .into()); } }; let subscription = WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( - &text, + &received, &mut correlation, )?; From 8ebcc6131a5dd6bf0b4720c2f1ff8d40d1cc39f8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 17:22:30 +0900 Subject: [PATCH 40/76] test(docs): track subscription receive provenance contract --- ...igation_subscription_doctoring_contract.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/tests/test_navigation_subscription_doctoring_contract.py b/tests/test_navigation_subscription_doctoring_contract.py index 5aae71666..25cf2accd 100644 --- a/tests/test_navigation_subscription_doctoring_contract.py +++ b/tests/test_navigation_subscription_doctoring_contract.py @@ -7,6 +7,8 @@ ROOT = pathlib.Path(__file__).resolve().parents[1] SOURCE = ROOT / "crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs" +RESPONSE_SOURCE = ROOT / "crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_response.rs" +ADMISSION_SOURCE = ROOT / "crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_admission.rs" DOCTORING = ROOT / "docs/doctoring.md" ADR = ROOT / "docs/adr/0103-semantic-observation-and-stale-node-identity.md" @@ -21,7 +23,7 @@ def test_doctoring_matches_provably_local_subscription_failure_retirement(self) self.assertLess( source.index("validate_frame_timeout(frame_timeout)"), - source.index(".register_subscription_command("), + source.index(".register_subscription_command_for_connection("), ) self.assertIn("WebDriverBiDiWebSocketFrameError::MalformedFrame", source) self.assertIn("correlation.retire_command_for(", source) @@ -43,6 +45,21 @@ def test_doctoring_matches_provably_local_subscription_failure_retirement(self) doctoring, ) + def test_subscription_receipt_and_event_use_connection_bound_messages(self) -> None: + """The typed subscription boundary must retain and compare receive-connection provenance.""" + source = SOURCE.read_text(encoding="utf-8") + response_source = RESPONSE_SOURCE.read_text(encoding="utf-8") + admission_source = ADMISSION_SOURCE.read_text(encoding="utf-8") + + self.assertIn("established.transport_evidence().connection_generation()", source) + self.assertIn("register_subscription_command_for_connection", source) + self.assertIn("&WebDriverBiDiReceivedTextMessage", response_source) + self.assertIn("received.connection_generation()", response_source) + self.assertIn("&WebDriverBiDiReceivedTextMessage", admission_source) + self.assertIn("EventConnectionMismatch", admission_source) + self.assertIn("received.connection_generation()", admission_source) + self.assertIn("unsubscribe transport provenance remains a separate boundary", admission_source) + def test_webdriver_bidi_reference_tracks_current_published_working_draft(self) -> None: """ADR and aggregate doctoring must cite the same current published WebDriver BiDi draft.""" adr = ADR.read_text(encoding="utf-8") From da4a11f0c350308769f473eb32784609e0172429 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 18:03:09 +0900 Subject: [PATCH 41/76] fix(network): share connection provenance validation --- .../src/webdriver_bidi_command_correlation.rs | 19 +++++++++++++------ 1 file changed, 13 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 11f5130a2..cd95cd140 100644 --- a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs +++ b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs @@ -230,14 +230,13 @@ impl WebDriverBiDiCommandCorrelation { command_id, WebDriverBiDiCommandKind::NavigationCommittedSubscription, )?; + let expected_connection_generation = + Self::require_connection_generation(&outstanding, command_id)?; let subscription_intent = outstanding.subscription_intent.ok_or( WebDriverBiDiCommandCorrelationError::CommandSubscriptionProvenanceMissing { command_id, }, )?; - let expected_connection_generation = outstanding.connection_generation.ok_or( - WebDriverBiDiCommandCorrelationError::CommandConnectionProvenanceMissing { command_id }, - )?; if expected_connection_generation != received_connection_generation { return Err( WebDriverBiDiCommandCorrelationError::ResponseConnectionMismatch { command_id }, @@ -371,6 +370,15 @@ impl WebDriverBiDiCommandCorrelation { Ok(actual) } + fn require_connection_generation( + outstanding: &OutstandingCommand, + command_id: u64, + ) -> Result { + outstanding.connection_generation.ok_or( + WebDriverBiDiCommandCorrelationError::CommandConnectionProvenanceMissing { command_id }, + ) + } + fn complete( &mut self, command_id: u64, @@ -394,9 +402,8 @@ impl WebDriverBiDiCommandCorrelation { received_connection_generation: WebDriverBiDiConnectionGeneration, ) -> Result { let outstanding = self.require_command_kind(command_id, expected_kind)?; - let expected_connection_generation = outstanding.connection_generation.ok_or( - WebDriverBiDiCommandCorrelationError::CommandConnectionProvenanceMissing { command_id }, - )?; + let expected_connection_generation = + Self::require_connection_generation(&outstanding, command_id)?; if expected_connection_generation != received_connection_generation { return Err( WebDriverBiDiCommandCorrelationError::ResponseConnectionMismatch { command_id }, From 701fcaad7080b8ca99aa6e742254af6b3de2992f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 18:03:55 +0900 Subject: [PATCH 42/76] test(network): cover subscription connection mismatch diagnostic --- ...vigation_committed_subscription_admission_failures.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission_failures.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission_failures.rs index 574ea4c69..e65a6084b 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission_failures.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission_failures.rs @@ -14,6 +14,7 @@ use originweave_network::{ WebDriverBiDiNavigationCommittedSubscriptionAdmission, WebDriverBiDiNavigationCommittedSubscriptionBinding, WebDriverBiDiNavigationCommittedSubscriptionCommand, + WebDriverBiDiNavigationCommittedSubscriptionEventError, WebDriverBiDiNavigationCommittedSubscriptionResult, WebDriverBiDiReceivedTextMessage, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketHandshakePlan, @@ -202,6 +203,14 @@ fn subscription_event_failures_keep_specific_public_diagnostics() -> Result<(), ®istry, )?; + let crossed_connection = + WebDriverBiDiNavigationCommittedSubscriptionEventError::EventConnectionMismatch; + assert_eq!( + crossed_connection.to_string(), + "WebDriver BiDi navigation event arrived on a different subscription connection" + ); + assert!(crossed_connection.source().is_none()); + let missing_navigation = admission .admit(&missing_navigation_event, ®istry, EXPECTED_URL) .err() From 6fca12b410c63241385ed45475bc9444cb1b18f5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 18:05:32 +0900 Subject: [PATCH 43/76] style(network): format subscription document tests --- .../tests/webdriver_bidi_navigation_document_advance.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_document_advance.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_document_advance.rs index 6f531fbf1..8c0f359cd 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_document_advance.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_document_advance.rs @@ -14,8 +14,7 @@ const CONTEXT_ID: &str = "context-a"; const EXPECTED_URL: &str = "https://example.test/after"; #[test] -fn accepted_navigation_advances_only_the_exact_pre_action_document_epoch() --> Result<(), Box> { +fn accepted_navigation_advances_only_the_exact_pre_action_document_epoch() -> Result<(), Box> { let mut registry = BrowserAuthorityRegistry::new(); let session = registry.register_session(SESSION_ID)?; let context = registry.register_context(session, CONTEXT_ID)?; @@ -77,8 +76,7 @@ fn accepted_navigation_advances_only_the_exact_pre_action_document_epoch() } #[test] -fn retired_context_between_observation_and_advance_fails_closed_with_typed_source() --> Result<(), Box> { +fn retired_context_between_observation_and_advance_fails_closed_with_typed_source() -> Result<(), Box> { let mut registry = BrowserAuthorityRegistry::new(); let session = registry.register_session(SESSION_ID)?; let context = registry.register_context(session, CONTEXT_ID)?; From d2769a9637b1e9db6f33e0b4325ae68aca9c7877 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 18:05:59 +0900 Subject: [PATCH 44/76] style(network): format navigation origin tests --- .../tests/webdriver_bidi_navigation_origin_binding.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_origin_binding.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_origin_binding.rs index 081d89b46..24cd96c4d 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_origin_binding.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_origin_binding.rs @@ -18,8 +18,7 @@ fn fixture_origin(value: &str) -> Result> { } #[test] -fn committed_navigation_rotates_document_and_binds_canonical_observed_origin() --> Result<(), Box> { +fn committed_navigation_rotates_document_and_binds_canonical_observed_origin() -> Result<(), Box> { let mut registry = BrowserAuthorityRegistry::new(); let session = registry.register_session(SESSION_ID)?; let context = registry.register_context(session, CONTEXT_ID)?; @@ -56,8 +55,7 @@ fn committed_navigation_rotates_document_and_binds_canonical_observed_origin() } #[test] -fn invalid_observed_origin_fails_before_document_authority_is_rotated() -> Result<(), Box> -{ +fn invalid_observed_origin_fails_before_document_authority_is_rotated() -> Result<(), Box> { let mut registry = BrowserAuthorityRegistry::new(); let session = registry.register_session(SESSION_ID)?; let context = registry.register_context(session, CONTEXT_ID)?; From fa015c82b1d63392d58a73f6ff9b259d737c6ccb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 18:07:19 +0900 Subject: [PATCH 45/76] style(network): format transport provenance tests --- ...ver_bidi_navigation_subscription_transport_provenance.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_subscription_transport_provenance.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_subscription_transport_provenance.rs index 4e15a089b..a9ef54121 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_subscription_transport_provenance.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_subscription_transport_provenance.rs @@ -159,8 +159,7 @@ fn spawn_unsolicited_message_sender( } #[test] -fn subscription_receipt_from_another_verified_connection_is_rejected() --> Result<(), Box> { +fn subscription_receipt_from_another_verified_connection_is_rejected() -> Result<(), Box> { let mut registry = BrowserAuthorityRegistry::new(); let session = registry.register_session(SESSION_ID)?; let context = registry.register_context(session, CONTEXT_ID)?; @@ -211,8 +210,7 @@ fn subscription_receipt_from_another_verified_connection_is_rejected() } #[test] -fn subscription_event_from_another_verified_connection_is_rejected() --> Result<(), Box> { +fn subscription_event_from_another_verified_connection_is_rejected() -> Result<(), Box> { let mut registry = BrowserAuthorityRegistry::new(); let session = registry.register_session(SESSION_ID)?; let context = registry.register_context(session, CONTEXT_ID)?; From f87f09524b8741109c8c80c13065030b4333408f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 18:08:46 +0900 Subject: [PATCH 46/76] style(network): format subscription response failure tests --- ...i_navigation_committed_subscription_response_failures.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_response_failures.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_response_failures.rs index c015711bc..f084b6dfb 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_response_failures.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_response_failures.rs @@ -191,8 +191,7 @@ fn sent_subscription_response( } #[test] -fn malformed_and_invalid_success_responses_preserve_outstanding_correlation() --> Result<(), Box> { +fn malformed_and_invalid_success_responses_preserve_outstanding_correlation() -> Result<(), Box> { let (malformed, mut correlation) = sent_subscription_response(MALFORMED_SUCCESS_RESPONSE)?; assert_eq!( WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( @@ -291,8 +290,7 @@ fn subscription_response_cannot_consume_another_command_kind() -> Result<(), Box } #[test] -fn event_response_is_rejected_without_consuming_outstanding_command() -> Result<(), Box> -{ +fn event_response_is_rejected_without_consuming_outstanding_command() -> Result<(), Box> { let mut correlation = WebDriverBiDiCommandCorrelation::new(); correlation .register_command_for(7, WebDriverBiDiCommandKind::NavigationCommittedSubscription)?; From 52cff96954c3fec7b8cda5409e4560e970bfcbdf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 18:11:10 +0900 Subject: [PATCH 47/76] style(network): format subscription admission tests --- ...di_navigation_committed_subscription_admission.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs index c421adbdb..96e11e8af 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs @@ -192,8 +192,7 @@ fn receive_subscription_result( } #[test] -fn committed_navigation_requires_the_exact_active_subscription_before_document_mutation() --> Result<(), Box> { +fn committed_navigation_requires_the_exact_active_subscription_before_document_mutation() -> Result<(), Box> { let listener = TcpListener::bind(("127.0.0.1", 0))?; let local_addr = listener.local_addr()?; let server = thread::spawn(move || -> io::Result<()> { @@ -371,8 +370,7 @@ fn committed_navigation_requires_the_exact_active_subscription_before_document_m } #[test] -fn sent_subscription_cannot_be_rebound_to_an_unsent_same_id_context() -> Result<(), Box> -{ +fn sent_subscription_cannot_be_rebound_to_an_unsent_same_id_context() -> Result<(), Box> { let mut registry = BrowserAuthorityRegistry::new(); let session = registry.register_session(SESSION_ID)?; let context = registry.register_context(session, CONTEXT_ID)?; @@ -404,8 +402,7 @@ fn sent_subscription_cannot_be_rebound_to_an_unsent_same_id_context() -> Result< } #[test] -fn identical_unsent_command_fields_do_not_recreate_sent_command_identity() --> Result<(), Box> { +fn identical_unsent_command_fields_do_not_recreate_sent_command_identity() -> Result<(), Box> { let mut registry = BrowserAuthorityRegistry::new(); let session = registry.register_session(SESSION_ID)?; let context = registry.register_context(session, CONTEXT_ID)?; @@ -460,8 +457,7 @@ fn subscription_identity_does_not_collide_across_registries() -> Result<(), Box< } #[test] -fn subscription_admission_rejects_mismatched_command_and_retired_context() --> Result<(), Box> { +fn subscription_admission_rejects_mismatched_command_and_retired_context() -> Result<(), Box> { let mut registry = BrowserAuthorityRegistry::new(); let session = registry.register_session(SESSION_ID)?; let context = registry.register_context(session, CONTEXT_ID)?; From 3c8da79dec4b5ea8a43a5b01c6ea770f0eb852fb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 18:14:59 +0900 Subject: [PATCH 48/76] docs: bind combined provenance proof to its exact local revision Record the verified bc7a5166 source lineage and coverage artifact without transferring local proof to newer shared heads, hosted checks, protected main or release acceptance. Commit-Message-Assisted-by: Codex (via Codex) Signed-off-by: Seongho Bae --- docs/doctoring.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/doctoring.md b/docs/doctoring.md index 11eaa7809..54c0fb045 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -22,6 +22,8 @@ The separate published checkpoint `8ebcc6131a5dd6bf0b4720c2f1ff8d40d1cc39f8` als The local integration candidate preserves both commit histories and both sets of socket regressions. It retains the published event-error variant and typed-send fixtures, carries the private original-connection recovery and document-state checks, and uses the existing shared connection-generation validator for both completion paths. A successful subscription receipt always retains a generation; an optional missing-generation state used only by a private debug fixture is removed, while the same redaction/accessor assertions run against a real receipt. No shared-branch push or combined-candidate verification is claimed by either checkpoint's earlier results. +Follow-up local integration `bc7a51666250f08e0e2b090e63b9417a71eee0c5` ordinarily incorporates both checkpoints and the intervening remote lineage through `f87f09524b8741109c8c80c13065030b4333408f`, including `da4a11f0` shared-validation intent and `701fcaad` direct event-error diagnostics. One shared presence-and-equality validator retains the earlier command-instance diagnostic precedence without duplicating validation. Actual Rust 1.97.1 formatting reconciles the inherited style-only changes without changing any test body. Fresh locked all-target/all-feature workspace check and tests, strict Clippy, warning-denying rustdoc, formatting, all 145 Python contracts and compileall pass. Pinned coverage and its unchanged verifier pass at 1273/1273 functions, 13317/13317 lines, 16984/16984 regions and 1432/1432 branches; coverage artifact SHA-256 is `7e6f2d1450814f1c52ff36a85c62ba7918fe7bff365cb2e73d62ef6b55601082`. Independent read-only comparison found no actionable loss of valid contributor changes or authority regression. This local candidate is unpublished, does not include later remote commits, and does not inherit shared-writer authority, hosted acceptance, protected-main status or release eligibility from those results. + ### Subscription receipt and command-instance integrity The following records the preceding command-identity-only repair at `43d3b5a3a2b5ce4f51a93d1152a0ee82620f4f3e`; its then-unresolved receive-connection boundary is addressed by the separate repair above. From 2a9fdc5418b9af10353cdad0f6f6470655bf457d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 18:21:26 +0900 Subject: [PATCH 49/76] docs: record final shared lineage adoption boundary Retain earlier local measurements as dated evidence and record ordinary adoption of released52cff969 without claiming publication or hosted acceptance. Commit-Message-Assisted-by: Codex (via Codex) Signed-off-by: Seongho Bae --- docs/doctoring.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/doctoring.md b/docs/doctoring.md index 54c0fb045..a9549ad48 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -22,7 +22,9 @@ The separate published checkpoint `8ebcc6131a5dd6bf0b4720c2f1ff8d40d1cc39f8` als The local integration candidate preserves both commit histories and both sets of socket regressions. It retains the published event-error variant and typed-send fixtures, carries the private original-connection recovery and document-state checks, and uses the existing shared connection-generation validator for both completion paths. A successful subscription receipt always retains a generation; an optional missing-generation state used only by a private debug fixture is removed, while the same redaction/accessor assertions run against a real receipt. No shared-branch push or combined-candidate verification is claimed by either checkpoint's earlier results. -Follow-up local integration `bc7a51666250f08e0e2b090e63b9417a71eee0c5` ordinarily incorporates both checkpoints and the intervening remote lineage through `f87f09524b8741109c8c80c13065030b4333408f`, including `da4a11f0` shared-validation intent and `701fcaad` direct event-error diagnostics. One shared presence-and-equality validator retains the earlier command-instance diagnostic precedence without duplicating validation. Actual Rust 1.97.1 formatting reconciles the inherited style-only changes without changing any test body. Fresh locked all-target/all-feature workspace check and tests, strict Clippy, warning-denying rustdoc, formatting, all 145 Python contracts and compileall pass. Pinned coverage and its unchanged verifier pass at 1273/1273 functions, 13317/13317 lines, 16984/16984 regions and 1432/1432 branches; coverage artifact SHA-256 is `7e6f2d1450814f1c52ff36a85c62ba7918fe7bff365cb2e73d62ef6b55601082`. Independent read-only comparison found no actionable loss of valid contributor changes or authority regression. This local candidate is unpublished, does not include later remote commits, and does not inherit shared-writer authority, hosted acceptance, protected-main status or release eligibility from those results. +Follow-up local integration `bc7a51666250f08e0e2b090e63b9417a71eee0c5` ordinarily incorporates both checkpoints and the intervening remote lineage through `f87f09524b8741109c8c80c13065030b4333408f`, including `da4a11f0` shared-validation intent and `701fcaad` direct event-error diagnostics. One shared presence-and-equality validator retains the earlier command-instance diagnostic precedence without duplicating validation. Actual Rust 1.97.1 formatting reconciles the inherited style-only changes without changing any test body. Fresh locked all-target/all-feature workspace check and tests, strict Clippy, warning-denying rustdoc, formatting, all 145 Python contracts and compileall pass. Pinned coverage and its unchanged verifier pass at 1273/1273 functions, 13317/13317 lines, 16984/16984 regions and 1432/1432 branches; coverage artifact SHA-256 is `7e6f2d1450814f1c52ff36a85c62ba7918fe7bff365cb2e73d62ef6b55601082`. Independent read-only comparison found no actionable loss of valid contributor changes or authority regression. At that observation this local candidate was unpublished and excluded later remote commits; it did not inherit shared-writer authority, hosted acceptance, protected-main status or release eligibility from those results. + +After explicit shared-writer release [5558261278](https://github.com/ContextualWisdomLab/OriginWeave/pull/264#issuecomment-5558261278), ordinary merge `ad14ce1f` also incorporates the final shared checkpoint `52cff96954c3fec7b8cda5409e4560e970bfcbdf`. Its Rust and test tree is identical to verified `bc7a5166` after running the pinned formatter, while all contributor histories remain ancestors. This lineage comparison is not a substitute for fresh pre-publication gates or subsequent exact-head hosted acceptance. ### Subscription receipt and command-instance integrity From c41e737b6d20e0d61e81f556a45030b1dc23ea8b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 18:45:56 +0900 Subject: [PATCH 50/76] test(network): reject substituted navigation registries Commit-Message-Assisted-by: Codex (via Codex) Signed-off-by: Seongho Bae --- ...gation_committed_subscription_admission.rs | 85 +++++++++++++++++++ ...igation_committed_subscription_failures.rs | 47 ++++++++++ 2 files changed, 132 insertions(+) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs index 8067ef9e1..03aac2dcd 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs @@ -578,6 +578,91 @@ fn identical_unsent_command_fields_do_not_recreate_sent_command_identity() Ok(()) } +#[test] +fn original_binding_rejects_replacement_registry_at_receipt_admission() -> Result<(), Box> +{ + let mut original = BrowserAuthorityRegistry::new(); + let session = original.register_session(SESSION_ID)?; + let context = original.register_context(session, CONTEXT_ID)?; + let (receipt, binding, _) = receive_subscription_result(&original, session, context, 7)?; + let mut replacement = BrowserAuthorityRegistry::with_identifier_limit(8); + let replacement_session = replacement.register_session(SESSION_ID)?; + let replacement_context = replacement.register_context(replacement_session, CONTEXT_ID)?; + assert_eq!( + (session, context), + (replacement_session, replacement_context) + ); + assert!( + WebDriverBiDiNavigationCommittedSubscriptionAdmission::new(receipt, binding, &replacement) + .is_err(), + "an original receipt and binding must not authorize a replacement registry" + ); + Ok(()) +} + +#[test] +fn original_event_rejects_replacement_registry_without_consuming_replay_state() +-> Result<(), Box> { + let mut original = BrowserAuthorityRegistry::new(); + let session = original.register_session(SESSION_ID)?; + let context = original.register_context(session, CONTEXT_ID)?; + let (receipt, binding, event) = receive_subscription_result(&original, session, context, 7)?; + let mut admission = + WebDriverBiDiNavigationCommittedSubscriptionAdmission::new(receipt, binding, &original)?; + let mut replacement = BrowserAuthorityRegistry::new(); + let replacement_session = replacement.register_session(SESSION_ID)?; + let replacement_context = replacement.register_context(replacement_session, CONTEXT_ID)?; + assert_eq!( + (session, context), + (replacement_session, replacement_context) + ); + let before = original.current_epoch(context)?; + assert!( + admission.admit(&event, &replacement, EXPECTED_URL).is_err(), + "a valid original-connection event must reject another registry with colliding IDs" + ); + assert_eq!(replacement.current_epoch(replacement_context)?, before); + let admitted = admission.admit(&event, &original, EXPECTED_URL)?; + let advanced = + advance_webdriver_bidi_navigation_document_epoch(admitted, &mut original, before)?; + assert_eq!(advanced.current_epoch().value(), before.value() + 1); + assert_eq!(replacement.current_epoch(replacement_context)?, before); + Ok(()) +} + +#[test] +fn admitted_observation_rejects_replacement_registry_at_document_mutation() +-> Result<(), Box> { + let mut original = BrowserAuthorityRegistry::new(); + let session = original.register_session(SESSION_ID)?; + let context = original.register_context(session, CONTEXT_ID)?; + let (receipt, binding, event) = receive_subscription_result(&original, session, context, 7)?; + let mut admission = + WebDriverBiDiNavigationCommittedSubscriptionAdmission::new(receipt, binding, &original)?; + let admitted = admission.admit(&event, &original, EXPECTED_URL)?; + let mut replacement = BrowserAuthorityRegistry::new(); + let replacement_session = replacement.register_session("unrelated-session")?; + let replacement_context = replacement.register_context(replacement_session, "unrelated-tab")?; + assert_eq!( + (session, context), + (replacement_session, replacement_context) + ); + let origin = originweave_core::Origin::parse("https://unrelated.test")?; + let before = + replacement.bind_context_origin(replacement_session, replacement_context, &origin)?; + assert!( + advance_webdriver_bidi_navigation_document_epoch(admitted, &mut replacement, before) + .is_err(), + "an observation admitted in one registry must not mutate another registry" + ); + assert_eq!(original.current_epoch(context)?, before); + assert_eq!( + replacement.require_context_origin(replacement_session, replacement_context, &origin)?, + before + ); + Ok(()) +} + #[test] fn subscription_identity_does_not_collide_across_registries() -> Result<(), Box> { let mut original_registry = BrowserAuthorityRegistry::new(); diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_failures.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_failures.rs index e7decd258..1fa02877d 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_failures.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_failures.rs @@ -81,6 +81,53 @@ fn establish_websocket( .read_opening_response(Duration::from_millis(500))?) } +#[test] +fn replacement_registry_is_rejected_before_correlation_or_command_write() +-> Result<(), Box> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let address = listener.local_addr()?; + let server = spawn_no_command_server(listener); + let mut original = BrowserAuthorityRegistry::new(); + let session = original.register_session(SESSION_ID)?; + let context = original.register_context(session, "context-a")?; + let command = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( + 7, + &original, + session, + context, + "context-a", + )?; + let mut replacement = BrowserAuthorityRegistry::new(); + let replacement_session = replacement.register_session(SESSION_ID)?; + let replacement_context = replacement.register_context(replacement_session, "context-a")?; + assert_eq!( + (session, context), + (replacement_session, replacement_context) + ); + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + correlation.register_command_for(8, WebDriverBiDiCommandKind::SessionStatus)?; + let outcome = command.send( + &replacement, + establish_websocket(address)?, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + Duration::from_millis(500), + ); + let rejected = outcome.is_err(); + drop(outcome); + let server_outcome = server + .join() + .map_err(|_| io::Error::other("registry fixture panicked"))?; + assert!( + rejected, + "a replacement registry must not send the original command" + ); + server_outcome?; + assert_eq!(correlation.outstanding_count(), 1); + correlation.register_command_for(7, WebDriverBiDiCommandKind::SessionStatus)?; + Ok(()) +} + #[test] fn retired_context_is_rejected_before_correlation_or_command_write() -> Result<(), Box> { let listener = TcpListener::bind(("127.0.0.1", 0))?; From b3ffeac99202a72761a0f06183601c56c18f2fe7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 18:46:37 +0900 Subject: [PATCH 51/76] test(network): adapt fixture origin error to test result Commit-Message-Assisted-by: Codex (via Codex) Signed-off-by: Seongho Bae --- ...bdriver_bidi_navigation_committed_subscription_admission.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs index 03aac2dcd..89a664ed9 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs @@ -647,7 +647,8 @@ fn admitted_observation_rejects_replacement_registry_at_document_mutation() (session, context), (replacement_session, replacement_context) ); - let origin = originweave_core::Origin::parse("https://unrelated.test")?; + let origin = originweave_core::Origin::parse("https://unrelated.test") + .map_err(|error| io::Error::other(format!("{error:?}")))?; let before = replacement.bind_context_origin(replacement_session, replacement_context, &origin)?; assert!( From d5600aac4017310fc0f76d8d35e28e01344182a9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 18:48:08 +0900 Subject: [PATCH 52/76] fix(network): preserve original registry through navigation mutation Commit-Message-Assisted-by: Codex (via Codex) Signed-off-by: Seongho Bae --- .../src/browser_authority_registry.rs | 36 +++++++++++++++++++ .../originweave-core/src/browser_registry.rs | 5 +++ crates/originweave-core/src/lib.rs | 2 +- ..._bidi_navigation_committed_subscription.rs | 11 +++++- ...gation_committed_subscription_admission.rs | 16 ++++++++- ...driver_bidi_navigation_document_advance.rs | 3 ++ 6 files changed, 70 insertions(+), 3 deletions(-) diff --git a/crates/originweave-core/src/browser_authority_registry.rs b/crates/originweave-core/src/browser_authority_registry.rs index 3af93cb07..2d40dadb8 100644 --- a/crates/originweave-core/src/browser_authority_registry.rs +++ b/crates/originweave-core/src/browser_authority_registry.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use crate::browser_registry::BrowserAuthorityRegistry as RawBrowserAuthorityRegistry; use crate::{ BrowserRegistryError, BrowserSessionId, BrowsingContextId, DocumentEpoch, ObservedNodeHandle, @@ -15,14 +17,24 @@ use crate::{ /// before atomically minting handles. pub struct BrowserAuthorityRegistry { inner: RawBrowserAuthorityRegistry, + identity: Arc<()>, } +/// Opaque process-local identity of one browser-authority registry allocation. +/// +/// Cloning preserves the same registry identity without keeping its mutable state alive. This +/// witness contains no wire identifier, address, durable identity, or grant of browser authority. +/// It must be captured by a trusted admission boundary and revalidated with current context state. +#[derive(Clone)] +pub struct BrowserRegistryIdentity(Arc<()>); + impl BrowserAuthorityRegistry { /// Create an empty registry with the reviewed default per-namespace identifier capacity. #[must_use] pub fn new() -> Self { Self { inner: RawBrowserAuthorityRegistry::new(), + identity: Arc::new(()), } } @@ -34,7 +46,31 @@ impl BrowserAuthorityRegistry { pub fn with_identifier_limit(maximum_identifier: u64) -> Self { Self { inner: RawBrowserAuthorityRegistry::with_identifier_limit(maximum_identifier), + identity: Arc::new(()), + } + } + + /// Capture this registry's opaque identity for later exact-owner revalidation. + /// + /// The identity survives moves of this registry but cannot match a replacement registry, + /// including one whose local session and context identifiers have identical numeric values. + #[must_use] + pub fn identity(&self) -> BrowserRegistryIdentity { + BrowserRegistryIdentity(Arc::clone(&self.identity)) + } + + /// Reject a witness issued by any other registry before consulting registry-local identifiers. + /// + /// Success proves only registry ownership; callers must still validate live session, context, + /// document epoch, origin and operation-specific authority at their actual use boundary. + pub fn require_identity( + &self, + identity: &BrowserRegistryIdentity, + ) -> Result<(), BrowserRegistryError> { + if !Arc::ptr_eq(&self.identity, &identity.0) { + return Err(BrowserRegistryError::RegistryInstanceMismatch); } + Ok(()) } /// Register one opaque external browser-session identifier. diff --git a/crates/originweave-core/src/browser_registry.rs b/crates/originweave-core/src/browser_registry.rs index 26a05249f..7525ee695 100644 --- a/crates/originweave-core/src/browser_registry.rs +++ b/crates/originweave-core/src/browser_registry.rs @@ -427,6 +427,8 @@ impl Default for BrowserAuthorityRegistry { /// A fail-closed error produced while translating external browser identifiers into local authority. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum BrowserRegistryError { + /// A retained authority witness belongs to a different registry instance. + RegistryInstanceMismatch, /// An external identifier was empty, contained control, whitespace, or Unicode format text, or exceeded the reviewed byte bound. InvalidExternalIdentifier, /// The supplied OriginWeave browser session is not registered in this registry. @@ -457,6 +459,9 @@ pub enum BrowserRegistryError { impl fmt::Display for BrowserRegistryError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + Self::RegistryInstanceMismatch => { + formatter.write_str("browser authority belongs to another registry instance") + } Self::InvalidExternalIdentifier => formatter.write_str( "external browser identifier must contain 1 to 512 UTF-8 bytes without control, whitespace, or Unicode format characters", ), diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index 3fa97bffa..211b75348 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -41,7 +41,7 @@ mod webdriver_bidi_result; mod webdriver_bidi_websocket_connect_target; mod webdriver_bidi_websocket_endpoint; -pub use browser_authority_registry::BrowserAuthorityRegistry; +pub use browser_authority_registry::{BrowserAuthorityRegistry, BrowserRegistryIdentity}; pub use browser_protocol::{ BrowserProtocolAdapterDescriptor, BrowserProtocolCapability, BrowserProtocolCapabilityRequirementError, BrowserProtocolDescriptorError, BrowserProtocolKind, diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs index ed33d8faf..8e94b77c6 100644 --- a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs +++ b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs @@ -1,7 +1,8 @@ use std::{error::Error, fmt, sync::Arc, time::Duration}; use originweave_core::{ - BrowserAuthorityRegistry, BrowserRegistryError, BrowserSessionId, BrowsingContextId, + BrowserAuthorityRegistry, BrowserRegistryError, BrowserRegistryIdentity, BrowserSessionId, + BrowsingContextId, }; use crate::webdriver_bidi_websocket_frame::validate_frame_timeout; @@ -30,6 +31,7 @@ pub struct WebDriverBiDiNavigationCommittedSubscriptionCommand { browsing_context: BrowsingContextId, external_context: String, subscription_intent: Arc<()>, + registry_identity: BrowserRegistryIdentity, } impl WebDriverBiDiNavigationCommittedSubscriptionCommand { @@ -64,6 +66,7 @@ impl WebDriverBiDiNavigationCommittedSubscriptionCommand { browsing_context, external_context: external_context.to_owned(), subscription_intent: Arc::new(()), + registry_identity: registry.identity(), }) } @@ -104,6 +107,7 @@ impl WebDriverBiDiNavigationCommittedSubscriptionCommand { self.browsing_context, &self.external_context, Arc::clone(&self.subscription_intent), + self.registry_identity.clone(), ) } @@ -128,6 +132,11 @@ impl WebDriverBiDiNavigationCommittedSubscriptionCommand { WebDriverBiDiWebSocketEstablished, WebDriverBiDiNavigationCommittedSubscriptionCommandError, > { + registry + .require_identity(&self.registry_identity) + .map_err(|source| { + WebDriverBiDiNavigationCommittedSubscriptionCommandError::ContextBinding { source } + })?; require_registered_context( registry, self.browser_session, diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_admission.rs b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_admission.rs index 2c4a4df94..89528c724 100644 --- a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_admission.rs +++ b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_admission.rs @@ -1,7 +1,8 @@ use std::{error::Error, fmt, sync::Arc}; use originweave_core::{ - BrowserAuthorityRegistry, BrowserRegistryError, BrowserSessionId, BrowsingContextId, + BrowserAuthorityRegistry, BrowserRegistryError, BrowserRegistryIdentity, BrowserSessionId, + BrowsingContextId, }; use crate::{ @@ -31,6 +32,7 @@ pub struct WebDriverBiDiNavigationCommittedSubscriptionBinding { browsing_context: BrowsingContextId, external_context: String, subscription_intent: Arc<()>, + registry_identity: BrowserRegistryIdentity, } impl fmt::Debug for WebDriverBiDiNavigationCommittedSubscriptionBinding { @@ -52,6 +54,7 @@ impl WebDriverBiDiNavigationCommittedSubscriptionBinding { browsing_context: BrowsingContextId, external_context: &str, subscription_intent: Arc<()>, + registry_identity: BrowserRegistryIdentity, ) -> Self { Self { command_id, @@ -59,6 +62,7 @@ impl WebDriverBiDiNavigationCommittedSubscriptionBinding { browsing_context, external_context: external_context.to_owned(), subscription_intent, + registry_identity, } } @@ -233,6 +237,7 @@ impl WebDriverBiDiNavigationCommittedSubscriptionAdmission { self.admitted_navigation_ids.push(navigation_id.to_owned()); Ok(WebDriverBiDiNavigationCommittedSubscribedObservation( observation, + self.binding.registry_identity.clone(), )) } @@ -258,6 +263,7 @@ fn require_current_binding( registry: &BrowserAuthorityRegistry, binding: &WebDriverBiDiNavigationCommittedSubscriptionBinding, ) -> Result<(), BrowserRegistryError> { + registry.require_identity(&binding.registry_identity)?; registry.require_registered_context_external_identifier( binding.browser_session, binding.browsing_context, @@ -274,6 +280,7 @@ fn require_current_binding( /// Agent authority. pub struct WebDriverBiDiNavigationCommittedSubscribedObservation( WebDriverBiDiNavigationCommittedObservation, + BrowserRegistryIdentity, ); impl fmt::Debug for WebDriverBiDiNavigationCommittedSubscribedObservation { @@ -286,6 +293,13 @@ impl fmt::Debug for WebDriverBiDiNavigationCommittedSubscribedObservation { } impl WebDriverBiDiNavigationCommittedSubscribedObservation { + pub(crate) fn require_registry( + &self, + registry: &BrowserAuthorityRegistry, + ) -> Result<(), BrowserRegistryError> { + registry.require_identity(&self.1) + } + /// Return the exact OriginWeave browser session whose active subscription admitted the event. #[must_use] pub const fn browser_session(&self) -> BrowserSessionId { diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_document_advance.rs b/crates/originweave-network/src/webdriver_bidi_navigation_document_advance.rs index d0ec1f521..cbcfd2e38 100644 --- a/crates/originweave-network/src/webdriver_bidi_navigation_document_advance.rs +++ b/crates/originweave-network/src/webdriver_bidi_navigation_document_advance.rs @@ -116,6 +116,9 @@ pub fn advance_webdriver_bidi_navigation_document_epoch( WebDriverBiDiNavigationCommittedDocumentAdvance, WebDriverBiDiNavigationCommittedDocumentAdvanceError, > { + observation.require_registry(registry).map_err(|source| { + WebDriverBiDiNavigationCommittedDocumentAdvanceError::RegistryState { source } + })?; let browser_session = observation.browser_session(); let browsing_context = observation.browsing_context(); match advance_registered_document_if_expected( From e686b3a002a2fa33741c8ebffd8978e2cf0e187c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 18:50:07 +0900 Subject: [PATCH 53/76] docs: specify original registry ownership and recovery evidence Commit-Message-Assisted-by: Codex (via Codex) Signed-off-by: Seongho Bae --- CHANGELOG.md | 1 + .../src/browser_authority_registry.rs | 12 +++--- ..._bidi_navigation_committed_subscription.rs | 4 +- ...gation_committed_subscription_admission.rs | 6 ++- ...driver_bidi_navigation_document_advance.rs | 2 +- ...gation_committed_subscription_admission.rs | 21 +++++++++-- .../0107-browser-protocol-adapter-strategy.md | 19 ++++++++++ docs/doctoring.md | 37 +++++++++++++++++++ 8 files changed, 88 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7c8915fc..8f7c768c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Changed +- Keep navigation subscriptions and accepted navigation events attached to their original browser state, so a replacement state with matching local identifiers cannot send a request or change another document. - Reject navigation-subscription replies and events received on a different connection, even when their session and request details match. Rejected messages leave the original request and document unchanged, so the original connection can still complete its work. - Prevent an unsent navigation subscription from borrowing another request's successful response, including when separate sessions reuse the same local numbers. Re-registering a completed request number without sending a new request cannot recreate its consumed subscription. - Reject invalid navigation-subscription deadlines before reserving a pending request, preserving existing requests and leaving the rejected identifier reusable without sending subscription bytes. diff --git a/crates/originweave-core/src/browser_authority_registry.rs b/crates/originweave-core/src/browser_authority_registry.rs index 2d40dadb8..e3b7ce6b6 100644 --- a/crates/originweave-core/src/browser_authority_registry.rs +++ b/crates/originweave-core/src/browser_authority_registry.rs @@ -17,7 +17,7 @@ use crate::{ /// before atomically minting handles. pub struct BrowserAuthorityRegistry { inner: RawBrowserAuthorityRegistry, - identity: Arc<()>, + registry_identity: Arc<()>, } /// Opaque process-local identity of one browser-authority registry allocation. @@ -34,7 +34,7 @@ impl BrowserAuthorityRegistry { pub fn new() -> Self { Self { inner: RawBrowserAuthorityRegistry::new(), - identity: Arc::new(()), + registry_identity: Arc::new(()), } } @@ -46,7 +46,7 @@ impl BrowserAuthorityRegistry { pub fn with_identifier_limit(maximum_identifier: u64) -> Self { Self { inner: RawBrowserAuthorityRegistry::with_identifier_limit(maximum_identifier), - identity: Arc::new(()), + registry_identity: Arc::new(()), } } @@ -55,8 +55,8 @@ impl BrowserAuthorityRegistry { /// The identity survives moves of this registry but cannot match a replacement registry, /// including one whose local session and context identifiers have identical numeric values. #[must_use] - pub fn identity(&self) -> BrowserRegistryIdentity { - BrowserRegistryIdentity(Arc::clone(&self.identity)) + pub fn registry_identity(&self) -> BrowserRegistryIdentity { + BrowserRegistryIdentity(Arc::clone(&self.registry_identity)) } /// Reject a witness issued by any other registry before consulting registry-local identifiers. @@ -67,7 +67,7 @@ impl BrowserAuthorityRegistry { &self, identity: &BrowserRegistryIdentity, ) -> Result<(), BrowserRegistryError> { - if !Arc::ptr_eq(&self.identity, &identity.0) { + if !Arc::ptr_eq(&self.registry_identity, &identity.0) { return Err(BrowserRegistryError::RegistryInstanceMismatch); } Ok(()) diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs index 8e94b77c6..b629fdfab 100644 --- a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs +++ b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs @@ -66,7 +66,7 @@ impl WebDriverBiDiNavigationCommittedSubscriptionCommand { browsing_context, external_context: external_context.to_owned(), subscription_intent: Arc::new(()), - registry_identity: registry.identity(), + registry_identity: registry.registry_identity(), }) } @@ -113,7 +113,7 @@ impl WebDriverBiDiNavigationCommittedSubscriptionCommand { /// Revalidate, register, and write this exact subscription on an established verified BiDi stream. /// - /// Context binding is revalidated immediately before command correlation and network I/O so a + /// The original registry identity and context binding are revalidated before correlation and I/O so a /// command retained across registry retirement cannot subscribe a stale or replacement context. /// Invalid frame deadlines fail before correlation registration. Registration then binds both the /// private command-instance identity and this established connection's process-local generation diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_admission.rs b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_admission.rs index 89528c724..9e2e583cf 100644 --- a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_admission.rs +++ b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_admission.rs @@ -26,6 +26,7 @@ pub const MAX_WEBDRIVER_BIDI_NAVIGATION_COMMITTED_ADMISSIONS: usize = 256; /// context identifier is retained privately for immediate registry revalidation and is not exposed /// as durable OriginWeave authority. A private allocation identity binds this value to the exact /// command instance; matching caller-supplied numbers cannot recreate that identity. +/// A separate core-issued witness preserves the original registry instance across later use. pub struct WebDriverBiDiNavigationCommittedSubscriptionBinding { command_id: u64, browser_session: BrowserSessionId, @@ -90,7 +91,7 @@ impl WebDriverBiDiNavigationCommittedSubscriptionBinding { /// Construction requires both the correlated remote subscription receipt and the immutable binding /// captured from the exact command that requested it. The command identifiers and private command /// allocation identity must match, and the original external context mapping must still resolve to -/// the exact OriginWeave session/context. Event admission additionally requires the event message to +/// the exact OriginWeave session/context in the original registry. Event admission additionally requires the event message to /// have been assembled on the same verified connection generation that carried the subscription /// command and receipt. Holding this value is therefore narrower than holding an opaque protocol /// subscription string. It grants only admission of the matching committed-navigation event through @@ -275,7 +276,8 @@ fn require_current_binding( /// /// Unlike the lower-level protocol observation, this value proves that local admission was bound to /// the exact typed `session.subscribe` command/receipt pair and the same verified transport -/// generation for the registered context at the time the event was admitted. It still does not prove +/// generation for the registered context at the time the event was admitted. It retains the original +/// registry witness for revalidation at the eventual document-mutation boundary. It still does not prove /// action causality or grant destination, origin, policy, node, secret, process, profile, or reusable /// Agent authority. pub struct WebDriverBiDiNavigationCommittedSubscribedObservation( diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_document_advance.rs b/crates/originweave-network/src/webdriver_bidi_navigation_document_advance.rs index cbcfd2e38..faf8df0f3 100644 --- a/crates/originweave-network/src/webdriver_bidi_navigation_document_advance.rs +++ b/crates/originweave-network/src/webdriver_bidi_navigation_document_advance.rs @@ -99,7 +99,7 @@ fn advance_registered_document_if_expected( /// post-condition is being evaluated. The subscribed observation is consumed so one admitted event /// cannot be reused to rotate the registry twice, and raw protocol observations cannot cross this /// state-changing boundary without first being bound to an active exact `session.subscribe` -/// command/receipt. The exact session/context pair and caller-captured epoch are revalidated +/// command/receipt. The original registry instance, exact session/context pair and caller-captured epoch are revalidated /// immediately before mutation, and stale state fails closed without mutation. /// /// A successful advance delegates to [`BrowserAuthorityRegistry::advance_document`], which clears diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs index 89a664ed9..32e1da1b4 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs @@ -592,10 +592,24 @@ fn original_binding_rejects_replacement_registry_at_receipt_admission() -> Resul (session, context), (replacement_session, replacement_context) ); - assert!( + let error = WebDriverBiDiNavigationCommittedSubscriptionAdmission::new(receipt, binding, &replacement) - .is_err(), - "an original receipt and binding must not authorize a replacement registry" + .err() + .ok_or_else(|| { + io::Error::other( + "an original receipt and binding must not authorize a replacement registry", + ) + })?; + let source = error + .source() + .ok_or_else(|| io::Error::other("missing registry failure"))?; + assert_eq!( + source.downcast_ref::(), + Some(&originweave_core::BrowserRegistryError::RegistryInstanceMismatch), + ); + assert_eq!( + source.to_string(), + "browser authority belongs to another registry instance" ); Ok(()) } @@ -622,6 +636,7 @@ fn original_event_rejects_replacement_registry_without_consuming_replay_state() "a valid original-connection event must reject another registry with colliding IDs" ); assert_eq!(replacement.current_epoch(replacement_context)?, before); + let mut original = Box::new(original); let admitted = admission.admit(&event, &original, EXPECTED_URL)?; let advanced = advance_webdriver_bidi_navigation_document_epoch(admitted, &mut original, before)?; diff --git a/docs/adr/0107-browser-protocol-adapter-strategy.md b/docs/adr/0107-browser-protocol-adapter-strategy.md index 8923616be..b64723be1 100644 --- a/docs/adr/0107-browser-protocol-adapter-strategy.md +++ b/docs/adr/0107-browser-protocol-adapter-strategy.md @@ -36,6 +36,25 @@ MCP version negotiation is independent of the OriginWeave Protocol version. As o ## Consequences +### Proposed refinement: original registry identity (2026-09-06) + +In the context of a connection-bound navigation subscription whose local session/context numbers +can also exist in another registry, facing the risk that a genuine receipt or admitted event +changes unrelated document authority, we decided for a core-owned opaque registry witness retained +from command construction through event admission to the shared document-mutation boundary, and +against numeric/text equality, a constructor-only check, or globally renumbering every browser +identifier, to preserve exact-owner authority before side effects, accepting one small allocation +per registry and reference-counted witnesses while commands or observations remain live. + +The proposal reuses current context-liveness and expected-epoch checks. It does not freeze a +context-wide subscription at its initial document epoch, authenticate the browser session associated +with a stream, or turn the witness into a durable ID or capability grant. The witness has no public +constructor or serialization. It cannot preserve a removed context or recreate a retired registry. +The four real-socket failures recorded at `b3ffeac9` exercise send, receipt admission, event admission +and a correctly admitted observation presented to a different mutation target. Status remains +Proposed; local source acceptance does not approve the architecture or complete hosted, protected-main +or browser compatibility gates. + OriginWeave carries adapter maintenance and version negotiation but gains a durable customer API. Multiple browser/control transports can coexist. New upstream capabilities do not silently change risk or action semantics. Compatibility matrices become release artifacts. ## Failure and degraded behavior diff --git a/docs/doctoring.md b/docs/doctoring.md index a9549ad48..ae5a337c9 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -4,6 +4,40 @@ This document records external evidence that changes OriginWeave architecture, t ## Decision trace +### Original registry ownership through subscribed navigation + +Four real-socket regressions at `b3ffeac9` on published #264 `2a9fdc54` fail at distinct +use boundaries: a command created in registry A can send using registry B; its original receipt and +binding can create admission in B; a genuine original-connection event can enter B's admission; +and an observation correctly admitted in A can advance B's document. Separate real registries +legitimately allocate the same local numeric IDs. The last case does not even require matching wire +context text. Earlier unsent-binding tests covered command-instance identity, not this owner swap. +The initial fixture compile error at `c41e737b` was corrected before claiming these behavioral failures. + +The repair gives the canonical core registry one opaque process-local allocation identity and +retains its witness through the existing command, binding and subscribed observation. The existing +context validator checks ownership before numeric/text liveness checks; the sender checks before +correlation or I/O, event admission checks before replay insertion, and the shared document-advance +sink checks before mutation. Origin binding already delegates to that sink. Existing connection +and command-instance identities remain independent requirements. A rejected foreign event leaves +replay state available for the original event; moving the original registry preserves its identity. +No new transport, registry implementation, global counter, dependency, exclusion or quality gate is introduced. + +Rust's standard-library documentation defines `Arc::ptr_eq` as allocation identity rather than +value equality; clones retain the same allocation. A retained witness keeps only that identity +alive, not mutable registry state, preventing a later allocation from impersonating a dropped owner. +The API uses no raw address or unsafe operation and is compiled against Rust 1.97.1. The online +reference currently describes Rust 1.98.1; it is not substituted for the pinned build evidence. +The small per-registry allocation and reference-count cost are accepted in the Proposed refinement +of ADR 0107. Numeric/text matching and constructor-only checks miss the reproduced sink swap; +global identifier renumbering would affect unrelated consumers without expressing this ownership invariant. + +This slice pins the registry that constructed the command; it does not prove that its registered +browser session is authenticated by the transport. Same-connection resend freshness, action causality, +unsubscribe lifetime/transport provenance and broader adapter ownership remain separate unfinished +boundaries. A document change alone must not retire a context-wide subscription. #264 stays Draft +behind #195/#279; local tests and numerical coverage do not prove hosted, protected-main or browser acceptance. + ### Subscription response and event receive-connection integrity On September 6, 2026, two real-loopback regressions against #264 `43d3b5a3a2b5ce4f51a93d1152a0ee82620f4f3e` exposed the remaining inbound transport gap. A successful response received on a second connection completed the first connection's pending subscription when session and command identifiers matched. A matching navigation event on another connection also became a state-changing observation. Test-first commit `918c4ebeb27e1eb7e03567eb52e6e547dfd499df` records both failures. @@ -198,6 +232,9 @@ Review `5120077272` remains actionable: this intermediate closure value retains ## References +The Rust Project Developers. (n.d.). *Arc in std::sync*. Rust standard library documentation. +Retrieved September 6, 2026, from https://doc.rust-lang.org/std/sync/struct.Arc.html#method.ptr_eq + Amazon Web Services. (n.d.). *Set up the Amazon EKS Pod Identity Agent*. Retrieved August 6, 2026, from https://docs.aws.amazon.com/eks/latest/userguide/pod-id-agent-setup.html Autio, C., Schwartz, R., Dunietz, J., Jain, S., Stanley, M., Tabassi, E., Hall, P., & Roberts, K. (2024). *Artificial intelligence risk management framework: Generative artificial intelligence profile* (NIST AI 600-1). National Institute of Standards and Technology. https://doi.org/10.6028/NIST.AI.600-1 From 3378761785fd53b9092fc97400c1c653a1e5b00b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 19:00:02 +0900 Subject: [PATCH 54/76] test(core): cover retained registry identity after owner retirement Exercise the real dropped-owner mismatch through the existing core diagnostic contract after exact coverage identified its untested unit-copy arm. Retain the socket receipt path and original-registry move recovery. Commit-Message-Assisted-by: Codex (via Codex) Signed-off-by: Seongho Bae --- .../src/browser_authority_registry.rs | 4 ++-- crates/originweave-core/src/browser_registry.rs | 13 +++++++++++++ ...i_navigation_committed_subscription_admission.rs | 4 ++-- .../webdriver_bidi_navigation_document_advance.rs | 4 ++-- ...i_navigation_committed_subscription_admission.rs | 1 + docs/doctoring.md | 5 +++++ 6 files changed, 25 insertions(+), 6 deletions(-) diff --git a/crates/originweave-core/src/browser_authority_registry.rs b/crates/originweave-core/src/browser_authority_registry.rs index e3b7ce6b6..7db633958 100644 --- a/crates/originweave-core/src/browser_authority_registry.rs +++ b/crates/originweave-core/src/browser_authority_registry.rs @@ -65,9 +65,9 @@ impl BrowserAuthorityRegistry { /// document epoch, origin and operation-specific authority at their actual use boundary. pub fn require_identity( &self, - identity: &BrowserRegistryIdentity, + expected_identity: &BrowserRegistryIdentity, ) -> Result<(), BrowserRegistryError> { - if !Arc::ptr_eq(&self.registry_identity, &identity.0) { + if !Arc::ptr_eq(&self.registry_identity, &expected_identity.0) { return Err(BrowserRegistryError::RegistryInstanceMismatch); } Ok(()) diff --git a/crates/originweave-core/src/browser_registry.rs b/crates/originweave-core/src/browser_registry.rs index 7525ee695..4410f42f2 100644 --- a/crates/originweave-core/src/browser_registry.rs +++ b/crates/originweave-core/src/browser_registry.rs @@ -841,11 +841,24 @@ mod tests { #[test] fn browser_registry_errors_have_non_sensitive_deterministic_text() { + let original_registry = crate::BrowserAuthorityRegistry::new(); + let retained_identity = original_registry.registry_identity().clone(); + assert!( + original_registry + .require_identity(&retained_identity) + .is_ok() + ); + drop(original_registry); + let replacement_registry = crate::BrowserAuthorityRegistry::with_identifier_limit(8); + let replacement_error = replacement_registry + .require_identity(&retained_identity) + .expect_err("retained identity must not authorize a replacement registry"); let expected_values = values(BrowserSessionId::new(1)); let actual_values = values(BrowserSessionId::new(2)); assert_eq!(expected_values.len(), 1); assert_eq!(actual_values.len(), 1); let errors = [ + replacement_error, BrowserRegistryError::InvalidExternalIdentifier, BrowserRegistryError::UnknownBrowserSession, BrowserRegistryError::UnknownBrowsingContext, diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_admission.rs b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_admission.rs index 9e2e583cf..c84df6577 100644 --- a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_admission.rs +++ b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_admission.rs @@ -91,7 +91,7 @@ impl WebDriverBiDiNavigationCommittedSubscriptionBinding { /// Construction requires both the correlated remote subscription receipt and the immutable binding /// captured from the exact command that requested it. The command identifiers and private command /// allocation identity must match, and the original external context mapping must still resolve to -/// the exact OriginWeave session/context in the original registry. Event admission additionally requires the event message to +/// the exact OriginWeave session/context in the original registry. Event admission requires the message to /// have been assembled on the same verified connection generation that carried the subscription /// command and receipt. Holding this value is therefore narrower than holding an opaque protocol /// subscription string. It grants only admission of the matching committed-navigation event through @@ -277,7 +277,7 @@ fn require_current_binding( /// Unlike the lower-level protocol observation, this value proves that local admission was bound to /// the exact typed `session.subscribe` command/receipt pair and the same verified transport /// generation for the registered context at the time the event was admitted. It retains the original -/// registry witness for revalidation at the eventual document-mutation boundary. It still does not prove +/// registry witness for revalidation at the eventual document-mutation boundary. It does not prove /// action causality or grant destination, origin, policy, node, secret, process, profile, or reusable /// Agent authority. pub struct WebDriverBiDiNavigationCommittedSubscribedObservation( diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_document_advance.rs b/crates/originweave-network/src/webdriver_bidi_navigation_document_advance.rs index faf8df0f3..fb06d4447 100644 --- a/crates/originweave-network/src/webdriver_bidi_navigation_document_advance.rs +++ b/crates/originweave-network/src/webdriver_bidi_navigation_document_advance.rs @@ -99,8 +99,8 @@ fn advance_registered_document_if_expected( /// post-condition is being evaluated. The subscribed observation is consumed so one admitted event /// cannot be reused to rotate the registry twice, and raw protocol observations cannot cross this /// state-changing boundary without first being bound to an active exact `session.subscribe` -/// command/receipt. The original registry instance, exact session/context pair and caller-captured epoch are revalidated -/// immediately before mutation, and stale state fails closed without mutation. +/// command/receipt. The original registry instance, exact session/context pair and caller-captured +/// epoch are revalidated immediately before mutation, and stale state fails closed without mutation. /// /// A successful advance delegates to [`BrowserAuthorityRegistry::advance_document`], which clears /// the previous canonical-origin binding and all node bindings owned by the context. The new diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs index 32e1da1b4..591f2b3bb 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs @@ -585,6 +585,7 @@ fn original_binding_rejects_replacement_registry_at_receipt_admission() -> Resul let session = original.register_session(SESSION_ID)?; let context = original.register_context(session, CONTEXT_ID)?; let (receipt, binding, _) = receive_subscription_result(&original, session, context, 7)?; + drop(original); let mut replacement = BrowserAuthorityRegistry::with_identifier_limit(8); let replacement_session = replacement.register_session(SESSION_ID)?; let replacement_context = replacement.register_context(replacement_session, CONTEXT_ID)?; diff --git a/docs/doctoring.md b/docs/doctoring.md index ae5a337c9..1072b9ca5 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -21,6 +21,11 @@ correlation or I/O, event admission checks before replay insertion, and the shar sink checks before mutation. Origin binding already delegates to that sink. Existing connection and command-instance identities remain independent requirements. A rejected foreign event leaves replay state available for the original event; moving the original registry preserves its identity. +The receipt regression also drops the original registry before constructing its replacement. The +core diagnostic contract verifies that a cloned witness matches its live owner but not a new registry +after the owner is dropped. Initial `e686b3a0` coverage passed all functions and branches but missed +one line and three regions in the core unit-crate diagnostic copy; exercising this real failure in +the existing diagnostic test closes that test gap without exclusions or production changes. No new transport, registry implementation, global counter, dependency, exclusion or quality gate is introduced. Rust's standard-library documentation defines `Arc::ptr_eq` as allocation identity rather than From 10f138f8787d596e8b556fe50c9e4e52bc1295b7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 19:03:55 +0900 Subject: [PATCH 55/76] test(core): preserve panic-free registry diagnostic assertions Replace test expect_err with the existing collection and cardinality assertion pattern required by strict workspace Clippy. Commit-Message-Assisted-by: Codex (via Codex) Signed-off-by: Seongho Bae --- crates/originweave-core/src/browser_registry.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/originweave-core/src/browser_registry.rs b/crates/originweave-core/src/browser_registry.rs index 4410f42f2..c5592e67a 100644 --- a/crates/originweave-core/src/browser_registry.rs +++ b/crates/originweave-core/src/browser_registry.rs @@ -850,15 +850,18 @@ mod tests { ); drop(original_registry); let replacement_registry = crate::BrowserAuthorityRegistry::with_identifier_limit(8); - let replacement_error = replacement_registry + let replacement_errors: Vec<_> = replacement_registry .require_identity(&retained_identity) - .expect_err("retained identity must not authorize a replacement registry"); + .err() + .into_iter() + .collect(); + assert_eq!(replacement_errors.len(), 1); let expected_values = values(BrowserSessionId::new(1)); let actual_values = values(BrowserSessionId::new(2)); assert_eq!(expected_values.len(), 1); assert_eq!(actual_values.len(), 1); let errors = [ - replacement_error, + replacement_errors[0], BrowserRegistryError::InvalidExternalIdentifier, BrowserRegistryError::UnknownBrowserSession, BrowserRegistryError::UnknownBrowsingContext, From 92fd0b07a0729c1b305ccd9d556d05eee09f828c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 19:34:02 +0900 Subject: [PATCH 56/76] test: reject stale responses after actual subscription resend Commit-Message-Assisted-by: Codex (via Codex) Signed-off-by: Seongho Bae --- ...gation_committed_subscription_admission.rs | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs index 591f2b3bb..97bc71fdd 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs @@ -217,6 +217,166 @@ fn establish_connection( .read_opening_response(Duration::from_millis(500))?) } +fn reject_stale_response_after_actual_resend(lifecycle: &str) -> Result<(), Box> { + for first_payload in [ + SUBSCRIBE_RESPONSE, + br#"{"type":"error","id":7,"error":"invalid argument","message":"old rejection"}"#, + ] { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let (release_sender, release_receiver) = std::sync::mpsc::channel(); + let server = thread::spawn(move || -> io::Result { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + let expected = br#"{"id":7,"method":"session.subscribe","params":{"events":["browsingContext.navigationCommitted"],"contexts":["context-a"]}}"#; + assert_eq!(read_masked_text_frame(&mut stream)?, expected); + write_text_frame(&mut stream, first_payload)?; + match read_masked_text_frame(&mut stream) { + Err(error) if error.kind() == io::ErrorKind::UnexpectedEof => Ok(false), + Err(error) => Err(error), + Ok(second_command) => { + assert_eq!(second_command, expected); + release_receiver + .recv_timeout(Duration::from_secs(2)) + .map_err(io::Error::other)?; + write_text_frame(&mut stream, + br#"{"type":"success","id":7,"result":{"subscription":"subscription-new"}}"#)?; + Ok(true) + } + } + }); + let mut registry = BrowserAuthorityRegistry::new(); + let session = registry.register_session(SESSION_ID)?; + let context = registry.register_context(session, CONTEXT_ID)?; + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + let command = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( + 7, ®istry, session, context, CONTEXT_ID, + )?; + let mut established = command.send( + ®istry, + establish_connection(local_addr)?, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + Duration::from_millis(500), + )?; + let mut old_message = None; + if lifecycle != "buffered" { + let (next_stream, message) = next_text(established)?; + established = next_stream; + old_message = Some(message); + } + if lifecycle == "completed" { + if let Some(message) = &old_message { + let first_result = + WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( + message, + &mut correlation, + ); + assert_eq!(first_result.is_ok(), first_payload == SUBSCRIBE_RESPONSE); + assert_eq!(correlation.outstanding_count(), 0); + } + } else { + correlation.retire_command_for( + 7, + originweave_network::WebDriverBiDiCommandKind::NavigationCommittedSubscription, + )?; + } + if lifecycle == "replacement" { + correlation = WebDriverBiDiCommandCorrelation::new(); + } + let second_command = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( + 7, ®istry, session, context, CONTEXT_ID, + )?; + let second_binding = second_command.admission_binding(); + let second_send = second_command.send( + ®istry, + established, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([5, 6, 7, 8]), + Duration::from_millis(500), + ); + match second_send { + Err(error) => { + drop(release_sender); + let emitted = server + .join() + .map_err(|_| io::Error::other("resend server panicked"))??; + assert!(!emitted, "preflight rejection must emit no second command"); + assert!(matches!(error, + originweave_network::WebDriverBiDiNavigationCommittedSubscriptionCommandError::FrameWrite { + source: originweave_network::WebDriverBiDiWebSocketFrameError::MalformedFrame { .. } + })); + assert_eq!(correlation.outstanding_count(), 0); + } + Ok(mut next_stream) => { + if old_message.is_none() { + let (read_stream, message) = next_text(next_stream)?; + next_stream = read_stream; + old_message = Some(message); + } + let message = + old_message.ok_or_else(|| io::Error::other("old response missing"))?; + let old_result = + WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( + &message, + &mut correlation, + ); + let pending_after_old = correlation.outstanding_count(); + let old_admission = old_result.ok().map(|result| { + WebDriverBiDiNavigationCommittedSubscriptionAdmission::new( + result, + second_binding, + ®istry, + ) + .is_ok() + }); + release_sender.send(())?; + let (final_stream, fresh_message) = next_text(next_stream)?; + let fresh_result = + WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( + &fresh_message, + &mut correlation, + ); + drop(final_stream); + assert!( + server + .join() + .map_err(|_| io::Error::other("resend server panicked"))?? + ); + assert_eq!( + pending_after_old, 1, + "old response consumed the actual second command; old admission={old_admission:?}" + ); + assert_eq!(fresh_result?.subscription_id(), "subscription-new"); + assert_eq!(correlation.outstanding_count(), 0); + } + } + } + Ok(()) +} + +#[test] +fn completed_response_cannot_complete_an_actual_resend() -> Result<(), Box> { + reject_stale_response_after_actual_resend("completed") +} + +#[test] +fn unparsed_retired_response_cannot_complete_an_actual_resend() -> Result<(), Box> { + reject_stale_response_after_actual_resend("retired") +} + +#[test] +fn replacement_correlation_cannot_accept_a_response_from_before_actual_resend() +-> Result<(), Box> { + reject_stale_response_after_actual_resend("replacement") +} + +#[test] +fn buffered_response_cannot_complete_an_actual_resend() -> Result<(), Box> { + reject_stale_response_after_actual_resend("buffered") +} + #[test] fn foreign_connection_cannot_complete_a_subscription_command() -> Result<(), Box> { for foreign_payload in [ From 15aea15eca004729d0716062702504dadb7ba67b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 19:36:41 +0900 Subject: [PATCH 57/76] test: expose stale error retirement and pre-upgrade socket alias Commit-Message-Assisted-by: Codex (via Codex) Signed-off-by: Seongho Bae --- .../originweave-network/src/webdriver_bidi_connection.rs | 7 +++++++ ...ver_bidi_navigation_committed_subscription_admission.rs | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/originweave-network/src/webdriver_bidi_connection.rs b/crates/originweave-network/src/webdriver_bidi_connection.rs index f1701a923..94cd553fc 100644 --- a/crates/originweave-network/src/webdriver_bidi_connection.rs +++ b/crates/originweave-network/src/webdriver_bidi_connection.rs @@ -208,6 +208,13 @@ impl WebDriverBiDiSocketConnector for SystemWebDriverBiDiConnector { /// transport to the expected browser process/session, and pass separate action-policy checks. A /// private process-local connection generation follows this exact stream so later evidence cannot /// be mixed with another connection that happens to use the same session or command identifier. +/// Raw socket aliases cannot coexist with this upgradeable connection owner: +/// +/// ```compile_fail +/// fn retain_alias(connection: &originweave_network::WebDriverBiDiTcpConnection) { +/// let _alias = connection.stream().try_clone(); +/// } +/// ``` #[derive(Debug)] pub struct WebDriverBiDiTcpConnection { stream: TcpStream, diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs index 97bc71fdd..7ea4dc72f 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs @@ -219,8 +219,9 @@ fn establish_connection( fn reject_stale_response_after_actual_resend(lifecycle: &str) -> Result<(), Box> { for first_payload in [ + br#"{"type":"error","id":7,"error":"invalid argument","message":"old rejection"}"# + .as_slice(), SUBSCRIBE_RESPONSE, - br#"{"type":"error","id":7,"error":"invalid argument","message":"old rejection"}"#, ] { let listener = TcpListener::bind(("127.0.0.1", 0))?; let local_addr = listener.local_addr()?; From d75f780744d08d30cade3d02fc64bdfc7de5cfd2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 19:42:54 +0900 Subject: [PATCH 58/76] fix: seal typed command dispatch history on each connection Reject ambiguous identifier reuse and raw/typed text mixing at the shared frame owner; remove nonconsuming socket aliases and preserve the real revoked-socket test in the owner module. Commit-Message-Assisted-by: Codex (via Codex) Signed-off-by: Seongho Bae --- .../src/webdriver_bidi_command_correlation.rs | 4 +- .../src/webdriver_bidi_connection.rs | 6 -- .../src/webdriver_bidi_connection/tests.rs | 38 +++++++++++- ..._bidi_navigation_committed_subscription.rs | 2 +- ...r_bidi_navigation_committed_unsubscribe.rs | 2 +- .../webdriver_bidi_pointer_click_transport.rs | 2 +- .../src/webdriver_bidi_session_end_command.rs | 2 +- .../webdriver_bidi_session_status_command.rs | 2 +- .../src/webdriver_bidi_websocket_frame.rs | 54 ++++++++++++++++- .../webdriver_bidi_websocket_handshake.rs | 60 +------------------ 10 files changed, 99 insertions(+), 73 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs index e67d675c2..98326c47a 100644 --- a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs +++ b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs @@ -188,7 +188,9 @@ impl WebDriverBiDiCommandCorrelation { /// /// Identifiers are unique only while outstanding. A completed or explicitly retired id may be /// 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. + /// any command family is still outstanding fails before replacing its provenance. This generic + /// table does not authorize dispatch: the typed connection owner independently requires strictly + /// increasing identifiers across all typed command families for its complete lifetime. pub fn register_command_for( &mut self, command_id: u64, diff --git a/crates/originweave-network/src/webdriver_bidi_connection.rs b/crates/originweave-network/src/webdriver_bidi_connection.rs index 94cd553fc..b80aebc9b 100644 --- a/crates/originweave-network/src/webdriver_bidi_connection.rs +++ b/crates/originweave-network/src/webdriver_bidi_connection.rs @@ -225,12 +225,6 @@ pub struct WebDriverBiDiTcpConnection { } impl WebDriverBiDiTcpConnection { - /// Borrow the verified TCP stream. - #[must_use] - pub const fn stream(&self) -> &TcpStream { - &self.stream - } - /// Borrow the session-correlated exact peer evidence consumed by this connection. #[must_use] pub const fn verified_peer(&self) -> &VerifiedWebDriverBiDiSocketPeer { diff --git a/crates/originweave-network/src/webdriver_bidi_connection/tests.rs b/crates/originweave-network/src/webdriver_bidi_connection/tests.rs index 63564283f..5a58a89b6 100644 --- a/crates/originweave-network/src/webdriver_bidi_connection/tests.rs +++ b/crates/originweave-network/src/webdriver_bidi_connection/tests.rs @@ -25,6 +25,42 @@ fn socket_address() -> SocketAddr { SocketAddr::from(([127, 0, 0, 1], 9515)) } +#[test] +fn opening_write_fails_closed_after_verified_stream_is_locally_revoked() -> Result<(), Box> { + use std::{net::Shutdown, sync::mpsc, thread}; + use crate::{WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketOpeningWriteError}; + + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let (release_server, await_release) = mpsc::sync_channel(0); + let server = thread::spawn(move || -> io::Result<()> { + let accepted = listener.accept()?; + await_release.recv_timeout(Duration::from_secs(2)).map_err(io::Error::other)?; + drop(accepted); + Ok(()) + }); + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? + .correlate_session_id(SESSION_ID)?.into_explicit_connect_target()?; + let connection = WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; + connection.stream.shutdown(Shutdown::Both)?; + let key = WebDriverBiDiWebSocketClientKey::new("dGhlIHNhbXBsZSBub25jZQ==")?; + let write = WebDriverBiDiWebSocketHandshakePlan::new(connection, key)? + .write_opening_request(Duration::from_secs(1)); + let failed_closed_without_writing = match write { + Err(WebDriverBiDiWebSocketOpeningWriteError::WriteFailed { bytes_written: 0, .. }) => true, + Err(WebDriverBiDiWebSocketOpeningWriteError::WriteTimeoutConfigurationFailed { + bytes_written: 0, source, + }) => source.kind() == io::ErrorKind::InvalidInput, + _ => false, + }; + release_server.send(())?; + server.join().map_err(|_| io::Error::other("revoked stream server panicked"))??; + assert!(failed_closed_without_writing); + Ok(()) +} + enum ConnectOutcome { Success(TcpStream), Error(io::ErrorKind), @@ -185,7 +221,7 @@ fn verified_peer_is_required_before_stream_exposure() { .connect_with(&connector) .expect("verified connection"); - assert!(connection.stream().peer_addr().is_ok()); + assert!(connection.stream.peer_addr().is_ok()); assert_eq!(connection.verified_peer().socket_addr(), socket_address()); assert!(connection.verified_peer().requires_tls()); assert_eq!(connection.verified_peer().session_id(), SESSION_ID); diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs index b629fdfab..75ebb65e1 100644 --- a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs +++ b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs @@ -157,7 +157,7 @@ impl WebDriverBiDiNavigationCommittedSubscriptionCommand { WebDriverBiDiNavigationCommittedSubscriptionCommandError::Correlation { source } })?; let message = self.serialized(); - match established.write_text_frame(&message, masking_key, frame_timeout) { + match established.write_command_frame(self.command_id, &message, masking_key, frame_timeout) { Ok(established) => Ok(established), Err(source) => Err(map_frame_failure(correlation, self.command_id, source)), } diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_committed_unsubscribe.rs b/crates/originweave-network/src/webdriver_bidi_navigation_committed_unsubscribe.rs index 35fd48aa3..a30fe6f6d 100644 --- a/crates/originweave-network/src/webdriver_bidi_navigation_committed_unsubscribe.rs +++ b/crates/originweave-network/src/webdriver_bidi_navigation_committed_unsubscribe.rs @@ -88,7 +88,7 @@ impl WebDriverBiDiNavigationCommittedUnsubscribeCommand { WebDriverBiDiNavigationCommittedUnsubscribeCommandError::Correlation { source } })?; let message = self.serialized(); - match established.write_text_frame(&message, masking_key, frame_timeout) { + match established.write_command_frame(self.command_id, &message, masking_key, frame_timeout) { Ok(established) => Ok(established), Err(source) => Err(map_frame_failure(correlation, self.command_id, source)), } diff --git a/crates/originweave-network/src/webdriver_bidi_pointer_click_transport.rs b/crates/originweave-network/src/webdriver_bidi_pointer_click_transport.rs index 1c5b399fc..b4c3550d5 100644 --- a/crates/originweave-network/src/webdriver_bidi_pointer_click_transport.rs +++ b/crates/originweave-network/src/webdriver_bidi_pointer_click_transport.rs @@ -77,7 +77,7 @@ pub fn send_webdriver_bidi_pointer_click( return Err(WebDriverBiDiPointerClickSendError::Correlation { source }); } } - match established.write_text_frame(command.as_json(), masking_key, frame_timeout) { + match established.write_command_frame(command.command_id(), command.as_json(), masking_key, frame_timeout) { Ok(established) => Ok(established), Err(source) => Err(map_frame_failure(correlation, command.command_id(), source)), } diff --git a/crates/originweave-network/src/webdriver_bidi_session_end_command.rs b/crates/originweave-network/src/webdriver_bidi_session_end_command.rs index 82dc95cf7..5ff4d3142 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_end_command.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_end_command.rs @@ -71,7 +71,7 @@ impl WebDriverBiDiSessionEndCommand { ) .map_err(|source| WebDriverBiDiSessionEndCommandError::Correlation { source })?; let message = self.serialized(); - match established.write_text_frame(&message, masking_key, frame_timeout) { + match established.write_command_frame(self.command_id, &message, masking_key, frame_timeout) { Ok(established) => Ok(established), Err(source) => Err(map_frame_failure(correlation, self.command_id, source)), } diff --git a/crates/originweave-network/src/webdriver_bidi_session_status_command.rs b/crates/originweave-network/src/webdriver_bidi_session_status_command.rs index e6341c416..fb933cb4b 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_status_command.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_status_command.rs @@ -67,7 +67,7 @@ impl WebDriverBiDiSessionStatusCommand { .register_command_for(self.command_id, WebDriverBiDiCommandKind::SessionStatus) .map_err(|source| WebDriverBiDiSessionStatusCommandError::Correlation { source })?; let message = self.serialized(); - match established.write_text_frame(&message, masking_key, frame_timeout) { + match established.write_command_frame(self.command_id, &message, masking_key, frame_timeout) { Ok(established) => Ok(established), Err(source) => Err(map_frame_failure(correlation, self.command_id, source)), } diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs b/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs index e931dd4b8..59ab374ef 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs @@ -58,6 +58,14 @@ struct ClientMaskKeyHistory { previous_key: Option<[u8; 4]>, } +#[derive(Default)] +enum CommandWriteState { + #[default] + NoCommands, + RawText, + TypedCommand { command_id: u64 }, +} + impl ClientMaskKeyHistory { fn reserve( &mut self, @@ -172,6 +180,7 @@ impl WebDriverBiDiWebSocketOpeningRequestSent { WebDriverBiDiWebSocketEstablished { raw, client_mask_keys: ClientMaskKeyHistory::default(), + command_write_state: CommandWriteState::default(), } }) } @@ -186,6 +195,7 @@ impl WebDriverBiDiWebSocketOpeningRequestSent { pub struct WebDriverBiDiWebSocketEstablished { raw: handshake::WebDriverBiDiWebSocketEstablished, client_mask_keys: ClientMaskKeyHistory, + command_write_state: CommandWriteState, } impl fmt::Debug for WebDriverBiDiWebSocketEstablished { @@ -237,16 +247,58 @@ impl WebDriverBiDiWebSocketEstablished { self.raw.write_timeout() } - /// Write one final masked UTF-8 text frame on this verified stream. + /// Write one final masked UTF-8 text frame on a raw-text-only verified stream. /// /// The state is consumed. Invalid bounds, adjacent masking-key reuse, partial writes, deadline /// expiry, I/O failure, and timeout-cleanup failure return no reusable stream. No retry changes /// destination or connection authority. + /// Raw text and typed commands cannot share one connection in either order: arbitrary text + /// cannot establish or preserve the typed command identifier history. Use a separate connection + /// for raw protocol work. Pong frames do not select or change this connection's text lane. pub fn write_text_frame( mut self, text: &str, masking_key: WebDriverBiDiWebSocketMaskKey, frame_timeout: Duration, + ) -> Result { + if matches!(self.command_write_state, CommandWriteState::TypedCommand { .. }) { + return Err(WebDriverBiDiWebSocketFrameError::MalformedFrame { + reason: "raw text and typed commands cannot share an established WebSocket", + }); + } + self.command_write_state = CommandWriteState::RawText; + self.write_text_payload(text, masking_key, frame_timeout) + } + + pub(crate) fn write_command_frame( + mut self, + command_id: u64, + text: &str, + masking_key: WebDriverBiDiWebSocketMaskKey, + frame_timeout: Duration, + ) -> Result { + match self.command_write_state { + CommandWriteState::RawText => { + return Err(WebDriverBiDiWebSocketFrameError::MalformedFrame { + reason: "raw text and typed commands cannot share an established WebSocket", + }); + } + CommandWriteState::TypedCommand { command_id: previous_id } if command_id <= previous_id => { + return Err(WebDriverBiDiWebSocketFrameError::MalformedFrame { + reason: "typed command identifiers must increase on an established WebSocket", + }); + } + _ => {} + } + self.command_write_state = CommandWriteState::TypedCommand { command_id }; + self.write_text_payload(text, masking_key, frame_timeout) + } + + fn write_text_payload( + mut self, + text: &str, + masking_key: WebDriverBiDiWebSocketMaskKey, + frame_timeout: Duration, ) -> Result { validate_frame_timeout(frame_timeout)?; if text.len() > MAX_WEBSOCKET_FRAME_PAYLOAD_BYTES { diff --git a/crates/originweave-network/tests/webdriver_bidi_websocket_handshake.rs b/crates/originweave-network/tests/webdriver_bidi_websocket_handshake.rs index a08549581..306f4048a 100644 --- a/crates/originweave-network/tests/webdriver_bidi_websocket_handshake.rs +++ b/crates/originweave-network/tests/webdriver_bidi_websocket_handshake.rs @@ -1,6 +1,5 @@ use std::{ - net::{Shutdown, TcpListener}, - sync::mpsc, + net::TcpListener, thread, time::Duration, }; @@ -139,63 +138,6 @@ fn plain_bidi_connection_serializes_exact_rfc6455_opening_request() { } } -#[test] -fn opening_write_fails_closed_after_verified_stream_is_locally_revoked() { - let listener = TcpListener::bind(("127.0.0.1", 0)); - assert!(listener.is_ok(), "{listener:?}"); - let Ok(listener) = listener else { - return; - }; - let local_addr = listener.local_addr(); - assert!(local_addr.is_ok(), "{local_addr:?}"); - let Ok(local_addr) = local_addr else { - return; - }; - let (release_server, await_release) = mpsc::sync_channel(0); - let server = thread::spawn(move || { - let accepted = listener.accept()?; - await_release.recv().map_err(std::io::Error::other)?; - drop(accepted); - Ok::<(), std::io::Error>(()) - }); - - let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); - let connection = connect(&endpoint); - let shutdown = connection.stream().shutdown(Shutdown::Both); - assert!(shutdown.is_ok(), "{shutdown:?}"); - - let key = WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY); - assert!(key.is_ok(), "{key:?}"); - let Ok(key) = key else { - return; - }; - let plan = WebDriverBiDiWebSocketHandshakePlan::new(connection, key); - assert!(plan.is_ok(), "{plan:?}"); - let Ok(plan) = plan else { - return; - }; - - let write = plan.write_opening_request(Duration::from_secs(1)); - let failed_closed_without_writing = match write { - Err(WebDriverBiDiWebSocketOpeningWriteError::WriteFailed { - bytes_written: 0, .. - }) => true, - Err(WebDriverBiDiWebSocketOpeningWriteError::WriteTimeoutConfigurationFailed { - bytes_written: 0, - source, - }) => source.kind() == std::io::ErrorKind::InvalidInput, - _ => false, - }; - assert!(failed_closed_without_writing); - assert!(release_server.send(()).is_ok()); - - let server_result = server.join(); - assert!(server_result.is_ok(), "{server_result:?}"); - if let Ok(accept_result) = server_result { - assert!(accept_result.is_ok(), "{accept_result:?}"); - } -} - #[test] fn handshake_errors_render_actionable_fail_closed_messages() { assert_eq!( From 02793e08fa132f7d405ffbbdf9ad5f237c142dfb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 19:44:00 +0900 Subject: [PATCH 59/76] test: verify exclusive dispatch lanes and preserve socket failure probes Commit-Message-Assisted-by: Codex (via Codex) Signed-off-by: Seongho Bae --- .../src/webdriver_bidi_connection/tests.rs | 30 ++- ..._bidi_navigation_committed_subscription.rs | 3 +- ...r_bidi_navigation_committed_unsubscribe.rs | 3 +- .../webdriver_bidi_pointer_click_transport.rs | 7 +- .../src/webdriver_bidi_session_end_command.rs | 3 +- .../webdriver_bidi_session_status_command.rs | 3 +- .../src/webdriver_bidi_websocket_frame.rs | 13 +- .../webdriver_bidi_pointer_click_send.rs | 28 +- .../webdriver_bidi_session_end_command.rs | 10 +- ...er_bidi_session_status_command_failures.rs | 8 +- .../webdriver_bidi_websocket_handshake.rs | 6 +- ...driver_bidi_websocket_masking_key_reuse.rs | 253 +++++++++++++++++- 12 files changed, 321 insertions(+), 46 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_connection/tests.rs b/crates/originweave-network/src/webdriver_bidi_connection/tests.rs index 5a58a89b6..00dee93a6 100644 --- a/crates/originweave-network/src/webdriver_bidi_connection/tests.rs +++ b/crates/originweave-network/src/webdriver_bidi_connection/tests.rs @@ -26,37 +26,49 @@ fn socket_address() -> SocketAddr { } #[test] -fn opening_write_fails_closed_after_verified_stream_is_locally_revoked() -> Result<(), Box> { +fn opening_write_fails_closed_after_verified_stream_is_locally_revoked() +-> Result<(), Box> { + use crate::{ + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketOpeningWriteError, + }; use std::{net::Shutdown, sync::mpsc, thread}; - use crate::{WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketOpeningWriteError}; let listener = TcpListener::bind(("127.0.0.1", 0))?; let local_addr = listener.local_addr()?; let (release_server, await_release) = mpsc::sync_channel(0); let server = thread::spawn(move || -> io::Result<()> { let accepted = listener.accept()?; - await_release.recv_timeout(Duration::from_secs(2)).map_err(io::Error::other)?; + await_release + .recv_timeout(Duration::from_secs(2)) + .map_err(io::Error::other)?; drop(accepted); Ok(()) }); let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? - .correlate_session_id(SESSION_ID)?.into_explicit_connect_target()?; - let connection = WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; + .correlate_session_id(SESSION_ID)? + .into_explicit_connect_target()?; + let connection = + WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; connection.stream.shutdown(Shutdown::Both)?; let key = WebDriverBiDiWebSocketClientKey::new("dGhlIHNhbXBsZSBub25jZQ==")?; let write = WebDriverBiDiWebSocketHandshakePlan::new(connection, key)? .write_opening_request(Duration::from_secs(1)); let failed_closed_without_writing = match write { - Err(WebDriverBiDiWebSocketOpeningWriteError::WriteFailed { bytes_written: 0, .. }) => true, + Err(WebDriverBiDiWebSocketOpeningWriteError::WriteFailed { + bytes_written: 0, .. + }) => true, Err(WebDriverBiDiWebSocketOpeningWriteError::WriteTimeoutConfigurationFailed { - bytes_written: 0, source, + bytes_written: 0, + source, }) => source.kind() == io::ErrorKind::InvalidInput, _ => false, }; release_server.send(())?; - server.join().map_err(|_| io::Error::other("revoked stream server panicked"))??; + server + .join() + .map_err(|_| io::Error::other("revoked stream server panicked"))??; assert!(failed_closed_without_writing); Ok(()) } diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs index 75ebb65e1..01b4b2095 100644 --- a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs +++ b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs @@ -157,7 +157,8 @@ impl WebDriverBiDiNavigationCommittedSubscriptionCommand { WebDriverBiDiNavigationCommittedSubscriptionCommandError::Correlation { source } })?; let message = self.serialized(); - match established.write_command_frame(self.command_id, &message, masking_key, frame_timeout) { + match established.write_command_frame(self.command_id, &message, masking_key, frame_timeout) + { Ok(established) => Ok(established), Err(source) => Err(map_frame_failure(correlation, self.command_id, source)), } diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_committed_unsubscribe.rs b/crates/originweave-network/src/webdriver_bidi_navigation_committed_unsubscribe.rs index a30fe6f6d..d3aeacd4c 100644 --- a/crates/originweave-network/src/webdriver_bidi_navigation_committed_unsubscribe.rs +++ b/crates/originweave-network/src/webdriver_bidi_navigation_committed_unsubscribe.rs @@ -88,7 +88,8 @@ impl WebDriverBiDiNavigationCommittedUnsubscribeCommand { WebDriverBiDiNavigationCommittedUnsubscribeCommandError::Correlation { source } })?; let message = self.serialized(); - match established.write_command_frame(self.command_id, &message, masking_key, frame_timeout) { + match established.write_command_frame(self.command_id, &message, masking_key, frame_timeout) + { Ok(established) => Ok(established), Err(source) => Err(map_frame_failure(correlation, self.command_id, source)), } diff --git a/crates/originweave-network/src/webdriver_bidi_pointer_click_transport.rs b/crates/originweave-network/src/webdriver_bidi_pointer_click_transport.rs index b4c3550d5..e27a421e8 100644 --- a/crates/originweave-network/src/webdriver_bidi_pointer_click_transport.rs +++ b/crates/originweave-network/src/webdriver_bidi_pointer_click_transport.rs @@ -77,7 +77,12 @@ pub fn send_webdriver_bidi_pointer_click( return Err(WebDriverBiDiPointerClickSendError::Correlation { source }); } } - match established.write_command_frame(command.command_id(), command.as_json(), masking_key, frame_timeout) { + match established.write_command_frame( + command.command_id(), + command.as_json(), + masking_key, + frame_timeout, + ) { Ok(established) => Ok(established), Err(source) => Err(map_frame_failure(correlation, command.command_id(), source)), } diff --git a/crates/originweave-network/src/webdriver_bidi_session_end_command.rs b/crates/originweave-network/src/webdriver_bidi_session_end_command.rs index 5ff4d3142..f64b63167 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_end_command.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_end_command.rs @@ -71,7 +71,8 @@ impl WebDriverBiDiSessionEndCommand { ) .map_err(|source| WebDriverBiDiSessionEndCommandError::Correlation { source })?; let message = self.serialized(); - match established.write_command_frame(self.command_id, &message, masking_key, frame_timeout) { + match established.write_command_frame(self.command_id, &message, masking_key, frame_timeout) + { Ok(established) => Ok(established), Err(source) => Err(map_frame_failure(correlation, self.command_id, source)), } diff --git a/crates/originweave-network/src/webdriver_bidi_session_status_command.rs b/crates/originweave-network/src/webdriver_bidi_session_status_command.rs index fb933cb4b..b92cd330c 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_status_command.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_status_command.rs @@ -67,7 +67,8 @@ impl WebDriverBiDiSessionStatusCommand { .register_command_for(self.command_id, WebDriverBiDiCommandKind::SessionStatus) .map_err(|source| WebDriverBiDiSessionStatusCommandError::Correlation { source })?; let message = self.serialized(); - match established.write_command_frame(self.command_id, &message, masking_key, frame_timeout) { + match established.write_command_frame(self.command_id, &message, masking_key, frame_timeout) + { Ok(established) => Ok(established), Err(source) => Err(map_frame_failure(correlation, self.command_id, source)), } diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs b/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs index 59ab374ef..7f2197673 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs @@ -63,7 +63,9 @@ enum CommandWriteState { #[default] NoCommands, RawText, - TypedCommand { command_id: u64 }, + TypedCommand { + command_id: u64, + }, } impl ClientMaskKeyHistory { @@ -261,7 +263,10 @@ impl WebDriverBiDiWebSocketEstablished { masking_key: WebDriverBiDiWebSocketMaskKey, frame_timeout: Duration, ) -> Result { - if matches!(self.command_write_state, CommandWriteState::TypedCommand { .. }) { + if matches!( + self.command_write_state, + CommandWriteState::TypedCommand { .. } + ) { return Err(WebDriverBiDiWebSocketFrameError::MalformedFrame { reason: "raw text and typed commands cannot share an established WebSocket", }); @@ -283,7 +288,9 @@ impl WebDriverBiDiWebSocketEstablished { reason: "raw text and typed commands cannot share an established WebSocket", }); } - CommandWriteState::TypedCommand { command_id: previous_id } if command_id <= previous_id => { + CommandWriteState::TypedCommand { + command_id: previous_id, + } if command_id <= previous_id => { return Err(WebDriverBiDiWebSocketFrameError::MalformedFrame { reason: "typed command identifiers must increase on an established WebSocket", }); diff --git a/crates/originweave-network/tests/webdriver_bidi_pointer_click_send.rs b/crates/originweave-network/tests/webdriver_bidi_pointer_click_send.rs index 92bcc845f..a00500c46 100644 --- a/crates/originweave-network/tests/webdriver_bidi_pointer_click_send.rs +++ b/crates/originweave-network/tests/webdriver_bidi_pointer_click_send.rs @@ -38,10 +38,10 @@ fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { Ok(()) } -fn read_masked_text_frame(stream: &mut TcpStream) -> io::Result> { +fn read_masked_client_frame(stream: &mut TcpStream, expected_header: u8) -> io::Result> { let mut header = [0_u8; 2]; stream.read_exact(&mut header)?; - if header[0] != 0x81 || header[1] & 0x80 == 0 { + if header[0] != expected_header || header[1] & 0x80 == 0 { return Err(io::Error::new( io::ErrorKind::InvalidData, "expected one final masked client text frame", @@ -104,7 +104,7 @@ fn pointer_click_command_writes_exact_masked_bidi_frame_and_stays_outstanding() let (mut stream, _) = listener.accept()?; read_opening_request(&mut stream)?; stream.write_all(OPENING_RESPONSE)?; - let command = read_masked_text_frame(&mut stream)?; + let command = read_masked_client_frame(&mut stream, 0x81)?; if command != expected_json { return Err(io::Error::new( io::ErrorKind::InvalidData, @@ -154,7 +154,7 @@ fn pointer_click_reused_mask_key_rejection_retires_correlation() -> Result<(), B let (mut stream, _) = listener.accept()?; read_opening_request(&mut stream)?; stream.write_all(OPENING_RESPONSE)?; - let seed = read_masked_text_frame(&mut stream)?; + let seed = read_masked_client_frame(&mut stream, 0x8a)?; if seed != b"{}" { return Err(io::Error::new( io::ErrorKind::InvalidData, @@ -176,7 +176,7 @@ fn pointer_click_reused_mask_key_rejection_retires_correlation() -> Result<(), B .read_opening_response(Duration::from_millis(500))?; let repeated_key = WebDriverBiDiWebSocketMaskKey::new([9, 10, 11, 12]); let established = - established.write_text_frame("{}", repeated_key, Duration::from_millis(500))?; + established.write_pong_frame(b"{}", repeated_key, Duration::from_millis(500))?; let command = WebDriverBiDiPointerClickCommand::new( 43, @@ -232,22 +232,23 @@ fn pointer_click_ambiguous_socket_write_keeps_correlation() -> Result<(), Box Result<(), Box { - correlation.retire_command_for(44, WebDriverBiDiCommandKind::PointerClick)?; + correlation + .retire_command_for(command_id, WebDriverBiDiCommandKind::PointerClick)?; established = next; } Err(error) => { diff --git a/crates/originweave-network/tests/webdriver_bidi_session_end_command.rs b/crates/originweave-network/tests/webdriver_bidi_session_end_command.rs index 71eeecc7c..b5e99ff6e 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_end_command.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_end_command.rs @@ -34,10 +34,10 @@ fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { Ok(()) } -fn read_masked_text_frame(stream: &mut TcpStream) -> io::Result> { +fn read_masked_client_frame(stream: &mut TcpStream, expected_header: u8) -> io::Result> { let mut header = [0_u8; 2]; stream.read_exact(&mut header)?; - if header[0] != 0x81 || header[1] & 0x80 == 0 { + if header[0] != expected_header || header[1] & 0x80 == 0 { return Err(io::Error::new( io::ErrorKind::InvalidData, "expected one final masked client text frame", @@ -69,7 +69,7 @@ fn session_end_command_writes_the_exact_typed_frame_without_claiming_completion( let (mut stream, _) = listener.accept()?; read_opening_request(&mut stream)?; stream.write_all(OPENING_RESPONSE)?; - let command = read_masked_text_frame(&mut stream)?; + let command = read_masked_client_frame(&mut stream, 0x81)?; if command != br#"{"id":11,"method":"session.end","params":{}}"# { return Err(io::Error::new( io::ErrorKind::InvalidData, @@ -119,7 +119,7 @@ fn session_end_reused_mask_key_rejection_retires_exact_correlation() -> Result<( let (mut stream, _) = listener.accept()?; read_opening_request(&mut stream)?; stream.write_all(OPENING_RESPONSE)?; - let seed = read_masked_text_frame(&mut stream)?; + let seed = read_masked_client_frame(&mut stream, 0x8a)?; if seed != b"{}" { return Err(io::Error::new( io::ErrorKind::InvalidData, @@ -141,7 +141,7 @@ fn session_end_reused_mask_key_rejection_retires_exact_correlation() -> Result<( .read_opening_response(Duration::from_millis(500))?; let repeated_key = WebDriverBiDiWebSocketMaskKey::new([9, 10, 11, 12]); let established = - established.write_text_frame("{}", repeated_key, Duration::from_millis(500))?; + established.write_pong_frame(b"{}", repeated_key, Duration::from_millis(500))?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); let command = WebDriverBiDiSessionEndCommand::new(13)?; diff --git a/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs b/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs index d19ae45dd..e4a394d81 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_status_command_failures.rs @@ -40,10 +40,10 @@ fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { Ok(()) } -fn read_masked_text_frame(stream: &mut TcpStream) -> io::Result> { +fn read_masked_client_frame(stream: &mut TcpStream, expected_header: u8) -> io::Result> { let mut header = [0_u8; 2]; stream.read_exact(&mut header)?; - if header[0] != 0x81 || header[1] & 0x80 == 0 { + if header[0] != expected_header || header[1] & 0x80 == 0 { return Err(io::Error::new( io::ErrorKind::InvalidData, "expected one final masked client text frame", @@ -168,7 +168,7 @@ fn session_status_reused_mask_key_rejection_does_not_leave_correlation_outstandi let (mut stream, _) = listener.accept()?; read_opening_request(&mut stream)?; stream.write_all(OPENING_RESPONSE)?; - let seed = read_masked_text_frame(&mut stream)?; + let seed = read_masked_client_frame(&mut stream, 0x8a)?; if seed != b"{}" { return Err(io::Error::new( io::ErrorKind::InvalidData, @@ -190,7 +190,7 @@ fn session_status_reused_mask_key_rejection_does_not_leave_correlation_outstandi .read_opening_response(Duration::from_millis(500))?; let repeated_key = WebDriverBiDiWebSocketMaskKey::new([9, 10, 11, 12]); let established = - established.write_text_frame("{}", repeated_key, Duration::from_millis(500))?; + established.write_pong_frame(b"{}", repeated_key, Duration::from_millis(500))?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); let command = WebDriverBiDiSessionStatusCommand::new(13)?; diff --git a/crates/originweave-network/tests/webdriver_bidi_websocket_handshake.rs b/crates/originweave-network/tests/webdriver_bidi_websocket_handshake.rs index 306f4048a..742bb5028 100644 --- a/crates/originweave-network/tests/webdriver_bidi_websocket_handshake.rs +++ b/crates/originweave-network/tests/webdriver_bidi_websocket_handshake.rs @@ -1,8 +1,4 @@ -use std::{ - net::TcpListener, - thread, - time::Duration, -}; +use std::{net::TcpListener, thread, time::Duration}; use originweave_core::WebDriverBiDiWebSocketEndpoint; use originweave_network::{ diff --git a/crates/originweave-network/tests/webdriver_bidi_websocket_masking_key_reuse.rs b/crates/originweave-network/tests/webdriver_bidi_websocket_masking_key_reuse.rs index 34ce97174..61f834bf6 100644 --- a/crates/originweave-network/tests/webdriver_bidi_websocket_masking_key_reuse.rs +++ b/crates/originweave-network/tests/webdriver_bidi_websocket_masking_key_reuse.rs @@ -8,9 +8,13 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use originweave_network::{ - WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + MAX_WEBDRIVER_BIDI_JS_UINT, WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandKind, + WebDriverBiDiConnectionMessageRead, WebDriverBiDiReceivedTextMessage, + WebDriverBiDiSessionEndCommand, WebDriverBiDiSessionEndResult, + WebDriverBiDiSessionStatusCommand, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketFrameError, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMaskKey, + WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageReader, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -119,6 +123,251 @@ fn require_peer_closed_without_frame(stream: &mut TcpStream) -> io::Result<()> { } } +fn read_received_text( + established: WebDriverBiDiWebSocketEstablished, +) -> Result< + ( + WebDriverBiDiWebSocketEstablished, + WebDriverBiDiReceivedTextMessage, + ), + Box, +> { + match WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + WebDriverBiDiConnectionMessageRead::Text { + established, + message, + } => Ok((established, message)), + other => Err(io::Error::other(format!( + "expected a complete command response, got {other:?}" + )) + .into()), + } +} + +fn write_command_success(stream: &mut TcpStream, command_id: u64) -> io::Result<()> { + let response = format!("{{\"type\":\"success\",\"id\":{command_id},\"result\":{{}}}}"); + let length = u8::try_from(response.len()) + .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?; + stream.write_all(&[0x81, length])?; + stream.write_all(response.as_bytes()) +} + +#[test] +fn established_stream_rejects_typed_command_after_raw_text_and_reader_move() +-> Result<(), Box> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let server = thread::spawn(move || -> io::Result<(String, Vec)> { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + let first = read_masked_text(&mut stream)?; + write_command_success(&mut stream, 7)?; + let mut trailing = Vec::new(); + stream.read_to_end(&mut trailing)?; + Ok((first, trailing)) + }); + + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let established = establish(&endpoint)?.write_text_frame( + r#"{"id":7,"method":"session.end","params":{}}"#, + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + Duration::from_millis(500), + )?; + let (established, response) = read_received_text(established)?; + drop(response); + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + correlation.register_command_for(99, WebDriverBiDiCommandKind::SessionStatus)?; + let rejected = match WebDriverBiDiSessionEndCommand::new(7)?.send( + established, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([5, 6, 7, 8]), + Duration::from_millis(500), + ) { + Ok(established) => { + drop(established); + None + } + Err(error) => Some(error), + }; + let (first, trailing) = server + .join() + .map_err(|_| io::Error::other("raw-before-typed test server panicked"))??; + + assert_eq!(first, r#"{"id":7,"method":"session.end","params":{}}"#); + assert!( + trailing.is_empty(), + "typed command followed untracked raw text: {trailing:?}" + ); + let error = + rejected.ok_or_else(|| io::Error::other("typed send accepted a raw-text stream"))?; + assert!(error.source().is_some()); + assert_eq!(correlation.outstanding_count(), 1); + correlation.retire_command_for(99, WebDriverBiDiCommandKind::SessionStatus)?; + assert_eq!(correlation.outstanding_count(), 0); + Ok(()) +} + +#[test] +fn established_stream_rejects_raw_text_while_typed_command_is_outstanding() +-> Result<(), Box> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let server = thread::spawn(move || -> io::Result<(String, Vec)> { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + let first = read_masked_text(&mut stream)?; + let mut trailing = Vec::new(); + stream.read_to_end(&mut trailing)?; + Ok((first, trailing)) + }); + + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + let established = WebDriverBiDiSessionEndCommand::new(7)?.send( + establish(&endpoint)?, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([9, 10, 11, 12]), + Duration::from_millis(500), + )?; + let rejected = match established.write_text_frame( + r#"{"id":7,"method":"session.end","params":{}}"#, + WebDriverBiDiWebSocketMaskKey::new([13, 14, 15, 16]), + Duration::from_millis(500), + ) { + Ok(established) => { + drop(established); + None + } + Err(error) => Some(error), + }; + let (first, trailing) = server + .join() + .map_err(|_| io::Error::other("raw-during-typed test server panicked"))??; + + assert_eq!(first, r#"{"id":7,"method":"session.end","params":{}}"#); + assert!( + trailing.is_empty(), + "raw text bypassed pending typed dispatch: {trailing:?}" + ); + assert!( + rejected.is_some(), + "raw text write accepted a typed-command stream" + ); + assert_eq!(correlation.outstanding_count(), 1); + correlation.retire_command_for(7, WebDriverBiDiCommandKind::SessionEnd)?; + assert_eq!(correlation.outstanding_count(), 0); + Ok(()) +} + +#[test] +fn established_stream_preserves_dispatch_history_across_pong_readers_and_correlation() +-> Result<(), Box> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let server = thread::spawn(move || -> io::Result<_> { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + let mut pongs = vec![read_masked_frame(&mut stream, 0x0a)?]; + let commands = vec![ + read_masked_text(&mut stream)?, + read_masked_text(&mut stream)?, + ]; + pongs.push(read_masked_frame(&mut stream, 0x0a)?); + write_command_success(&mut stream, MAX_WEBDRIVER_BIDI_JS_UINT)?; + write_command_success(&mut stream, 0)?; + pongs.push(read_masked_frame(&mut stream, 0x0a)?); + let mut trailing = Vec::new(); + stream.read_to_end(&mut trailing)?; + Ok((pongs, commands, trailing)) + }); + + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let established = establish(&endpoint)?.write_pong_frame( + b"before", + WebDriverBiDiWebSocketMaskKey::new([17, 18, 19, 20]), + Duration::from_millis(500), + )?; + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + let established = WebDriverBiDiSessionEndCommand::new(0)?.send( + established, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([21, 22, 23, 24]), + Duration::from_millis(500), + )?; + let established = WebDriverBiDiSessionEndCommand::new(MAX_WEBDRIVER_BIDI_JS_UINT)?.send( + established, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([25, 26, 27, 28]), + Duration::from_millis(500), + )?; + let established = established.write_pong_frame( + b"between", + WebDriverBiDiWebSocketMaskKey::new([29, 30, 31, 32]), + Duration::from_millis(500), + )?; + let mut outstanding_counts = vec![correlation.outstanding_count()]; + let (established, later_response) = read_received_text(established)?; + let later_result = + WebDriverBiDiSessionEndResult::parse_and_correlate(&later_response, &mut correlation)?; + outstanding_counts.push(correlation.outstanding_count()); + let (established, earlier_response) = read_received_text(established)?; + let earlier_result = + WebDriverBiDiSessionEndResult::parse_and_correlate(&earlier_response, &mut correlation)?; + outstanding_counts.push(correlation.outstanding_count()); + let established = established.write_pong_frame( + b"after", + WebDriverBiDiWebSocketMaskKey::new([33, 34, 35, 36]), + Duration::from_millis(500), + )?; + let mut replacement = WebDriverBiDiCommandCorrelation::new(); + let rejected = match WebDriverBiDiSessionStatusCommand::new(MAX_WEBDRIVER_BIDI_JS_UINT)?.send( + established, + &mut replacement, + WebDriverBiDiWebSocketMaskKey::new([37, 38, 39, 40]), + Duration::from_millis(500), + ) { + Ok(established) => { + drop(established); + None + } + Err(error) => Some(error), + }; + let (pongs, commands, trailing) = server + .join() + .map_err(|_| io::Error::other("dispatch-history test server panicked"))??; + + assert_eq!( + pongs, + [b"before".to_vec(), b"between".to_vec(), b"after".to_vec()] + ); + assert_eq!( + commands, + [ + r#"{"id":0,"method":"session.end","params":{}}"#.to_owned(), + format!( + "{{\"id\":{MAX_WEBDRIVER_BIDI_JS_UINT},\"method\":\"session.end\",\"params\":{{}}}}" + ), + ] + ); + assert_eq!(outstanding_counts, [2, 1, 0]); + assert_eq!(later_result.command_id(), MAX_WEBDRIVER_BIDI_JS_UINT); + assert_eq!(earlier_result.command_id(), 0); + assert!( + trailing.is_empty(), + "dispatch history reset after completed replies: {trailing:?}" + ); + let error = + rejected.ok_or_else(|| io::Error::other("another command family reused the last id"))?; + assert!(error.source().is_some()); + assert_eq!(replacement.outstanding_count(), 0); + Ok(()) +} + #[test] fn established_stream_rejects_client_mask_reuse_across_sequential_frames() -> Result<(), Box> { From 7a944b2bacce41914a50143cd7c3e4cf551a5852 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 19:48:36 +0900 Subject: [PATCH 60/76] docs: record sealed dispatch policy and realistic stale-response evidence Commit-Message-Assisted-by: Codex (via Codex) Signed-off-by: Seongho Bae --- CHANGELOG.md | 2 + docs/API_CONTRACT.md | 6 +++ docs/TRD.md | 5 +++ .../0107-browser-protocol-adapter-strategy.md | 26 ++++++++++++ docs/doctoring.md | 41 +++++++++++++++++++ 5 files changed, 80 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f7c768c4..7fdbe37a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Changed +- Prevent an earlier browser reply from completing a later request that reuses its number. Typed browser requests now use increasing numbers on each connection, and low-level protocol traffic uses a separate connection. + - Keep navigation subscriptions and accepted navigation events attached to their original browser state, so a replacement state with matching local identifiers cannot send a request or change another document. - Reject navigation-subscription replies and events received on a different connection, even when their session and request details match. Rejected messages leave the original request and document unchanged, so the original connection can still complete its work. - Prevent an unsent navigation subscription from borrowing another request's successful response, including when separate sessions reuse the same local numbers. Re-registering a completed request number without sending a new request cannot recreate its consumed subscription. diff --git a/docs/API_CONTRACT.md b/docs/API_CONTRACT.md index 4ac62061d..e7a5f40f4 100644 --- a/docs/API_CONTRACT.md +++ b/docs/API_CONTRACT.md @@ -364,6 +364,12 @@ Protocol transport authentication proves the calling human/workload identity. Au Maps session/user-context/browsing-context capabilities into scoped OriginWeave identities. BiDi element/context identifiers remain adapter-local. +Non-shipped #264 source requires typed request identifiers to increase across a connection's entire +lifetime, including after completion, retirement or replacement of local correlation state. Raw text +uses a separate connection; Pong and reading responses do not reset this rule. Callers migrating from +the earlier draft must stop reusing request numbers and use the consuming handoff for raw sockets. +This local correlation policy does not authenticate the browser or prove an action's visible result. + ### Chrome DevTools Protocol Maps selected versioned Network/DOMSnapshot/Accessibility/Tracing and other explicitly reviewed domains. `Runtime.evaluate` is not automatically mapped to standard `browser.act`. diff --git a/docs/TRD.md b/docs/TRD.md index 3e8030012..dd1b9d80c 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -319,6 +319,11 @@ Generic network evidence retains bounded names and canonical locators while valu **Planned.** WebDriver BiDi is an evolving W3C adapter contract. Its session/user-context/browsing-context identifiers are translated into OriginWeave-scoped internal identities. Core lifetime authority is already Implemented; active PR #40 is non-shipped registry implementation evidence. +The non-shipped #264 dispatch refinement retains exclusive connection ownership and strictly +increasing typed command IDs so old local replies cannot complete a later reused request. Raw-text +work uses a separate connection. This local socket evidence does not change the adapter's Planned +status or satisfy real-browser, protected-foundation or release acceptance. + ### Chrome DevTools Protocol **Planned.** The **Chrome DevTools Protocol** supplies Chromium-specific observation, diagnostics and experimental capabilities. OriginWeave binds supported protocol versions and does not expose unrestricted Runtime evaluation as a normal agent action. diff --git a/docs/adr/0107-browser-protocol-adapter-strategy.md b/docs/adr/0107-browser-protocol-adapter-strategy.md index b64723be1..c699b9488 100644 --- a/docs/adr/0107-browser-protocol-adapter-strategy.md +++ b/docs/adr/0107-browser-protocol-adapter-strategy.md @@ -36,6 +36,32 @@ MCP version negotiation is independent of the OriginWeave Protocol version. As o ## Consequences +### Proposed refinement: sealed command dispatch history (2026-09-06) + +In the context of successive browser commands on one verified connection, facing retained or +buffered replies completing a later command with the same wire identifier, we decided for one +connection-owned strictly increasing typed-command namespace and mutually exclusive raw-text and +typed-command lanes, and against receive-order counters, consuming response wrappers alone, or a +resettable correlation-table ledger, to prevent ambiguity between local dispatches with constant +memory, accepting that callers must choose increasing IDs and use separate connections for raw text. + +WebDriver BiDi permits identifier reuse; this is an OriginWeave local policy, not a protocol mandate. +The first typed ID may be zero and the JavaScript-safe maximum is usable once; exhaustion requires +an explicit new connection rather than wraparound. Pong frames and message reads preserve the mode +and last ID. All typed senders use the shared frame owner. Raw text cannot precede typed dispatch or +be inserted while typed responses are pending. The nonconsuming TCP stream borrow is removed because +a cloned handle could write outside that owner; the consuming raw-stream handoff remains available +but cannot reconstruct an upgradeable connection. No generic HTTP/TLS stream contract changes. + +The real locally-revoked opening-write test moves into the connection owner's unit module so it can +retain its OS-socket failure checks without publishing a cloneable socket. Preflight failures still +retire only the newly registered command; ambiguous I/O failures retain pending correlation. A +bounded tombstone set would eventually force arbitrary eviction or reconnection, while an unbounded +set creates lifetime memory growth. Receive ordering cannot distinguish a buffered old reply first +read after resend, and consuming wrappers misses retirement before parsing. This proposal does not +authenticate the remote browser, reject invented peer replies, or prove navigation causality. +The adapter remains non-shipped, and this refinement remains Proposed pending governance and gates. + ### Proposed refinement: original registry identity (2026-09-06) In the context of a connection-bound navigation subscription whose local session/context numbers diff --git a/docs/doctoring.md b/docs/doctoring.md index 1072b9ca5..2882fc771 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -4,6 +4,47 @@ This document records external evidence that changes OriginWeave architecture, t ## Decision trace +### Successive command responses and exclusive stream ownership + +Actual-socket regressions at `92fd0b07` send the same subscription command ID twice on one connection. +An old successful response consumes the second pending command and creates admission against its +new binding. At `15aea15e`, the same four lifecycles show an old error retiring the new command: +completed response reuse, unparsed response after explicit retirement, replacement correlation, and +an old response buffered until after the second send. The server validates both emitted commands, +withholds the new reply until old-response parsing, and is joined before assertions. These failures +are separate from the earlier original-registry repair and its coverage evidence. + +Independent review also traced public raw text writes and a pre-upgrade TCP stream borrow. A +compile-fail test at `15aea15e` unexpectedly compiles because callers can retain a socket clone +before handing the original connection to the WebSocket owner. The Rust documentation states that +cloned handles share the same stream and socket options. Removing the nonconsuming borrow closes +that bypass; consuming raw handoff remains, without any public reconstruction path. The original +real locally-revoked socket test is preserved inside the connection owner rather than deleted. + +The bounded repair uses the existing frame owner, one private text-lane state and the last typed +command ID. IDs must strictly increase across all five typed senders on a connection. Raw and typed +text cannot mix in either order; Pong and received messages preserve dispatch history. Three further +real-socket regressions failed on forbidden wire bytes before this repair and now exercise both lane +directions and cross-kind reuse after out-of-order replies, Pong, reader moves and a new correlation +table. Masking-key fixtures now seed Pong; the ambiguous pointer-write probe uses increasing IDs so +a local freshness rejection cannot masquerade as a socket failure. Existing preflight retirement and +ambiguous-write retention are unchanged. No new dependency, parser, unbounded ledger or gate is added. + +W3C's current Editor's Draft permits command IDs to recur; it does not require recurrence. The stricter +local policy trades caller flexibility for unambiguous replies to previous local dispatches. A receive +counter cannot identify old replies first read after resend; a by-value wrapper cannot cover retirement +before parsing; a replaceable correlation table cannot own connection-lifetime history. See Proposed +ADR 0107 for consequences and alternatives. Neither local socket checks nor GitHub page inspection +prove browser authentication, unsolicited-peer response truth, navigation causality, unsubscribe +lifetime, protected integration or release readiness. Rust 1.97.1 remains the build baseline; the online +standard-library reference currently describes 1.98.1. + +Rust Project Developers. (2026). *TcpStream::try_clone*. Rust standard library documentation. +Retrieved September 6, 2026, from https://doc.rust-lang.org/std/net/struct.TcpStream.html#method.try_clone + +World Wide Web Consortium. (2026, September 3). *WebDriver BiDi: Commands* [Editor's Draft]. +Retrieved September 6, 2026, from https://w3c.github.io/webdriver-bidi/#commands + ### Original registry ownership through subscribed navigation Four real-socket regressions at `b3ffeac9` on published #264 `2a9fdc54` fail at distinct From 6009682a22b6f4aab77785a85f2cc65dad53080f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 19:49:32 +0900 Subject: [PATCH 61/76] test: synchronize buffered response before rejected resend Wait for server emission without reading the response so the buffered replay case cannot race client closure. Preserve the buffered-message threat and no-second-write assertions. Commit-Message-Assisted-by: Codex (via Codex) Signed-off-by: Seongho Bae --- ...bdriver_bidi_navigation_committed_subscription_admission.rs | 3 +++ .../tests/webdriver_bidi_websocket_handshake.rs | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs index 7ea4dc72f..0c252f5db 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs @@ -226,6 +226,7 @@ fn reject_stale_response_after_actual_resend(lifecycle: &str) -> Result<(), Box< let listener = TcpListener::bind(("127.0.0.1", 0))?; let local_addr = listener.local_addr()?; let (release_sender, release_receiver) = std::sync::mpsc::channel(); + let (response_sender, response_receiver) = std::sync::mpsc::channel(); let server = thread::spawn(move || -> io::Result { let (mut stream, _) = listener.accept()?; read_opening_request(&mut stream)?; @@ -233,6 +234,7 @@ fn reject_stale_response_after_actual_resend(lifecycle: &str) -> Result<(), Box< let expected = br#"{"id":7,"method":"session.subscribe","params":{"events":["browsingContext.navigationCommitted"],"contexts":["context-a"]}}"#; assert_eq!(read_masked_text_frame(&mut stream)?, expected); write_text_frame(&mut stream, first_payload)?; + response_sender.send(()).map_err(io::Error::other)?; match read_masked_text_frame(&mut stream) { Err(error) if error.kind() == io::ErrorKind::UnexpectedEof => Ok(false), Err(error) => Err(error), @@ -262,6 +264,7 @@ fn reject_stale_response_after_actual_resend(lifecycle: &str) -> Result<(), Box< Duration::from_millis(500), )?; let mut old_message = None; + response_receiver.recv_timeout(Duration::from_secs(2))?; if lifecycle != "buffered" { let (next_stream, message) = next_text(established)?; established = next_stream; diff --git a/crates/originweave-network/tests/webdriver_bidi_websocket_handshake.rs b/crates/originweave-network/tests/webdriver_bidi_websocket_handshake.rs index 742bb5028..93550245a 100644 --- a/crates/originweave-network/tests/webdriver_bidi_websocket_handshake.rs +++ b/crates/originweave-network/tests/webdriver_bidi_websocket_handshake.rs @@ -4,7 +4,6 @@ use originweave_core::WebDriverBiDiWebSocketEndpoint; use originweave_network::{ WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakeError, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketOpeningWriteError, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; From e6c02cfabf3edf02cd251e97ed09394c46a01309 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 19:51:01 +0900 Subject: [PATCH 62/76] test: observe zero-byte closure of unread buffered responses A rejected resend drops a socket with unread data, so macOS may reset the peer. Peek for the first additional byte before parsing; only EOF or reset before any byte satisfies non-emission. Keep real second-command parsing for the vulnerable branch. Commit-Message-Assisted-by: Codex (via Codex) Signed-off-by: Seongho Bae --- ...r_bidi_navigation_committed_subscription_admission.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs index 0c252f5db..ebdcbdfe2 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs @@ -235,10 +235,13 @@ fn reject_stale_response_after_actual_resend(lifecycle: &str) -> Result<(), Box< assert_eq!(read_masked_text_frame(&mut stream)?, expected); write_text_frame(&mut stream, first_payload)?; response_sender.send(()).map_err(io::Error::other)?; - match read_masked_text_frame(&mut stream) { - Err(error) if error.kind() == io::ErrorKind::UnexpectedEof => Ok(false), + let mut next_byte = [0_u8; 1]; + match stream.peek(&mut next_byte) { + Ok(0) => Ok(false), + Err(error) if error.kind() == io::ErrorKind::ConnectionReset => Ok(false), Err(error) => Err(error), - Ok(second_command) => { + Ok(_) => { + let second_command = read_masked_text_frame(&mut stream)?; assert_eq!(second_command, expected); release_receiver .recv_timeout(Duration::from_secs(2)) From 805051527cf95e14ba126c9dd3159db86d190224 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 20:07:51 +0900 Subject: [PATCH 63/76] test(network): exercise the public opening deadline boundary Retain the private real revoked-socket case and cover error propagation through the public library without restoring a cloneable socket alias. Commit-Message-Assisted-by: Codex (via Codex) Signed-off-by: Seongho Bae --- .../webdriver_bidi_websocket_handshake.rs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/crates/originweave-network/tests/webdriver_bidi_websocket_handshake.rs b/crates/originweave-network/tests/webdriver_bidi_websocket_handshake.rs index 93550245a..22fb9c155 100644 --- a/crates/originweave-network/tests/webdriver_bidi_websocket_handshake.rs +++ b/crates/originweave-network/tests/webdriver_bidi_websocket_handshake.rs @@ -145,6 +145,39 @@ fn handshake_errors_render_actionable_fail_closed_messages() { ); } +#[test] +fn opening_request_cannot_outlive_a_one_nanosecond_deadline() +-> Result<(), Box> { + use std::io::{self, Read}; + + use originweave_network::WebDriverBiDiWebSocketOpeningWriteError; + + 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()?; + stream.set_read_timeout(Some(Duration::from_secs(2)))?; + let mut received = Vec::new(); + stream.read_to_end(&mut received)?; + Ok(received) + }); + let connection = connect(&format!("ws://{local_addr}/session/{SESSION_ID}")); + let key = WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?; + let plan = WebDriverBiDiWebSocketHandshakePlan::new(connection, key)?; + let request = plan.request_bytes().to_vec(); + let error = plan.write_opening_request(Duration::from_nanos(1)).err(); + let received = server + .join() + .map_err(|_| io::Error::other("opening deadline server panicked"))??; + + assert!(matches!( + error, + Some(WebDriverBiDiWebSocketOpeningWriteError::WriteDeadlineExceeded { .. }) + )); + assert!(request.starts_with(&received)); + Ok(()) +} + #[test] fn handshake_plan_rejects_tls_required_stream_and_noncanonical_client_keys() { let invalid_length = WebDriverBiDiWebSocketClientKey::new("dGhlIHNhbXBsZSBub25jZQ="); From 2c45cea8e2419c801430fdae640b0207fa6f0bb3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 20:32:52 +0900 Subject: [PATCH 64/76] test(network): expose retained admission after unsubscribe construction Use an actual sent subscription and received event to prove that borrowing its receipt for teardown leaves active event admission constructible. Commit-Message-Assisted-by: Codex (via Codex) Signed-off-by: Seongho Bae --- ...navigation_committed_subscription_admission.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs index ebdcbdfe2..4a5797eac 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs @@ -217,6 +217,21 @@ fn establish_connection( .read_opening_response(Duration::from_millis(500))?) } +#[test] +fn teardown_construction_cannot_leave_subscription_event_admission_available() +-> Result<(), Box> { + let mut registry = BrowserAuthorityRegistry::new(); + let session = registry.register_session(SESSION_ID)?; + let context = registry.register_context(session, CONTEXT_ID)?; + let (receipt, binding, event) = receive_subscription_result(®istry, session, context, 7)?; + let _teardown = + originweave_network::WebDriverBiDiNavigationCommittedUnsubscribeCommand::new(8, &receipt)?; + let mut admission = + WebDriverBiDiNavigationCommittedSubscriptionAdmission::new(receipt, binding, ®istry)?; + assert!(admission.admit(&event, ®istry, EXPECTED_URL).is_err()); + Ok(()) +} + fn reject_stale_response_after_actual_resend(lifecycle: &str) -> Result<(), Box> { for first_payload in [ br#"{"type":"error","id":7,"error":"invalid argument","message":"old rejection"}"# From ce6f6fd4acab0a435a80a621c3c57a8abddbd327 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 20:37:13 +0900 Subject: [PATCH 65/76] test(network): expose unsubscribe transport provenance gaps Commit-Message-Assisted-by: Codex (via Codex) Signed-off-by: Seongho Bae --- ...gation_unsubscribe_transport_provenance.rs | 303 ++++++++++++++++++ 1 file changed, 303 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_navigation_unsubscribe_transport_provenance.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_unsubscribe_transport_provenance.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_unsubscribe_transport_provenance.rs new file mode 100644 index 000000000..73a6cc3d7 --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_unsubscribe_transport_provenance.rs @@ -0,0 +1,303 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{SocketAddr, TcpListener, TcpStream}, + sync::mpsc, + thread, + time::Duration, +}; + +use originweave_core::{BrowserAuthorityRegistry, WebDriverBiDiWebSocketEndpoint}; +use originweave_network::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandKind, + WebDriverBiDiConnectionMessageRead, WebDriverBiDiNavigationCommittedSubscriptionCommand, + WebDriverBiDiNavigationCommittedSubscriptionResult, + WebDriverBiDiNavigationCommittedUnsubscribeCommand, + WebDriverBiDiNavigationCommittedUnsubscribeResponseError, + WebDriverBiDiNavigationCommittedUnsubscribeResult, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketEstablished, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, + WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, + WebDriverBiDiWebSocketMessageReader, WebDriverBiDiWebSocketTextMessage, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; +const CONTEXT_ID: &str = "context-a"; +const FRAME_TIMEOUT: Duration = Duration::from_millis(500); +const SUBSCRIBE_MASK: [u8; 4] = [1, 2, 3, 4]; +const UNSUBSCRIBE_MASK: [u8; 4] = [5, 6, 7, 8]; +const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; +const SUBSCRIBE_COMMAND: &[u8] = br#"{"id":7,"method":"session.subscribe","params":{"events":["browsingContext.navigationCommitted"],"contexts":["context-a"]}}"#; +const SUBSCRIBE_RESPONSE: &[u8] = + br#"{"type":"success","id":7,"result":{"subscription":"subscription-a"}}"#; +const UNSUBSCRIBE_COMMAND: &[u8] = + br#"{"id":8,"method":"session.unsubscribe","params":{"subscriptions":["subscription-a"]}}"#; +const UNSUBSCRIBE_SUCCESS: &[u8] = br#"{"type":"success","id":8,"result":{}}"#; +const UNSUBSCRIBE_ERROR: &[u8] = + br#"{"type":"error","id":8,"error":"invalid argument","message":"foreign unsubscribe error"}"#; + +type TestResult = Result>; + +fn accept_websocket(listener: TcpListener) -> io::Result { + let (mut stream, _) = listener.accept()?; + stream.set_read_timeout(Some(Duration::from_secs(2)))?; + stream.set_write_timeout(Some(Duration::from_secs(2)))?; + let mut request = Vec::new(); + while !request.ends_with(b"\r\n\r\n") { + let mut byte = [0_u8]; + stream.read_exact(&mut byte)?; + request.push(byte[0]); + } + stream.write_all(OPENING_RESPONSE)?; + Ok(stream) +} + +fn receive_exact_command( + stream: &mut TcpStream, + payload: &[u8], + masking_key: [u8; 4], +) -> io::Result<()> { + let mut expected = vec![0x81]; + if payload.len() <= 125 { + expected.push(0x80 | payload.len() as u8); + } else { + expected.push(0xfe); + expected.extend_from_slice(&(payload.len() as u16).to_be_bytes()); + } + expected.extend_from_slice(&masking_key); + expected.extend( + payload + .iter() + .enumerate() + .map(|(index, byte)| byte ^ masking_key[index % masking_key.len()]), + ); + let mut received = vec![0_u8; expected.len()]; + stream.read_exact(&mut received)?; + if received != expected { + return Err(io::Error::other(format!( + "client command differs from literal expected wire bytes: {received:?}" + ))); + } + Ok(()) +} + +fn write_text_frame(stream: &mut TcpStream, payload: &[u8]) -> io::Result<()> { + if payload.len() > 125 { + return Err(io::Error::other("fixture reply exceeded short-frame encoding")); + } + stream.write_all(&[0x81, payload.len() as u8])?; + stream.write_all(payload) +} + +fn read_until_closed(mut stream: TcpStream) -> io::Result> { + let mut received = Vec::new(); + stream.read_to_end(&mut received)?; + Ok(received) +} + +fn join_server(server: thread::JoinHandle>>) -> TestResult> { + Ok(server + .join() + .map_err(|_| io::Error::other("unsubscribe transport fixture server panicked"))??) +} + +fn establish(local_addr: SocketAddr) -> TestResult { + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? + .correlate_session_id(SESSION_ID)? + .into_explicit_connect_target()?; + let connection = + WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; + Ok(WebDriverBiDiWebSocketHandshakePlan::new( + connection, + WebDriverBiDiWebSocketClientKey::new("dGhlIHNhbXBsZSBub25jZQ==")?, + )? + .write_opening_request(FRAME_TIMEOUT)? + .read_opening_response(FRAME_TIMEOUT)?) +} + +fn subscribe( + established: WebDriverBiDiWebSocketEstablished, + correlation: &mut WebDriverBiDiCommandCorrelation, +) -> TestResult<( + WebDriverBiDiWebSocketEstablished, + WebDriverBiDiNavigationCommittedSubscriptionResult, +)> { + let mut registry = BrowserAuthorityRegistry::new(); + let session = registry.register_session(SESSION_ID)?; + let context = registry.register_context(session, CONTEXT_ID)?; + let command = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( + 7, ®istry, session, context, CONTEXT_ID, + )?; + let established = command.send( + ®istry, + established, + correlation, + WebDriverBiDiWebSocketMaskKey::new(SUBSCRIBE_MASK), + FRAME_TIMEOUT, + )?; + match WebDriverBiDiWebSocketMessageReader::new(established).read_next(FRAME_TIMEOUT)? { + WebDriverBiDiConnectionMessageRead::Text { + established, + message, + } => Ok(( + established, + WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( + &message, + correlation, + )?, + )), + other => Err(io::Error::other(format!( + "expected actual connection-bound subscription receipt, got {other:?}" + )) + .into()), + } +} + +fn read_unsubscribe_text( + established: WebDriverBiDiWebSocketEstablished, +) -> TestResult<( + WebDriverBiDiWebSocketEstablished, + WebDriverBiDiWebSocketTextMessage, +)> { + let (established, frame) = established.read_frame(FRAME_TIMEOUT)?; + match WebDriverBiDiWebSocketMessageAssembler::new().push_frame(frame)? { + WebDriverBiDiWebSocketMessageAssembly::Text(message) => Ok((established, message)), + other => Err(io::Error::other(format!( + "expected actual unsubscribe response text frame, got {other:?}" + )) + .into()), + } +} + +#[test] +fn subscription_receipt_cannot_dispatch_unsubscribe_on_another_connection() -> TestResult<()> { + let sent_listener = TcpListener::bind(("127.0.0.1", 0))?; + let sent_addr = sent_listener.local_addr()?; + let sent_server = thread::spawn(move || { + let mut stream = accept_websocket(sent_listener)?; + receive_exact_command(&mut stream, SUBSCRIBE_COMMAND, SUBSCRIBE_MASK)?; + write_text_frame(&mut stream, SUBSCRIBE_RESPONSE)?; + read_until_closed(stream) + }); + let foreign_listener = TcpListener::bind(("127.0.0.1", 0))?; + let foreign_addr = foreign_listener.local_addr()?; + let foreign_server = thread::spawn(move || read_until_closed(accept_websocket(foreign_listener)?)); + + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + correlation.register_command_for(99, WebDriverBiDiCommandKind::SessionStatus)?; + let (sent_established, subscription) = subscribe(establish(sent_addr)?, &mut correlation)?; + let before_send = correlation.outstanding_count(); + let result = WebDriverBiDiNavigationCommittedUnsubscribeCommand::new(8, &subscription)?.send( + establish(foreign_addr)?, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new(UNSUBSCRIBE_MASK), + FRAME_TIMEOUT, + ); + let rejected = result.is_err(); + let after_send = correlation.outstanding_count(); + drop(result); + drop(sent_established); + let sent_extra = join_server(sent_server)?; + let foreign_bytes = join_server(foreign_server)?; + let unrelated_retained = correlation + .retire_command_for(99, WebDriverBiDiCommandKind::SessionStatus) + .is_ok(); + let unsubscribe_registered = correlation + .retire_command_for(8, WebDriverBiDiCommandKind::NavigationCommittedUnsubscribe) + .is_ok(); + + assert!(sent_extra.is_empty(), "unexpected A bytes: {sent_extra:?}"); + assert!( + foreign_bytes.is_empty(), + "foreign connection emitted unsubscribe wire bytes: {foreign_bytes:?}" + ); + assert!(rejected, "a receipt from A must not authorize dispatch on B"); + assert_eq!((before_send, after_send), (1, 1)); + assert!(unrelated_retained); + assert!(!unsubscribe_registered); + Ok(()) +} + +fn foreign_unsubscribe_reply_preserves_original_command(foreign_reply: &'static [u8]) -> TestResult<()> { + let sent_listener = TcpListener::bind(("127.0.0.1", 0))?; + let sent_addr = sent_listener.local_addr()?; + let (release_sender, release_receiver) = mpsc::sync_channel::<()>(0); + let sent_server = thread::spawn(move || { + let mut stream = accept_websocket(sent_listener)?; + receive_exact_command(&mut stream, SUBSCRIBE_COMMAND, SUBSCRIBE_MASK)?; + write_text_frame(&mut stream, SUBSCRIBE_RESPONSE)?; + receive_exact_command(&mut stream, UNSUBSCRIBE_COMMAND, UNSUBSCRIBE_MASK)?; + release_receiver + .recv_timeout(Duration::from_secs(2)) + .map_err(|error| io::Error::other(format!("original response barrier failed: {error}")))?; + write_text_frame(&mut stream, UNSUBSCRIBE_SUCCESS)?; + read_until_closed(stream) + }); + let foreign_listener = TcpListener::bind(("127.0.0.1", 0))?; + let foreign_addr = foreign_listener.local_addr()?; + let foreign_server = thread::spawn(move || { + let mut stream = accept_websocket(foreign_listener)?; + write_text_frame(&mut stream, foreign_reply)?; + read_until_closed(stream) + }); + + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + correlation.register_command_for(99, WebDriverBiDiCommandKind::SessionStatus)?; + let (sent_established, subscription) = subscribe(establish(sent_addr)?, &mut correlation)?; + let sent_established = WebDriverBiDiNavigationCommittedUnsubscribeCommand::new(8, &subscription)? + .send( + sent_established, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new(UNSUBSCRIBE_MASK), + FRAME_TIMEOUT, + )?; + let before_foreign = correlation.outstanding_count(); + let (foreign_established, foreign_message) = read_unsubscribe_text(establish(foreign_addr)?)?; + let foreign_result = WebDriverBiDiNavigationCommittedUnsubscribeResult::parse_and_correlate( + &foreign_message, + &mut correlation, + ); + let after_foreign = correlation.outstanding_count(); + + release_sender.send(())?; + let (sent_established, original_message) = read_unsubscribe_text(sent_established)?; + let original_result = WebDriverBiDiNavigationCommittedUnsubscribeResult::parse_and_correlate( + &original_message, + &mut correlation, + ); + let after_original = correlation.outstanding_count(); + drop(sent_established); + drop(foreign_established); + let sent_extra = join_server(sent_server)?; + let foreign_bytes = join_server(foreign_server)?; + let unrelated_retained = correlation + .retire_command_for(99, WebDriverBiDiCommandKind::SessionStatus) + .is_ok(); + + assert!(sent_extra.is_empty(), "unexpected A bytes: {sent_extra:?}"); + assert!(foreign_bytes.is_empty(), "unexpected B bytes: {foreign_bytes:?}"); + assert_eq!( + (before_foreign, after_foreign, after_original), + (2, 2, 1), + "foreign response must preserve pending A until its genuine reply; foreign={foreign_result:?}, original={original_result:?}" + ); + assert!(matches!( + foreign_result, + Err(WebDriverBiDiNavigationCommittedUnsubscribeResponseError::Correlation { .. }) + )); + assert_eq!(original_result?.command_id(), 8); + assert!(unrelated_retained); + assert_eq!(correlation.outstanding_count(), 0); + Ok(()) +} + +#[test] +fn foreign_unsubscribe_success_preserves_pending_until_original_reply() -> TestResult<()> { + foreign_unsubscribe_reply_preserves_original_command(UNSUBSCRIBE_SUCCESS) +} + +#[test] +fn foreign_unsubscribe_error_preserves_pending_until_original_reply() -> TestResult<()> { + foreign_unsubscribe_reply_preserves_original_command(UNSUBSCRIBE_ERROR) +} From 267b8c33a13ec4850d82e7e6374eed1397c3562b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 20:42:04 +0900 Subject: [PATCH 66/76] fix(network): consume and bind subscription teardown authority Commit-Message-Assisted-by: Codex (via Codex) Signed-off-by: Seongho Bae --- CHANGELOG.md | 2 + ...gation_committed_subscription_admission.rs | 6 +-- ...r_bidi_navigation_committed_unsubscribe.rs | 47 +++++++++++++++++-- ...vigation_committed_unsubscribe_response.rs | 9 ++-- ...gation_committed_subscription_admission.rs | 15 ------ ...r_bidi_navigation_committed_unsubscribe.rs | 16 +++---- docs/API_CONTRACT.md | 5 ++ .../0107-browser-protocol-adapter-strategy.md | 24 ++++++++++ docs/doctoring.md | 25 ++++++++++ .../webdriver-bidi-navigation-unsubscribe.md | 11 +++++ 10 files changed, 123 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fdbe37a8..d1ddeaa5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Changed +- Keep subscription shutdown on its original connection and reject replies from another connection without losing the pending request. Beginning shutdown ends local event admission; a failed shutdown requires a new subscription before admission resumes. + - Prevent an earlier browser reply from completing a later request that reuses its number. Typed browser requests now use increasing numbers on each connection, and low-level protocol traffic uses a separate connection. - Keep navigation subscriptions and accepted navigation events attached to their original browser state, so a replacement state with matching local identifiers cannot send a request or change another document. diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_admission.rs b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_admission.rs index c84df6577..cdfe8f19a 100644 --- a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_admission.rs +++ b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_admission.rs @@ -247,8 +247,8 @@ impl WebDriverBiDiNavigationCommittedSubscriptionAdmission { /// Consumption deliberately ends local event admission before the unsubscribe command can be /// emitted. If later transport or remote teardown fails, callers must explicitly establish a new /// typed subscription before admitting more events; ambiguous teardown never restores authority. - /// This handoff does not claim that the existing unsubscribe command or its receipt is bound to - /// the subscription connection; unsubscribe transport provenance remains a separate boundary. + /// Teardown retains the subscription connection identity and accepts its acknowledgment only + /// from that same connection. Previously admitted observations are not retroactively revoked. pub fn into_unsubscribe( self, command_id: u64, @@ -256,7 +256,7 @@ impl WebDriverBiDiNavigationCommittedSubscriptionAdmission { WebDriverBiDiNavigationCommittedUnsubscribeCommand, WebDriverBiDiNavigationCommittedUnsubscribeCommandError, > { - WebDriverBiDiNavigationCommittedUnsubscribeCommand::new(command_id, &self.subscription) + WebDriverBiDiNavigationCommittedUnsubscribeCommand::new(command_id, self.subscription) } } diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_committed_unsubscribe.rs b/crates/originweave-network/src/webdriver_bidi_navigation_committed_unsubscribe.rs index d3aeacd4c..2a6845c99 100644 --- a/crates/originweave-network/src/webdriver_bidi_navigation_committed_unsubscribe.rs +++ b/crates/originweave-network/src/webdriver_bidi_navigation_committed_unsubscribe.rs @@ -1,5 +1,6 @@ use std::{error::Error, fmt, time::Duration}; +use crate::webdriver_bidi_connection::WebDriverBiDiConnectionGeneration; use crate::webdriver_bidi_websocket_frame::validate_frame_timeout; use crate::{ MAX_WEBDRIVER_BIDI_JS_UINT, WebDriverBiDiCommandCorrelation, @@ -16,10 +17,11 @@ const SESSION_UNSUBSCRIBE_METHOD: &str = "session.unsubscribe"; /// `session.subscribe` response boundary. It cannot introduce arbitrary event names, contexts, /// user contexts, or ambient subscription identifiers. Writing the frame does not prove remote /// teardown; callers must admit and correlate the later protocol response separately. -#[derive(Clone, Eq, PartialEq)] +#[derive(Eq, PartialEq)] pub struct WebDriverBiDiNavigationCommittedUnsubscribeCommand { command_id: u64, subscription_id: String, + connection_generation: WebDriverBiDiConnectionGeneration, } impl fmt::Debug for WebDriverBiDiNavigationCommittedUnsubscribeCommand { @@ -33,10 +35,28 @@ impl fmt::Debug for WebDriverBiDiNavigationCommittedUnsubscribeCommand { } impl WebDriverBiDiNavigationCommittedUnsubscribeCommand { - /// Construct one unsubscribe command from an already validated typed subscription receipt. + /// Consume one validated receipt, ending its availability for local event admission. + /// + /// Even an invalid command identifier consumes the receipt; failure never restores admission. + /// A receipt transferred to teardown cannot be used to construct another admission owner. + /// + /// ```compile_fail,E0382 + /// use originweave_core::BrowserAuthorityRegistry; + /// use originweave_network::{WebDriverBiDiNavigationCommittedSubscriptionResult, + /// WebDriverBiDiNavigationCommittedSubscriptionBinding, + /// WebDriverBiDiNavigationCommittedSubscriptionAdmission, + /// WebDriverBiDiNavigationCommittedUnsubscribeCommand}; + /// fn retained_admission(receipt: WebDriverBiDiNavigationCommittedSubscriptionResult, + /// binding: WebDriverBiDiNavigationCommittedSubscriptionBinding, + /// registry: &BrowserAuthorityRegistry) { + /// let _teardown = WebDriverBiDiNavigationCommittedUnsubscribeCommand::new(8, receipt); + /// let _admission = WebDriverBiDiNavigationCommittedSubscriptionAdmission::new( + /// receipt, binding, registry); + /// } + /// ``` pub fn new( command_id: u64, - subscription: &WebDriverBiDiNavigationCommittedSubscriptionResult, + subscription: WebDriverBiDiNavigationCommittedSubscriptionResult, ) -> Result { if command_id > MAX_WEBDRIVER_BIDI_JS_UINT { return Err( @@ -49,6 +69,7 @@ impl WebDriverBiDiNavigationCommittedUnsubscribeCommand { Ok(Self { command_id, subscription_id: subscription.subscription_id().to_owned(), + connection_generation: subscription.connection_generation, }) } @@ -79,10 +100,14 @@ impl WebDriverBiDiNavigationCommittedUnsubscribeCommand { validate_frame_timeout(frame_timeout).map_err(|source| { WebDriverBiDiNavigationCommittedUnsubscribeCommandError::FrameWrite { source } })?; + if self.connection_generation != established.transport_evidence().connection_generation() { + return Err(WebDriverBiDiNavigationCommittedUnsubscribeCommandError::SubscriptionConnectionMismatch); + } correlation - .register_command_for( + .register_command_for_connection( self.command_id, WebDriverBiDiCommandKind::NavigationCommittedUnsubscribe, + self.connection_generation, ) .map_err(|source| { WebDriverBiDiNavigationCommittedUnsubscribeCommandError::Correlation { source } @@ -120,6 +145,8 @@ fn map_frame_failure( /// Fail-closed errors while constructing or sending one typed `session.unsubscribe` command. #[derive(Debug)] pub enum WebDriverBiDiNavigationCommittedUnsubscribeCommandError { + /// The supplied connection is not the one that issued the consumed subscription receipt. + SubscriptionConnectionMismatch, /// The requested command identifier is outside WebDriver BiDi's `js-uint` range. CommandIdOutOfRange { /// Rejected command identifier. @@ -142,6 +169,9 @@ pub enum WebDriverBiDiNavigationCommittedUnsubscribeCommandError { impl fmt::Display for WebDriverBiDiNavigationCommittedUnsubscribeCommandError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + Self::SubscriptionConnectionMismatch => { + formatter.write_str("WebDriver BiDi subscription belongs to a different connection") + } Self::CommandIdOutOfRange { .. } => formatter.write_str( "WebDriver BiDi session.unsubscribe command id is outside the js-uint range", ), @@ -157,7 +187,7 @@ impl fmt::Display for WebDriverBiDiNavigationCommittedUnsubscribeCommandError { impl Error for WebDriverBiDiNavigationCommittedUnsubscribeCommandError { fn source(&self) -> Option<&(dyn Error + 'static)> { match self { - Self::CommandIdOutOfRange { .. } => None, + Self::CommandIdOutOfRange { .. } | Self::SubscriptionConnectionMismatch => None, Self::Correlation { source } => Some(source), Self::FrameWrite { source } => Some(source), } @@ -239,6 +269,13 @@ mod tests { #[test] fn command_errors_have_stable_messages_and_typed_sources() { + let mismatch = + WebDriverBiDiNavigationCommittedUnsubscribeCommandError::SubscriptionConnectionMismatch; + assert_eq!( + mismatch.to_string(), + "WebDriver BiDi subscription belongs to a different connection" + ); + assert!(mismatch.source().is_none()); let range = WebDriverBiDiNavigationCommittedUnsubscribeCommandError::CommandIdOutOfRange { command_id: MAX_WEBDRIVER_BIDI_JS_UINT + 1, maximum_command_id: MAX_WEBDRIVER_BIDI_JS_UINT, diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_committed_unsubscribe_response.rs b/crates/originweave-network/src/webdriver_bidi_navigation_committed_unsubscribe_response.rs index 9334ea680..6ce65c1f1 100644 --- a/crates/originweave-network/src/webdriver_bidi_navigation_committed_unsubscribe_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_navigation_committed_unsubscribe_response.rs @@ -3,7 +3,7 @@ use std::{error::Error, fmt}; use crate::{ WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, WebDriverBiDiCommandKind, WebDriverBiDiCorrelatedResponseOutcome, WebDriverBiDiJsonEnvelope, - WebDriverBiDiJsonEnvelopeError, WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiJsonEnvelopeError, WebDriverBiDiReceivedTextMessage, }; /// Typed protocol acknowledgment for one correlated WebDriver BiDi `session.unsubscribe` command. @@ -27,16 +27,17 @@ impl WebDriverBiDiNavigationCommittedUnsubscribeResult { /// unknown ids, and responses for another command family fail closed without consuming the /// outstanding command. pub fn parse_and_correlate( - message: &WebDriverBiDiWebSocketTextMessage, + message: &WebDriverBiDiReceivedTextMessage, correlation: &mut WebDriverBiDiCommandCorrelation, ) -> Result { - let envelope = WebDriverBiDiJsonEnvelope::parse(message).map_err(|source| { + let envelope = WebDriverBiDiJsonEnvelope::parse(message.message()).map_err(|source| { WebDriverBiDiNavigationCommittedUnsubscribeResponseError::Envelope { source } })?; let completed = correlation - .correlate_response_for( + .correlate_response_for_connection( &envelope, WebDriverBiDiCommandKind::NavigationCommittedUnsubscribe, + message.connection_generation(), ) .map_err(|source| { WebDriverBiDiNavigationCommittedUnsubscribeResponseError::Correlation { source } diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs index 4a5797eac..ebdcbdfe2 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_admission.rs @@ -217,21 +217,6 @@ fn establish_connection( .read_opening_response(Duration::from_millis(500))?) } -#[test] -fn teardown_construction_cannot_leave_subscription_event_admission_available() --> Result<(), Box> { - let mut registry = BrowserAuthorityRegistry::new(); - let session = registry.register_session(SESSION_ID)?; - let context = registry.register_context(session, CONTEXT_ID)?; - let (receipt, binding, event) = receive_subscription_result(®istry, session, context, 7)?; - let _teardown = - originweave_network::WebDriverBiDiNavigationCommittedUnsubscribeCommand::new(8, &receipt)?; - let mut admission = - WebDriverBiDiNavigationCommittedSubscriptionAdmission::new(receipt, binding, ®istry)?; - assert!(admission.admit(&event, ®istry, EXPECTED_URL).is_err()); - Ok(()) -} - fn reject_stale_response_after_actual_resend(lifecycle: &str) -> Result<(), Box> { for first_payload in [ br#"{"type":"error","id":7,"error":"invalid argument","message":"old rejection"}"# diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe.rs index 5966340d6..34a0c34fa 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe.rs @@ -14,8 +14,7 @@ use originweave_network::{ WebDriverBiDiNavigationCommittedUnsubscribeCommand, WebDriverBiDiNavigationCommittedUnsubscribeResult, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler, - WebDriverBiDiWebSocketMessageAssembly, WebDriverBiDiWebSocketMessageReader, + WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageReader, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -174,7 +173,7 @@ fn validated_subscription_can_be_unsubscribed_without_losing_opaque_text() )?; assert_eq!(subscription.subscription_id(), "sub-\"\\\n\u{0001}-구독"); - let unsubscribe = WebDriverBiDiNavigationCommittedUnsubscribeCommand::new(8, &subscription)?; + let unsubscribe = WebDriverBiDiNavigationCommittedUnsubscribeCommand::new(8, subscription)?; assert_eq!(unsubscribe.command_id(), 8); let established = unsubscribe.send( established, @@ -184,13 +183,10 @@ fn validated_subscription_can_be_unsubscribed_without_losing_opaque_text() )?; assert_eq!(correlation.outstanding_count(), 1); - // Unsubscribe receive provenance is intentionally still the pre-existing raw-message boundary. - // This test keeps that separate limitation visible rather than treating the subscription repair - // as proof that teardown receipts are connection-bound. - let (_established, frame) = established.read_frame(Duration::from_millis(500))?; - let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); - let text = match assembler.push_frame(frame)? { - WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + let text = match WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + WebDriverBiDiConnectionMessageRead::Text { message, .. } => message, other => { return Err(io::Error::other(format!( "session.unsubscribe response produced unexpected assembly state: {other:?}" diff --git a/docs/API_CONTRACT.md b/docs/API_CONTRACT.md index e7a5f40f4..9319e2a31 100644 --- a/docs/API_CONTRACT.md +++ b/docs/API_CONTRACT.md @@ -12,6 +12,11 @@ OriginWeave needs one stable authority model even though different deployments m This is a contract baseline, not a claim that the complete network service or SDK is implemented on protected `main`. +The active #264 source proposal ends local navigation-event admission when shutdown begins and +keeps shutdown requests and replies on the original connection. Failure requires a new subscription +before admission resumes. This proposal is not shipped browser behavior; its scope and alternatives +are recorded in [ADR 0107](adr/0107-browser-protocol-adapter-strategy.md). + ## 2. Protocol design goals - transport-neutral; diff --git a/docs/adr/0107-browser-protocol-adapter-strategy.md b/docs/adr/0107-browser-protocol-adapter-strategy.md index c699b9488..f131ff3c3 100644 --- a/docs/adr/0107-browser-protocol-adapter-strategy.md +++ b/docs/adr/0107-browser-protocol-adapter-strategy.md @@ -36,6 +36,30 @@ MCP version negotiation is independent of the OriginWeave Protocol version. As o ## Consequences +### Proposed refinement: consuming subscription teardown (2026-09-06) + +In the context of ending a navigation subscription, facing a borrowed receipt that permits continued +event admission and teardown messages crossing connections, we decided for consuming the existing +non-cloneable receipt and retaining its connection identity through dispatch and acknowledgment, +and against shared revocation flags or matching session text alone, to close local admission before +teardown without duplicating lifecycle state, accepting that even construction or transport failure +requires a new subscription before local admission can resume. + +This source proposal reuses the existing received-message wrapper and connection-aware correlation +owner. It adds no registry, dependency, reconnection or mutable revocation service. A different +connection is rejected before pending-command insertion or wire emission; a foreign success or error +cannot consume the original pending command. An unrelated outstanding command remains untouched. +The receipt and teardown command cannot be cloned. Previously admitted observations are not revoked, +and acknowledgment does not prove that buffered events have drained or that browser cleanup occurred. + +The real-socket lifetime failure is retained in commit `2c45cea8`; its successor is the constructor's +`E0382` compile-fail example, because the repaired API makes the offending receipt reuse unrepresentable. +Commit `ce6f6fd4` records three separate real-socket transport failures. The existing admission-to-teardown +path and escaped-identifier round trip remain runtime checks. Shared flags would require extra checks +at every lifetime consumer while ownership already enforces this transition. Session or identifier +equality cannot distinguish two connections. Status remains Proposed, pending exact-head gates and +parent-first protected integration; this is not a browser-runtime acceptance or release decision. + ### Proposed refinement: sealed command dispatch history (2026-09-06) In the context of successive browser commands on one verified connection, facing retained or diff --git a/docs/doctoring.md b/docs/doctoring.md index 2882fc771..08e454448 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -4,6 +4,31 @@ This document records external evidence that changes OriginWeave architecture, t ## Decision trace +### Subscription teardown ownership and connection provenance + +The actual-socket regression at `2c45cea8` shows that constructing teardown from a borrowed receipt +still permits event admission. Three more failures at `ce6f6fd4` show 91 masked unsubscribe bytes +emitted on another connection, and foreign success/error replies consuming the original pending +command. Genuine original replies then fail as no longer outstanding. Both fixture servers are +joined before assertions; these failures are not timeouts or setup errors. + +The repair consumes the existing non-cloneable subscription receipt, stores its existing connection +identity in the teardown command, and reuses connection-aware registration and received-message +correlation. The lifetime regression becomes an `E0382` compile-fail example proving that the moved +receipt cannot create admission; its original failing runtime test remains in history. Existing +admission-to-teardown and exact-wire escaping checks retain runtime coverage. Invalid construction +also consumes local admission authority, and no failure restores it. Already admitted observations +are not revoked. Proposed ADR 0107 records why shared revocation flags add unnecessary state. + +W3C's current Editor's Draft defines by-ID removal against the session's known subscriptions and an +`EmptyResult` return type. Same-connection use and consuming local admission are stricter OriginWeave +policy, not additional requirements attributed to W3C. A protocol acknowledgment is not event-drain +or process-cleanup evidence. Browser authentication, navigation causality, hosted exact-head checks, +protected integration and release acceptance remain separate. Full repair verification is pending. + +World Wide Web Consortium. (2026, September 3). *WebDriver BiDi: The session.unsubscribe command* +[Editor's Draft]. Retrieved September 6, 2026, from https://w3c.github.io/webdriver-bidi/#command-session-unsubscribe + ### Successive command responses and exclusive stream ownership Actual-socket regressions at `92fd0b07` send the same subscription command ID twice on one connection. diff --git a/docs/traceability/webdriver-bidi-navigation-unsubscribe.md b/docs/traceability/webdriver-bidi-navigation-unsubscribe.md index b59e2f2ea..b7a83c923 100644 --- a/docs/traceability/webdriver-bidi-navigation-unsubscribe.md +++ b/docs/traceability/webdriver-bidi-navigation-unsubscribe.md @@ -19,6 +19,14 @@ OriginWeave uses only the by-id form and accepts the identifier only through its ## Decision +The current #264 source refinement consumes the non-cloneable subscription receipt instead of +borrowing it. The teardown command retains that receipt's private connection generation and cannot +be cloned. Its sender rejects a different connection before correlation or I/O; its response parser +accepts only the existing connection-owned received-message type and checks the registered generation +before consuming pending state. Generic public correlation registration cannot manufacture an ACK. +Receipt reuse for event admission is a compile-time ownership error, including after failed teardown +construction. Already admitted observations remain unchanged; this is not retroactive revocation. + 1. Give committed-navigation unsubscribe its own `WebDriverBiDiCommandKind::NavigationCommittedUnsubscribe` provenance rather than reusing the subscription kind or a generic correlation path. 2. Reject an invalid frame timeout before registering correlation or writing command bytes. 3. Register the exact unsubscribe kind immediately before the first possible remote side effect. @@ -36,6 +44,9 @@ OriginWeave uses only the by-id form and accepts the identifier only through its ## Executable evidence required on the exact head +- consuming receipt reuse rejected as `E0382`, with the original actual-socket RED retained at `2c45cea8`; +- foreign-connection dispatch emits no bytes or new correlation; +- foreign success and error preserve pending state until the genuine original-connection reply; - loopback TCP → RFC 6455 opening exchange → typed committed-navigation subscribe → opaque subscription receipt → by-id unsubscribe → exact correlated `EmptyResult` success; - opaque identifier escaping across quote, backslash, control and Unicode text without logging the identifier itself; - command-id range and duplicate outstanding-id rejection; From 69866d82f996fd597fffbb161780528ca750dd79 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 20:43:26 +0900 Subject: [PATCH 67/76] test(network): preserve teardown failures on their original connection Commit-Message-Assisted-by: Codex (via Codex) Signed-off-by: Seongho Bae --- ...vigation_committed_unsubscribe_failures.rs | 261 ++++++++++++------ ...gation_unsubscribe_transport_provenance.rs | 70 +++-- 2 files changed, 221 insertions(+), 110 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe_failures.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe_failures.rs index 52b953b11..95a6288c8 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe_failures.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe_failures.rs @@ -15,12 +15,11 @@ use originweave_network::{ WebDriverBiDiNavigationCommittedUnsubscribeCommand, WebDriverBiDiNavigationCommittedUnsubscribeCommandError, WebDriverBiDiNavigationCommittedUnsubscribeResponseError, - WebDriverBiDiNavigationCommittedUnsubscribeResult, WebDriverBiDiTcpConnectionPlan, - WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketEstablished, - WebDriverBiDiWebSocketFrameError, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler, - WebDriverBiDiWebSocketMessageAssembly, WebDriverBiDiWebSocketMessageReader, - WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiNavigationCommittedUnsubscribeResult, WebDriverBiDiReceivedTextMessage, + WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketFrameError, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, + WebDriverBiDiWebSocketMessageReader, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -150,13 +149,28 @@ fn require_no_client_command(stream: &mut TcpStream) -> io::Result<()> { fn spawn_no_command_server(listener: TcpListener) -> thread::JoinHandle> { thread::spawn(move || { - let (mut stream, _) = listener.accept()?; - read_opening_request(&mut stream)?; - stream.write_all(OPENING_RESPONSE)?; + let mut stream = accept_subscription(listener)?; require_no_client_command(&mut stream) }) } +fn accept_subscription(listener: TcpListener) -> io::Result { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + let subscribe = read_masked_text_frame(&mut stream)?; + if subscribe + != br#"{"id":7,"method":"session.subscribe","params":{"events":["browsingContext.navigationCommitted"],"contexts":["context-a"]}}"# + { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("unexpected session.subscribe command: {}", String::from_utf8_lossy(&subscribe)), + )); + } + write_unmasked_text_frame(&mut stream, SUBSCRIBE_RESPONSE.as_bytes())?; + Ok(stream) +} + fn establish_websocket( local_addr: SocketAddr, ) -> Result> { @@ -172,34 +186,18 @@ fn establish_websocket( .read_opening_response(Duration::from_millis(500))?) } -fn obtain_subscription_receipt() --> 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)?; - - let subscribe = read_masked_text_frame(&mut stream)?; - if subscribe - != br#"{"id":7,"method":"session.subscribe","params":{"events":["browsingContext.navigationCommitted"],"contexts":["context-a"]}}"# - { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - format!( - "unexpected session.subscribe command: {}", - String::from_utf8_lossy(&subscribe) - ), - )); - } - write_unmasked_text_frame(&mut stream, SUBSCRIBE_RESPONSE.as_bytes()) - }); - +fn obtain_subscription_receipt( + established: WebDriverBiDiWebSocketEstablished, +) -> Result< + ( + WebDriverBiDiWebSocketEstablished, + WebDriverBiDiNavigationCommittedSubscriptionResult, + ), + Box, +> { let mut registry = BrowserAuthorityRegistry::new(); let session = registry.register_session(SESSION_ID)?; let context = registry.register_context(session, CONTEXT_ID)?; - let established = establish_websocket(local_addr)?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); let subscribe = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( 7, ®istry, session, context, CONTEXT_ID, @@ -211,10 +209,13 @@ fn obtain_subscription_receipt() WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), Duration::from_millis(500), )?; - let received = match WebDriverBiDiWebSocketMessageReader::new(established) + let (established, received) = match WebDriverBiDiWebSocketMessageReader::new(established) .read_next(Duration::from_millis(500))? { - WebDriverBiDiConnectionMessageRead::Text { message, .. } => message, + WebDriverBiDiConnectionMessageRead::Text { + established, + message, + } => (established, message), other => { return Err(io::Error::other(format!( "session.subscribe response produced unexpected connection-bound state: {other:?}" @@ -227,15 +228,49 @@ fn obtain_subscription_receipt() &mut correlation, )?; + Ok((established, subscription)) +} + +fn standalone_subscription_receipt() +-> Result> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let server = spawn_no_command_server(listener); + let (established, subscription) = + obtain_subscription_receipt(establish_websocket(local_addr)?)?; + drop(established); server .join() .map_err(|_| io::Error::other("subscription receipt test server panicked"))??; Ok(subscription) } +fn read_received_text( + established: WebDriverBiDiWebSocketEstablished, +) -> Result< + ( + WebDriverBiDiWebSocketEstablished, + WebDriverBiDiReceivedTextMessage, + ), + Box, +> { + match WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + WebDriverBiDiConnectionMessageRead::Text { + established, + message, + } => Ok((established, message)), + other => Err(io::Error::other(format!( + "session.unsubscribe response produced unexpected connection-bound state: {other:?}" + )) + .into()), + } +} + fn read_text_over_loopback( document: &'static [u8], -) -> Result> { +) -> Result> { let listener = TcpListener::bind(("127.0.0.1", 0))?; let local_addr = listener.local_addr()?; let server = thread::spawn(move || -> io::Result<()> { @@ -245,18 +280,8 @@ fn read_text_over_loopback( write_unmasked_text_frame(&mut stream, document) }); - let established = establish_websocket(local_addr)?; - let (_established, frame) = established.read_frame(Duration::from_millis(500))?; - let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); - let text = match assembler.push_frame(frame)? { - WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, - other => { - return Err(io::Error::other(format!( - "session.unsubscribe response produced unexpected assembly state: {other:?}" - )) - .into()); - } - }; + let (established, text) = read_received_text(establish_websocket(local_addr)?)?; + drop(established); server .join() @@ -266,10 +291,10 @@ fn read_text_over_loopback( #[test] fn command_validation_and_debug_are_public_and_subscription_safe() -> Result<(), Box> { - let subscription = obtain_subscription_receipt()?; + let subscription = standalone_subscription_receipt()?; let range = match WebDriverBiDiNavigationCommittedUnsubscribeCommand::new( MAX_WEBDRIVER_BIDI_JS_UINT + 1, - &subscription, + subscription, ) { Ok(_) => { return Err( @@ -292,25 +317,24 @@ fn command_validation_and_debug_are_public_and_subscription_safe() -> Result<(), && *maximum_command_id == MAX_WEBDRIVER_BIDI_JS_UINT )); - let command = WebDriverBiDiNavigationCommittedUnsubscribeCommand::new(8, &subscription)?; + let subscription = standalone_subscription_receipt()?; + let subscription_id = subscription.subscription_id().to_owned(); + let command = WebDriverBiDiNavigationCommittedUnsubscribeCommand::new(8, subscription)?; let debug = format!("{command:?}"); assert!(debug.contains("command_id: 8")); - assert!(debug.contains(&format!( - "subscription_id_len: {}", - subscription.subscription_id().len() - ))); - assert!(!debug.contains(subscription.subscription_id())); + assert!(debug.contains(&format!("subscription_id_len: {}", subscription_id.len()))); + assert!(!debug.contains(&subscription_id)); Ok(()) } #[test] fn duplicate_command_id_is_rejected_before_unsubscribe_write() -> Result<(), Box> { - let subscription = obtain_subscription_receipt()?; let listener = TcpListener::bind(("127.0.0.1", 0))?; let local_addr = listener.local_addr()?; let server = spawn_no_command_server(listener); - let established = establish_websocket(local_addr)?; - let command = WebDriverBiDiNavigationCommittedUnsubscribeCommand::new(8, &subscription)?; + let (established, subscription) = + obtain_subscription_receipt(establish_websocket(local_addr)?)?; + let command = WebDriverBiDiNavigationCommittedUnsubscribeCommand::new(8, subscription)?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); correlation @@ -321,6 +345,10 @@ fn duplicate_command_id_is_rejected_before_unsubscribe_write() -> Result<(), Box WebDriverBiDiWebSocketMaskKey::new([5, 6, 7, 8]), Duration::from_millis(500), ); + let result = result.map(drop); + server + .join() + .map_err(|_| io::Error::other("duplicate-command test server panicked"))??; let error = match result { Ok(_) => { return Err(io::Error::other("duplicate command id sent unsubscribe command").into()); @@ -338,21 +366,18 @@ fn duplicate_command_id_is_rejected_before_unsubscribe_write() -> Result<(), Box )); assert_eq!(correlation.outstanding_count(), 1); - server - .join() - .map_err(|_| io::Error::other("duplicate-command test server panicked"))??; Ok(()) } #[test] fn invalid_frame_timeout_fails_before_unsubscribe_correlation_or_write() -> Result<(), Box> { - let subscription = obtain_subscription_receipt()?; let listener = TcpListener::bind(("127.0.0.1", 0))?; let local_addr = listener.local_addr()?; let server = spawn_no_command_server(listener); - let established = establish_websocket(local_addr)?; - let command = WebDriverBiDiNavigationCommittedUnsubscribeCommand::new(8, &subscription)?; + let (established, subscription) = + obtain_subscription_receipt(establish_websocket(local_addr)?)?; + let command = WebDriverBiDiNavigationCommittedUnsubscribeCommand::new(8, subscription)?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); let result = command.send( @@ -361,6 +386,10 @@ fn invalid_frame_timeout_fails_before_unsubscribe_correlation_or_write() WebDriverBiDiWebSocketMaskKey::new([5, 6, 7, 8]), Duration::ZERO, ); + let result = result.map(drop); + server + .join() + .map_err(|_| io::Error::other("frame-timeout test server panicked"))??; let error = match result { Ok(_) => { return Err(io::Error::other("zero frame timeout sent unsubscribe command").into()); @@ -378,33 +407,25 @@ fn invalid_frame_timeout_fails_before_unsubscribe_correlation_or_write() )); assert_eq!(correlation.outstanding_count(), 0); - server - .join() - .map_err(|_| io::Error::other("frame-timeout test server panicked"))??; Ok(()) } #[test] fn adjacent_mask_key_reuse_is_rejected_inside_unsubscribe_send_and_retires_correlation() -> Result<(), Box> { - let subscription = obtain_subscription_receipt()?; let listener = TcpListener::bind(("127.0.0.1", 0))?; let local_addr = listener.local_addr()?; let server = thread::spawn(move || -> io::Result<()> { - let (mut stream, _) = listener.accept()?; - read_opening_request(&mut stream)?; - stream.write_all(OPENING_RESPONSE)?; + let mut stream = accept_subscription(listener)?; read_empty_masked_pong_frame(&mut stream, [5, 6, 7, 8])?; require_no_client_command(&mut stream) }); let masking_key = WebDriverBiDiWebSocketMaskKey::new([5, 6, 7, 8]); - let established = establish_websocket(local_addr)?.write_pong_frame( - &[], - masking_key, - Duration::from_millis(500), - )?; - let command = WebDriverBiDiNavigationCommittedUnsubscribeCommand::new(8, &subscription)?; + let (established, subscription) = + obtain_subscription_receipt(establish_websocket(local_addr)?)?; + let established = established.write_pong_frame(&[], masking_key, Duration::from_millis(500))?; + let command = WebDriverBiDiNavigationCommittedUnsubscribeCommand::new(8, subscription)?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); let result = command.send( established, @@ -412,6 +433,10 @@ fn adjacent_mask_key_reuse_is_rejected_inside_unsubscribe_send_and_retires_corre masking_key, Duration::from_millis(500), ); + let result = result.map(drop); + server + .join() + .map_err(|_| io::Error::other("mask-reuse test server panicked"))??; let error = match result { Ok(_) => { return Err(io::Error::other("reused masking key sent unsubscribe command").into()); @@ -426,9 +451,6 @@ fn adjacent_mask_key_reuse_is_rejected_inside_unsubscribe_send_and_retires_corre )); assert_eq!(correlation.outstanding_count(), 0); - server - .join() - .map_err(|_| io::Error::other("mask-reuse test server panicked"))??; Ok(()) } @@ -514,15 +536,42 @@ fn unsubscribe_response_cannot_consume_subscription_command_kind() -> Result<(), #[test] fn matched_unsubscribe_protocol_error_consumes_only_its_command() -> Result<(), Box> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let server = thread::spawn(move || -> io::Result<()> { + let mut stream = accept_subscription(listener)?; + let unsubscribe = read_masked_text_frame(&mut stream)?; + if unsubscribe + != r#"{"id":8,"method":"session.unsubscribe","params":{"subscriptions":["sub-\"\\\n\u0001-구독"]}}"# + .as_bytes() + { + return Err(io::Error::other("unexpected session.unsubscribe command")); + } + write_unmasked_text_frame(&mut stream, MATCHED_UNSUBSCRIBE_ERROR)?; + require_no_client_command(&mut stream) + }); + let (established, subscription) = + obtain_subscription_receipt(establish_websocket(local_addr)?)?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); - correlation - .register_command_for(8, WebDriverBiDiCommandKind::NavigationCommittedUnsubscribe)?; - let matched = read_text_over_loopback(MATCHED_UNSUBSCRIBE_ERROR)?; - - let error = match WebDriverBiDiNavigationCommittedUnsubscribeResult::parse_and_correlate( + correlation.register_command_for(99, WebDriverBiDiCommandKind::SessionStatus)?; + let established = WebDriverBiDiNavigationCommittedUnsubscribeCommand::new(8, subscription)? + .send( + established, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([5, 6, 7, 8]), + Duration::from_millis(500), + )?; + let before_response = correlation.outstanding_count(); + let (established, matched) = read_received_text(established)?; + let result = WebDriverBiDiNavigationCommittedUnsubscribeResult::parse_and_correlate( &matched, &mut correlation, - ) { + ); + drop(established); + server + .join() + .map_err(|_| io::Error::other("matched unsubscribe error server panicked"))??; + let error = match result { Ok(_) => { return Err( io::Error::other("protocol-error unsubscribe response was accepted").into(), @@ -541,6 +590,42 @@ fn matched_unsubscribe_protocol_error_consumes_only_its_command() -> Result<(), command_id: 8, } )); + assert_eq!(before_response, 2); + assert_eq!(correlation.outstanding_count(), 1); + correlation.retire_command_for(99, WebDriverBiDiCommandKind::SessionStatus)?; assert_eq!(correlation.outstanding_count(), 0); Ok(()) } + +#[test] +fn generic_unsubscribe_registration_cannot_admit_received_success_or_error() +-> Result<(), Box> { + for document in [MATCHED_UNSUBSCRIBE_SUCCESS, MATCHED_UNSUBSCRIBE_ERROR] { + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + correlation + .register_command_for(8, WebDriverBiDiCommandKind::NavigationCommittedUnsubscribe)?; + correlation.register_command_for(99, WebDriverBiDiCommandKind::SessionStatus)?; + let received = read_text_over_loopback(document)?; + let result = WebDriverBiDiNavigationCommittedUnsubscribeResult::parse_and_correlate( + &received, + &mut correlation, + ); + assert!(matches!( + result, + Err( + WebDriverBiDiNavigationCommittedUnsubscribeResponseError::Correlation { + source: + WebDriverBiDiCommandCorrelationError::CommandConnectionProvenanceMissing { + command_id: 8, + } + } + ) + )); + assert_eq!(correlation.outstanding_count(), 2); + correlation + .retire_command_for(8, WebDriverBiDiCommandKind::NavigationCommittedUnsubscribe)?; + correlation.retire_command_for(99, WebDriverBiDiCommandKind::SessionStatus)?; + assert_eq!(correlation.outstanding_count(), 0); + } + Ok(()) +} diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_unsubscribe_transport_provenance.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_unsubscribe_transport_provenance.rs index 73a6cc3d7..aab6eed8d 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_unsubscribe_transport_provenance.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_unsubscribe_transport_provenance.rs @@ -9,16 +9,17 @@ use std::{ use originweave_core::{BrowserAuthorityRegistry, WebDriverBiDiWebSocketEndpoint}; use originweave_network::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandKind, - WebDriverBiDiConnectionMessageRead, WebDriverBiDiNavigationCommittedSubscriptionCommand, + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, + WebDriverBiDiCommandKind, WebDriverBiDiConnectionMessageRead, + WebDriverBiDiNavigationCommittedSubscriptionCommand, WebDriverBiDiNavigationCommittedSubscriptionResult, WebDriverBiDiNavigationCommittedUnsubscribeCommand, + WebDriverBiDiNavigationCommittedUnsubscribeCommandError, WebDriverBiDiNavigationCommittedUnsubscribeResponseError, - WebDriverBiDiNavigationCommittedUnsubscribeResult, WebDriverBiDiTcpConnectionPlan, - WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketEstablished, - WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, - WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, - WebDriverBiDiWebSocketMessageReader, WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiNavigationCommittedUnsubscribeResult, WebDriverBiDiReceivedTextMessage, + WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageReader, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -83,7 +84,9 @@ fn receive_exact_command( fn write_text_frame(stream: &mut TcpStream, payload: &[u8]) -> io::Result<()> { if payload.len() > 125 { - return Err(io::Error::other("fixture reply exceeded short-frame encoding")); + return Err(io::Error::other( + "fixture reply exceeded short-frame encoding", + )); } stream.write_all(&[0x81, payload.len() as u8])?; stream.write_all(payload) @@ -158,11 +161,13 @@ fn read_unsubscribe_text( established: WebDriverBiDiWebSocketEstablished, ) -> TestResult<( WebDriverBiDiWebSocketEstablished, - WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiReceivedTextMessage, )> { - let (established, frame) = established.read_frame(FRAME_TIMEOUT)?; - match WebDriverBiDiWebSocketMessageAssembler::new().push_frame(frame)? { - WebDriverBiDiWebSocketMessageAssembly::Text(message) => Ok((established, message)), + match WebDriverBiDiWebSocketMessageReader::new(established).read_next(FRAME_TIMEOUT)? { + WebDriverBiDiConnectionMessageRead::Text { + established, + message, + } => Ok((established, message)), other => Err(io::Error::other(format!( "expected actual unsubscribe response text frame, got {other:?}" )) @@ -182,19 +187,24 @@ fn subscription_receipt_cannot_dispatch_unsubscribe_on_another_connection() -> T }); let foreign_listener = TcpListener::bind(("127.0.0.1", 0))?; let foreign_addr = foreign_listener.local_addr()?; - let foreign_server = thread::spawn(move || read_until_closed(accept_websocket(foreign_listener)?)); + let foreign_server = + thread::spawn(move || read_until_closed(accept_websocket(foreign_listener)?)); let mut correlation = WebDriverBiDiCommandCorrelation::new(); correlation.register_command_for(99, WebDriverBiDiCommandKind::SessionStatus)?; let (sent_established, subscription) = subscribe(establish(sent_addr)?, &mut correlation)?; let before_send = correlation.outstanding_count(); - let result = WebDriverBiDiNavigationCommittedUnsubscribeCommand::new(8, &subscription)?.send( + let result = WebDriverBiDiNavigationCommittedUnsubscribeCommand::new(8, subscription)?.send( establish(foreign_addr)?, &mut correlation, WebDriverBiDiWebSocketMaskKey::new(UNSUBSCRIBE_MASK), FRAME_TIMEOUT, ); - let rejected = result.is_err(); + let rejected = + matches!( + &result, + Err(WebDriverBiDiNavigationCommittedUnsubscribeCommandError::SubscriptionConnectionMismatch) + ); let after_send = correlation.outstanding_count(); drop(result); drop(sent_established); @@ -212,14 +222,19 @@ fn subscription_receipt_cannot_dispatch_unsubscribe_on_another_connection() -> T foreign_bytes.is_empty(), "foreign connection emitted unsubscribe wire bytes: {foreign_bytes:?}" ); - assert!(rejected, "a receipt from A must not authorize dispatch on B"); + assert!( + rejected, + "a receipt from A must not authorize dispatch on B" + ); assert_eq!((before_send, after_send), (1, 1)); assert!(unrelated_retained); assert!(!unsubscribe_registered); Ok(()) } -fn foreign_unsubscribe_reply_preserves_original_command(foreign_reply: &'static [u8]) -> TestResult<()> { +fn foreign_unsubscribe_reply_preserves_original_command( + foreign_reply: &'static [u8], +) -> TestResult<()> { let sent_listener = TcpListener::bind(("127.0.0.1", 0))?; let sent_addr = sent_listener.local_addr()?; let (release_sender, release_receiver) = mpsc::sync_channel::<()>(0); @@ -230,7 +245,9 @@ fn foreign_unsubscribe_reply_preserves_original_command(foreign_reply: &'static receive_exact_command(&mut stream, UNSUBSCRIBE_COMMAND, UNSUBSCRIBE_MASK)?; release_receiver .recv_timeout(Duration::from_secs(2)) - .map_err(|error| io::Error::other(format!("original response barrier failed: {error}")))?; + .map_err(|error| { + io::Error::other(format!("original response barrier failed: {error}")) + })?; write_text_frame(&mut stream, UNSUBSCRIBE_SUCCESS)?; read_until_closed(stream) }); @@ -245,8 +262,8 @@ fn foreign_unsubscribe_reply_preserves_original_command(foreign_reply: &'static let mut correlation = WebDriverBiDiCommandCorrelation::new(); correlation.register_command_for(99, WebDriverBiDiCommandKind::SessionStatus)?; let (sent_established, subscription) = subscribe(establish(sent_addr)?, &mut correlation)?; - let sent_established = WebDriverBiDiNavigationCommittedUnsubscribeCommand::new(8, &subscription)? - .send( + let sent_established = + WebDriverBiDiNavigationCommittedUnsubscribeCommand::new(8, subscription)?.send( sent_established, &mut correlation, WebDriverBiDiWebSocketMaskKey::new(UNSUBSCRIBE_MASK), @@ -276,7 +293,10 @@ fn foreign_unsubscribe_reply_preserves_original_command(foreign_reply: &'static .is_ok(); assert!(sent_extra.is_empty(), "unexpected A bytes: {sent_extra:?}"); - assert!(foreign_bytes.is_empty(), "unexpected B bytes: {foreign_bytes:?}"); + assert!( + foreign_bytes.is_empty(), + "unexpected B bytes: {foreign_bytes:?}" + ); assert_eq!( (before_foreign, after_foreign, after_original), (2, 2, 1), @@ -284,7 +304,13 @@ fn foreign_unsubscribe_reply_preserves_original_command(foreign_reply: &'static ); assert!(matches!( foreign_result, - Err(WebDriverBiDiNavigationCommittedUnsubscribeResponseError::Correlation { .. }) + Err( + WebDriverBiDiNavigationCommittedUnsubscribeResponseError::Correlation { + source: WebDriverBiDiCommandCorrelationError::ResponseConnectionMismatch { + command_id: 8 + } + } + ) )); assert_eq!(original_result?.command_id(), 8); assert!(unrelated_retained); From 7fb93e77b800f27187a5c02333298cc31a025bd6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 20:43:49 +0900 Subject: [PATCH 68/76] test(docs): track consuming subscription teardown handoff Commit-Message-Assisted-by: Codex (via Codex) Signed-off-by: Seongho Bae --- tests/test_navigation_subscription_doctoring_contract.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_navigation_subscription_doctoring_contract.py b/tests/test_navigation_subscription_doctoring_contract.py index 25cf2accd..830c410fe 100644 --- a/tests/test_navigation_subscription_doctoring_contract.py +++ b/tests/test_navigation_subscription_doctoring_contract.py @@ -58,7 +58,10 @@ def test_subscription_receipt_and_event_use_connection_bound_messages(self) -> N self.assertIn("&WebDriverBiDiReceivedTextMessage", admission_source) self.assertIn("EventConnectionMismatch", admission_source) self.assertIn("received.connection_generation()", admission_source) - self.assertIn("unsubscribe transport provenance remains a separate boundary", admission_source) + self.assertIn( + "WebDriverBiDiNavigationCommittedUnsubscribeCommand::new(command_id, self.subscription)", + admission_source, + ) def test_webdriver_bidi_reference_tracks_current_published_working_draft(self) -> None: """ADR and aggregate doctoring must cite the same current published WebDriver BiDi draft.""" From b4702cd503fa3f721e0d1f44b355563753dac0a2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 21:03:12 +0900 Subject: [PATCH 69/76] test: reject registry session on foreign BiDi transport --- ...scription_registry_transport_provenance.rs | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_navigation_subscription_registry_transport_provenance.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_subscription_registry_transport_provenance.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_subscription_registry_transport_provenance.rs new file mode 100644 index 000000000..283087249 --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_subscription_registry_transport_provenance.rs @@ -0,0 +1,129 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{SocketAddr, TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::{BrowserAuthorityRegistry, WebDriverBiDiWebSocketEndpoint}; +use originweave_network::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiNavigationCommittedSubscriptionCommand, + WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketMaskKey, +}; + +const REGISTRY_SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; +const FOREIGN_TRANSPORT_SESSION_ID: &str = "fedcba98-7654-3210-fedc-ba9876543210"; +const CONTEXT_ID: &str = "context-a"; +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, + "opening request ended before the header terminator", + )); + } + request.extend_from_slice(&buffer[..count]); + } + Ok(()) +} + +fn spawn_foreign_transport_server( + listener: TcpListener, +) -> thread::JoinHandle> { + thread::spawn(move || { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + stream.set_read_timeout(Some(Duration::from_millis(500)))?; + + let mut first_command_byte = [0_u8; 1]; + match stream.read(&mut first_command_byte) { + Ok(0) => Ok(false), + Ok(_) => Ok(true), + Err(source) + if matches!( + source.kind(), + io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut + ) => + { + Ok(false) + } + Err(source) => Err(source), + } + }) +} + +fn establish( + local_addr: SocketAddr, + session_id: &str, +) -> Result> { + let endpoint = format!("ws://{local_addr}/session/{session_id}"); + let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? + .correlate_session_id(session_id)? + .into_explicit_connect_target()?; + let connection = + WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; + Ok(WebDriverBiDiWebSocketHandshakePlan::new( + connection, + WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?, + )? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?) +} + +#[test] +fn registry_bound_subscription_is_rejected_before_writing_to_a_foreign_session_transport( +) -> Result<(), Box> { + let mut registry = BrowserAuthorityRegistry::new(); + let session = registry.register_session(REGISTRY_SESSION_ID)?; + let context = registry.register_context(session, CONTEXT_ID)?; + let command = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( + 7, + ®istry, + session, + context, + CONTEXT_ID, + )?; + + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let server = spawn_foreign_transport_server(listener); + let established = establish(local_addr, FOREIGN_TRANSPORT_SESSION_ID)?; + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + + let send_result = command.send( + ®istry, + established, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + Duration::from_millis(500), + ); + let command_byte_seen = server + .join() + .map_err(|_| io::Error::other("foreign-session fixture server panicked"))??; + + assert!( + send_result.is_err(), + "registry session A unexpectedly dispatched on transport session B" + ); + assert_eq!( + correlation.outstanding_count(), + 0, + "foreign-session rejection must happen before correlation registration" + ); + assert!( + !command_byte_seen, + "foreign-session rejection must happen before any command-frame byte" + ); + Ok(()) +} From 53df6529aa8f171e706cfbbd531faf368654526a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 21:51:55 +0900 Subject: [PATCH 70/76] fix(network): reject foreign registry session before subscription dispatch Commit-Message-Assisted-by: Codex (via Codex) Signed-off-by: Seongho Bae --- .../src/browser_authority_registry.rs | 14 ++++++++++++++ crates/originweave-core/src/browser_registry.rs | 17 +++++++++++++++++ ...er_bidi_navigation_committed_subscription.rs | 11 +++++++++++ 3 files changed, 42 insertions(+) diff --git a/crates/originweave-core/src/browser_authority_registry.rs b/crates/originweave-core/src/browser_authority_registry.rs index 7db633958..309a011c8 100644 --- a/crates/originweave-core/src/browser_authority_registry.rs +++ b/crates/originweave-core/src/browser_authority_registry.rs @@ -81,6 +81,20 @@ impl BrowserAuthorityRegistry { self.inner.register_session(external_identifier) } + /// Require external protocol session text to name this exact currently registered session. + /// + /// This read-only check reuses the canonical mapping and never creates authority from transport + /// text. Unknown, retired and mismatched sessions fail closed. Success does not authenticate a + /// browser process or authorize an action; callers must also revalidate registry ownership. + pub fn require_registered_session_external_identifier( + &self, + browser_session: BrowserSessionId, + external_identifier: &str, + ) -> Result<(), BrowserRegistryError> { + self.inner + .require_session_external_identifier(browser_session, external_identifier) + } + /// Register one opaque external browsing-context identifier inside a known browser session. pub fn register_context( &mut self, diff --git a/crates/originweave-core/src/browser_registry.rs b/crates/originweave-core/src/browser_registry.rs index c5592e67a..8e2ab81a4 100644 --- a/crates/originweave-core/src/browser_registry.rs +++ b/crates/originweave-core/src/browser_registry.rs @@ -108,6 +108,18 @@ impl BrowserAuthorityRegistry { }) } + pub(crate) fn require_session_external_identifier( + &self, + browser_session: BrowserSessionId, + external_identifier: &str, + ) -> Result<(), BrowserRegistryError> { + validate_external_identifier(external_identifier)?; + if self.session_by_external.get(external_identifier).copied() != Some(browser_session) { + return Err(BrowserRegistryError::SessionExternalIdentifierMismatch); + } + Ok(()) + } + /// Register one opaque external browsing-context identifier inside a known browser session. /// /// A newly registered context starts at document epoch one. The same external context text in @@ -433,6 +445,8 @@ pub enum BrowserRegistryError { InvalidExternalIdentifier, /// The supplied OriginWeave browser session is not registered in this registry. UnknownBrowserSession, + /// The transport-level session identifier does not name the supplied registered session. + SessionExternalIdentifierMismatch, /// The supplied OriginWeave browsing context is not registered in this registry. UnknownBrowsingContext, /// The browsing context belongs to another browser session. @@ -468,6 +482,9 @@ impl fmt::Display for BrowserRegistryError { Self::UnknownBrowserSession => { formatter.write_str("browser session is not registered in this authority registry") } + Self::SessionExternalIdentifierMismatch => formatter.write_str( + "browser session external identifier does not match the registered session", + ), Self::UnknownBrowsingContext => { formatter.write_str("browsing context is not registered in this authority registry") } diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs index 01b4b2095..c07c39636 100644 --- a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs +++ b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs @@ -146,6 +146,17 @@ impl WebDriverBiDiNavigationCommittedSubscriptionCommand { validate_frame_timeout(frame_timeout).map_err(|source| { WebDriverBiDiNavigationCommittedSubscriptionCommandError::FrameWrite { source } })?; + registry + .require_registered_session_external_identifier( + self.browser_session, + established + .transport_evidence() + .verified_peer() + .session_id(), + ) + .map_err(|source| { + WebDriverBiDiNavigationCommittedSubscriptionCommandError::ContextBinding { source } + })?; let connection_generation = established.transport_evidence().connection_generation(); correlation .register_subscription_command_for_connection( From 657d0c9b1394b4aa2dd4d6c703d5123a18c52e13 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 21:53:34 +0900 Subject: [PATCH 71/76] test(core): cover exact and retired session provenance Commit-Message-Assisted-by: Codex (via Codex) Signed-off-by: Seongho Bae --- .../originweave-core/src/browser_registry.rs | 1 + .../browser_registry_session_provenance.rs | 46 +++++++++++++++++++ ..._bidi_navigation_committed_subscription.rs | 4 +- ...scription_registry_transport_provenance.rs | 14 ++---- 4 files changed, 54 insertions(+), 11 deletions(-) create mode 100644 crates/originweave-core/tests/browser_registry_session_provenance.rs diff --git a/crates/originweave-core/src/browser_registry.rs b/crates/originweave-core/src/browser_registry.rs index 8e2ab81a4..82e93e005 100644 --- a/crates/originweave-core/src/browser_registry.rs +++ b/crates/originweave-core/src/browser_registry.rs @@ -108,6 +108,7 @@ impl BrowserAuthorityRegistry { }) } + /// Check the existing canonical external-session mapping without creating authority. pub(crate) fn require_session_external_identifier( &self, browser_session: BrowserSessionId, diff --git a/crates/originweave-core/tests/browser_registry_session_provenance.rs b/crates/originweave-core/tests/browser_registry_session_provenance.rs new file mode 100644 index 000000000..ca1128476 --- /dev/null +++ b/crates/originweave-core/tests/browser_registry_session_provenance.rs @@ -0,0 +1,46 @@ +use originweave_core::{BrowserAuthorityRegistry, BrowserRegistryError, BrowserSessionId}; + +#[test] +fn session_mapping_is_read_only_exact_and_revoked_on_retirement() -> Result<(), BrowserRegistryError> +{ + let mut registry = BrowserAuthorityRegistry::new(); + let first = registry.register_session("session-a")?; + let second = registry.register_session("session-b")?; + registry.require_registered_session_external_identifier(first, "session-a")?; + for (session, external) in [ + (first, "session-b"), + (second, "session-a"), + (first, "unknown"), + ( + BrowserSessionId::new(99) + .map_err(|_| BrowserRegistryError::InternalAuthorityInvariant)?, + "session-a", + ), + ] { + assert_eq!( + registry.require_registered_session_external_identifier(session, external), + Err(BrowserRegistryError::SessionExternalIdentifierMismatch) + ); + } + assert_eq!( + registry.require_registered_session_external_identifier(first, "\n"), + Err(BrowserRegistryError::InvalidExternalIdentifier) + ); + registry.remove_session(first)?; + assert_eq!( + registry.require_registered_session_external_identifier(first, "session-a"), + Err(BrowserRegistryError::SessionExternalIdentifierMismatch) + ); + let replacement = registry.register_session("session-a")?; + assert_ne!(first, replacement); + registry.require_registered_session_external_identifier(replacement, "session-a")?; + assert_eq!( + registry.require_registered_session_external_identifier(first, "session-a"), + Err(BrowserRegistryError::SessionExternalIdentifierMismatch) + ); + assert_eq!( + BrowserRegistryError::SessionExternalIdentifierMismatch.to_string(), + "browser session external identifier does not match the registered session" + ); + Ok(()) +} diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs index c07c39636..866bfa296 100644 --- a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs +++ b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs @@ -115,6 +115,8 @@ impl WebDriverBiDiNavigationCommittedSubscriptionCommand { /// /// The original registry identity and context binding are revalidated before correlation and I/O so a /// command retained across registry retirement cannot subscribe a stale or replacement context. + /// The verified transport's protocol session must also match the registry's canonical external + /// session mapping; this comparison does not authenticate the browser process. /// Invalid frame deadlines fail before correlation registration. Registration then binds both the /// private command-instance identity and this established connection's process-local generation /// before the first possible remote side effect. A frame-owner preflight rejection that proves no @@ -242,7 +244,7 @@ pub enum WebDriverBiDiNavigationCommittedSubscriptionCommandError { /// Largest JavaScript-safe identifier admitted by this boundary. maximum_command_id: u64, }, - /// The external protocol context does not name the exact registered OriginWeave context. + /// The protocol session or context does not match the exact registered OriginWeave authority. ContextBinding { /// Exact typed browser-registry authority failure. source: BrowserRegistryError, diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_subscription_registry_transport_provenance.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_subscription_registry_transport_provenance.rs index 283087249..a69560b12 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_subscription_registry_transport_provenance.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_subscription_registry_transport_provenance.rs @@ -37,9 +37,7 @@ fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { Ok(()) } -fn spawn_foreign_transport_server( - listener: TcpListener, -) -> thread::JoinHandle> { +fn spawn_foreign_transport_server(listener: TcpListener) -> thread::JoinHandle> { thread::spawn(move || { let (mut stream, _) = listener.accept()?; read_opening_request(&mut stream)?; @@ -82,17 +80,13 @@ fn establish( } #[test] -fn registry_bound_subscription_is_rejected_before_writing_to_a_foreign_session_transport( -) -> Result<(), Box> { +fn registry_bound_subscription_is_rejected_before_writing_to_a_foreign_session_transport() +-> Result<(), Box> { let mut registry = BrowserAuthorityRegistry::new(); let session = registry.register_session(REGISTRY_SESSION_ID)?; let context = registry.register_context(session, CONTEXT_ID)?; let command = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( - 7, - ®istry, - session, - context, - CONTEXT_ID, + 7, ®istry, session, context, CONTEXT_ID, )?; let listener = TcpListener::bind(("127.0.0.1", 0))?; From 2fc4fe6c705f0d68be5de58df990e9e197ff7fa4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 21:55:39 +0900 Subject: [PATCH 72/76] docs: record subscription session provenance boundary Commit-Message-Assisted-by: Codex (via Codex) Signed-off-by: Seongho Bae --- CHANGELOG.md | 2 ++ ...bscription_registry_transport_provenance.rs | 18 ++++++++++++++---- .../0107-browser-protocol-adapter-strategy.md | 15 +++++++++++++++ docs/doctoring.md | 17 +++++++++++++++++ 4 files changed, 48 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1ddeaa5d..3323514af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Changed +- Reject a navigation subscription aimed at a different browser session before sending it, without creating or replacing browser state. + - Keep subscription shutdown on its original connection and reject replies from another connection without losing the pending request. Beginning shutdown ends local event admission; a failed shutdown requires a new subscription before admission resumes. - Prevent an earlier browser reply from completing a later request that reuses its number. Typed browser requests now use increasing numbers on each connection, and low-level protocol traffic uses a separate connection. diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_subscription_registry_transport_provenance.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_subscription_registry_transport_provenance.rs index a69560b12..12485de50 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_subscription_registry_transport_provenance.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_subscription_registry_transport_provenance.rs @@ -6,12 +6,14 @@ use std::{ time::Duration, }; -use originweave_core::{BrowserAuthorityRegistry, WebDriverBiDiWebSocketEndpoint}; +use originweave_core::{ + BrowserAuthorityRegistry, BrowserRegistryError, WebDriverBiDiWebSocketEndpoint, +}; use originweave_network::{ WebDriverBiDiCommandCorrelation, WebDriverBiDiNavigationCommittedSubscriptionCommand, - WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, - WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMaskKey, + WebDriverBiDiNavigationCommittedSubscriptionCommandError, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketEstablished, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, }; const REGISTRY_SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -110,6 +112,14 @@ fn registry_bound_subscription_is_rejected_before_writing_to_a_foreign_session_t send_result.is_err(), "registry session A unexpectedly dispatched on transport session B" ); + assert!(matches!( + send_result, + Err( + WebDriverBiDiNavigationCommittedSubscriptionCommandError::ContextBinding { + source: BrowserRegistryError::SessionExternalIdentifierMismatch + } + ) + )); assert_eq!( correlation.outstanding_count(), 0, diff --git a/docs/adr/0107-browser-protocol-adapter-strategy.md b/docs/adr/0107-browser-protocol-adapter-strategy.md index f131ff3c3..be20513a1 100644 --- a/docs/adr/0107-browser-protocol-adapter-strategy.md +++ b/docs/adr/0107-browser-protocol-adapter-strategy.md @@ -36,6 +36,21 @@ MCP version negotiation is independent of the OriginWeave Protocol version. As o ## Consequences +### Proposed refinement: registry-to-transport session provenance (2026-09-06) + +In the context of dispatching a registry-bound subscription on an established transport, facing +a valid context from session A being sent over session B, we decided for a read-only comparison +against the existing canonical external-session mapping and against a duplicate reverse registry +or registration from transport text, to reject mismatches before correlation and command bytes, +accepting one bounded lookup per subscription and rejection of adapters using unrelated session aliases. + +This supplements original-registry and current-context checks rather than replacing them. The +existing connection-generation binding continues to govern replies and teardown. Unknown or retired +session mappings fail closed without creating state. Matching protocol text is not browser-process +authentication or navigation causality. The actual loopback RED at `b4702cd5` and its successor +check the no-correlation/no-wire boundary; status remains Proposed pending exact-head gates and +protected parent-first integration, not a release or architecture approval. + ### Proposed refinement: consuming subscription teardown (2026-09-06) In the context of ending a navigation subscription, facing a borrowed receipt that permits continued diff --git a/docs/doctoring.md b/docs/doctoring.md index 08e454448..bae28fecd 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -4,6 +4,23 @@ This document records external evidence that changes OriginWeave architecture, t ## Decision trace +### Subscription registry-to-transport session provenance + +The real-loopback test at `b4702cd5`, executed locally before repair, dispatched a subscription +bound to registry session A over a transport correlated to session B. The repaired sender checks +the existing canonical external-session mapping before correlation registration or command bytes. +The regression now requires a typed mismatch, zero pending commands and zero command bytes; +registry tests also cover matching, unknown, malformed, retired and re-registered identities. + +The current W3C Editor's Draft associates each WebSocket connection with at most one BiDi session. +Its connection acceptance algorithm maps the resource's session ID to an active session. OriginWeave's +read-only registry comparison is a local fail-closed policy connecting that transport evidence to +existing authority. It does not authenticate a browser process, authorize an action, or create a +session from transport text. Proposed ADR 0107 records the boundary and alternatives. + +World Wide Web Consortium. (2026, September 3). *WebDriver BiDi: Transport* [Editor's Draft]. +Retrieved September 6, 2026, from https://w3c.github.io/webdriver-bidi/#transport + ### Subscription teardown ownership and connection provenance The actual-socket regression at `2c45cea8` shows that constructing teardown from a borrowed receipt From 11861c1b0c09d4d6bff5ad1ccdbcdb2f83db2eec Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 22:01:36 +0900 Subject: [PATCH 73/76] test(network): synchronize seed Pong before simulated disconnect Commit-Message-Assisted-by: Codex (via Codex) Signed-off-by: Seongho Bae --- CHANGELOG.md | 1 + .../tests/webdriver_bidi_pointer_click_send.rs | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3323514af..b9f591f90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Changed - Reject a navigation subscription aimed at a different browser session before sending it, without creating or replacing browser state. +- Keep connection-failure checks focused on the requested action by completing fixture setup before the simulated peer disconnects. - Keep subscription shutdown on its original connection and reject replies from another connection without losing the pending request. Beginning shutdown ends local event admission; a failed shutdown requires a new subscription before admission resumes. diff --git a/crates/originweave-network/tests/webdriver_bidi_pointer_click_send.rs b/crates/originweave-network/tests/webdriver_bidi_pointer_click_send.rs index a00500c46..6f66e1e2c 100644 --- a/crates/originweave-network/tests/webdriver_bidi_pointer_click_send.rs +++ b/crates/originweave-network/tests/webdriver_bidi_pointer_click_send.rs @@ -210,12 +210,16 @@ fn pointer_click_ambiguous_socket_write_keeps_correlation() -> Result<(), Box io::Result<()> { let (mut stream, _) = listener.accept()?; read_opening_request(&mut stream)?; stream.write_all(OPENING_RESPONSE)?; let mut first_frame_byte = [0_u8; 1]; stream.read_exact(&mut first_frame_byte)?; + seed_ready_receiver + .recv_timeout(Duration::from_secs(1)) + .map_err(|_| io::Error::other("seed Pong did not finish before peer closure"))?; drop(stream); closed_sender .send(()) @@ -237,6 +241,7 @@ fn pointer_click_ambiguous_socket_write_keeps_correlation() -> Result<(), Box Date: Sun, 6 Sep 2026 22:03:49 +0900 Subject: [PATCH 74/76] test(core): cover session mismatch in unit error contract Commit-Message-Assisted-by: Codex (via Codex) Signed-off-by: Seongho Bae --- crates/originweave-core/src/browser_registry.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/originweave-core/src/browser_registry.rs b/crates/originweave-core/src/browser_registry.rs index 82e93e005..9ef319f24 100644 --- a/crates/originweave-core/src/browser_registry.rs +++ b/crates/originweave-core/src/browser_registry.rs @@ -882,6 +882,7 @@ mod tests { replacement_errors[0], BrowserRegistryError::InvalidExternalIdentifier, BrowserRegistryError::UnknownBrowserSession, + BrowserRegistryError::SessionExternalIdentifierMismatch, BrowserRegistryError::UnknownBrowsingContext, BrowserRegistryError::ContextSessionMismatch { expected: expected_values[0], From 637fd97dbc9e0ac4fd6cdcc402ca2527bc0eb07d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 07:59:01 +0900 Subject: [PATCH 75/76] test(network): replay replacement pointer replies before parent adoption Commit-Message-Assisted-by: Codex (via Codex) Signed-off-by: Seongho Bae --- ...er_click_response_connection_provenance.rs | 203 ++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_pointer_click_response_connection_provenance.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_pointer_click_response_connection_provenance.rs b/crates/originweave-network/tests/webdriver_bidi_pointer_click_response_connection_provenance.rs new file mode 100644 index 000000000..3e04d1ded --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_pointer_click_response_connection_provenance.rs @@ -0,0 +1,203 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{SocketAddr, TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::{ + WebDriverBiDiPointerClickCommand, WebDriverBiDiRemoteNodeReference, + WebDriverBiDiWebSocketEndpoint, +}; +use originweave_network::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, + WebDriverBiDiCommandKind, WebDriverBiDiPointerClickResponseError, + WebDriverBiDiPointerClickResult, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketEstablished, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, + WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, + WebDriverBiDiWebSocketTextMessage, send_webdriver_bidi_pointer_click, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; +const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; +const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; +const CLICK_SUCCESS_RESPONSE: &[u8] = + br#"{"type":"success","id":42,"result":{"vendorExtension":{"observed":false}}}"#; +const CLICK_ERROR_RESPONSE: &[u8] = + br#"{"type":"error","id":42,"error":"invalid argument","message":"blocked","stacktrace":"remote"}"#; + +fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { + stream.set_read_timeout(Some(Duration::from_secs(2)))?; + let mut request = Vec::new(); + let mut buffer = [0_u8; 512]; + while !request.ends_with(b"\r\n\r\n") { + let count = stream.read(&mut buffer)?; + if count == 0 { + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "client opening request ended before the header terminator", + )); + } + request.extend_from_slice(&buffer[..count]); + } + Ok(()) +} + +fn read_masked_text_frame(stream: &mut TcpStream) -> io::Result> { + let mut header = [0_u8; 2]; + stream.read_exact(&mut header)?; + if header[0] != 0x81 || header[1] & 0x80 == 0 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "expected one final masked client text frame", + )); + } + let marker = header[1] & 0x7f; + let length = match marker { + 0..=125 => usize::from(marker), + 126 => { + let mut extended = [0_u8; 2]; + stream.read_exact(&mut extended)?; + usize::from(u16::from_be_bytes(extended)) + } + 127 => { + let mut extended = [0_u8; 8]; + stream.read_exact(&mut extended)?; + let length = u64::from_be_bytes(extended); + usize::try_from(length).map_err(|_| { + io::Error::new( + io::ErrorKind::InvalidData, + "pointer frame length exceeds usize", + ) + })? + } + _ => unreachable!(), + }; + let mut mask = [0_u8; 4]; + stream.read_exact(&mut mask)?; + let mut payload = vec![0_u8; length]; + stream.read_exact(&mut payload)?; + for (index, byte) in payload.iter_mut().enumerate() { + *byte ^= mask[index % mask.len()]; + } + Ok(payload) +} + +fn establish(local_addr: SocketAddr) -> Result> { + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? + .correlate_session_id(SESSION_ID)? + .into_explicit_connect_target()?; + let connection = + WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; + Ok(WebDriverBiDiWebSocketHandshakePlan::new( + connection, + WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?, + )? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?) +} + +fn read_response( + established: WebDriverBiDiWebSocketEstablished, +) -> Result> { + let (_established, frame) = established.read_frame(Duration::from_millis(500))?; + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); + let text = match assembler.push_frame(frame)? { + WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + other => { + return Err(io::Error::other(format!( + "replacement pointer connection produced unexpected assembly state: {other:?}" + )) + .into()); + } + }; + Ok(text) +} + +fn assert_replacement_rejected(foreign_response: &'static [u8]) -> Result<(), Box> { + let original_listener = TcpListener::bind(("127.0.0.1", 0))?; + let original_addr = original_listener.local_addr()?; + let expected = WebDriverBiDiPointerClickCommand::new( + 42, + "context-a", + &WebDriverBiDiRemoteNodeReference::new("node", Some("shared-node-42"))?, + )?; + let expected_json = expected.as_json().as_bytes().to_vec(); + let original_server = thread::spawn(move || -> io::Result<()> { + let (mut stream, _) = original_listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + let command = read_masked_text_frame(&mut stream)?; + if command != expected_json { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "unexpected pointer command on original connection", + )); + } + let (mut replacement, _) = original_listener.accept()?; + read_opening_request(&mut replacement)?; + replacement.write_all(OPENING_RESPONSE)?; + replacement.write_all(&[0x81, foreign_response.len() as u8])?; + replacement.write_all(foreign_response)?; + stream.write_all(&[0x81, CLICK_SUCCESS_RESPONSE.len() as u8])?; + stream.write_all(CLICK_SUCCESS_RESPONSE) + }); + + let original = establish(original_addr)?; + let command = WebDriverBiDiPointerClickCommand::new( + 42, + "context-a", + &WebDriverBiDiRemoteNodeReference::new("node", Some("shared-node-42"))?, + )?; + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + correlation.register_command_for(43, WebDriverBiDiCommandKind::SessionStatus)?; + let original = send_webdriver_bidi_pointer_click( + &command, + original, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + Duration::from_millis(500), + )?; + assert_eq!(correlation.outstanding_count(), 2); + + let replacement_response = read_response(establish(original_addr)?)?; + let parsed = WebDriverBiDiPointerClickResult::parse_and_correlate( + &replacement_response, + &mut correlation, + ); + + let original_response = read_response(original)?; + original_server + .join() + .map_err(|_| io::Error::other("original pointer server panicked"))??; + assert!( + matches!( + parsed, + Err(WebDriverBiDiPointerClickResponseError::Correlation { + source: WebDriverBiDiCommandCorrelationError::ResponseConnectionMismatch { + command_id: 42 + } + }) + ), + "replacement response must fail for exact connection mismatch: {parsed:?}" + ); + assert_eq!(correlation.outstanding_count(), 2); + let accepted = + WebDriverBiDiPointerClickResult::parse_and_correlate(&original_response, &mut correlation)?; + assert_eq!(accepted.command_id(), 42); + assert_eq!(correlation.outstanding_count(), 1); + Ok(()) +} + +#[test] +fn replacement_success_cannot_consume_original_pointer_command() -> Result<(), Box> { + assert_replacement_rejected(CLICK_SUCCESS_RESPONSE) +} + +#[test] +fn replacement_error_cannot_consume_original_pointer_command() -> Result<(), Box> { + assert_replacement_rejected(CLICK_ERROR_RESPONSE) +} From 433957117ad9e29b26715b062f5adcc9789744ba Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 08:03:34 +0900 Subject: [PATCH 76/76] docs(network): distinguish parent evidence from stronger child safeguards Commit-Message-Assisted-by: Codex (via Codex) Signed-off-by: Seongho Bae --- CHANGELOG.md | 2 ++ docs/doctoring.md | 8 ++++++++ .../webdriver-bidi-navigation-unsubscribe.md | 11 ++++++++++- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd3f68426..425580bc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Changed +- Reject replacement-connection click replies while retaining increasing request numbers, original subscription ownership and same-connection shutdown checks. + - Reject a navigation subscription aimed at a different browser session before sending it, without creating or replacing browser state. - Keep connection-failure checks focused on the requested action by completing fixture setup before the simulated peer disconnects. diff --git a/docs/doctoring.md b/docs/doctoring.md index 353de826e..42018a593 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -172,6 +172,14 @@ Fresh combined-tree verification passes nine focused reconnect/admission/documen ### Browser automation and interoperability +The subscription repair paragraph below records the historical #277 parent-only cut. +Current #264 already retains registry-to-transport session validation and consuming, +connection-bound subscription teardown described above. Ordinary parent adoption +`92e576c8` preserves those stronger child contracts and adds the parent's sealed pointer +responses while retaining monotonic typed dispatch. Actual replacement-pointer success +and error both failed at `637fd97d` before adoption. This combined source still requires +its own complete verification and does not establish protected-main or browser acceptance. + The subscription response repair reuses the existing sender-owned connection generation and sealed receiving-message capability. On regression head `122ca139`, two real connections to the same listener and session reproduced replacement success and protocol-error responses consuming the original subscription. Source repair `8b1508c8` rejects both with an exact connection mismatch, preserves two outstanding requests, and accepts the original connection's response while leaving the unrelated request outstanding. Manually registered commands lack sender provenance and cannot bypass that check. Required subscription projection still precedes correlation consumption; invalid deadlines precede registration, proven no-write failures retire only their exact request, and ambiguous writes retain correlation. The 14 focused subscription tests pass. This closes response-connection substitution only: outbound registry-to-endpoint session binding, authenticated later navigation events, protected-main asset preservation, hosted checks and browser-runtime acceptance remain separate work. Fresh deadline-repair verification passed 11 focused subscription loopback tests and all 142 Python contracts, plus the complete locked Rust workspace checks/tests, formatting, all-feature Clippy, warning-denying rustdoc, compileall, and diff checks. Numeric production coverage is 100% for 1,221 functions, 12,781 lines, 16,404 regions, and 1,418 branches; the unstable branch-measurement warning remains. The 1,220-function result below belongs to the earlier parent-adoption tree, not this subsequent source change. diff --git a/docs/traceability/webdriver-bidi-navigation-unsubscribe.md b/docs/traceability/webdriver-bidi-navigation-unsubscribe.md index 8341aa2a1..b16195059 100644 --- a/docs/traceability/webdriver-bidi-navigation-unsubscribe.md +++ b/docs/traceability/webdriver-bidi-navigation-unsubscribe.md @@ -59,7 +59,16 @@ construction. Already admitted observations remain unchanged; this is not retroa ## Authority and follow-up -### Subscription receipt parent adoption +### Historical parent-only subscription receipt adoption + +The following checkpoint describes #263, not the current #264 tree. This child retains +consuming unsubscribe receipt ownership, original-connection dispatch and sealed reply +admission, plus registry-session and monotonic command-ID guards. Its adoption of +#263 at `92e576c8` additionally binds pointer replies to their sending connection. +Regression `637fd97d` first reproduced both replacement pointer success and error +consuming the original request (0/2 passing). The integration combines the parent's +connection registration with the child's existing typed dispatch, rather than replacing +either safeguard. Current-head verification is required for this combined tree. Ordinary merge `9e85cadc` adopts #277 `46ae62aa31e35c702cd61c16322d05c7a9c35da1` without changing either unsubscribe production module. Canonical regression replay