From 160365b5ad55dbf975cb8941eacfdc176e0c7379 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 10:41:56 +0900 Subject: [PATCH 01/64] test(network): require correlated text-value postcondition admission --- ...er_bidi_text_value_observation_response.rs | 254 ++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs b/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs new file mode 100644 index 000000000..aa2370d6a --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs @@ -0,0 +1,254 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::{ + AdmittedNodeHandle, BoundedWebDriverBiDiResponseDocument, BrowserAuthorityRegistry, + BrowserContextDispatchTarget, BrowserContextOriginDispatchTarget, + BrowserContextOriginEpochDispatchTarget, BrowserProtocolAdapterDescriptor, + BrowserProtocolCapability, BrowserProtocolKind, Origin, OriginWeaveProtocolVersion, + ValidatedBrowserProtocolUse, WebDriverBiDiAccessibilityQuery, WebDriverBiDiLocateNodesCommand, + WebDriverBiDiRemoteNodeReference, WebDriverBiDiTextValueObservationCommand, + WebDriverBiDiWebSocketEndpoint, +}; +use originweave_network::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiTextValueObservationResult, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, + WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, + send_webdriver_bidi_text_value_observation, +}; + +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 OBSERVATION_SUCCESS_RESPONSE: &[u8] = br#"{"type":"success","id":43,"result":{"type":"success","result":{"type":"string","value":"Quarterly review"},"realm":"realm-1"}}"#; +const ORIGINWEAVE_PROTOCOL_VERSION: OriginWeaveProtocolVersion = + OriginWeaveProtocolVersion::new(0, 1); +const ADAPTER_VERSION: &str = "originweave-bidi-v1"; +const PROTOCOL_REVISION: &str = "webdriver-bidi-wd-2026-06-01"; +const BROWSER_REVISION: &str = "chromium-r1639810"; + +type AdmittedTextFieldFixture = ( + BrowserAuthorityRegistry, + AdmittedNodeHandle, + WebDriverBiDiRemoteNodeReference, +); + +fn semantic_observation_proof() -> Result> { + let descriptor = BrowserProtocolAdapterDescriptor::new( + BrowserProtocolKind::WebDriverBiDi, + ORIGINWEAVE_PROTOCOL_VERSION, + ADAPTER_VERSION, + PROTOCOL_REVISION, + BROWSER_REVISION, + &[BrowserProtocolCapability::SemanticObservation], + )?; + Ok(descriptor.validate_use( + ORIGINWEAVE_PROTOCOL_VERSION, + BrowserProtocolKind::WebDriverBiDi, + ADAPTER_VERSION, + PROTOCOL_REVISION, + BROWSER_REVISION, + BrowserProtocolCapability::SemanticObservation, + )?) +} + +fn admitted_text_field_fixture() -> Result> { + let mut registry = BrowserAuthorityRegistry::new(); + let browser_session = registry.register_session(SESSION_ID)?; + let browsing_context = registry.register_context(browser_session, "context-a")?; + let origin = Origin::parse("https://app.example").map_err(|error| { + io::Error::other(format!("fixture origin rejected unexpectedly: {error:?}")) + })?; + let epoch = registry.bind_context_origin(browser_session, browsing_context, &origin)?; + let target = BrowserContextOriginEpochDispatchTarget::new( + BrowserContextOriginDispatchTarget::new( + BrowserContextDispatchTarget::new(browser_session, browsing_context), + &origin, + ), + epoch, + ); + let query = WebDriverBiDiAccessibilityQuery::new(Some("textbox"), Some("Task title"), 1)?; + let locate = WebDriverBiDiLocateNodesCommand::new(41, "context-a", &query)?; + let document = BoundedWebDriverBiDiResponseDocument::new( + r#"{"type":"success","id":41,"result":{"nodes":[{"type":"node","sharedId":"shared-node-42"}]}}"#, + )?; + let handle = locate + .bind_response_document_nodes( + document, + semantic_observation_proof()?, + &mut registry, + target, + )? + .into_iter() + .next() + .ok_or_else(|| io::Error::other("locateNodes fixture did not bind its node"))?; + let remote = WebDriverBiDiRemoteNodeReference::new("node", Some("shared-node-42"))?; + Ok((registry, handle, 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)?; + usize::try_from(u64::from_be_bytes(extended)).map_err(|_| { + io::Error::new(io::ErrorKind::InvalidData, "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 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 response unexpectedly required 64-bit framing", + )); + } + } + stream.write_all(payload) +} + +#[test] +fn observed_text_postcondition_consumes_exact_command_without_exposing_page_text() +-> Result<(), Box> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let (registry, handle, remote) = admitted_text_field_fixture()?; + let expected_json = WebDriverBiDiTextValueObservationCommand::new_for_current_node( + 43, + "context-a", + &handle, + &remote, + ®istry, + )? + .as_json() + .as_bytes() + .to_vec(); + + 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_json { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "unexpected script.callFunction text-value observation command", + )); + } + write_text_frame(&mut stream, OBSERVATION_SUCCESS_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 mut correlation = WebDriverBiDiCommandCorrelation::new(); + let established = send_webdriver_bidi_text_value_observation( + semantic_observation_proof()?, + 43, + "context-a", + &handle, + &remote, + ®istry, + established, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + Duration::from_millis(500), + )?; + 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 => { + return Err(io::Error::other(format!( + "text-value observation response produced unexpected assembly state: {other:?}" + )) + .into()); + } + }; + let result = WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( + &text, + "Quarterly review", + &mut correlation, + )?; + assert_eq!(result.command_id(), 43); + assert!(result.matches_expected_text()); + assert_eq!(result.observed_text_bytes(), "Quarterly review".len()); + assert_eq!(correlation.outstanding_count(), 0); + let debug = format!("{result:?}"); + assert!(!debug.contains("Quarterly review")); + + server + .join() + .map_err(|_| io::Error::other("text-value observation response server panicked"))??; + Ok(()) +} From 924e027ce7cb99096d7f9a5971c0377cc939ab33 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 10:49:29 +0900 Subject: [PATCH 02/64] feat(network): admit correlated text-value postconditions --- ...er_bidi_text_value_observation_response.rs | 751 ++++++++++++++++++ 1 file changed, 751 insertions(+) create mode 100644 crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs diff --git a/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs b/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs new file mode 100644 index 000000000..1ac0b8f82 --- /dev/null +++ b/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs @@ -0,0 +1,751 @@ +use std::{collections::HashSet, error::Error, fmt}; + +use originweave_core::{ + MAX_WEBDRIVER_BIDI_TYPE_TEXT_BYTES, UNICODE_PROTOCOL_FORMAT_INJECTION_CHARS, +}; + +use crate::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, + WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeError, WebDriverBiDiJsonEnvelopeKind, + WebDriverBiDiWebSocketTextMessage, +}; + +const MAX_SCRIPT_RESULT_OBJECT_MEMBERS: usize = 64; +const MAX_SCRIPT_RESULT_MEMBER_NAME_BYTES: usize = 128; +const MAX_SCRIPT_RESULT_NESTING_DEPTH: usize = 64; + +/// Credential-minimal result of comparing one correlated text-value observation with the exact +/// already-authorized non-secret text that preceded it. +/// +/// The observed page string is never retained. This value keeps only the matched command id, the +/// observed UTF-8 byte count, and whether the observed string exactly matched the caller-supplied +/// expected text. A mismatch is valid negative post-condition evidence rather than transport or +/// parser success for the preceding action. +#[derive(Clone, Copy, Eq, PartialEq)] +pub struct WebDriverBiDiTextValueObservationResult { + command_id: u64, + observed_text_bytes: usize, + matches_expected_text: bool, +} + +impl fmt::Debug for WebDriverBiDiTextValueObservationResult { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("WebDriverBiDiTextValueObservationResult") + .field("command_id", &self.command_id) + .field("observed_text_bytes", &self.observed_text_bytes) + .field("matches_expected_text", &self.matches_expected_text) + .finish() + } +} + +impl WebDriverBiDiTextValueObservationResult { + /// Parse one bounded local-end response, consume its exact outstanding command, and compare + /// the successful string RemoteValue with the already-authorized expected non-secret text. + /// + /// The expected text is revalidated against the same reviewed local byte and character policy + /// used by node-bound text input before any response or correlation state is touched. Common + /// WebDriver BiDi envelope validation and command-specific `script.EvaluateResult` projection + /// likewise complete before correlation can be consumed. Malformed, duplicate, unsupported, + /// or over-budget result shapes therefore leave unrelated outstanding command state intact. + /// + /// A correlatable top-level protocol error and a valid `script` exception consume their exact + /// command id and return typed failures. A successful string result consumes the exact id and + /// immediately drops the page-controlled string after computing byte count and equality. This + /// boundary does not retry, grant browser or policy authority, retain a realm identifier, or + /// claim success when the observed value differs from the expected text. + pub fn parse_correlate_and_compare( + message: &WebDriverBiDiWebSocketTextMessage, + expected_text: &str, + correlation: &mut WebDriverBiDiCommandCorrelation, + ) -> Result { + validate_expected_text(expected_text)?; + let envelope = WebDriverBiDiJsonEnvelope::parse(message) + .map_err(|source| WebDriverBiDiTextValueObservationResponseError::Envelope { source })?; + + match envelope.kind() { + WebDriverBiDiJsonEnvelopeKind::Event => { + Err(WebDriverBiDiTextValueObservationResponseError::UnexpectedEvent) + } + WebDriverBiDiJsonEnvelopeKind::Error => { + let completed = correlation.correlate_response(&envelope).map_err(|source| { + WebDriverBiDiTextValueObservationResponseError::Correlation { source } + })?; + Err(WebDriverBiDiTextValueObservationResponseError::RemoteProtocolError { + command_id: completed.command_id(), + }) + } + WebDriverBiDiJsonEnvelopeKind::Success => { + let projection = project_script_result(message.as_str()).map_err(|source| { + WebDriverBiDiTextValueObservationResponseError::Projection { source } + })?; + let completed = correlation.correlate_response(&envelope).map_err(|source| { + WebDriverBiDiTextValueObservationResponseError::Correlation { source } + })?; + match projection { + ScriptResultProjection::Exception => { + Err(WebDriverBiDiTextValueObservationResponseError::ScriptException { + command_id: completed.command_id(), + }) + } + ScriptResultProjection::String(observed_text) => { + let observed_text_bytes = observed_text.len(); + let matches_expected_text = observed_text == expected_text; + Ok(Self { + command_id: completed.command_id(), + observed_text_bytes, + matches_expected_text, + }) + } + } + } + } + } + + /// Return the exact local command identifier consumed by this observation response. + #[must_use] + pub const fn command_id(&self) -> u64 { + self.command_id + } + + /// Return the UTF-8 byte length of the observed string without retaining the string itself. + #[must_use] + pub const fn observed_text_bytes(&self) -> usize { + self.observed_text_bytes + } + + /// Return whether the observed page value exactly matched the expected non-secret text. + #[must_use] + pub const fn matches_expected_text(&self) -> bool { + self.matches_expected_text + } +} + +/// Fail-closed failures while admitting and comparing one text-value post-condition response. +#[derive(Debug)] +pub enum WebDriverBiDiTextValueObservationResponseError { + /// The caller supplied an empty expected text value. + EmptyExpectedText, + /// The caller supplied expected text above the reviewed text-input byte budget. + ExpectedTextTooLong, + /// The expected text contains a disallowed control, whitespace, or reviewed format character. + InvalidExpectedText, + /// Common bounded WebDriver BiDi local-end envelope validation failed. + Envelope { + /// Exact common-envelope validation failure. + source: WebDriverBiDiJsonEnvelopeError, + }, + /// A WebDriver BiDi event was supplied where this command-specific response boundary requires + /// a correlated success or error response. + UnexpectedEvent, + /// The command-specific `script.EvaluateResult` shape was malformed or unsupported. + Projection { + /// Exact non-sensitive projection failure. + source: WebDriverBiDiTextValueObservationProjectionError, + }, + /// Exact command-response correlation failed without consuming unrelated state. + Correlation { + /// Exact typed correlation failure. + source: WebDriverBiDiCommandCorrelationError, + }, + /// The remote end returned a correlatable top-level WebDriver BiDi protocol error. + RemoteProtocolError { + /// Exact local command identifier consumed by the protocol-error response. + command_id: u64, + }, + /// The remote end completed `script.callFunction` with a typed script exception. + ScriptException { + /// Exact local command identifier consumed by the script-exception result. + command_id: u64, + }, +} + +impl fmt::Display for WebDriverBiDiTextValueObservationResponseError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(match self { + Self::EmptyExpectedText => "expected text-value postcondition must not be empty", + Self::ExpectedTextTooLong => "expected text-value postcondition exceeds the local byte budget", + Self::InvalidExpectedText => "expected text-value postcondition contains a disallowed character", + Self::Envelope { .. } => "WebDriver BiDi text-value observation envelope is invalid", + Self::UnexpectedEvent => "WebDriver BiDi text-value observation received an event instead of a command response", + Self::Projection { .. } => "WebDriver BiDi text-value observation result is invalid", + Self::Correlation { .. } => "WebDriver BiDi text-value observation response correlation failed", + Self::RemoteProtocolError { .. } => "WebDriver BiDi text-value observation returned a protocol error", + Self::ScriptException { .. } => "WebDriver BiDi text-value observation returned a script exception", + }) + } +} + +impl Error for WebDriverBiDiTextValueObservationResponseError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::Envelope { source } => Some(source), + Self::Projection { source } => Some(source), + Self::Correlation { source } => Some(source), + Self::EmptyExpectedText + | Self::ExpectedTextTooLong + | Self::InvalidExpectedText + | Self::UnexpectedEvent + | Self::RemoteProtocolError { .. } + | Self::ScriptException { .. } => None, + } + } +} + +/// Non-sensitive structural failures while projecting a successful `script.callFunction` result. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum WebDriverBiDiTextValueObservationProjectionError { + /// An object required by the command-specific result contract was absent or malformed. + InvalidObject { + /// Stable non-sensitive protocol member path. + member: &'static str, + }, + /// A required command-specific member was absent. + MissingMember { + /// Stable non-sensitive protocol member path. + member: &'static str, + }, + /// A command-specific object repeated a member name. + DuplicateMember, + /// A command-specific object exceeded the reviewed member-count budget. + TooManyMembers, + /// A command-specific member name exceeded the reviewed byte budget. + MemberNameTooLong, + /// A JSON string could not be projected without violating the bounded decoder contract. + InvalidString, + /// The `script.EvaluateResult` discriminator was neither `success` nor `exception`. + UnsupportedScriptResultType, + /// A successful script RemoteValue was not a string value. + UnsupportedRemoteValueType, + /// The observed page string exceeded the reviewed text-input byte budget. + ObservedTextTooLong, + /// A nested value exceeded the reviewed command-specific projection depth budget. + NestingTooDeep, + /// A command-specific value ended unexpectedly despite prior common-envelope validation. + InvalidValue, +} + +impl fmt::Display for WebDriverBiDiTextValueObservationProjectionError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidObject { member } => write!(formatter, "invalid object at {member}"), + Self::MissingMember { member } => write!(formatter, "missing member {member}"), + Self::DuplicateMember => formatter.write_str("duplicate command-specific result member"), + Self::TooManyMembers => formatter.write_str("command-specific result object has too many members"), + Self::MemberNameTooLong => formatter.write_str("command-specific result member name is too long"), + Self::InvalidString => formatter.write_str("invalid command-specific JSON string"), + Self::UnsupportedScriptResultType => formatter.write_str("unsupported script result type"), + Self::UnsupportedRemoteValueType => formatter.write_str("text-value observation did not return a string RemoteValue"), + Self::ObservedTextTooLong => formatter.write_str("observed text-value postcondition exceeds the local byte budget"), + Self::NestingTooDeep => formatter.write_str("command-specific result nesting is too deep"), + Self::InvalidValue => formatter.write_str("invalid command-specific result value"), + } + } +} + +impl Error for WebDriverBiDiTextValueObservationProjectionError {} + +fn validate_expected_text( + expected_text: &str, +) -> Result<(), WebDriverBiDiTextValueObservationResponseError> { + if expected_text.is_empty() { + return Err(WebDriverBiDiTextValueObservationResponseError::EmptyExpectedText); + } + if expected_text.len() > MAX_WEBDRIVER_BIDI_TYPE_TEXT_BYTES { + return Err(WebDriverBiDiTextValueObservationResponseError::ExpectedTextTooLong); + } + if expected_text.chars().any(|character| { + (character.is_whitespace() && character != ' ') + || character.is_control() + || UNICODE_PROTOCOL_FORMAT_INJECTION_CHARS.contains(&character) + }) { + return Err(WebDriverBiDiTextValueObservationResponseError::InvalidExpectedText); + } + Ok(()) +} + +enum ScriptResultProjection { + String(String), + Exception, +} + +fn project_script_result( + text: &str, +) -> Result { + let top_level = parse_object_members(text, "response")?; + let script_result = required_object_member(&top_level, "result", "result")?; + let script_members = parse_object_members(script_result, "result")?; + let result_type = required_string_member(&script_members, "type", "result.type")?; + let _realm = required_string_member(&script_members, "realm", "result.realm")?; + + match result_type.as_str() { + "success" => { + let remote = required_object_member(&script_members, "result", "result.result")?; + let remote_members = parse_object_members(remote, "result.result")?; + let remote_type = + required_string_member(&remote_members, "type", "result.result.type")?; + if remote_type != "string" { + return Err( + WebDriverBiDiTextValueObservationProjectionError::UnsupportedRemoteValueType, + ); + } + let observed = + required_string_member(&remote_members, "value", "result.result.value")?; + if observed.len() > MAX_WEBDRIVER_BIDI_TYPE_TEXT_BYTES { + return Err(WebDriverBiDiTextValueObservationProjectionError::ObservedTextTooLong); + } + Ok(ScriptResultProjection::String(observed)) + } + "exception" => { + let _details = required_object_member( + &script_members, + "exceptionDetails", + "result.exceptionDetails", + )?; + Ok(ScriptResultProjection::Exception) + } + _ => Err(WebDriverBiDiTextValueObservationProjectionError::UnsupportedScriptResultType), + } +} + +fn required_object_member<'a>( + members: &'a [(String, &'a str)], + name: &str, + path: &'static str, +) -> Result<&'a str, WebDriverBiDiTextValueObservationProjectionError> { + let value = members + .iter() + .find_map(|(member, value)| (member == name).then_some(*value)) + .ok_or(WebDriverBiDiTextValueObservationProjectionError::MissingMember { member: path })?; + let trimmed = value.trim(); + if !trimmed.starts_with('{') { + return Err(WebDriverBiDiTextValueObservationProjectionError::InvalidObject { + member: path, + }); + } + Ok(trimmed) +} + +fn required_string_member( + members: &[(String, &str)], + name: &str, + path: &'static str, +) -> Result { + let value = members + .iter() + .find_map(|(member, value)| (member == name).then_some(*value)) + .ok_or(WebDriverBiDiTextValueObservationProjectionError::MissingMember { member: path })?; + decode_json_string(value.trim()) +} + +fn parse_object_members<'a>( + text: &'a str, + path: &'static str, +) -> Result, WebDriverBiDiTextValueObservationProjectionError> { + let bytes = text.as_bytes(); + let mut index = skip_whitespace(bytes, 0); + if bytes.get(index) != Some(&b'{') { + return Err(WebDriverBiDiTextValueObservationProjectionError::InvalidObject { + member: path, + }); + } + index += 1; + let mut members = Vec::new(); + let mut names = HashSet::new(); + + loop { + index = skip_whitespace(bytes, index); + if bytes.get(index) == Some(&b'}') { + index += 1; + index = skip_whitespace(bytes, index); + if index != bytes.len() { + return Err(WebDriverBiDiTextValueObservationProjectionError::InvalidValue); + } + return Ok(members); + } + if members.len() >= MAX_SCRIPT_RESULT_OBJECT_MEMBERS { + return Err(WebDriverBiDiTextValueObservationProjectionError::TooManyMembers); + } + let key_end = scan_string_end(bytes, index)?; + let key = decode_json_string(&text[index..key_end])?; + if key.len() > MAX_SCRIPT_RESULT_MEMBER_NAME_BYTES { + return Err(WebDriverBiDiTextValueObservationProjectionError::MemberNameTooLong); + } + if !names.insert(key.clone()) { + return Err(WebDriverBiDiTextValueObservationProjectionError::DuplicateMember); + } + index = skip_whitespace(bytes, key_end); + if bytes.get(index) != Some(&b':') { + return Err(WebDriverBiDiTextValueObservationProjectionError::InvalidValue); + } + index += 1; + index = skip_whitespace(bytes, index); + let value_start = index; + let value_end = scan_value_end(bytes, value_start, 0)?; + members.push((key, &text[value_start..value_end])); + index = skip_whitespace(bytes, value_end); + match bytes.get(index) { + Some(b',') => index += 1, + Some(b'}') => {} + _ => return Err(WebDriverBiDiTextValueObservationProjectionError::InvalidValue), + } + } +} + +fn skip_whitespace(bytes: &[u8], mut index: usize) -> usize { + while matches!(bytes.get(index), Some(b' ' | b'\t' | b'\n' | b'\r')) { + index += 1; + } + index +} + +fn scan_value_end( + bytes: &[u8], + index: usize, + depth: usize, +) -> Result { + if depth >= MAX_SCRIPT_RESULT_NESTING_DEPTH { + return Err(WebDriverBiDiTextValueObservationProjectionError::NestingTooDeep); + } + match bytes.get(index) { + Some(b'"') => scan_string_end(bytes, index), + Some(b'{') => scan_container_end(bytes, index, b'{', b'}', depth + 1), + Some(b'[') => scan_container_end(bytes, index, b'[', b']', depth + 1), + Some(_) => { + let mut end = index; + while let Some(byte) = bytes.get(end) { + if matches!(byte, b',' | b'}' | b']' | b' ' | b'\t' | b'\n' | b'\r') { + break; + } + end += 1; + } + if end == index { + Err(WebDriverBiDiTextValueObservationProjectionError::InvalidValue) + } else { + Ok(end) + } + } + None => Err(WebDriverBiDiTextValueObservationProjectionError::InvalidValue), + } +} + +fn scan_container_end( + bytes: &[u8], + start: usize, + open: u8, + close: u8, + depth: usize, +) -> Result { + if depth > MAX_SCRIPT_RESULT_NESTING_DEPTH { + return Err(WebDriverBiDiTextValueObservationProjectionError::NestingTooDeep); + } + if bytes.get(start) != Some(&open) { + return Err(WebDriverBiDiTextValueObservationProjectionError::InvalidValue); + } + let mut stack = vec![close]; + let mut index = start + 1; + while let Some(byte) = bytes.get(index).copied() { + match byte { + b'"' => index = scan_string_end(bytes, index)?, + b'{' => { + if stack.len() >= MAX_SCRIPT_RESULT_NESTING_DEPTH { + return Err(WebDriverBiDiTextValueObservationProjectionError::NestingTooDeep); + } + stack.push(b'}'); + index += 1; + } + b'[' => { + if stack.len() >= MAX_SCRIPT_RESULT_NESTING_DEPTH { + return Err(WebDriverBiDiTextValueObservationProjectionError::NestingTooDeep); + } + stack.push(b']'); + index += 1; + } + b'}' | b']' => { + if stack.pop() != Some(byte) { + return Err(WebDriverBiDiTextValueObservationProjectionError::InvalidValue); + } + index += 1; + if stack.is_empty() { + return Ok(index); + } + } + _ => index += 1, + } + } + Err(WebDriverBiDiTextValueObservationProjectionError::InvalidValue) +} + +fn scan_string_end( + bytes: &[u8], + start: usize, +) -> Result { + if bytes.get(start) != Some(&b'"') { + return Err(WebDriverBiDiTextValueObservationProjectionError::InvalidString); + } + let mut index = start + 1; + while let Some(byte) = bytes.get(index).copied() { + match byte { + b'"' => return Ok(index + 1), + b'\\' => { + index += 2; + if index > bytes.len() { + return Err(WebDriverBiDiTextValueObservationProjectionError::InvalidString); + } + } + 0x00..=0x1f => { + return Err(WebDriverBiDiTextValueObservationProjectionError::InvalidString); + } + _ => index += 1, + } + } + Err(WebDriverBiDiTextValueObservationProjectionError::InvalidString) +} + +fn decode_json_string( + value: &str, +) -> Result { + if !value.starts_with('"') || !value.ends_with('"') || value.len() < 2 { + return Err(WebDriverBiDiTextValueObservationProjectionError::InvalidString); + } + let inner = &value[1..value.len() - 1]; + let mut characters = inner.chars(); + let mut output = String::new(); + while let Some(character) = characters.next() { + if character != '\\' { + if character.is_control() { + return Err(WebDriverBiDiTextValueObservationProjectionError::InvalidString); + } + output.push(character); + continue; + } + let escaped = characters + .next() + .ok_or(WebDriverBiDiTextValueObservationProjectionError::InvalidString)?; + match escaped { + '"' => output.push('"'), + '\\' => output.push('\\'), + '/' => output.push('/'), + 'b' => output.push('\u{0008}'), + 'f' => output.push('\u{000c}'), + 'n' => output.push('\n'), + 'r' => output.push('\r'), + 't' => output.push('\t'), + 'u' => { + let first = decode_hex_quad(&mut characters)?; + if (0xd800..=0xdbff).contains(&first) { + if characters.next() != Some('\\') || characters.next() != Some('u') { + return Err(WebDriverBiDiTextValueObservationProjectionError::InvalidString); + } + let second = decode_hex_quad(&mut characters)?; + if !(0xdc00..=0xdfff).contains(&second) { + return Err(WebDriverBiDiTextValueObservationProjectionError::InvalidString); + } + let scalar = 0x1_0000 + + ((u32::from(first) - 0xd800) << 10) + + (u32::from(second) - 0xdc00); + output.push( + char::from_u32(scalar) + .ok_or(WebDriverBiDiTextValueObservationProjectionError::InvalidString)?, + ); + } else if (0xdc00..=0xdfff).contains(&first) { + return Err(WebDriverBiDiTextValueObservationProjectionError::InvalidString); + } else { + output.push( + char::from_u32(u32::from(first)) + .ok_or(WebDriverBiDiTextValueObservationProjectionError::InvalidString)?, + ); + } + } + _ => return Err(WebDriverBiDiTextValueObservationProjectionError::InvalidString), + } + } + Ok(output) +} + +fn decode_hex_quad( + characters: &mut impl Iterator, +) -> Result { + let mut value = 0_u16; + for _ in 0..4 { + let digit = characters + .next() + .and_then(|character| character.to_digit(16)) + .ok_or(WebDriverBiDiTextValueObservationProjectionError::InvalidString)?; + value = (value << 4) | digit as u16; + } + Ok(value) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn projection_accepts_extensions_and_decodes_string_escapes() { + let response = r#"{"type":"success","id":7,"vendor":true,"result":{"realm":"realm-1","type":"success","vendor":{"nested":[1,true,null]},"result":{"value":"A\\\"B\\\\C\\/D\\b\\f\\n\\r\\t\\u20ac\\ud83d\\ude00","type":"string","vendor":0}}}"#; + match project_script_result(response).expect("valid projection") { + ScriptResultProjection::String(value) => { + assert_eq!(value, "A\"B\\C/D\u{0008}\u{000c}\n\r\t€😀") + } + ScriptResultProjection::Exception => panic!("success response projected as exception"), + } + } + + #[test] + fn projection_accepts_typed_script_exception_without_retaining_details() { + let response = r#"{"type":"success","id":8,"result":{"type":"exception","realm":"realm-1","exceptionDetails":{"text":"page-secret","columnNumber":1,"lineNumber":1,"stackTrace":{"callFrames":[]}}}}"#; + assert!(matches!( + project_script_result(response), + Ok(ScriptResultProjection::Exception) + )); + } + + #[test] + fn projection_rejects_unsupported_and_oversized_remote_values() { + let wrong_type = r#"{"type":"success","id":9,"result":{"type":"success","realm":"realm-1","result":{"type":"number","value":1}}}"#; + assert_eq!( + project_script_result(wrong_type).err(), + Some(WebDriverBiDiTextValueObservationProjectionError::UnsupportedRemoteValueType) + ); + + let oversized = "x".repeat(MAX_WEBDRIVER_BIDI_TYPE_TEXT_BYTES + 1); + let response = format!( + "{{\"type\":\"success\",\"id\":10,\"result\":{{\"type\":\"success\",\"realm\":\"realm-1\",\"result\":{{\"type\":\"string\",\"value\":\"{oversized}\"}}}}}}" + ); + assert_eq!( + project_script_result(&response).err(), + Some(WebDriverBiDiTextValueObservationProjectionError::ObservedTextTooLong) + ); + } + + #[test] + fn projection_rejects_missing_duplicate_and_invalid_members() { + let missing = r#"{"type":"success","id":11,"result":{"type":"success","realm":"realm-1"}}"#; + assert_eq!( + project_script_result(missing).err(), + Some(WebDriverBiDiTextValueObservationProjectionError::MissingMember { + member: "result.result" + }) + ); + + let duplicate = r#"{"type":"success","id":12,"result":{"type":"success","type":"success","realm":"realm-1","result":{"type":"string","value":"x"}}}"#; + assert_eq!( + project_script_result(duplicate).err(), + Some(WebDriverBiDiTextValueObservationProjectionError::DuplicateMember) + ); + + let invalid_object = r#"{"type":"success","id":13,"result":false}"#; + assert_eq!( + project_script_result(invalid_object).err(), + Some(WebDriverBiDiTextValueObservationProjectionError::InvalidObject { + member: "result" + }) + ); + + let unsupported = r#"{"type":"success","id":14,"result":{"type":"future","realm":"realm-1"}}"#; + assert_eq!( + project_script_result(unsupported).err(), + Some(WebDriverBiDiTextValueObservationProjectionError::UnsupportedScriptResultType) + ); + } + + #[test] + fn projection_helpers_reject_invalid_strings_values_and_resource_exhaustion() { + assert_eq!( + decode_json_string("not-a-string").err(), + Some(WebDriverBiDiTextValueObservationProjectionError::InvalidString) + ); + assert_eq!( + decode_json_string(r#""\uDC00""#).err(), + Some(WebDriverBiDiTextValueObservationProjectionError::InvalidString) + ); + assert_eq!( + decode_json_string(r#""\uD800x""#).err(), + Some(WebDriverBiDiTextValueObservationProjectionError::InvalidString) + ); + assert_eq!( + decode_json_string(r#""\uD800\u0041""#).err(), + Some(WebDriverBiDiTextValueObservationProjectionError::InvalidString) + ); + assert_eq!( + decode_json_string(r#""\q""#).err(), + Some(WebDriverBiDiTextValueObservationProjectionError::InvalidString) + ); + assert_eq!( + decode_json_string(r#""\u12xz""#).err(), + Some(WebDriverBiDiTextValueObservationProjectionError::InvalidString) + ); + + assert_eq!( + parse_object_members("[]", "root").err(), + Some(WebDriverBiDiTextValueObservationProjectionError::InvalidObject { member: "root" }) + ); + assert_eq!( + parse_object_members("{\"a\":1} trailing", "root").err(), + Some(WebDriverBiDiTextValueObservationProjectionError::InvalidValue) + ); + assert_eq!( + scan_value_end(b"", 0, 0).err(), + Some(WebDriverBiDiTextValueObservationProjectionError::InvalidValue) + ); + assert_eq!( + scan_value_end(b"x", 0, MAX_SCRIPT_RESULT_NESTING_DEPTH).err(), + Some(WebDriverBiDiTextValueObservationProjectionError::NestingTooDeep) + ); + assert_eq!( + scan_container_end(b"[]", 0, b'{', b'}', 1).err(), + Some(WebDriverBiDiTextValueObservationProjectionError::InvalidValue) + ); + assert_eq!( + scan_string_end(b"x", 0).err(), + Some(WebDriverBiDiTextValueObservationProjectionError::InvalidString) + ); + } + + #[test] + fn response_and_projection_errors_are_stable_and_non_sensitive() { + let cases: Vec = vec![ + WebDriverBiDiTextValueObservationResponseError::EmptyExpectedText, + WebDriverBiDiTextValueObservationResponseError::ExpectedTextTooLong, + WebDriverBiDiTextValueObservationResponseError::InvalidExpectedText, + WebDriverBiDiTextValueObservationResponseError::UnexpectedEvent, + WebDriverBiDiTextValueObservationResponseError::RemoteProtocolError { command_id: 7 }, + WebDriverBiDiTextValueObservationResponseError::ScriptException { command_id: 8 }, + ]; + for error in cases { + assert!(error.source().is_none()); + assert!(!error.to_string().is_empty()); + } + + let envelope = WebDriverBiDiTextValueObservationResponseError::Envelope { + source: WebDriverBiDiJsonEnvelopeError::InvalidJson, + }; + assert!(envelope.source().is_some()); + let projection = WebDriverBiDiTextValueObservationResponseError::Projection { + source: WebDriverBiDiTextValueObservationProjectionError::InvalidValue, + }; + assert!(projection.source().is_some()); + let correlation = WebDriverBiDiTextValueObservationResponseError::Correlation { + source: WebDriverBiDiCommandCorrelationError::CommandNotOutstanding, + }; + assert!(correlation.source().is_some()); + + let projection_errors = [ + WebDriverBiDiTextValueObservationProjectionError::InvalidObject { member: "result" }, + WebDriverBiDiTextValueObservationProjectionError::MissingMember { member: "result" }, + WebDriverBiDiTextValueObservationProjectionError::DuplicateMember, + WebDriverBiDiTextValueObservationProjectionError::TooManyMembers, + WebDriverBiDiTextValueObservationProjectionError::MemberNameTooLong, + WebDriverBiDiTextValueObservationProjectionError::InvalidString, + WebDriverBiDiTextValueObservationProjectionError::UnsupportedScriptResultType, + WebDriverBiDiTextValueObservationProjectionError::UnsupportedRemoteValueType, + WebDriverBiDiTextValueObservationProjectionError::ObservedTextTooLong, + WebDriverBiDiTextValueObservationProjectionError::NestingTooDeep, + WebDriverBiDiTextValueObservationProjectionError::InvalidValue, + ]; + for error in projection_errors { + assert!(!error.to_string().is_empty()); + } + } +} From 384643ffbc9b93662393b8b5cd33a6710d3ecc2d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 10:49:58 +0900 Subject: [PATCH 03/64] feat(network): export typed text-value response admission --- crates/originweave-network/src/lib.rs | 35 +++++++++++++++------------ 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/crates/originweave-network/src/lib.rs b/crates/originweave-network/src/lib.rs index 2075dff25..05ff2cbef 100644 --- a/crates/originweave-network/src/lib.rs +++ b/crates/originweave-network/src/lib.rs @@ -10,21 +10,21 @@ //! classifies complete local-end JSON envelopes, tracks bounded command-response //! correlation, transports narrowly typed pointer-click and node-bound non-secret //! text-input actions plus fixed sandboxed text-value observations, admits typed -//! correlated protocol acknowledgments, 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 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. +//! correlated protocol acknowledgments and text-value post-condition comparisons, +//! 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 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)] @@ -48,6 +48,7 @@ mod webdriver_bidi_session_end_response; mod webdriver_bidi_session_status_command; mod webdriver_bidi_session_status_response; mod webdriver_bidi_session_teardown; +mod webdriver_bidi_text_value_observation_response; mod webdriver_bidi_text_value_observation_transport; mod webdriver_bidi_type_text_response; mod webdriver_bidi_type_text_transport; @@ -141,6 +142,10 @@ pub use webdriver_bidi_session_teardown::{ WebDriverBiDiSessionTeardownAssessment, WebDriverBiDiSessionTeardownDisposition, WebDriverBiDiSessionTeardownObservations, }; +pub use webdriver_bidi_text_value_observation_response::{ + WebDriverBiDiTextValueObservationProjectionError, + WebDriverBiDiTextValueObservationResponseError, WebDriverBiDiTextValueObservationResult, +}; pub use webdriver_bidi_text_value_observation_transport::{ WebDriverBiDiTextValueObservationSendError, send_webdriver_bidi_text_value_observation, }; From cecc0ac4255e40606618f523f0bf4aa74b541f3b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 11:05:34 +0900 Subject: [PATCH 04/64] style(network): apply rustfmt to text-value response admission --- ...er_bidi_text_value_observation_response.rs | 120 +++++++++++------- 1 file changed, 76 insertions(+), 44 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs b/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs index 1ac0b8f82..18504e8e6 100644 --- a/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs @@ -60,34 +60,41 @@ impl WebDriverBiDiTextValueObservationResult { correlation: &mut WebDriverBiDiCommandCorrelation, ) -> Result { validate_expected_text(expected_text)?; - let envelope = WebDriverBiDiJsonEnvelope::parse(message) - .map_err(|source| WebDriverBiDiTextValueObservationResponseError::Envelope { source })?; + let envelope = WebDriverBiDiJsonEnvelope::parse(message).map_err(|source| { + WebDriverBiDiTextValueObservationResponseError::Envelope { source } + })?; match envelope.kind() { WebDriverBiDiJsonEnvelopeKind::Event => { Err(WebDriverBiDiTextValueObservationResponseError::UnexpectedEvent) } WebDriverBiDiJsonEnvelopeKind::Error => { - let completed = correlation.correlate_response(&envelope).map_err(|source| { - WebDriverBiDiTextValueObservationResponseError::Correlation { source } - })?; - Err(WebDriverBiDiTextValueObservationResponseError::RemoteProtocolError { - command_id: completed.command_id(), - }) + let completed = correlation + .correlate_response(&envelope) + .map_err(|source| { + WebDriverBiDiTextValueObservationResponseError::Correlation { source } + })?; + Err( + WebDriverBiDiTextValueObservationResponseError::RemoteProtocolError { + command_id: completed.command_id(), + }, + ) } WebDriverBiDiJsonEnvelopeKind::Success => { let projection = project_script_result(message.as_str()).map_err(|source| { WebDriverBiDiTextValueObservationResponseError::Projection { source } })?; - let completed = correlation.correlate_response(&envelope).map_err(|source| { - WebDriverBiDiTextValueObservationResponseError::Correlation { source } - })?; + let completed = correlation + .correlate_response(&envelope) + .map_err(|source| { + WebDriverBiDiTextValueObservationResponseError::Correlation { source } + })?; match projection { - ScriptResultProjection::Exception => { - Err(WebDriverBiDiTextValueObservationResponseError::ScriptException { + ScriptResultProjection::Exception => Err( + WebDriverBiDiTextValueObservationResponseError::ScriptException { command_id: completed.command_id(), - }) - } + }, + ), ScriptResultProjection::String(observed_text) => { let observed_text_bytes = observed_text.len(); let matches_expected_text = observed_text == expected_text; @@ -230,14 +237,27 @@ impl fmt::Display for WebDriverBiDiTextValueObservationProjectionError { match self { Self::InvalidObject { member } => write!(formatter, "invalid object at {member}"), Self::MissingMember { member } => write!(formatter, "missing member {member}"), - Self::DuplicateMember => formatter.write_str("duplicate command-specific result member"), - Self::TooManyMembers => formatter.write_str("command-specific result object has too many members"), - Self::MemberNameTooLong => formatter.write_str("command-specific result member name is too long"), + Self::DuplicateMember => { + formatter.write_str("duplicate command-specific result member") + } + Self::TooManyMembers => { + formatter.write_str("command-specific result object has too many members") + } + Self::MemberNameTooLong => { + formatter.write_str("command-specific result member name is too long") + } Self::InvalidString => formatter.write_str("invalid command-specific JSON string"), - Self::UnsupportedScriptResultType => formatter.write_str("unsupported script result type"), - Self::UnsupportedRemoteValueType => formatter.write_str("text-value observation did not return a string RemoteValue"), - Self::ObservedTextTooLong => formatter.write_str("observed text-value postcondition exceeds the local byte budget"), - Self::NestingTooDeep => formatter.write_str("command-specific result nesting is too deep"), + Self::UnsupportedScriptResultType => { + formatter.write_str("unsupported script result type") + } + Self::UnsupportedRemoteValueType => { + formatter.write_str("text-value observation did not return a string RemoteValue") + } + Self::ObservedTextTooLong => formatter + .write_str("observed text-value postcondition exceeds the local byte budget"), + Self::NestingTooDeep => { + formatter.write_str("command-specific result nesting is too deep") + } Self::InvalidValue => formatter.write_str("invalid command-specific result value"), } } @@ -289,8 +309,7 @@ fn project_script_result( WebDriverBiDiTextValueObservationProjectionError::UnsupportedRemoteValueType, ); } - let observed = - required_string_member(&remote_members, "value", "result.result.value")?; + let observed = required_string_member(&remote_members, "value", "result.result.value")?; if observed.len() > MAX_WEBDRIVER_BIDI_TYPE_TEXT_BYTES { return Err(WebDriverBiDiTextValueObservationProjectionError::ObservedTextTooLong); } @@ -319,9 +338,9 @@ fn required_object_member<'a>( .ok_or(WebDriverBiDiTextValueObservationProjectionError::MissingMember { member: path })?; let trimmed = value.trim(); if !trimmed.starts_with('{') { - return Err(WebDriverBiDiTextValueObservationProjectionError::InvalidObject { - member: path, - }); + return Err( + WebDriverBiDiTextValueObservationProjectionError::InvalidObject { member: path }, + ); } Ok(trimmed) } @@ -345,9 +364,9 @@ fn parse_object_members<'a>( let bytes = text.as_bytes(); let mut index = skip_whitespace(bytes, 0); if bytes.get(index) != Some(&b'{') { - return Err(WebDriverBiDiTextValueObservationProjectionError::InvalidObject { - member: path, - }); + return Err( + WebDriverBiDiTextValueObservationProjectionError::InvalidObject { member: path }, + ); } index += 1; let mut members = Vec::new(); @@ -535,25 +554,31 @@ fn decode_json_string( let first = decode_hex_quad(&mut characters)?; if (0xd800..=0xdbff).contains(&first) { if characters.next() != Some('\\') || characters.next() != Some('u') { - return Err(WebDriverBiDiTextValueObservationProjectionError::InvalidString); + return Err( + WebDriverBiDiTextValueObservationProjectionError::InvalidString, + ); } let second = decode_hex_quad(&mut characters)?; if !(0xdc00..=0xdfff).contains(&second) { - return Err(WebDriverBiDiTextValueObservationProjectionError::InvalidString); + return Err( + WebDriverBiDiTextValueObservationProjectionError::InvalidString, + ); } let scalar = 0x1_0000 + ((u32::from(first) - 0xd800) << 10) + (u32::from(second) - 0xdc00); output.push( - char::from_u32(scalar) - .ok_or(WebDriverBiDiTextValueObservationProjectionError::InvalidString)?, + char::from_u32(scalar).ok_or( + WebDriverBiDiTextValueObservationProjectionError::InvalidString, + )?, ); } else if (0xdc00..=0xdfff).contains(&first) { return Err(WebDriverBiDiTextValueObservationProjectionError::InvalidString); } else { output.push( - char::from_u32(u32::from(first)) - .ok_or(WebDriverBiDiTextValueObservationProjectionError::InvalidString)?, + char::from_u32(u32::from(first)).ok_or( + WebDriverBiDiTextValueObservationProjectionError::InvalidString, + )?, ); } } @@ -624,9 +649,11 @@ mod tests { let missing = r#"{"type":"success","id":11,"result":{"type":"success","realm":"realm-1"}}"#; assert_eq!( project_script_result(missing).err(), - Some(WebDriverBiDiTextValueObservationProjectionError::MissingMember { - member: "result.result" - }) + Some( + WebDriverBiDiTextValueObservationProjectionError::MissingMember { + member: "result.result" + } + ) ); let duplicate = r#"{"type":"success","id":12,"result":{"type":"success","type":"success","realm":"realm-1","result":{"type":"string","value":"x"}}}"#; @@ -638,12 +665,15 @@ mod tests { let invalid_object = r#"{"type":"success","id":13,"result":false}"#; assert_eq!( project_script_result(invalid_object).err(), - Some(WebDriverBiDiTextValueObservationProjectionError::InvalidObject { - member: "result" - }) + Some( + WebDriverBiDiTextValueObservationProjectionError::InvalidObject { + member: "result" + } + ) ); - let unsupported = r#"{"type":"success","id":14,"result":{"type":"future","realm":"realm-1"}}"#; + let unsupported = + r#"{"type":"success","id":14,"result":{"type":"future","realm":"realm-1"}}"#; assert_eq!( project_script_result(unsupported).err(), Some(WebDriverBiDiTextValueObservationProjectionError::UnsupportedScriptResultType) @@ -679,7 +709,9 @@ mod tests { assert_eq!( parse_object_members("[]", "root").err(), - Some(WebDriverBiDiTextValueObservationProjectionError::InvalidObject { member: "root" }) + Some( + WebDriverBiDiTextValueObservationProjectionError::InvalidObject { member: "root" } + ) ); assert_eq!( parse_object_members("{\"a\":1} trailing", "root").err(), From 724e323e1f09c98da4fcd8f5ed3be53ac05d6002 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 11:10:47 +0900 Subject: [PATCH 05/64] test(network): use valid JSON escape fixture for text observation --- .../src/webdriver_bidi_text_value_observation_response.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs b/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs index 18504e8e6..05edd46e9 100644 --- a/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs @@ -608,7 +608,7 @@ mod tests { #[test] fn projection_accepts_extensions_and_decodes_string_escapes() { - let response = r#"{"type":"success","id":7,"vendor":true,"result":{"realm":"realm-1","type":"success","vendor":{"nested":[1,true,null]},"result":{"value":"A\\\"B\\\\C\\/D\\b\\f\\n\\r\\t\\u20ac\\ud83d\\ude00","type":"string","vendor":0}}}"#; + let response = r#"{"type":"success","id":7,"vendor":true,"result":{"realm":"realm-1","type":"success","vendor":{"nested":[1,true,null]},"result":{"value":"A\"B\\C\/D\b\f\n\r\t\u20ac\ud83d\ude00","type":"string","vendor":0}}}"#; match project_script_result(response).expect("valid projection") { ScriptResultProjection::String(value) => { assert_eq!(value, "A\"B\\C/D\u{0008}\u{000c}\n\r\t€😀") From 8861c03606367869e5a94692e0c73f5110230d5b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 12:22:09 +0900 Subject: [PATCH 06/64] test(network): keep projection regression clippy-clean --- ...webdriver_bidi_text_value_observation_response.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs b/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs index 05edd46e9..e93e7a54c 100644 --- a/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs @@ -609,12 +609,12 @@ mod tests { #[test] fn projection_accepts_extensions_and_decodes_string_escapes() { let response = r#"{"type":"success","id":7,"vendor":true,"result":{"realm":"realm-1","type":"success","vendor":{"nested":[1,true,null]},"result":{"value":"A\"B\\C\/D\b\f\n\r\t\u20ac\ud83d\ude00","type":"string","vendor":0}}}"#; - match project_script_result(response).expect("valid projection") { - ScriptResultProjection::String(value) => { - assert_eq!(value, "A\"B\\C/D\u{0008}\u{000c}\n\r\t€😀") - } - ScriptResultProjection::Exception => panic!("success response projected as exception"), - } + let projection = project_script_result(response); + assert!(matches!( + projection, + Ok(ScriptResultProjection::String(ref value)) + if value == "A\"B\\C/D\u{0008}\u{000c}\n\r\t€😀" + )); } #[test] From d897909e2837896b7f0aa0a24f01cacc7d3bc7d0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 12:42:09 +0900 Subject: [PATCH 07/64] fix(network): close BiDi response coverage gaps --- ...er_bidi_text_value_observation_response.rs | 162 ++++++++++++++++-- 1 file changed, 148 insertions(+), 14 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs b/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs index e93e7a54c..2c29ce8c5 100644 --- a/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs @@ -564,22 +564,13 @@ fn decode_json_string( WebDriverBiDiTextValueObservationProjectionError::InvalidString, ); } - let scalar = 0x1_0000 - + ((u32::from(first) - 0xd800) << 10) - + (u32::from(second) - 0xdc00); - output.push( - char::from_u32(scalar).ok_or( - WebDriverBiDiTextValueObservationProjectionError::InvalidString, - )?, - ); + let units = [first, second]; + output.push_str(&String::from_utf16_lossy(&units)); } else if (0xdc00..=0xdfff).contains(&first) { return Err(WebDriverBiDiTextValueObservationProjectionError::InvalidString); } else { - output.push( - char::from_u32(u32::from(first)).ok_or( - WebDriverBiDiTextValueObservationProjectionError::InvalidString, - )?, - ); + let units = [first]; + output.push_str(&String::from_utf16_lossy(&units)); } } _ => return Err(WebDriverBiDiTextValueObservationProjectionError::InvalidString), @@ -589,7 +580,7 @@ fn decode_json_string( } fn decode_hex_quad( - characters: &mut impl Iterator, + characters: &mut std::str::Chars<'_>, ) -> Result { let mut value = 0_u16; for _ in 0..4 { @@ -735,6 +726,149 @@ mod tests { ); } + #[test] + fn expected_text_validation_covers_budget_and_injection_policy() { + assert!(matches!( + validate_expected_text(""), + Err(WebDriverBiDiTextValueObservationResponseError::EmptyExpectedText) + )); + let oversized = "x".repeat(MAX_WEBDRIVER_BIDI_TYPE_TEXT_BYTES + 1); + assert!(matches!( + validate_expected_text(&oversized), + Err(WebDriverBiDiTextValueObservationResponseError::ExpectedTextTooLong) + )); + assert!(validate_expected_text("ordinary space").is_ok()); + assert!(matches!( + validate_expected_text("tab\tvalue"), + Err(WebDriverBiDiTextValueObservationResponseError::InvalidExpectedText) + )); + assert!(matches!( + validate_expected_text("control\u{0001}"), + Err(WebDriverBiDiTextValueObservationResponseError::InvalidExpectedText) + )); + assert!(matches!( + validate_expected_text("bidi\u{202e}override"), + Err(WebDriverBiDiTextValueObservationResponseError::InvalidExpectedText) + )); + } + + #[test] + fn projection_helpers_cover_structural_and_terminal_failures() { + let missing_exception_details = + r#"{"type":"success","id":15,"result":{"type":"exception","realm":"realm-1"}}"#; + assert_eq!( + project_script_result(missing_exception_details).err(), + Some( + WebDriverBiDiTextValueObservationProjectionError::MissingMember { + member: "result.exceptionDetails" + } + ) + ); + let invalid_exception_details = r#"{"type":"success","id":16,"result":{"type":"exception","realm":"realm-1","exceptionDetails":false}}"#; + assert_eq!( + project_script_result(invalid_exception_details).err(), + Some( + WebDriverBiDiTextValueObservationProjectionError::InvalidObject { + member: "result.exceptionDetails" + } + ) + ); + + let many_members = (0..=MAX_SCRIPT_RESULT_OBJECT_MEMBERS) + .map(|index| format!("\"k{index}\":0")) + .collect::>() + .join(","); + let too_many = format!("{{{many_members}}}"); + assert_eq!( + parse_object_members(&too_many, "root").err(), + Some(WebDriverBiDiTextValueObservationProjectionError::TooManyMembers) + ); + + let long_name = "k".repeat(MAX_SCRIPT_RESULT_MEMBER_NAME_BYTES + 1); + let long_member = format!("{{\"{long_name}\":0}}"); + assert_eq!( + parse_object_members(&long_member, "root").err(), + Some(WebDriverBiDiTextValueObservationProjectionError::MemberNameTooLong) + ); + assert_eq!( + parse_object_members(r#"{"a" 1}"#, "root").err(), + Some(WebDriverBiDiTextValueObservationProjectionError::InvalidValue) + ); + assert_eq!( + parse_object_members(r#"{"a":1]"#, "root").err(), + Some(WebDriverBiDiTextValueObservationProjectionError::InvalidValue) + ); + + assert_eq!(scan_value_end(b"[]", 0, 0), Ok(2)); + assert_eq!( + scan_value_end(b",", 0, 0).err(), + Some(WebDriverBiDiTextValueObservationProjectionError::InvalidValue) + ); + assert_eq!( + scan_container_end( + b"{}", + 0, + b'{', + b'}', + MAX_SCRIPT_RESULT_NESTING_DEPTH + 1 + ) + .err(), + Some(WebDriverBiDiTextValueObservationProjectionError::NestingTooDeep) + ); + + let deep_objects = "{".repeat(MAX_SCRIPT_RESULT_NESTING_DEPTH + 1); + assert_eq!( + scan_container_end(deep_objects.as_bytes(), 0, b'{', b'}', 1).err(), + Some(WebDriverBiDiTextValueObservationProjectionError::NestingTooDeep) + ); + let deep_arrays = "[".repeat(MAX_SCRIPT_RESULT_NESTING_DEPTH + 1); + assert_eq!( + scan_container_end(deep_arrays.as_bytes(), 0, b'[', b']', 1).err(), + Some(WebDriverBiDiTextValueObservationProjectionError::NestingTooDeep) + ); + assert_eq!( + scan_container_end(b"{]", 0, b'{', b'}', 1).err(), + Some(WebDriverBiDiTextValueObservationProjectionError::InvalidValue) + ); + assert_eq!( + scan_container_end(b"{", 0, b'{', b'}', 1).err(), + Some(WebDriverBiDiTextValueObservationProjectionError::InvalidValue) + ); + + assert_eq!( + scan_string_end(b"\"\\", 0).err(), + Some(WebDriverBiDiTextValueObservationProjectionError::InvalidString) + ); + assert_eq!( + scan_string_end(b"\"\x01\"", 0).err(), + Some(WebDriverBiDiTextValueObservationProjectionError::InvalidString) + ); + assert_eq!( + scan_string_end(b"\"x", 0).err(), + Some(WebDriverBiDiTextValueObservationProjectionError::InvalidString) + ); + + assert_eq!( + decode_json_string("\"").err(), + Some(WebDriverBiDiTextValueObservationProjectionError::InvalidString) + ); + assert_eq!( + decode_json_string("\"x").err(), + Some(WebDriverBiDiTextValueObservationProjectionError::InvalidString) + ); + let raw_control = format!("\"{}\"", '\u{0001}'); + assert_eq!( + decode_json_string(&raw_control).err(), + Some(WebDriverBiDiTextValueObservationProjectionError::InvalidString) + ); + assert_eq!(decode_json_string(r#""\uD83D\uDE00""#).as_deref(), Ok("😀")); + assert_eq!(decode_json_string(r#""\u20AC""#).as_deref(), Ok("€")); + assert_eq!( + decode_json_string(r#""\u12""#).err(), + Some(WebDriverBiDiTextValueObservationProjectionError::InvalidString) + ); + } + #[test] fn response_and_projection_errors_are_stable_and_non_sensitive() { let cases: Vec = vec![ From 8357e9ed62912361fb10d3bf2415e17c2baca30e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 12:43:07 +0900 Subject: [PATCH 08/64] test(network): exercise BiDi response failure paths --- ...er_bidi_text_value_observation_response.rs | 157 +++++++++++++++++- 1 file changed, 154 insertions(+), 3 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs b/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs index aa2370d6a..1b1b5b72e 100644 --- a/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs +++ b/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs @@ -17,9 +17,10 @@ use originweave_core::{ }; use originweave_network::{ WebDriverBiDiCommandCorrelation, WebDriverBiDiTcpConnectionPlan, - WebDriverBiDiTextValueObservationResult, WebDriverBiDiWebSocketClientKey, - WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, - WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, + WebDriverBiDiTextValueObservationResponseError, WebDriverBiDiTextValueObservationResult, + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler, + WebDriverBiDiWebSocketMessageAssembly, WebDriverBiDiWebSocketTextMessage, send_webdriver_bidi_text_value_observation, }; @@ -165,6 +166,46 @@ fn write_text_frame(stream: &mut TcpStream, payload: &[u8]) -> io::Result<()> { stream.write_all(payload) } +fn receive_server_text(payload: &[u8]) -> Result> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let response = payload.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_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 (_established, frame) = established.read_frame(Duration::from_millis(500))?; + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); + let message = match assembler.push_frame(frame)? { + WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + other => { + return Err(io::Error::other(format!( + "fixture produced unexpected message assembly state: {other:?}" + )) + .into()); + } + }; + server + .join() + .map_err(|_| io::Error::other("response fixture server panicked"))??; + Ok(message) +} + #[test] fn observed_text_postcondition_consumes_exact_command_without_exposing_page_text() -> Result<(), Box> { @@ -252,3 +293,113 @@ fn observed_text_postcondition_consumes_exact_command_without_exposing_page_text .map_err(|_| io::Error::other("text-value observation response server panicked"))??; Ok(()) } + +#[test] +fn response_admission_failures_preserve_or_consume_exact_correlation_state() +-> Result<(), Box> { + let invalid = receive_server_text(b"not-json")?; + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + correlation.register_command(70)?; + assert!(matches!( + WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( + &invalid, + "expected", + &mut correlation, + ), + Err(WebDriverBiDiTextValueObservationResponseError::Envelope { .. }) + )); + assert_eq!(correlation.outstanding_count(), 1); + + let event = receive_server_text( + br#"{"type":"event","method":"log.entryAdded","params":{}}"#, + )?; + assert!(matches!( + WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( + &event, + "expected", + &mut correlation, + ), + Err(WebDriverBiDiTextValueObservationResponseError::UnexpectedEvent) + )); + assert_eq!(correlation.outstanding_count(), 1); + + correlation.register_command(71)?; + let protocol_error = receive_server_text( + br#"{"type":"error","id":71,"error":"unknown error","message":"remote failure"}"#, + )?; + assert!(matches!( + WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( + &protocol_error, + "expected", + &mut correlation, + ), + Err(WebDriverBiDiTextValueObservationResponseError::RemoteProtocolError { + command_id: 71 + }) + )); + assert_eq!(correlation.outstanding_count(), 1); + + let unknown_success = receive_server_text( + br#"{"type":"success","id":72,"result":{"type":"success","realm":"realm-1","result":{"type":"string","value":"expected"}}}"#, + )?; + assert!(matches!( + WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( + &unknown_success, + "expected", + &mut correlation, + ), + Err(WebDriverBiDiTextValueObservationResponseError::Correlation { .. }) + )); + assert_eq!(correlation.outstanding_count(), 1); + + correlation.register_command(73)?; + let malformed_projection = receive_server_text( + br#"{"type":"success","id":73,"result":{"type":"success","result":{"type":"string","value":"expected"}}}"#, + )?; + assert!(matches!( + WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( + &malformed_projection, + "expected", + &mut correlation, + ), + Err(WebDriverBiDiTextValueObservationResponseError::Projection { .. }) + )); + assert_eq!(correlation.outstanding_count(), 2); + + let script_exception = receive_server_text( + br#"{"type":"success","id":73,"result":{"type":"exception","realm":"realm-1","exceptionDetails":{}}}"#, + )?; + assert!(matches!( + WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( + &script_exception, + "expected", + &mut correlation, + ), + Err(WebDriverBiDiTextValueObservationResponseError::ScriptException { + command_id: 73 + }) + )); + assert_eq!(correlation.outstanding_count(), 1); + + let valid_success = receive_server_text( + br#"{"type":"success","id":70,"result":{"type":"success","realm":"realm-1","result":{"type":"string","value":"expected"}}}"#, + )?; + assert!(matches!( + WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( + &valid_success, + "bad\ttext", + &mut correlation, + ), + Err(WebDriverBiDiTextValueObservationResponseError::InvalidExpectedText) + )); + assert_eq!(correlation.outstanding_count(), 1); + + let result = WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( + &valid_success, + "expected", + &mut correlation, + )?; + assert!(result.matches_expected_text()); + assert_eq!(correlation.outstanding_count(), 0); + Ok(()) +} From 9a27068f22dc8799359b56ae0acbf3bdd59f7d5d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 12:48:22 +0900 Subject: [PATCH 09/64] style(network): apply canonical rustfmt diagnostics --- .../webdriver_bidi_text_value_observation_response.rs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs b/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs index 2c29ce8c5..6cf987765 100644 --- a/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs @@ -805,14 +805,7 @@ mod tests { Some(WebDriverBiDiTextValueObservationProjectionError::InvalidValue) ); assert_eq!( - scan_container_end( - b"{}", - 0, - b'{', - b'}', - MAX_SCRIPT_RESULT_NESTING_DEPTH + 1 - ) - .err(), + scan_container_end(b"{}", 0, b'{', b'}', MAX_SCRIPT_RESULT_NESTING_DEPTH + 1).err(), Some(WebDriverBiDiTextValueObservationProjectionError::NestingTooDeep) ); From 11d65881634b34aaca9f380a31ae52166ad8057c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 12:49:28 +0900 Subject: [PATCH 10/64] style(network): apply remaining rustfmt diagnostics --- ...river_bidi_text_value_observation_response.rs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs b/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs index 1b1b5b72e..cbdc29b20 100644 --- a/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs +++ b/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs @@ -166,7 +166,9 @@ fn write_text_frame(stream: &mut TcpStream, payload: &[u8]) -> io::Result<()> { stream.write_all(payload) } -fn receive_server_text(payload: &[u8]) -> Result> { +fn receive_server_text( + payload: &[u8], +) -> Result> { let listener = TcpListener::bind(("127.0.0.1", 0))?; let local_addr = listener.local_addr()?; let response = payload.to_vec(); @@ -310,9 +312,7 @@ fn response_admission_failures_preserve_or_consume_exact_correlation_state() )); assert_eq!(correlation.outstanding_count(), 1); - let event = receive_server_text( - br#"{"type":"event","method":"log.entryAdded","params":{}}"#, - )?; + let event = receive_server_text(br#"{"type":"event","method":"log.entryAdded","params":{}}"#)?; assert!(matches!( WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( &event, @@ -333,9 +333,7 @@ fn response_admission_failures_preserve_or_consume_exact_correlation_state() "expected", &mut correlation, ), - Err(WebDriverBiDiTextValueObservationResponseError::RemoteProtocolError { - command_id: 71 - }) + Err(WebDriverBiDiTextValueObservationResponseError::RemoteProtocolError { command_id: 71 }) )); assert_eq!(correlation.outstanding_count(), 1); @@ -375,9 +373,7 @@ fn response_admission_failures_preserve_or_consume_exact_correlation_state() "expected", &mut correlation, ), - Err(WebDriverBiDiTextValueObservationResponseError::ScriptException { - command_id: 73 - }) + Err(WebDriverBiDiTextValueObservationResponseError::ScriptException { command_id: 73 }) )); assert_eq!(correlation.outstanding_count(), 1); From 8c09f1aeb008f1b973f6a35455c1df1bf7c2be95 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 16:08:50 +0900 Subject: [PATCH 11/64] test(network): cover observation error correlation --- ...bidi_text_value_observation_correlation.rs | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_text_value_observation_correlation.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_text_value_observation_correlation.rs b/crates/originweave-network/tests/webdriver_bidi_text_value_observation_correlation.rs new file mode 100644 index 000000000..4b8bd257d --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_text_value_observation_correlation.rs @@ -0,0 +1,160 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::WebDriverBiDiWebSocketEndpoint; +use originweave_network::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiTextValueObservationResponseError, WebDriverBiDiTextValueObservationResult, + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, + WebDriverBiDiWebSocketTextMessage, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; +const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; +const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; + +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_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 response unexpectedly required 64-bit framing", + )); + } + } + stream.write_all(payload) +} + +fn receive_server_text( + payload: &[u8], +) -> Result> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let response = payload.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_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 (_established, frame) = established.read_frame(Duration::from_millis(500))?; + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); + let message = match assembler.push_frame(frame)? { + WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + other => { + return Err(io::Error::other(format!( + "fixture produced unexpected message assembly state: {other:?}" + )) + .into()); + } + }; + server + .join() + .map_err(|_| io::Error::other("response fixture server panicked"))??; + Ok(message) +} + +fn require_observation_error( + message: &WebDriverBiDiWebSocketTextMessage, + correlation: &mut WebDriverBiDiCommandCorrelation, +) -> Result> { + match WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( + message, + "expected", + correlation, + ) { + Err(error) => Ok(error), + Ok(_) => Err(io::Error::other("fixture unexpectedly admitted an invalid response").into()), + } +} + +#[test] +fn protocol_error_correlation_and_diagnostics_fail_closed_without_consuming_other_state() +-> Result<(), Box> { + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + correlation.register_command(70)?; + + let unknown_protocol_error = receive_server_text( + br#"{"type":"error","id":71,"error":"unknown error","message":"remote failure"}"#, + )?; + let correlation_error = require_observation_error(&unknown_protocol_error, &mut correlation)?; + assert!(matches!( + &correlation_error, + WebDriverBiDiTextValueObservationResponseError::Correlation { .. } + )); + assert_eq!( + correlation_error.to_string(), + "WebDriver BiDi text-value observation response correlation failed" + ); + assert_eq!(correlation.outstanding_count(), 1); + + let invalid_envelope = receive_server_text(b"not-json")?; + let envelope_error = require_observation_error(&invalid_envelope, &mut correlation)?; + assert!(matches!( + &envelope_error, + WebDriverBiDiTextValueObservationResponseError::Envelope { .. } + )); + assert_eq!( + envelope_error.to_string(), + "WebDriver BiDi text-value observation envelope is invalid" + ); + assert_eq!(correlation.outstanding_count(), 1); + + let malformed_projection = receive_server_text( + br#"{"type":"success","id":70,"result":{"type":"success","result":{"type":"string","value":"expected"}}}"#, + )?; + let projection_error = require_observation_error(&malformed_projection, &mut correlation)?; + assert!(matches!( + &projection_error, + WebDriverBiDiTextValueObservationResponseError::Projection { .. } + )); + assert_eq!( + projection_error.to_string(), + "WebDriver BiDi text-value observation result is invalid" + ); + assert_eq!(correlation.outstanding_count(), 1); + + Ok(()) +} From 0ba5a4d12425887ba42e9388109f5fbc8e4424d4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 16:16:07 +0900 Subject: [PATCH 12/64] test(network): cover expected text rejection paths --- ...bidi_text_value_observation_correlation.rs | 55 ++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_text_value_observation_correlation.rs b/crates/originweave-network/tests/webdriver_bidi_text_value_observation_correlation.rs index 4b8bd257d..61862b71d 100644 --- a/crates/originweave-network/tests/webdriver_bidi_text_value_observation_correlation.rs +++ b/crates/originweave-network/tests/webdriver_bidi_text_value_observation_correlation.rs @@ -6,7 +6,7 @@ use std::{ time::Duration, }; -use originweave_core::WebDriverBiDiWebSocketEndpoint; +use originweave_core::{MAX_WEBDRIVER_BIDI_TYPE_TEXT_BYTES, WebDriverBiDiWebSocketEndpoint}; use originweave_network::{ WebDriverBiDiCommandCorrelation, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiTextValueObservationResponseError, WebDriverBiDiTextValueObservationResult, @@ -110,6 +110,21 @@ fn require_observation_error( } } +fn require_expected_text_error( + message: &WebDriverBiDiWebSocketTextMessage, + expected_text: &str, + correlation: &mut WebDriverBiDiCommandCorrelation, +) -> Result> { + match WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( + message, + expected_text, + correlation, + ) { + Err(error) => Ok(error), + Ok(_) => Err(io::Error::other("fixture unexpectedly admitted invalid expected text").into()), + } +} + #[test] fn protocol_error_correlation_and_diagnostics_fail_closed_without_consuming_other_state() -> Result<(), Box> { @@ -158,3 +173,41 @@ fn protocol_error_correlation_and_diagnostics_fail_closed_without_consuming_othe Ok(()) } + +#[test] +fn invalid_expected_text_fails_before_response_or_correlation_state_is_touched() +-> Result<(), Box> { + let invalid_envelope = receive_server_text(b"not-json")?; + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + correlation.register_command(70)?; + + let empty_error = + require_expected_text_error(&invalid_envelope, "", &mut correlation)?; + assert!(matches!( + empty_error, + WebDriverBiDiTextValueObservationResponseError::EmptyExpectedText + )); + assert_eq!(correlation.outstanding_count(), 1); + + let oversized = "x".repeat(MAX_WEBDRIVER_BIDI_TYPE_TEXT_BYTES + 1); + let oversized_error = + require_expected_text_error(&invalid_envelope, &oversized, &mut correlation)?; + assert!(matches!( + oversized_error, + WebDriverBiDiTextValueObservationResponseError::ExpectedTextTooLong + )); + assert_eq!(correlation.outstanding_count(), 1); + + let control_error = require_expected_text_error( + &invalid_envelope, + "bad\u{0001}value", + &mut correlation, + )?; + assert!(matches!( + control_error, + WebDriverBiDiTextValueObservationResponseError::InvalidExpectedText + )); + assert_eq!(correlation.outstanding_count(), 1); + + Ok(()) +} From 57af3c8fdae45d2f9448991b7d7fbffa35661848 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 16:17:37 +0900 Subject: [PATCH 13/64] style(network): apply canonical rustfmt --- ...iver_bidi_text_value_observation_correlation.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_text_value_observation_correlation.rs b/crates/originweave-network/tests/webdriver_bidi_text_value_observation_correlation.rs index 61862b71d..25deaea42 100644 --- a/crates/originweave-network/tests/webdriver_bidi_text_value_observation_correlation.rs +++ b/crates/originweave-network/tests/webdriver_bidi_text_value_observation_correlation.rs @@ -121,7 +121,9 @@ fn require_expected_text_error( correlation, ) { Err(error) => Ok(error), - Ok(_) => Err(io::Error::other("fixture unexpectedly admitted invalid expected text").into()), + Ok(_) => { + Err(io::Error::other("fixture unexpectedly admitted invalid expected text").into()) + } } } @@ -181,8 +183,7 @@ fn invalid_expected_text_fails_before_response_or_correlation_state_is_touched() let mut correlation = WebDriverBiDiCommandCorrelation::new(); correlation.register_command(70)?; - let empty_error = - require_expected_text_error(&invalid_envelope, "", &mut correlation)?; + let empty_error = require_expected_text_error(&invalid_envelope, "", &mut correlation)?; assert!(matches!( empty_error, WebDriverBiDiTextValueObservationResponseError::EmptyExpectedText @@ -198,11 +199,8 @@ fn invalid_expected_text_fails_before_response_or_correlation_state_is_touched() )); assert_eq!(correlation.outstanding_count(), 1); - let control_error = require_expected_text_error( - &invalid_envelope, - "bad\u{0001}value", - &mut correlation, - )?; + let control_error = + require_expected_text_error(&invalid_envelope, "bad\u{0001}value", &mut correlation)?; assert!(matches!( control_error, WebDriverBiDiTextValueObservationResponseError::InvalidExpectedText From 67d9046885b5418eb932843445f353ec14b7fb41 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 16:47:17 +0900 Subject: [PATCH 14/64] test(network): close typed-text coverage obligations --- ...er_bidi_text_value_observation_response.rs | 42 +++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs b/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs index 6cf987765..8d794ef56 100644 --- a/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs @@ -171,14 +171,26 @@ impl fmt::Display for WebDriverBiDiTextValueObservationResponseError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str(match self { Self::EmptyExpectedText => "expected text-value postcondition must not be empty", - Self::ExpectedTextTooLong => "expected text-value postcondition exceeds the local byte budget", - Self::InvalidExpectedText => "expected text-value postcondition contains a disallowed character", + Self::ExpectedTextTooLong => { + "expected text-value postcondition exceeds the local byte budget" + } + Self::InvalidExpectedText => { + "expected text-value postcondition contains a disallowed character" + } Self::Envelope { .. } => "WebDriver BiDi text-value observation envelope is invalid", - Self::UnexpectedEvent => "WebDriver BiDi text-value observation received an event instead of a command response", + Self::UnexpectedEvent => { + "WebDriver BiDi text-value observation received an event instead of a command response" + } Self::Projection { .. } => "WebDriver BiDi text-value observation result is invalid", - Self::Correlation { .. } => "WebDriver BiDi text-value observation response correlation failed", - Self::RemoteProtocolError { .. } => "WebDriver BiDi text-value observation returned a protocol error", - Self::ScriptException { .. } => "WebDriver BiDi text-value observation returned a script exception", + Self::Correlation { .. } => { + "WebDriver BiDi text-value observation response correlation failed" + } + Self::RemoteProtocolError { .. } => { + "WebDriver BiDi text-value observation returned a protocol error" + } + Self::ScriptException { .. } => { + "WebDriver BiDi text-value observation returned a script exception" + } }) } } @@ -284,6 +296,7 @@ fn validate_expected_text( Ok(()) } +#[derive(Debug, Eq, PartialEq)] enum ScriptResultProjection { String(String), Exception, @@ -600,12 +613,12 @@ mod tests { #[test] fn projection_accepts_extensions_and_decodes_string_escapes() { let response = r#"{"type":"success","id":7,"vendor":true,"result":{"realm":"realm-1","type":"success","vendor":{"nested":[1,true,null]},"result":{"value":"A\"B\\C\/D\b\f\n\r\t\u20ac\ud83d\ude00","type":"string","vendor":0}}}"#; - let projection = project_script_result(response); - assert!(matches!( - projection, - Ok(ScriptResultProjection::String(ref value)) - if value == "A\"B\\C/D\u{0008}\u{000c}\n\r\t€😀" - )); + assert_eq!( + project_script_result(response), + Ok(ScriptResultProjection::String( + "A\"B\\C/D\u{0008}\u{000c}\n\r\t€😀".to_owned(), + )) + ); } #[test] @@ -685,6 +698,10 @@ mod tests { decode_json_string(r#""\uD800x""#).err(), Some(WebDriverBiDiTextValueObservationProjectionError::InvalidString) ); + assert_eq!( + decode_json_string(r#""\uD800\x""#).err(), + Some(WebDriverBiDiTextValueObservationProjectionError::InvalidString) + ); assert_eq!( decode_json_string(r#""\uD800\u0041""#).err(), Some(WebDriverBiDiTextValueObservationProjectionError::InvalidString) @@ -800,6 +817,7 @@ mod tests { ); assert_eq!(scan_value_end(b"[]", 0, 0), Ok(2)); + assert_eq!(scan_value_end(b"terminal", 0, 0), Ok(b"terminal".len())); assert_eq!( scan_value_end(b",", 0, 0).err(), Some(WebDriverBiDiTextValueObservationProjectionError::InvalidValue) From 26b455d3e145802a40c58b39a41d6b694f1095e8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 16:54:47 +0900 Subject: [PATCH 15/64] test(network): cover malformed text observation paths --- ...er_bidi_text_value_observation_response.rs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs b/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs index 8d794ef56..b6c20193b 100644 --- a/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs @@ -702,6 +702,10 @@ mod tests { decode_json_string(r#""\uD800\x""#).err(), Some(WebDriverBiDiTextValueObservationProjectionError::InvalidString) ); + assert_eq!( + decode_json_string(r#""\uD800\u12xz""#).err(), + Some(WebDriverBiDiTextValueObservationProjectionError::InvalidString) + ); assert_eq!( decode_json_string(r#""\uD800\u0041""#).err(), Some(WebDriverBiDiTextValueObservationProjectionError::InvalidString) @@ -846,6 +850,19 @@ mod tests { Some(WebDriverBiDiTextValueObservationProjectionError::InvalidValue) ); + let unterminated_escaped_string_container = [b'{', b'"', b'\\']; + assert_eq!( + scan_container_end( + &unterminated_escaped_string_container, + 0, + b'{', + b'}', + 1, + ) + .err(), + Some(WebDriverBiDiTextValueObservationProjectionError::InvalidString) + ); + assert_eq!( scan_string_end(b"\"\\", 0).err(), Some(WebDriverBiDiTextValueObservationProjectionError::InvalidString) @@ -867,6 +884,11 @@ mod tests { decode_json_string("\"x").err(), Some(WebDriverBiDiTextValueObservationProjectionError::InvalidString) ); + let dangling_escape_json_string = format!("{}{}{}", '"', '\\', '"'); + assert_eq!( + decode_json_string(&dangling_escape_json_string).err(), + Some(WebDriverBiDiTextValueObservationProjectionError::InvalidString) + ); let raw_control = format!("\"{}\"", '\u{0001}'); assert_eq!( decode_json_string(&raw_control).err(), From edc4a7b5e34ef20aadf5f0abcb6beea33e0f620b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 17:01:37 +0900 Subject: [PATCH 16/64] test(network): replace non-causal coverage probes --- ...er_bidi_text_value_observation_response.rs | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs b/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs index b6c20193b..8d794ef56 100644 --- a/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs @@ -702,10 +702,6 @@ mod tests { decode_json_string(r#""\uD800\x""#).err(), Some(WebDriverBiDiTextValueObservationProjectionError::InvalidString) ); - assert_eq!( - decode_json_string(r#""\uD800\u12xz""#).err(), - Some(WebDriverBiDiTextValueObservationProjectionError::InvalidString) - ); assert_eq!( decode_json_string(r#""\uD800\u0041""#).err(), Some(WebDriverBiDiTextValueObservationProjectionError::InvalidString) @@ -850,19 +846,6 @@ mod tests { Some(WebDriverBiDiTextValueObservationProjectionError::InvalidValue) ); - let unterminated_escaped_string_container = [b'{', b'"', b'\\']; - assert_eq!( - scan_container_end( - &unterminated_escaped_string_container, - 0, - b'{', - b'}', - 1, - ) - .err(), - Some(WebDriverBiDiTextValueObservationProjectionError::InvalidString) - ); - assert_eq!( scan_string_end(b"\"\\", 0).err(), Some(WebDriverBiDiTextValueObservationProjectionError::InvalidString) @@ -884,11 +867,6 @@ mod tests { decode_json_string("\"x").err(), Some(WebDriverBiDiTextValueObservationProjectionError::InvalidString) ); - let dangling_escape_json_string = format!("{}{}{}", '"', '\\', '"'); - assert_eq!( - decode_json_string(&dangling_escape_json_string).err(), - Some(WebDriverBiDiTextValueObservationProjectionError::InvalidString) - ); let raw_control = format!("\"{}\"", '\u{0001}'); assert_eq!( decode_json_string(&raw_control).err(), From f8ed02a6137bfa932d62e7d9f76a30089b9b769e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 17:14:14 +0900 Subject: [PATCH 17/64] test(network): cover malformed observation propagation --- ...er_bidi_text_value_observation_response.rs | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs b/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs index 8d794ef56..c1fb39d58 100644 --- a/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs @@ -880,6 +880,67 @@ mod tests { ); } + #[test] + fn projection_propagates_malformed_nested_json_without_consuming_detail() { + assert_eq!( + project_script_result("[]").err(), + Some( + WebDriverBiDiTextValueObservationProjectionError::InvalidObject { + member: "response" + } + ) + ); + + let invalid_result_type = + r#"{"type":"success","id":17,"result":{"type":"\q","realm":"realm-1"}}"#; + assert_eq!( + project_script_result(invalid_result_type).err(), + Some(WebDriverBiDiTextValueObservationProjectionError::InvalidString) + ); + + let malformed_remote = r#"{"type":"success","id":18,"result":{"type":"success","realm":"realm-1","result":{"type" 1}}}"#; + assert_eq!( + project_script_result(malformed_remote).err(), + Some(WebDriverBiDiTextValueObservationProjectionError::InvalidValue) + ); + + let invalid_remote_type = r#"{"type":"success","id":19,"result":{"type":"success","realm":"realm-1","result":{"type":"\q","value":"x"}}}"#; + assert_eq!( + project_script_result(invalid_remote_type).err(), + Some(WebDriverBiDiTextValueObservationProjectionError::InvalidString) + ); + + let invalid_remote_value = r#"{"type":"success","id":20,"result":{"type":"success","realm":"realm-1","result":{"type":"string","value":"\q"}}}"#; + assert_eq!( + project_script_result(invalid_remote_value).err(), + Some(WebDriverBiDiTextValueObservationProjectionError::InvalidString) + ); + + assert_eq!( + parse_object_members(r#"{"unterminated}"#, "root").err(), + Some(WebDriverBiDiTextValueObservationProjectionError::InvalidString) + ); + assert_eq!( + parse_object_members(r#"{"\q":1}"#, "root").err(), + Some(WebDriverBiDiTextValueObservationProjectionError::InvalidString) + ); + assert_eq!( + parse_object_members(r#"{"a":"#, "root").err(), + Some(WebDriverBiDiTextValueObservationProjectionError::InvalidValue) + ); + + assert_eq!( + scan_container_end(b"{\"unterminated", 0, b'{', b'}', 1).err(), + Some(WebDriverBiDiTextValueObservationProjectionError::InvalidString) + ); + + let dangling_escape_json_string = format!("{}{}{}", '"', '\\', '"'); + assert_eq!( + decode_json_string(&dangling_escape_json_string).err(), + Some(WebDriverBiDiTextValueObservationProjectionError::InvalidString) + ); + } + #[test] fn response_and_projection_errors_are_stable_and_non_sensitive() { let cases: Vec = vec![ From 64c85887e7e96152200496fd88a501f1343251c6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 17:29:40 +0900 Subject: [PATCH 18/64] test(network): close exact observation coverage --- ...er_bidi_text_value_observation_response.rs | 52 ++++++++++--------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs b/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs index c1fb39d58..750dc095f 100644 --- a/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs @@ -624,10 +624,7 @@ mod tests { #[test] fn projection_accepts_typed_script_exception_without_retaining_details() { let response = r#"{"type":"success","id":8,"result":{"type":"exception","realm":"realm-1","exceptionDetails":{"text":"page-secret","columnNumber":1,"lineNumber":1,"stackTrace":{"callFrames":[]}}}}"#; - assert!(matches!( - project_script_result(response), - Ok(ScriptResultProjection::Exception) - )); + assert_eq!(project_script_result(response), Ok(ScriptResultProjection::Exception)); } #[test] @@ -702,6 +699,10 @@ mod tests { decode_json_string(r#""\uD800\x""#).err(), Some(WebDriverBiDiTextValueObservationProjectionError::InvalidString) ); + assert_eq!( + decode_json_string(r#""\uD800\u12xz""#).err(), + Some(WebDriverBiDiTextValueObservationProjectionError::InvalidString) + ); assert_eq!( decode_json_string(r#""\uD800\u0041""#).err(), Some(WebDriverBiDiTextValueObservationProjectionError::InvalidString) @@ -745,28 +746,31 @@ mod tests { #[test] fn expected_text_validation_covers_budget_and_injection_policy() { - assert!(matches!( - validate_expected_text(""), - Err(WebDriverBiDiTextValueObservationResponseError::EmptyExpectedText) - )); + assert_eq!( + validate_expected_text("") + .err() + .map(|error| error.to_string()) + .as_deref(), + Some("expected text-value postcondition must not be empty") + ); let oversized = "x".repeat(MAX_WEBDRIVER_BIDI_TYPE_TEXT_BYTES + 1); - assert!(matches!( - validate_expected_text(&oversized), - Err(WebDriverBiDiTextValueObservationResponseError::ExpectedTextTooLong) - )); + assert_eq!( + validate_expected_text(&oversized) + .err() + .map(|error| error.to_string()) + .as_deref(), + Some("expected text-value postcondition exceeds the local byte budget") + ); assert!(validate_expected_text("ordinary space").is_ok()); - assert!(matches!( - validate_expected_text("tab\tvalue"), - Err(WebDriverBiDiTextValueObservationResponseError::InvalidExpectedText) - )); - assert!(matches!( - validate_expected_text("control\u{0001}"), - Err(WebDriverBiDiTextValueObservationResponseError::InvalidExpectedText) - )); - assert!(matches!( - validate_expected_text("bidi\u{202e}override"), - Err(WebDriverBiDiTextValueObservationResponseError::InvalidExpectedText) - )); + for rejected in ["tab\tvalue", "control\u{0001}", "bidi\u{202e}override"] { + assert_eq!( + validate_expected_text(rejected) + .err() + .map(|error| error.to_string()) + .as_deref(), + Some("expected text-value postcondition contains a disallowed character") + ); + } } #[test] From 14ab832040dd443541b229fa494d0a3a14820be9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 17:43:10 +0900 Subject: [PATCH 19/64] test(network): close observation coverage instrumentation --- ...er_bidi_text_value_observation_response.rs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs b/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs index 750dc095f..0a8f2a039 100644 --- a/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs @@ -624,7 +624,10 @@ mod tests { #[test] fn projection_accepts_typed_script_exception_without_retaining_details() { let response = r#"{"type":"success","id":8,"result":{"type":"exception","realm":"realm-1","exceptionDetails":{"text":"page-secret","columnNumber":1,"lineNumber":1,"stackTrace":{"callFrames":[]}}}}"#; - assert_eq!(project_script_result(response), Ok(ScriptResultProjection::Exception)); + assert_eq!( + project_script_result(response), + Ok(ScriptResultProjection::Exception) + ); } #[test] @@ -761,7 +764,7 @@ mod tests { .as_deref(), Some("expected text-value postcondition exceeds the local byte budget") ); - assert!(validate_expected_text("ordinary space").is_ok()); + assert_eq!(validate_expected_text("ordinary space").is_ok(), true); for rejected in ["tab\tvalue", "control\u{0001}", "bidi\u{202e}override"] { assert_eq!( validate_expected_text(rejected) @@ -956,22 +959,22 @@ mod tests { WebDriverBiDiTextValueObservationResponseError::ScriptException { command_id: 8 }, ]; for error in cases { - assert!(error.source().is_none()); - assert!(!error.to_string().is_empty()); + assert_eq!(error.source().is_none(), true); + assert_eq!(error.to_string().is_empty(), false); } let envelope = WebDriverBiDiTextValueObservationResponseError::Envelope { source: WebDriverBiDiJsonEnvelopeError::InvalidJson, }; - assert!(envelope.source().is_some()); + assert_eq!(envelope.source().is_some(), true); let projection = WebDriverBiDiTextValueObservationResponseError::Projection { source: WebDriverBiDiTextValueObservationProjectionError::InvalidValue, }; - assert!(projection.source().is_some()); + assert_eq!(projection.source().is_some(), true); let correlation = WebDriverBiDiTextValueObservationResponseError::Correlation { source: WebDriverBiDiCommandCorrelationError::CommandNotOutstanding, }; - assert!(correlation.source().is_some()); + assert_eq!(correlation.source().is_some(), true); let projection_errors = [ WebDriverBiDiTextValueObservationProjectionError::InvalidObject { member: "result" }, @@ -987,7 +990,7 @@ mod tests { WebDriverBiDiTextValueObservationProjectionError::InvalidValue, ]; for error in projection_errors { - assert!(!error.to_string().is_empty()); + assert_eq!(error.to_string().is_empty(), false); } } } From 3c2135c49233474f84648d449aca81c16940e5fd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 18:18:49 +0900 Subject: [PATCH 20/64] test(network): satisfy coverage and strict clippy together --- ...bdriver_bidi_text_value_observation_response.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs b/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs index 0a8f2a039..3664caded 100644 --- a/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs @@ -764,7 +764,7 @@ mod tests { .as_deref(), Some("expected text-value postcondition exceeds the local byte budget") ); - assert_eq!(validate_expected_text("ordinary space").is_ok(), true); + assert_eq!(validate_expected_text("ordinary space"), Ok(())); for rejected in ["tab\tvalue", "control\u{0001}", "bidi\u{202e}override"] { assert_eq!( validate_expected_text(rejected) @@ -959,22 +959,22 @@ mod tests { WebDriverBiDiTextValueObservationResponseError::ScriptException { command_id: 8 }, ]; for error in cases { - assert_eq!(error.source().is_none(), true); - assert_eq!(error.to_string().is_empty(), false); + assert_eq!(error.source().map(|_| ()), None); + assert_ne!(error.to_string(), ""); } let envelope = WebDriverBiDiTextValueObservationResponseError::Envelope { source: WebDriverBiDiJsonEnvelopeError::InvalidJson, }; - assert_eq!(envelope.source().is_some(), true); + assert_eq!(envelope.source().map(|_| ()), Some(())); let projection = WebDriverBiDiTextValueObservationResponseError::Projection { source: WebDriverBiDiTextValueObservationProjectionError::InvalidValue, }; - assert_eq!(projection.source().is_some(), true); + assert_eq!(projection.source().map(|_| ()), Some(())); let correlation = WebDriverBiDiTextValueObservationResponseError::Correlation { source: WebDriverBiDiCommandCorrelationError::CommandNotOutstanding, }; - assert_eq!(correlation.source().is_some(), true); + assert_eq!(correlation.source().map(|_| ()), Some(())); let projection_errors = [ WebDriverBiDiTextValueObservationProjectionError::InvalidObject { member: "result" }, @@ -990,7 +990,7 @@ mod tests { WebDriverBiDiTextValueObservationProjectionError::InvalidValue, ]; for error in projection_errors { - assert_eq!(error.to_string().is_empty(), false); + assert_ne!(error.to_string(), ""); } } } From 1f2d2982880eb56407696ac2aa2bac7d02e6ce33 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 18:32:34 +0900 Subject: [PATCH 21/64] fix(network): keep text validation assertion type-local --- .../src/webdriver_bidi_text_value_observation_response.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs b/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs index 3664caded..f7c87f3bb 100644 --- a/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs @@ -764,7 +764,10 @@ mod tests { .as_deref(), Some("expected text-value postcondition exceeds the local byte budget") ); - assert_eq!(validate_expected_text("ordinary space"), Ok(())); + assert_eq!( + validate_expected_text("ordinary space").map_err(|_| ()), + Ok(()) + ); for rejected in ["tab\tvalue", "control\u{0001}", "bidi\u{202e}override"] { assert_eq!( validate_expected_text(rejected) @@ -797,7 +800,6 @@ mod tests { } ) ); - let many_members = (0..=MAX_SCRIPT_RESULT_OBJECT_MEMBERS) .map(|index| format!("\"k{index}\":0")) .collect::>() From 086d11b5a8a51a1b4070fe8715ec8e84263552ad Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 19:20:33 +0900 Subject: [PATCH 22/64] test(network): cover public text observation diagnostics --- ...er_bidi_text_value_observation_response.rs | 54 ++++++++++++++----- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs b/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs index cbdc29b20..dc86431c1 100644 --- a/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs +++ b/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs @@ -302,14 +302,22 @@ fn response_admission_failures_preserve_or_consume_exact_correlation_state() let invalid = receive_server_text(b"not-json")?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); correlation.register_command(70)?; + let Err(envelope_error) = WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( + &invalid, + "expected", + &mut correlation, + ) else { + return Err(io::Error::other("invalid envelope unexpectedly admitted").into()); + }; assert!(matches!( - WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( - &invalid, - "expected", - &mut correlation, - ), - Err(WebDriverBiDiTextValueObservationResponseError::Envelope { .. }) + &envelope_error, + WebDriverBiDiTextValueObservationResponseError::Envelope { .. } )); + assert_eq!( + envelope_error.to_string(), + "WebDriver BiDi text-value observation envelope is invalid" + ); + assert!(envelope_error.source().is_some()); assert_eq!(correlation.outstanding_count(), 1); let event = receive_server_text(br#"{"type":"event","method":"log.entryAdded","params":{}}"#)?; @@ -340,28 +348,48 @@ fn response_admission_failures_preserve_or_consume_exact_correlation_state() let unknown_success = receive_server_text( br#"{"type":"success","id":72,"result":{"type":"success","realm":"realm-1","result":{"type":"string","value":"expected"}}}"#, )?; - assert!(matches!( + let Err(correlation_error) = WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( &unknown_success, "expected", &mut correlation, - ), - Err(WebDriverBiDiTextValueObservationResponseError::Correlation { .. }) + ) + else { + return Err(io::Error::other("unknown response id unexpectedly correlated").into()); + }; + assert!(matches!( + &correlation_error, + WebDriverBiDiTextValueObservationResponseError::Correlation { .. } )); + assert_eq!( + correlation_error.to_string(), + "WebDriver BiDi text-value observation response correlation failed" + ); + assert!(correlation_error.source().is_some()); assert_eq!(correlation.outstanding_count(), 1); correlation.register_command(73)?; let malformed_projection = receive_server_text( br#"{"type":"success","id":73,"result":{"type":"success","result":{"type":"string","value":"expected"}}}"#, )?; - assert!(matches!( + let Err(projection_error) = WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( &malformed_projection, "expected", &mut correlation, - ), - Err(WebDriverBiDiTextValueObservationResponseError::Projection { .. }) + ) + else { + return Err(io::Error::other("malformed script result unexpectedly admitted").into()); + }; + assert!(matches!( + &projection_error, + WebDriverBiDiTextValueObservationResponseError::Projection { .. } )); + assert_eq!( + projection_error.to_string(), + "WebDriver BiDi text-value observation result is invalid" + ); + assert!(projection_error.source().is_some()); assert_eq!(correlation.outstanding_count(), 2); let script_exception = receive_server_text( @@ -398,4 +426,4 @@ fn response_admission_failures_preserve_or_consume_exact_correlation_state() assert!(result.matches_expected_text()); assert_eq!(correlation.outstanding_count(), 0); Ok(()) -} +} \ No newline at end of file From 8d38457cf192a3777468089c2597129c7451c00a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 19:37:15 +0900 Subject: [PATCH 23/64] style(network): restore rustfmt newline --- .../tests/webdriver_bidi_text_value_observation_response.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs b/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs index dc86431c1..12a74776e 100644 --- a/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs +++ b/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs @@ -426,4 +426,4 @@ fn response_admission_failures_preserve_or_consume_exact_correlation_state() assert!(result.matches_expected_text()); assert_eq!(correlation.outstanding_count(), 0); Ok(()) -} \ No newline at end of file +} From 364f2f2409aca202d8dfdffa219a5c1f754a348d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 19:50:09 +0900 Subject: [PATCH 24/64] test(network): cover unicode observation projection --- ...text_value_observation_unicode_response.rs | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_text_value_observation_unicode_response.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_text_value_observation_unicode_response.rs b/crates/originweave-network/tests/webdriver_bidi_text_value_observation_unicode_response.rs new file mode 100644 index 000000000..8ca584c69 --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_text_value_observation_unicode_response.rs @@ -0,0 +1,132 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::WebDriverBiDiWebSocketEndpoint; +use originweave_network::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiTextValueObservationResponseError, WebDriverBiDiTextValueObservationResult, + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, + WebDriverBiDiWebSocketTextMessage, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; +const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; +const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; + +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 receive_server_text( + payload: &[u8], +) -> Result> { + if payload.len() > 125 { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "fixture payload must fit one short server text frame", + ) + .into()); + } + + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let response = payload.to_vec(); + let server = thread::spawn(move || -> io::Result<()> { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + stream.write_all(&[0x81, response.len() as u8])?; + stream.write_all(&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 (_established, frame) = established.read_frame(Duration::from_millis(500))?; + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); + let message = match assembler.push_frame(frame)? { + WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + other => { + return Err(io::Error::other(format!( + "fixture produced unexpected message assembly state: {other:?}" + )) + .into()); + } + }; + server + .join() + .map_err(|_| io::Error::other("response fixture server panicked"))??; + Ok(message) +} + +#[test] +fn escaped_unicode_is_compared_after_bounded_response_projection() -> Result<(), Box> { + let response = receive_server_text( + br#"{"type":"success","id":81,"result":{"type":"success","realm":"r","result":{"type":"string","value":"\u20ac"}}}"#, + )?; + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + correlation.register_command(81)?; + + let result = WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( + &response, + "€", + &mut correlation, + )?; + + assert_eq!(result.command_id(), 81); + assert_eq!(result.observed_text_bytes(), "€".len()); + assert!(result.matches_expected_text()); + assert_eq!(correlation.outstanding_count(), 0); + Ok(()) +} + +#[test] +fn projection_error_diagnostic_remains_structural_and_non_sensitive() -> Result<(), Box> { + let response = receive_server_text( + br#"{"type":"success","id":82,"result":{"type":"success","result":{"type":"string","value":"x"}}}"#, + )?; + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + correlation.register_command(82)?; + + let Err(WebDriverBiDiTextValueObservationResponseError::Projection { source }) = + WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( + &response, + "x", + &mut correlation, + ) + else { + return Err(io::Error::other("malformed projection unexpectedly admitted").into()); + }; + + assert_eq!(source.to_string(), "missing member result.realm"); + assert_eq!(correlation.outstanding_count(), 1); + Ok(()) +} From 12656b1a05fe3662da17e4b35b3bf22e77c267c8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 21:10:39 +0900 Subject: [PATCH 25/64] style(network): apply canonical rustfmt to unicode response tests --- ...bdriver_bidi_text_value_observation_unicode_response.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_text_value_observation_unicode_response.rs b/crates/originweave-network/tests/webdriver_bidi_text_value_observation_unicode_response.rs index 8ca584c69..81537efba 100644 --- a/crates/originweave-network/tests/webdriver_bidi_text_value_observation_unicode_response.rs +++ b/crates/originweave-network/tests/webdriver_bidi_text_value_observation_unicode_response.rs @@ -90,7 +90,7 @@ fn receive_server_text( #[test] fn escaped_unicode_is_compared_after_bounded_response_projection() -> Result<(), Box> { let response = receive_server_text( - br#"{"type":"success","id":81,"result":{"type":"success","realm":"r","result":{"type":"string","value":"\u20ac"}}}"#, + br#"{\"type\":\"success\",\"id\":81,\"result\":{\"type\":\"success\",\"realm\":\"r\",\"result\":{\"type\":\"string\",\"value\":\"\\u20ac\"}}}"#, )?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); correlation.register_command(81)?; @@ -109,9 +109,10 @@ fn escaped_unicode_is_compared_after_bounded_response_projection() -> Result<(), } #[test] -fn projection_error_diagnostic_remains_structural_and_non_sensitive() -> Result<(), Box> { +fn projection_error_diagnostic_remains_structural_and_non_sensitive() -> Result<(), Box> +{ let response = receive_server_text( - br#"{"type":"success","id":82,"result":{"type":"success","result":{"type":"string","value":"x"}}}"#, + br#"{\"type\":\"success\",\"id\":82,\"result\":{\"type\":\"success\",\"result\":{\"type\":\"string\",\"value\":\"x\"}}}"#, )?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); correlation.register_command(82)?; From aa732cb7568d8c33e8f3b11e7ee332503944a657 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 21:11:22 +0900 Subject: [PATCH 26/64] fix(network): restore exact unicode JSON fixture bytes --- .../webdriver_bidi_text_value_observation_unicode_response.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_text_value_observation_unicode_response.rs b/crates/originweave-network/tests/webdriver_bidi_text_value_observation_unicode_response.rs index 81537efba..dda339a19 100644 --- a/crates/originweave-network/tests/webdriver_bidi_text_value_observation_unicode_response.rs +++ b/crates/originweave-network/tests/webdriver_bidi_text_value_observation_unicode_response.rs @@ -90,7 +90,7 @@ fn receive_server_text( #[test] fn escaped_unicode_is_compared_after_bounded_response_projection() -> Result<(), Box> { let response = receive_server_text( - br#"{\"type\":\"success\",\"id\":81,\"result\":{\"type\":\"success\",\"realm\":\"r\",\"result\":{\"type\":\"string\",\"value\":\"\\u20ac\"}}}"#, + br#"{"type":"success","id":81,"result":{"type":"success","realm":"r","result":{"type":"string","value":"\u20ac"}}}"#, )?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); correlation.register_command(81)?; @@ -112,7 +112,7 @@ fn escaped_unicode_is_compared_after_bounded_response_projection() -> Result<(), fn projection_error_diagnostic_remains_structural_and_non_sensitive() -> Result<(), Box> { let response = receive_server_text( - br#"{\"type\":\"success\",\"id\":82,\"result\":{\"type\":\"success\",\"result\":{\"type\":\"string\",\"value\":\"x\"}}}"#, + br#"{"type":"success","id":82,"result":{"type":"success","result":{"type":"string","value":"x"}}}"#, )?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); correlation.register_command(82)?; From a8d7a0a5a90837637645dbbc8ca3a5e55e78d3ef Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 22:17:25 +0900 Subject: [PATCH 27/64] test(network): exercise text-value public boundary in unit build --- ...value_observation_public_boundary_tests.rs | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 crates/originweave-network/src/webdriver_bidi_text_value_observation_public_boundary_tests.rs diff --git a/crates/originweave-network/src/webdriver_bidi_text_value_observation_public_boundary_tests.rs b/crates/originweave-network/src/webdriver_bidi_text_value_observation_public_boundary_tests.rs new file mode 100644 index 000000000..f7389e404 --- /dev/null +++ b/crates/originweave-network/src/webdriver_bidi_text_value_observation_public_boundary_tests.rs @@ -0,0 +1,171 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::WebDriverBiDiWebSocketEndpoint; + +use crate::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiTextValueObservationResponseError, WebDriverBiDiTextValueObservationResult, + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, + WebDriverBiDiWebSocketTextMessage, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; +const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; +const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; +const ERROR_UNKNOWN_COMMAND: &[u8] = + br#"{"type":"error","id":71,"error":"unknown error","message":"remote failure"}"#; +const MALFORMED_PROJECTION: &[u8] = br#"{"type":"success","id":70,"result":{"type":"success","result":{"type":"string","value":"expected"}}}"#; +const SUCCESS_UNKNOWN_COMMAND: &[u8] = br#"{"type":"success","id":72,"result":{"type":"success","realm":"realm-1","result":{"type":"string","value":"expected"}}}"#; +const VALID_SUCCESS: &[u8] = br#"{"type":"success","id":70,"result":{"type":"success","realm":"realm-1","result":{"type":"string","value":"expected"}}}"#; + +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, document: &[u8]) -> io::Result<()> { + if document.len() <= 125 { + let length = u8::try_from(document.len()).map_err(|_| { + io::Error::new(io::ErrorKind::InvalidData, "short frame length exceeds u8") + })?; + stream.write_all(&[0x81, length])?; + } else { + let length = u16::try_from(document.len()).map_err(|_| { + io::Error::new( + io::ErrorKind::InvalidData, + "unit JSON document exceeds two-byte frame length", + ) + })?; + stream.write_all(&[0x81, 126])?; + stream.write_all(&length.to_be_bytes())?; + } + stream.write_all(document) +} + +fn read_text_over_loopback( + document: &'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_unmasked_text_frame(&mut stream, document) + }); + + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? + .correlate_session_id(SESSION_ID)? + .into_explicit_connect_target()?; + let connection = + WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; + let established = WebDriverBiDiWebSocketHandshakePlan::new( + connection, + WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?, + )? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?; + let (_established, frame) = established.read_frame(Duration::from_millis(500))?; + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); + let text = match assembler.push_frame(frame)? { + WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + other => { + return Err(io::Error::other(format!( + "validated text frame produced unexpected assembly state: {other:?}" + )) + .into()); + } + }; + + server + .join() + .map_err(|_| io::Error::other("text-value unit server panicked"))??; + Ok(text) +} + +#[test] +fn public_text_value_boundary_covers_error_adapters_and_credential_safe_result() +-> Result<(), Box> { + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + correlation.register_command(70)?; + + let invalid = read_text_over_loopback(b"not-json")?; + assert!(matches!( + WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( + &invalid, + "expected", + &mut correlation, + ), + Err(WebDriverBiDiTextValueObservationResponseError::Envelope { .. }) + )); + assert_eq!(correlation.outstanding_count(), 1); + + let error_unknown = read_text_over_loopback(ERROR_UNKNOWN_COMMAND)?; + assert!(matches!( + WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( + &error_unknown, + "expected", + &mut correlation, + ), + Err(WebDriverBiDiTextValueObservationResponseError::Correlation { .. }) + )); + assert_eq!(correlation.outstanding_count(), 1); + + let malformed_projection = read_text_over_loopback(MALFORMED_PROJECTION)?; + assert!(matches!( + WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( + &malformed_projection, + "expected", + &mut correlation, + ), + Err(WebDriverBiDiTextValueObservationResponseError::Projection { .. }) + )); + assert_eq!(correlation.outstanding_count(), 1); + + let success_unknown = read_text_over_loopback(SUCCESS_UNKNOWN_COMMAND)?; + assert!(matches!( + WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( + &success_unknown, + "expected", + &mut correlation, + ), + Err(WebDriverBiDiTextValueObservationResponseError::Correlation { .. }) + )); + assert_eq!(correlation.outstanding_count(), 1); + + let valid_success = read_text_over_loopback(VALID_SUCCESS)?; + let result = WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( + &valid_success, + "expected", + &mut correlation, + )?; + assert_eq!(result.command_id(), 70); + assert_eq!(result.observed_text_bytes(), "expected".len()); + assert!(result.matches_expected_text()); + assert_eq!(correlation.outstanding_count(), 0); + + let debug = format!("{result:?}"); + assert!(debug.contains("WebDriverBiDiTextValueObservationResult")); + assert!(!debug.contains("expected")); + Ok(()) +} From 99031957b72c4050123525949125a2d4d15ce546 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 22:19:14 +0900 Subject: [PATCH 28/64] test(network): run text-value boundary in unit build --- crates/originweave-network/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/originweave-network/src/lib.rs b/crates/originweave-network/src/lib.rs index 05ff2cbef..66fb67637 100644 --- a/crates/originweave-network/src/lib.rs +++ b/crates/originweave-network/src/lib.rs @@ -60,6 +60,8 @@ mod webdriver_bidi_websocket_transport_closure; #[cfg(test)] mod webdriver_bidi_json_envelope_public_boundary_tests; +#[cfg(test)] +mod webdriver_bidi_text_value_observation_public_boundary_tests; pub use connection::{ ConnectionPlan, DirectTcpConnection, MAX_CONNECT_TIMEOUT, MAX_CONNECTION_ATTEMPTS, From 13afb69322052a79aaf52f9a3aa2d9b7da2ee3ed Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 22:21:57 +0900 Subject: [PATCH 29/64] test(network): assert exact page text stays redacted --- ..._bidi_text_value_observation_public_boundary_tests.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_text_value_observation_public_boundary_tests.rs b/crates/originweave-network/src/webdriver_bidi_text_value_observation_public_boundary_tests.rs index f7389e404..74a2ea46a 100644 --- a/crates/originweave-network/src/webdriver_bidi_text_value_observation_public_boundary_tests.rs +++ b/crates/originweave-network/src/webdriver_bidi_text_value_observation_public_boundary_tests.rs @@ -23,7 +23,8 @@ const ERROR_UNKNOWN_COMMAND: &[u8] = br#"{"type":"error","id":71,"error":"unknown error","message":"remote failure"}"#; const MALFORMED_PROJECTION: &[u8] = br#"{"type":"success","id":70,"result":{"type":"success","result":{"type":"string","value":"expected"}}}"#; const SUCCESS_UNKNOWN_COMMAND: &[u8] = br#"{"type":"success","id":72,"result":{"type":"success","realm":"realm-1","result":{"type":"string","value":"expected"}}}"#; -const VALID_SUCCESS: &[u8] = br#"{"type":"success","id":70,"result":{"type":"success","realm":"realm-1","result":{"type":"string","value":"expected"}}}"#; +const FINAL_EXPECTED_TEXT: &str = "Quarterly review"; +const VALID_SUCCESS: &[u8] = br#"{"type":"success","id":70,"result":{"type":"success","realm":"realm-1","result":{"type":"string","value":"Quarterly review"}}}"#; fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { stream.set_read_timeout(Some(Duration::from_secs(2)))?; @@ -156,16 +157,16 @@ fn public_text_value_boundary_covers_error_adapters_and_credential_safe_result() let valid_success = read_text_over_loopback(VALID_SUCCESS)?; let result = WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( &valid_success, - "expected", + FINAL_EXPECTED_TEXT, &mut correlation, )?; assert_eq!(result.command_id(), 70); - assert_eq!(result.observed_text_bytes(), "expected".len()); + assert_eq!(result.observed_text_bytes(), FINAL_EXPECTED_TEXT.len()); assert!(result.matches_expected_text()); assert_eq!(correlation.outstanding_count(), 0); let debug = format!("{result:?}"); assert!(debug.contains("WebDriverBiDiTextValueObservationResult")); - assert!(!debug.contains("expected")); + assert!(!debug.contains(FINAL_EXPECTED_TEXT)); Ok(()) } From 34e498026407a12ef1273443b37d0d420680d3a2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 22:53:04 +0900 Subject: [PATCH 30/64] test(network): exercise fail-closed text response paths --- ...er_bidi_text_value_response_fail_closed.rs | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_text_value_response_fail_closed.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_text_value_response_fail_closed.rs b/crates/originweave-network/tests/webdriver_bidi_text_value_response_fail_closed.rs new file mode 100644 index 000000000..2992656be --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_text_value_response_fail_closed.rs @@ -0,0 +1,148 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::WebDriverBiDiWebSocketEndpoint; +use originweave_network::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiTextValueObservationResponseError, WebDriverBiDiTextValueObservationResult, + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, + WebDriverBiDiWebSocketTextMessage, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; +const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; +const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; +const EVENT: &[u8] = + br#"{"type":"event","method":"browsingContext.load","params":{}}"#; +const PROTOCOL_ERROR: &[u8] = + br#"{"type":"error","id":73,"error":"unknown error","message":"page-controlled detail"}"#; +const SCRIPT_EXCEPTION: &[u8] = br#"{"type":"success","id":74,"result":{"type":"exception","realm":"realm-1","exceptionDetails":{"text":"page-controlled detail"}}}"#; + +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, document: &[u8]) -> io::Result<()> { + if document.len() <= 125 { + let length = u8::try_from(document.len()).map_err(|_| { + io::Error::new(io::ErrorKind::InvalidData, "short frame length exceeds u8") + })?; + stream.write_all(&[0x81, length])?; + } else { + let length = u16::try_from(document.len()).map_err(|_| { + io::Error::new( + io::ErrorKind::InvalidData, + "test JSON document exceeds two-byte frame length", + ) + })?; + stream.write_all(&[0x81, 126])?; + stream.write_all(&length.to_be_bytes())?; + } + stream.write_all(document) +} + +fn read_text_over_loopback( + document: &'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_unmasked_text_frame(&mut stream, document) + }); + + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? + .correlate_session_id(SESSION_ID)? + .into_explicit_connect_target()?; + let connection = + WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; + let established = WebDriverBiDiWebSocketHandshakePlan::new( + connection, + WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?, + )? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?; + let (_established, frame) = established.read_frame(Duration::from_millis(500))?; + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); + let text = match assembler.push_frame(frame)? { + WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + other => { + return Err(io::Error::other(format!( + "validated text frame produced unexpected assembly state: {other:?}" + )) + .into()); + } + }; + + server + .join() + .map_err(|_| io::Error::other("text-value integration server panicked"))??; + Ok(text) +} + +#[test] +fn production_instantiation_fails_closed_for_event_protocol_error_and_script_exception( +) -> Result<(), Box> { + let event = read_text_over_loopback(EVENT)?; + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + assert!(matches!( + WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( + &event, + "expected", + &mut correlation, + ), + Err(WebDriverBiDiTextValueObservationResponseError::UnexpectedEvent) + )); + assert_eq!(correlation.outstanding_count(), 0); + + correlation.register_command(73)?; + let protocol_error = read_text_over_loopback(PROTOCOL_ERROR)?; + assert!(matches!( + WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( + &protocol_error, + "expected", + &mut correlation, + ), + Err(WebDriverBiDiTextValueObservationResponseError::RemoteProtocolError { + command_id: 73 + }) + )); + assert_eq!(correlation.outstanding_count(), 0); + + correlation.register_command(74)?; + let script_exception = read_text_over_loopback(SCRIPT_EXCEPTION)?; + assert!(matches!( + WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( + &script_exception, + "expected", + &mut correlation, + ), + Err(WebDriverBiDiTextValueObservationResponseError::ScriptException { + command_id: 74 + }) + )); + assert_eq!(correlation.outstanding_count(), 0); + Ok(()) +} From eb05dd6824b398d67d44244fa15eac048994d0d5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 23:07:39 +0900 Subject: [PATCH 31/64] style(network): apply exact current-head rustfmt diagnostics --- ...driver_bidi_text_value_response_fail_closed.rs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_text_value_response_fail_closed.rs b/crates/originweave-network/tests/webdriver_bidi_text_value_response_fail_closed.rs index 2992656be..c51fb2291 100644 --- a/crates/originweave-network/tests/webdriver_bidi_text_value_response_fail_closed.rs +++ b/crates/originweave-network/tests/webdriver_bidi_text_value_response_fail_closed.rs @@ -18,8 +18,7 @@ use originweave_network::{ 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 EVENT: &[u8] = - br#"{"type":"event","method":"browsingContext.load","params":{}}"#; +const EVENT: &[u8] = br#"{"type":"event","method":"browsingContext.load","params":{}}"#; const PROTOCOL_ERROR: &[u8] = br#"{"type":"error","id":73,"error":"unknown error","message":"page-controlled detail"}"#; const SCRIPT_EXCEPTION: &[u8] = br#"{"type":"success","id":74,"result":{"type":"exception","realm":"realm-1","exceptionDetails":{"text":"page-controlled detail"}}}"#; @@ -103,8 +102,8 @@ fn read_text_over_loopback( } #[test] -fn production_instantiation_fails_closed_for_event_protocol_error_and_script_exception( -) -> Result<(), Box> { +fn production_instantiation_fails_closed_for_event_protocol_error_and_script_exception() +-> Result<(), Box> { let event = read_text_over_loopback(EVENT)?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); assert!(matches!( @@ -125,9 +124,7 @@ fn production_instantiation_fails_closed_for_event_protocol_error_and_script_exc "expected", &mut correlation, ), - Err(WebDriverBiDiTextValueObservationResponseError::RemoteProtocolError { - command_id: 73 - }) + Err(WebDriverBiDiTextValueObservationResponseError::RemoteProtocolError { command_id: 73 }) )); assert_eq!(correlation.outstanding_count(), 0); @@ -139,9 +136,7 @@ fn production_instantiation_fails_closed_for_event_protocol_error_and_script_exc "expected", &mut correlation, ), - Err(WebDriverBiDiTextValueObservationResponseError::ScriptException { - command_id: 74 - }) + Err(WebDriverBiDiTextValueObservationResponseError::ScriptException { command_id: 74 }) )); assert_eq!(correlation.outstanding_count(), 0); Ok(()) From 1bdd8aec51b43cf9f87086411e693c812b5d3597 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 23:15:04 +0900 Subject: [PATCH 32/64] test(network): cover typed text error display adapters --- ...value_observation_public_boundary_tests.rs | 57 ++++++++++++++----- 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_text_value_observation_public_boundary_tests.rs b/crates/originweave-network/src/webdriver_bidi_text_value_observation_public_boundary_tests.rs index 74a2ea46a..0016dde5f 100644 --- a/crates/originweave-network/src/webdriver_bidi_text_value_observation_public_boundary_tests.rs +++ b/crates/originweave-network/src/webdriver_bidi_text_value_observation_public_boundary_tests.rs @@ -111,14 +111,23 @@ fn public_text_value_boundary_covers_error_adapters_and_credential_safe_result() correlation.register_command(70)?; let invalid = read_text_over_loopback(b"not-json")?; + let envelope_result = WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( + &invalid, + "expected", + &mut correlation, + ); assert!(matches!( - WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( - &invalid, - "expected", - &mut correlation, - ), + &envelope_result, Err(WebDriverBiDiTextValueObservationResponseError::Envelope { .. }) )); + assert_eq!( + envelope_result + .as_ref() + .err() + .map(ToString::to_string) + .as_deref(), + Some("WebDriver BiDi text-value observation envelope is invalid") + ); assert_eq!(correlation.outstanding_count(), 1); let error_unknown = read_text_over_loopback(ERROR_UNKNOWN_COMMAND)?; @@ -133,25 +142,43 @@ fn public_text_value_boundary_covers_error_adapters_and_credential_safe_result() assert_eq!(correlation.outstanding_count(), 1); let malformed_projection = read_text_over_loopback(MALFORMED_PROJECTION)?; + let projection_result = WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( + &malformed_projection, + "expected", + &mut correlation, + ); assert!(matches!( - WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( - &malformed_projection, - "expected", - &mut correlation, - ), + &projection_result, Err(WebDriverBiDiTextValueObservationResponseError::Projection { .. }) )); + assert_eq!( + projection_result + .as_ref() + .err() + .map(ToString::to_string) + .as_deref(), + Some("WebDriver BiDi text-value observation result is invalid") + ); assert_eq!(correlation.outstanding_count(), 1); let success_unknown = read_text_over_loopback(SUCCESS_UNKNOWN_COMMAND)?; + let correlation_result = WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( + &success_unknown, + "expected", + &mut correlation, + ); assert!(matches!( - WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( - &success_unknown, - "expected", - &mut correlation, - ), + &correlation_result, Err(WebDriverBiDiTextValueObservationResponseError::Correlation { .. }) )); + assert_eq!( + correlation_result + .as_ref() + .err() + .map(ToString::to_string) + .as_deref(), + Some("WebDriver BiDi text-value observation response correlation failed") + ); assert_eq!(correlation.outstanding_count(), 1); let valid_success = read_text_over_loopback(VALID_SUCCESS)?; From d7c9878241f414d137e8831c7d36bac17ce87a4e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 03:17:43 +0900 Subject: [PATCH 33/64] test(network): require explicit text postcondition gate --- ...iver_bidi_text_value_postcondition_gate.rs | 174 ++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs b/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs new file mode 100644 index 000000000..c36e65f0f --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs @@ -0,0 +1,174 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::WebDriverBiDiWebSocketEndpoint; +use originweave_network::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiTextValuePostconditionError, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMessageAssembler, + WebDriverBiDiWebSocketMessageAssembly, WebDriverBiDiWebSocketTextMessage, + verify_webdriver_bidi_text_value_postcondition, +}; + +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_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 response unexpectedly required 64-bit framing", + )); + } + } + stream.write_all(payload) +} + +fn receive_server_text( + payload: &[u8], +) -> Result> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let response = payload.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_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 (_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!( + "fixture produced unexpected message assembly state: {other:?}" + )) + .into()); + } + }; + server + .join() + .map_err(|_| io::Error::other("postcondition fixture server panicked"))??; + Ok(text) +} + +#[test] +fn exact_match_is_the_only_successful_text_postcondition() -> Result<(), Box> { + let response = receive_server_text( + br#"{"type":"success","id":70,"result":{"type":"success","realm":"realm-1","result":{"type":"string","value":"expected"}}}"#, + )?; + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + correlation.register_command(70)?; + + let verified = verify_webdriver_bidi_text_value_postcondition( + &response, + "expected", + &mut correlation, + )?; + + assert_eq!(verified.command_id(), 70); + assert_eq!(verified.observed_text_bytes(), "expected".len()); + assert_eq!(correlation.outstanding_count(), 0); + assert!(!format!("{verified:?}").contains("expected")); + Ok(()) +} + +#[test] +fn mismatch_is_typed_failure_after_consuming_its_exact_response() -> Result<(), Box> { + let response = receive_server_text( + br#"{"type":"success","id":71,"result":{"type":"success","realm":"realm-1","result":{"type":"string","value":"unexpected"}}}"#, + )?; + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + correlation.register_command(71)?; + + let error = verify_webdriver_bidi_text_value_postcondition( + &response, + "expected", + &mut correlation, + ) + .expect_err("a mismatched page value must not be returned as successful postcondition evidence"); + + assert!(matches!( + error, + WebDriverBiDiTextValuePostconditionError::PostconditionMismatch { + command_id: 71, + observed_text_bytes: 10, + } + )); + assert_eq!(correlation.outstanding_count(), 0); + assert_eq!( + error.to_string(), + "WebDriver BiDi text-value postcondition did not match the authorized expected text" + ); + let debug = format!("{error:?}"); + assert!(!debug.contains("expected")); + assert!(!debug.contains("unexpected")); + Ok(()) +} + +#[test] +fn malformed_observation_stays_a_typed_source_error_without_consuming_state() +-> Result<(), Box> { + let response = receive_server_text(b"not-json")?; + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + correlation.register_command(72)?; + + let error = verify_webdriver_bidi_text_value_postcondition( + &response, + "expected", + &mut correlation, + ) + .expect_err("malformed observation must fail closed"); + + assert!(matches!( + &error, + WebDriverBiDiTextValuePostconditionError::Observation { .. } + )); + assert!(error.source().is_some()); + assert_eq!(correlation.outstanding_count(), 1); + Ok(()) +} From dce81cb38df55e2f784eb7cab10db34b3ed24af3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 03:19:09 +0900 Subject: [PATCH 34/64] feat(network): require verified text postcondition --- ...webdriver_bidi_text_value_postcondition.rs | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 crates/originweave-network/src/webdriver_bidi_text_value_postcondition.rs diff --git a/crates/originweave-network/src/webdriver_bidi_text_value_postcondition.rs b/crates/originweave-network/src/webdriver_bidi_text_value_postcondition.rs new file mode 100644 index 000000000..7952f1015 --- /dev/null +++ b/crates/originweave-network/src/webdriver_bidi_text_value_postcondition.rs @@ -0,0 +1,121 @@ +use std::{error::Error, fmt}; + +use crate::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiTextValueObservationResponseError, + WebDriverBiDiTextValueObservationResult, WebDriverBiDiWebSocketTextMessage, +}; + +/// Credential-minimal proof that one exact correlated text observation matched the authorized +/// expected value. +/// +/// The page-controlled string is discarded by the lower observation boundary before this value is +/// constructed. This type therefore carries only the consumed command identifier and observed byte +/// count. A caller can obtain this value only after exact equality succeeds; a mere command +/// response or successful parser result is not sufficient post-condition evidence. +#[derive(Clone, Copy, Eq, PartialEq)] +pub struct WebDriverBiDiTextValuePostcondition { + command_id: u64, + observed_text_bytes: usize, +} + +impl fmt::Debug for WebDriverBiDiTextValuePostcondition { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("WebDriverBiDiTextValuePostcondition") + .field("command_id", &self.command_id) + .field("observed_text_bytes", &self.observed_text_bytes) + .finish() + } +} + +impl WebDriverBiDiTextValuePostcondition { + /// Return the exact local command identifier consumed by the verified observation response. + #[must_use] + pub const fn command_id(&self) -> u64 { + self.command_id + } + + /// Return the UTF-8 byte count of the matched page value without retaining that value. + #[must_use] + pub const fn observed_text_bytes(&self) -> usize { + self.observed_text_bytes + } +} + +/// Failure to produce positive text-value post-condition evidence from one correlated response. +#[derive(Debug)] +pub enum WebDriverBiDiTextValuePostconditionError { + /// The underlying bounded response admission or correlation failed. + Observation { + /// Exact typed lower-boundary failure. + source: WebDriverBiDiTextValueObservationResponseError, + }, + /// The response was structurally valid and correlated, but the observed page value differed + /// from the exact already-authorized expected text. + PostconditionMismatch { + /// Exact local command identifier consumed by the negative observation. + command_id: u64, + /// UTF-8 byte count of the mismatched page value; the page text itself is not retained. + observed_text_bytes: usize, + }, +} + +impl fmt::Display for WebDriverBiDiTextValuePostconditionError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Observation { .. } => { + formatter.write_str("WebDriver BiDi text-value postcondition observation failed") + } + Self::PostconditionMismatch { .. } => formatter.write_str( + "WebDriver BiDi text-value postcondition did not match the authorized expected text", + ), + } + } +} + +impl Error for WebDriverBiDiTextValuePostconditionError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::Observation { source } => Some(source), + Self::PostconditionMismatch { .. } => None, + } + } +} + +/// Admit one bounded correlated text observation and return success only when its page value +/// exactly matches the already-authorized expected text. +/// +/// The lower boundary validates expected-text policy, response structure, script result shape, and +/// exact command correlation before this function evaluates the post-condition. A mismatching +/// observation consumes its correlated command because the response is complete, but returns a +/// typed negative result rather than `Ok`. This prevents command acknowledgment, parser success, or +/// correlation success from being mistaken for successful browser state mutation. +/// +/// No page-controlled text, expected text, realm identifier, credential, secret, browser authority, +/// or policy authority is retained in the returned value or error diagnostics. +pub fn verify_webdriver_bidi_text_value_postcondition( + message: &WebDriverBiDiWebSocketTextMessage, + expected_text: &str, + correlation: &mut WebDriverBiDiCommandCorrelation, +) -> Result { + let observation = WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( + message, + expected_text, + correlation, + ) + .map_err(|source| WebDriverBiDiTextValuePostconditionError::Observation { source })?; + + if !observation.matches_expected_text() { + return Err( + WebDriverBiDiTextValuePostconditionError::PostconditionMismatch { + command_id: observation.command_id(), + observed_text_bytes: observation.observed_text_bytes(), + }, + ); + } + + Ok(WebDriverBiDiTextValuePostcondition { + command_id: observation.command_id(), + observed_text_bytes: observation.observed_text_bytes(), + }) +} From aaec5337525ef2890ba11d07c6a60dc4da3a51f3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 03:19:43 +0900 Subject: [PATCH 35/64] feat(network): expose verified text postcondition --- crates/originweave-network/src/lib.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/originweave-network/src/lib.rs b/crates/originweave-network/src/lib.rs index 66fb67637..9fd93bb7b 100644 --- a/crates/originweave-network/src/lib.rs +++ b/crates/originweave-network/src/lib.rs @@ -11,6 +11,7 @@ //! correlation, transports narrowly typed pointer-click and node-bound non-secret //! text-input actions plus fixed sandboxed text-value observations, admits typed //! correlated protocol acknowledgments and text-value post-condition comparisons, +//! requires an explicit positive text-value post-condition gate before success, //! 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 with bounded fail-closed @@ -50,6 +51,7 @@ mod webdriver_bidi_session_status_response; mod webdriver_bidi_session_teardown; mod webdriver_bidi_text_value_observation_response; mod webdriver_bidi_text_value_observation_transport; +mod webdriver_bidi_text_value_postcondition; mod webdriver_bidi_type_text_response; mod webdriver_bidi_type_text_transport; mod webdriver_bidi_websocket_frame; @@ -151,6 +153,10 @@ pub use webdriver_bidi_text_value_observation_response::{ pub use webdriver_bidi_text_value_observation_transport::{ WebDriverBiDiTextValueObservationSendError, send_webdriver_bidi_text_value_observation, }; +pub use webdriver_bidi_text_value_postcondition::{ + WebDriverBiDiTextValuePostcondition, WebDriverBiDiTextValuePostconditionError, + verify_webdriver_bidi_text_value_postcondition, +}; pub use webdriver_bidi_type_text_response::{ WebDriverBiDiTypeTextResponseError, WebDriverBiDiTypeTextResult, }; From 6d5559a5b30b22414b21ccf02561426fbc00cc87 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 03:20:13 +0900 Subject: [PATCH 36/64] test(network): preserve postcondition error for diagnostics --- .../tests/webdriver_bidi_text_value_postcondition_gate.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs b/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs index c36e65f0f..223685a0d 100644 --- a/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs +++ b/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs @@ -133,7 +133,7 @@ fn mismatch_is_typed_failure_after_consuming_its_exact_response() -> Result<(), .expect_err("a mismatched page value must not be returned as successful postcondition evidence"); assert!(matches!( - error, + &error, WebDriverBiDiTextValuePostconditionError::PostconditionMismatch { command_id: 71, observed_text_bytes: 10, From 58af9ed492ef4c4985be8b069006468b2c298c39 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 03:21:34 +0900 Subject: [PATCH 37/64] test(network): cover postcondition error contracts --- .../tests/webdriver_bidi_text_value_postcondition_gate.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs b/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs index 223685a0d..f5091e17d 100644 --- a/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs +++ b/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs @@ -144,6 +144,7 @@ fn mismatch_is_typed_failure_after_consuming_its_exact_response() -> Result<(), error.to_string(), "WebDriver BiDi text-value postcondition did not match the authorized expected text" ); + assert!(error.source().is_none()); let debug = format!("{error:?}"); assert!(!debug.contains("expected")); assert!(!debug.contains("unexpected")); @@ -168,6 +169,10 @@ fn malformed_observation_stays_a_typed_source_error_without_consuming_state() &error, WebDriverBiDiTextValuePostconditionError::Observation { .. } )); + assert_eq!( + error.to_string(), + "WebDriver BiDi text-value postcondition observation failed" + ); assert!(error.source().is_some()); assert_eq!(correlation.outstanding_count(), 1); Ok(()) From 48aeba93e659999c51e76c69e58ff0bdeb1af863 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 03:38:20 +0900 Subject: [PATCH 38/64] fix(network): apply canonical rustfmt to postcondition tests --- ...iver_bidi_text_value_postcondition_gate.rs | 27 +++++++------------ 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs b/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs index f5091e17d..7eb98778d 100644 --- a/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs +++ b/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs @@ -104,11 +104,8 @@ fn exact_match_is_the_only_successful_text_postcondition() -> Result<(), Box Result<(), let mut correlation = WebDriverBiDiCommandCorrelation::new(); correlation.register_command(71)?; - let error = verify_webdriver_bidi_text_value_postcondition( - &response, - "expected", - &mut correlation, - ) - .expect_err("a mismatched page value must not be returned as successful postcondition evidence"); + let error = + verify_webdriver_bidi_text_value_postcondition(&response, "expected", &mut correlation) + .expect_err( + "a mismatched page value must not be returned as successful postcondition evidence", + ); assert!(matches!( &error, @@ -158,12 +154,9 @@ fn malformed_observation_stays_a_typed_source_error_without_consuming_state() let mut correlation = WebDriverBiDiCommandCorrelation::new(); correlation.register_command(72)?; - let error = verify_webdriver_bidi_text_value_postcondition( - &response, - "expected", - &mut correlation, - ) - .expect_err("malformed observation must fail closed"); + let error = + verify_webdriver_bidi_text_value_postcondition(&response, "expected", &mut correlation) + .expect_err("malformed observation must fail closed"); assert!(matches!( &error, From 61bd2e00d2c60f9d846d5a5d59c7329695d5a3ed Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 04:14:16 +0900 Subject: [PATCH 39/64] fix(network): avoid prohibited expect in postcondition tests --- ...driver_bidi_text_value_postcondition_gate.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs b/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs index 7eb98778d..9472f0a0f 100644 --- a/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs +++ b/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs @@ -122,11 +122,14 @@ fn mismatch_is_typed_failure_after_consuming_its_exact_response() -> Result<(), let mut correlation = WebDriverBiDiCommandCorrelation::new(); correlation.register_command(71)?; - let error = + let Err(error) = verify_webdriver_bidi_text_value_postcondition(&response, "expected", &mut correlation) - .expect_err( - "a mismatched page value must not be returned as successful postcondition evidence", - ); + else { + return Err(io::Error::other( + "a mismatched page value must not be returned as successful postcondition evidence", + ) + .into()); + }; assert!(matches!( &error, @@ -154,9 +157,11 @@ fn malformed_observation_stays_a_typed_source_error_without_consuming_state() let mut correlation = WebDriverBiDiCommandCorrelation::new(); correlation.register_command(72)?; - let error = + let Err(error) = verify_webdriver_bidi_text_value_postcondition(&response, "expected", &mut correlation) - .expect_err("malformed observation must fail closed"); + else { + return Err(io::Error::other("malformed observation must fail closed").into()); + }; assert!(matches!( &error, From 335667ecc4fa72c159e9ae43d2488d8b429d079b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 08:16:50 +0900 Subject: [PATCH 40/64] test(network): reject unbound postcondition response ids --- ...iver_bidi_text_value_postcondition_gate.rs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs b/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs index 9472f0a0f..7fe9400ee 100644 --- a/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs +++ b/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs @@ -175,3 +175,28 @@ fn malformed_observation_stays_a_typed_source_error_without_consuming_state() assert_eq!(correlation.outstanding_count(), 1); Ok(()) } + +#[test] +fn unrelated_outstanding_command_cannot_certify_text_postcondition() -> Result<(), Box> { + let response = receive_server_text( + br#"{"type":"success","id":73,"result":{"type":"success","realm":"realm-1","result":{"type":"string","value":"expected"}}}"#, + )?; + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + correlation.register_command(73)?; + + let Err(error) = + verify_webdriver_bidi_text_value_postcondition(&response, "expected", &mut correlation) + else { + return Err(io::Error::other( + "an unrelated outstanding command id must not certify a text-value postcondition", + ) + .into()); + }; + + assert!(matches!( + &error, + WebDriverBiDiTextValuePostconditionError::Observation { .. } + )); + assert_eq!(correlation.outstanding_count(), 1); + Ok(()) +} From ed1e14679a980fd5f308df5794866754fd6d4a12 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 08:23:15 +0900 Subject: [PATCH 41/64] chore(stack): inherit typed observation correlation provenance --- .../src/webdriver_bidi_command_correlation.rs | 157 ++++++++++++++---- 1 file changed, 123 insertions(+), 34 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs index 6adc3d43d..9cb33f051 100644 --- a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs +++ b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs @@ -1,14 +1,32 @@ -use std::{collections::BTreeSet, error::Error, fmt}; +use std::{collections::BTreeMap, error::Error, fmt}; use crate::{MAX_WEBDRIVER_BIDI_JS_UINT, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeKind}; /// Maximum number of local WebDriver BiDi commands retained as outstanding at once. /// /// WebDriver BiDi permits commands to complete out of order. OriginWeave therefore keeps a -/// bounded local correlation set instead of assuming response order, while this resource ceiling +/// bounded local correlation map instead of assuming response order, while this resource ceiling /// prevents an unbounded remote-control session from growing local correlation state indefinitely. pub const MAX_WEBDRIVER_BIDI_OUTSTANDING_COMMANDS: usize = 256; +/// Typed WebDriver BiDi command families that require response-family provenance. +/// +/// Existing legacy command slices may still use the unclassified correlation API while they are +/// migrated independently. A typed command is deliberately segregated from that compatibility +/// path: an unclassified response consumer cannot consume its id, and a typed consumer cannot +/// consume an unclassified id merely because the numeric correlation value matches. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum WebDriverBiDiCommandKind { + /// Fixed product-owned `script.callFunction` text-value observation. + TextValueObservation, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum OutstandingCommandKind { + Unclassified, + Typed(WebDriverBiDiCommandKind), +} + /// Outcome of a response after it has consumed the matching outstanding command identifier. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum WebDriverBiDiCorrelatedResponseOutcome { @@ -53,6 +71,8 @@ pub enum WebDriverBiDiCommandCorrelationError { OutstandingCommandLimit, /// No currently outstanding command matches the requested or returned identifier. CommandNotOutstanding, + /// The response consumer does not match the command provenance registered for this identifier. + CommandKindMismatch, /// An event is not a command response and cannot consume correlation state. EventIsNotResponse, /// A protocol error with a `null` id cannot be attributed to one outstanding command. @@ -66,6 +86,9 @@ impl fmt::Display for WebDriverBiDiCommandCorrelationError { Self::CommandAlreadyOutstanding => "WebDriver BiDi command id is already outstanding", Self::OutstandingCommandLimit => "WebDriver BiDi outstanding-command limit reached", Self::CommandNotOutstanding => "WebDriver BiDi command id is not outstanding", + Self::CommandKindMismatch => { + "WebDriver BiDi response command kind does not match the outstanding command" + } Self::EventIsNotResponse => "WebDriver BiDi event cannot be correlated as a response", Self::UncorrelatableErrorResponse => { "WebDriver BiDi error response has no correlatable command id" @@ -79,14 +102,16 @@ impl Error for WebDriverBiDiCommandCorrelationError {} /// Bounded local WebDriver BiDi command-response correlation state. /// -/// Register an id only after the caller has committed to one outbound command. A success or -/// correlatable error response consumes the id exactly once. Events and null-id errors leave all -/// outstanding state untouched. This type performs no I/O, retry, command serialization, browser -/// authentication, or authority grant. Debug output reports only the outstanding-count summary; -/// command identifiers remain private correlation state. +/// The compatibility [`Self::register_command`] path retains unclassified command provenance for +/// existing typed boundaries that have not yet migrated. Security-sensitive consumers that require +/// an exact command family use [`Self::register_command_for`] and [`Self::correlate_response_for`]. +/// Typed and unclassified entries cannot consume one another. Events, null-id errors, unknown ids, +/// and provenance mismatches leave outstanding state untouched. This type performs no I/O, retry, +/// command serialization, browser authentication, or authority grant. Debug output reports only +/// the outstanding-count summary; identifiers and command provenance remain private state. #[derive(Default)] pub struct WebDriverBiDiCommandCorrelation { - outstanding: BTreeSet, + outstanding: BTreeMap, } impl fmt::Debug for WebDriverBiDiCommandCorrelation { @@ -111,49 +136,94 @@ impl WebDriverBiDiCommandCorrelation { self.outstanding.len() } - /// Register one local command id before its response can be accepted. + /// Register one legacy/unclassified local command id before its response can be accepted. /// + /// This compatibility path cannot later be consumed by a typed response-family boundary. /// Identifiers are unique only while outstanding. A completed or explicitly retired id may be /// reused later, matching WebDriver BiDi's local-end correlation semantics. pub fn register_command( &mut self, command_id: u64, ) -> Result<(), WebDriverBiDiCommandCorrelationError> { - if command_id > MAX_WEBDRIVER_BIDI_JS_UINT { - return Err(WebDriverBiDiCommandCorrelationError::CommandIdOutOfRange); - } - if self.outstanding.contains(&command_id) { - return Err(WebDriverBiDiCommandCorrelationError::CommandAlreadyOutstanding); - } - if self.outstanding.len() >= MAX_WEBDRIVER_BIDI_OUTSTANDING_COMMANDS { - return Err(WebDriverBiDiCommandCorrelationError::OutstandingCommandLimit); - } - self.outstanding.insert(command_id); - Ok(()) + self.register(command_id, OutstandingCommandKind::Unclassified) + } + + /// Register one local command id together with its exact typed command family. + /// + /// A typed entry cannot later be consumed by the unclassified compatibility response path or by + /// another typed command family. Registration remains local correlation bookkeeping only and + /// does not authorize or dispatch the command. + pub fn register_command_for( + &mut self, + command_id: u64, + command_kind: WebDriverBiDiCommandKind, + ) -> Result<(), WebDriverBiDiCommandCorrelationError> { + self.register(command_id, OutstandingCommandKind::Typed(command_kind)) } /// Explicitly retire one outstanding command without accepting a response for it. /// /// This supports caller-owned cancellation or session teardown without retaining stale ids. + /// Retirement does not produce success evidence and therefore may remove either compatibility + /// or typed provenance when the caller owns the exact outstanding identifier. pub fn retire_command( &mut self, command_id: u64, ) -> Result<(), WebDriverBiDiCommandCorrelationError> { - if self.outstanding.remove(&command_id) { + if self.outstanding.remove(&command_id).is_some() { Ok(()) } else { Err(WebDriverBiDiCommandCorrelationError::CommandNotOutstanding) } } - /// Correlate one already parsed local-end envelope with the outstanding command set. + /// Correlate one parsed local-end envelope with an unclassified outstanding command. /// - /// Successful responses and error responses with ids consume exactly one matching command. - /// Unknown ids fail without consuming unrelated state. Events and null-id errors fail before - /// touching the set. + /// This compatibility consumer cannot consume an id that was registered through the typed + /// provenance API. Unknown ids and provenance mismatches fail without consuming state. Events + /// and null-id errors fail before touching the map. pub fn correlate_response( &mut self, envelope: &WebDriverBiDiJsonEnvelope, + ) -> Result { + self.correlate_response_kind(envelope, OutstandingCommandKind::Unclassified) + } + + /// Correlate one parsed local-end envelope with the exact typed outstanding command family. + /// + /// A matching numeric id is insufficient: the registered family must also match. This prevents + /// a response produced for an unrelated outstanding command from being consumed as typed + /// evidence by another protocol boundary. + pub fn correlate_response_for( + &mut self, + envelope: &WebDriverBiDiJsonEnvelope, + expected_kind: WebDriverBiDiCommandKind, + ) -> Result { + self.correlate_response_kind(envelope, OutstandingCommandKind::Typed(expected_kind)) + } + + fn register( + &mut self, + command_id: u64, + command_kind: OutstandingCommandKind, + ) -> Result<(), WebDriverBiDiCommandCorrelationError> { + if command_id > MAX_WEBDRIVER_BIDI_JS_UINT { + return Err(WebDriverBiDiCommandCorrelationError::CommandIdOutOfRange); + } + if self.outstanding.contains_key(&command_id) { + return Err(WebDriverBiDiCommandCorrelationError::CommandAlreadyOutstanding); + } + if self.outstanding.len() >= MAX_WEBDRIVER_BIDI_OUTSTANDING_COMMANDS { + return Err(WebDriverBiDiCommandCorrelationError::OutstandingCommandLimit); + } + let _previous = self.outstanding.insert(command_id, command_kind); + Ok(()) + } + + fn correlate_response_kind( + &mut self, + envelope: &WebDriverBiDiJsonEnvelope, + expected_kind: OutstandingCommandKind, ) -> Result { match envelope.kind() { WebDriverBiDiJsonEnvelopeKind::Event => { @@ -163,25 +233,40 @@ impl WebDriverBiDiCommandCorrelation { let Some(command_id) = envelope.command_id() else { return Err(WebDriverBiDiCommandCorrelationError::UncorrelatableErrorResponse); }; - self.complete(command_id, WebDriverBiDiCorrelatedResponseOutcome::Error) + self.complete( + command_id, + expected_kind, + WebDriverBiDiCorrelatedResponseOutcome::Error, + ) + } + WebDriverBiDiJsonEnvelopeKind::Success => { + let Some(command_id) = envelope.command_id() else { + return Err(WebDriverBiDiCommandCorrelationError::CommandNotOutstanding); + }; + self.complete( + command_id, + expected_kind, + WebDriverBiDiCorrelatedResponseOutcome::Success, + ) } - WebDriverBiDiJsonEnvelopeKind::Success => self.complete( - envelope - .command_id() - .unwrap_or(MAX_WEBDRIVER_BIDI_JS_UINT.saturating_add(1)), - WebDriverBiDiCorrelatedResponseOutcome::Success, - ), } } fn complete( &mut self, command_id: u64, + expected_kind: OutstandingCommandKind, outcome: WebDriverBiDiCorrelatedResponseOutcome, ) -> Result { - if !self.outstanding.remove(&command_id) { - return Err(WebDriverBiDiCommandCorrelationError::CommandNotOutstanding); + let actual_kind = self + .outstanding + .get(&command_id) + .copied() + .ok_or(WebDriverBiDiCommandCorrelationError::CommandNotOutstanding)?; + if actual_kind != expected_kind { + return Err(WebDriverBiDiCommandCorrelationError::CommandKindMismatch); } + let _removed = self.outstanding.remove(&command_id); Ok(WebDriverBiDiCorrelatedResponse { command_id, outcome, @@ -212,6 +297,10 @@ mod tests { WebDriverBiDiCommandCorrelationError::CommandNotOutstanding, "WebDriver BiDi command id is not outstanding", ), + ( + WebDriverBiDiCommandCorrelationError::CommandKindMismatch, + "WebDriver BiDi response command kind does not match the outstanding command", + ), ( WebDriverBiDiCommandCorrelationError::EventIsNotResponse, "WebDriver BiDi event cannot be correlated as a response", From b7e1c7b0755f2dea9e33e39817339176f245d248 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 08:24:05 +0900 Subject: [PATCH 42/64] chore(stack): inherit typed observation registration --- ...ver_bidi_text_value_observation_transport.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_text_value_observation_transport.rs b/crates/originweave-network/src/webdriver_bidi_text_value_observation_transport.rs index 48e2f38ae..9635e02ad 100644 --- a/crates/originweave-network/src/webdriver_bidi_text_value_observation_transport.rs +++ b/crates/originweave-network/src/webdriver_bidi_text_value_observation_transport.rs @@ -8,7 +8,7 @@ use originweave_core::{ use crate::{ WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, - WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketFrameError, + WebDriverBiDiCommandKind, WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketFrameError, WebDriverBiDiWebSocketMaskKey, }; @@ -83,10 +83,12 @@ impl Error for WebDriverBiDiTextValueObservationSendError { /// session, context, origin, document epoch, registry provenance, and exact admitted wire node /// identifier. Callers cannot supply function source, sandbox, or generic script arguments. /// -/// Registration occurs before the first possible remote side effect. A correlation failure writes -/// nothing. Once registration succeeds, a frame-write failure leaves the identifier outstanding -/// because partial or complete remote execution is ambiguous and the identifier must not be -/// silently reused. +/// Registration records [`WebDriverBiDiCommandKind::TextValueObservation`] before the first +/// possible remote side effect. A later typed response boundary must match both the exact id and +/// this command provenance; an unrelated outstanding command id therefore cannot certify a text +/// post-condition. A correlation failure writes nothing. Once registration succeeds, a frame-write +/// failure leaves the identifier outstanding because partial or complete remote execution is +/// ambiguous and the identifier must not be silently reused. /// /// Dispatch is only protocol-level observation transport. This function does not authenticate the /// browser, authorize the preceding text-input action, compare the eventual remote value with the @@ -132,7 +134,10 @@ pub fn send_webdriver_bidi_text_value_observation( .map_err(|source| WebDriverBiDiTextValueObservationSendError::Authority { source })?; correlation - .register_command(command.command_id()) + .register_command_for( + command.command_id(), + WebDriverBiDiCommandKind::TextValueObservation, + ) .map_err(|source| WebDriverBiDiTextValueObservationSendError::Correlation { source })?; established .write_text_frame(command.as_json(), masking_key, frame_timeout) From f67c8bf0cd8326ccbe86a8a1c9c6d44bc3da6e80 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 08:24:41 +0900 Subject: [PATCH 43/64] chore(stack): inherit typed command provenance regression --- ...webdriver_bidi_command_kind_correlation.rs | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_command_kind_correlation.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_command_kind_correlation.rs b/crates/originweave-network/tests/webdriver_bidi_command_kind_correlation.rs new file mode 100644 index 000000000..faa1728d9 --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_command_kind_correlation.rs @@ -0,0 +1,103 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::WebDriverBiDiWebSocketEndpoint; +use originweave_network::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, + WebDriverBiDiCommandKind, WebDriverBiDiCorrelatedResponseOutcome, WebDriverBiDiJsonEnvelope, + WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMessageAssembler, + WebDriverBiDiWebSocketMessageAssembly, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; +const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; +const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; + +fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { + stream.set_read_timeout(Some(Duration::from_secs(2)))?; + let mut request = Vec::new(); + let mut buffer = [0_u8; 512]; + while !request.ends_with(b"\r\n\r\n") { + let count = stream.read(&mut buffer)?; + if count == 0 { + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "client opening request ended before the header terminator", + )); + } + request.extend_from_slice(&buffer[..count]); + } + Ok(()) +} + +fn parse_success_over_loopback() -> Result> { + const DOCUMENT: &[u8] = br#"{"type":"success","id":42,"result":{}}"#; + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let server = thread::spawn(move || -> io::Result<()> { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + stream.write_all(&[0x81, DOCUMENT.len() as u8])?; + stream.write_all(DOCUMENT) + }); + + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? + .correlate_session_id(SESSION_ID)? + .into_explicit_connect_target()?; + let connection = + WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; + let key = WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?; + let established = WebDriverBiDiWebSocketHandshakePlan::new(connection, key)? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?; + let (_established, frame) = established.read_frame(Duration::from_millis(500))?; + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); + let text = match assembler.push_frame(frame)? { + WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + other => { + return Err(io::Error::other(format!( + "validated text frame produced unexpected assembly state: {other:?}" + )) + .into()); + } + }; + let envelope = WebDriverBiDiJsonEnvelope::parse(&text)?; + server + .join() + .map_err(|_| io::Error::other("command-kind correlation test server panicked"))??; + Ok(envelope) +} + +#[test] +fn typed_response_provenance_cannot_cross_the_legacy_correlation_boundary() +-> Result<(), Box> { + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + correlation.register_command_for(42, WebDriverBiDiCommandKind::TextValueObservation)?; + let response = parse_success_over_loopback()?; + + assert_eq!( + correlation.correlate_response(&response), + Err(WebDriverBiDiCommandCorrelationError::CommandKindMismatch) + ); + assert_eq!(correlation.outstanding_count(), 1); + + let completed = correlation.correlate_response_for( + &response, + WebDriverBiDiCommandKind::TextValueObservation, + )?; + assert_eq!(completed.command_id(), 42); + assert_eq!( + completed.outcome(), + WebDriverBiDiCorrelatedResponseOutcome::Success + ); + assert_eq!(correlation.outstanding_count(), 0); + Ok(()) +} From 89e19ecbd7507e12b842a2a89a65cc31001d8ba5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 03:32:17 +0900 Subject: [PATCH 44/64] fix(network): bind text observations to command kind Signed-off-by: Seongho Bae --- .../src/webdriver_bidi_command_correlation.rs | 20 +++++++++---------- .../src/webdriver_bidi_json_envelope.rs | 16 +++++++++++++++ ...value_observation_public_boundary_tests.rs | 4 ++-- ...er_bidi_text_value_observation_response.rs | 14 +++++++++---- ...webdriver_bidi_command_kind_correlation.rs | 6 ++---- ...bidi_text_value_observation_correlation.rs | 6 +++--- ...er_bidi_text_value_observation_response.rs | 8 ++++---- ...text_value_observation_unicode_response.rs | 6 +++--- ...iver_bidi_text_value_postcondition_gate.rs | 8 ++++---- ...er_bidi_text_value_response_fail_closed.rs | 6 +++--- 10 files changed, 57 insertions(+), 37 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs index 9cb33f051..027e2212f 100644 --- a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs +++ b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs @@ -239,16 +239,16 @@ impl WebDriverBiDiCommandCorrelation { WebDriverBiDiCorrelatedResponseOutcome::Error, ) } - WebDriverBiDiJsonEnvelopeKind::Success => { - let Some(command_id) = envelope.command_id() else { - return Err(WebDriverBiDiCommandCorrelationError::CommandNotOutstanding); - }; - self.complete( - command_id, - expected_kind, - WebDriverBiDiCorrelatedResponseOutcome::Success, - ) - } + WebDriverBiDiJsonEnvelopeKind::Success => envelope.command_id().map_or_else( + || Err(WebDriverBiDiCommandCorrelationError::CommandNotOutstanding), + |command_id| { + self.complete( + command_id, + expected_kind, + WebDriverBiDiCorrelatedResponseOutcome::Success, + ) + }, + ), } } diff --git a/crates/originweave-network/src/webdriver_bidi_json_envelope.rs b/crates/originweave-network/src/webdriver_bidi_json_envelope.rs index e99adf2c9..b48aa20b4 100644 --- a/crates/originweave-network/src/webdriver_bidi_json_envelope.rs +++ b/crates/originweave-network/src/webdriver_bidi_json_envelope.rs @@ -626,6 +626,7 @@ impl<'a> JsonCursor<'a> { #[cfg(test)] mod tests { use super::*; + use crate::{WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError}; fn parse(value: &str) -> Result { WebDriverBiDiJsonEnvelope::parse_str(value) @@ -716,6 +717,21 @@ mod tests { assert!(success.is_ok()); } + #[test] + fn correlation_rejects_a_success_envelope_if_its_parser_invariant_is_broken() { + let envelope = std::hint::black_box(WebDriverBiDiJsonEnvelope { + kind: WebDriverBiDiJsonEnvelopeKind::Success, + command_id: None, + method: None, + error_code: None, + }); + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + assert_eq!( + correlation.correlate_response(&envelope), + Err(WebDriverBiDiCommandCorrelationError::CommandNotOutstanding) + ); + } + #[test] fn envelope_shape_failures_are_typed() { let cases = [ diff --git a/crates/originweave-network/src/webdriver_bidi_text_value_observation_public_boundary_tests.rs b/crates/originweave-network/src/webdriver_bidi_text_value_observation_public_boundary_tests.rs index 0016dde5f..c566d8975 100644 --- a/crates/originweave-network/src/webdriver_bidi_text_value_observation_public_boundary_tests.rs +++ b/crates/originweave-network/src/webdriver_bidi_text_value_observation_public_boundary_tests.rs @@ -9,7 +9,7 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use crate::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandKind, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiTextValueObservationResponseError, WebDriverBiDiTextValueObservationResult, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, @@ -108,7 +108,7 @@ fn read_text_over_loopback( fn public_text_value_boundary_covers_error_adapters_and_credential_safe_result() -> Result<(), Box> { let mut correlation = WebDriverBiDiCommandCorrelation::new(); - correlation.register_command(70)?; + correlation.register_command_for(70, WebDriverBiDiCommandKind::TextValueObservation)?; let invalid = read_text_over_loopback(b"not-json")?; let envelope_result = WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( diff --git a/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs b/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs index f7c87f3bb..01ccde199 100644 --- a/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs @@ -6,8 +6,8 @@ use originweave_core::{ use crate::{ WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, - WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeError, WebDriverBiDiJsonEnvelopeKind, - WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiCommandKind, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeError, + WebDriverBiDiJsonEnvelopeKind, WebDriverBiDiWebSocketTextMessage, }; const MAX_SCRIPT_RESULT_OBJECT_MEMBERS: usize = 64; @@ -70,7 +70,10 @@ impl WebDriverBiDiTextValueObservationResult { } WebDriverBiDiJsonEnvelopeKind::Error => { let completed = correlation - .correlate_response(&envelope) + .correlate_response_for( + &envelope, + WebDriverBiDiCommandKind::TextValueObservation, + ) .map_err(|source| { WebDriverBiDiTextValueObservationResponseError::Correlation { source } })?; @@ -85,7 +88,10 @@ impl WebDriverBiDiTextValueObservationResult { WebDriverBiDiTextValueObservationResponseError::Projection { source } })?; let completed = correlation - .correlate_response(&envelope) + .correlate_response_for( + &envelope, + WebDriverBiDiCommandKind::TextValueObservation, + ) .map_err(|source| { WebDriverBiDiTextValueObservationResponseError::Correlation { source } })?; diff --git a/crates/originweave-network/tests/webdriver_bidi_command_kind_correlation.rs b/crates/originweave-network/tests/webdriver_bidi_command_kind_correlation.rs index faa1728d9..af499855f 100644 --- a/crates/originweave-network/tests/webdriver_bidi_command_kind_correlation.rs +++ b/crates/originweave-network/tests/webdriver_bidi_command_kind_correlation.rs @@ -89,10 +89,8 @@ fn typed_response_provenance_cannot_cross_the_legacy_correlation_boundary() ); assert_eq!(correlation.outstanding_count(), 1); - let completed = correlation.correlate_response_for( - &response, - WebDriverBiDiCommandKind::TextValueObservation, - )?; + let completed = correlation + .correlate_response_for(&response, WebDriverBiDiCommandKind::TextValueObservation)?; assert_eq!(completed.command_id(), 42); assert_eq!( completed.outcome(), diff --git a/crates/originweave-network/tests/webdriver_bidi_text_value_observation_correlation.rs b/crates/originweave-network/tests/webdriver_bidi_text_value_observation_correlation.rs index 25deaea42..856a7f6cb 100644 --- a/crates/originweave-network/tests/webdriver_bidi_text_value_observation_correlation.rs +++ b/crates/originweave-network/tests/webdriver_bidi_text_value_observation_correlation.rs @@ -8,7 +8,7 @@ use std::{ use originweave_core::{MAX_WEBDRIVER_BIDI_TYPE_TEXT_BYTES, WebDriverBiDiWebSocketEndpoint}; use originweave_network::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandKind, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiTextValueObservationResponseError, WebDriverBiDiTextValueObservationResult, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, @@ -131,7 +131,7 @@ fn require_expected_text_error( fn protocol_error_correlation_and_diagnostics_fail_closed_without_consuming_other_state() -> Result<(), Box> { let mut correlation = WebDriverBiDiCommandCorrelation::new(); - correlation.register_command(70)?; + correlation.register_command_for(70, WebDriverBiDiCommandKind::TextValueObservation)?; let unknown_protocol_error = receive_server_text( br#"{"type":"error","id":71,"error":"unknown error","message":"remote failure"}"#, @@ -181,7 +181,7 @@ fn invalid_expected_text_fails_before_response_or_correlation_state_is_touched() -> Result<(), Box> { let invalid_envelope = receive_server_text(b"not-json")?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); - correlation.register_command(70)?; + correlation.register_command_for(70, WebDriverBiDiCommandKind::TextValueObservation)?; let empty_error = require_expected_text_error(&invalid_envelope, "", &mut correlation)?; assert!(matches!( diff --git a/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs b/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs index 12a74776e..bcbb3d27c 100644 --- a/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs +++ b/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs @@ -16,7 +16,7 @@ use originweave_core::{ WebDriverBiDiWebSocketEndpoint, }; use originweave_network::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandKind, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiTextValueObservationResponseError, WebDriverBiDiTextValueObservationResult, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler, @@ -301,7 +301,7 @@ fn response_admission_failures_preserve_or_consume_exact_correlation_state() -> Result<(), Box> { let invalid = receive_server_text(b"not-json")?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); - correlation.register_command(70)?; + correlation.register_command_for(70, WebDriverBiDiCommandKind::TextValueObservation)?; let Err(envelope_error) = WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( &invalid, "expected", @@ -331,7 +331,7 @@ fn response_admission_failures_preserve_or_consume_exact_correlation_state() )); assert_eq!(correlation.outstanding_count(), 1); - correlation.register_command(71)?; + correlation.register_command_for(71, WebDriverBiDiCommandKind::TextValueObservation)?; let protocol_error = receive_server_text( br#"{"type":"error","id":71,"error":"unknown error","message":"remote failure"}"#, )?; @@ -368,7 +368,7 @@ fn response_admission_failures_preserve_or_consume_exact_correlation_state() assert!(correlation_error.source().is_some()); assert_eq!(correlation.outstanding_count(), 1); - correlation.register_command(73)?; + correlation.register_command_for(73, WebDriverBiDiCommandKind::TextValueObservation)?; let malformed_projection = receive_server_text( br#"{"type":"success","id":73,"result":{"type":"success","result":{"type":"string","value":"expected"}}}"#, )?; diff --git a/crates/originweave-network/tests/webdriver_bidi_text_value_observation_unicode_response.rs b/crates/originweave-network/tests/webdriver_bidi_text_value_observation_unicode_response.rs index dda339a19..5653cad2b 100644 --- a/crates/originweave-network/tests/webdriver_bidi_text_value_observation_unicode_response.rs +++ b/crates/originweave-network/tests/webdriver_bidi_text_value_observation_unicode_response.rs @@ -8,7 +8,7 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use originweave_network::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandKind, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiTextValueObservationResponseError, WebDriverBiDiTextValueObservationResult, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, @@ -93,7 +93,7 @@ fn escaped_unicode_is_compared_after_bounded_response_projection() -> Result<(), br#"{"type":"success","id":81,"result":{"type":"success","realm":"r","result":{"type":"string","value":"\u20ac"}}}"#, )?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); - correlation.register_command(81)?; + correlation.register_command_for(81, WebDriverBiDiCommandKind::TextValueObservation)?; let result = WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( &response, @@ -115,7 +115,7 @@ fn projection_error_diagnostic_remains_structural_and_non_sensitive() -> Result< br#"{"type":"success","id":82,"result":{"type":"success","result":{"type":"string","value":"x"}}}"#, )?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); - correlation.register_command(82)?; + correlation.register_command_for(82, WebDriverBiDiCommandKind::TextValueObservation)?; let Err(WebDriverBiDiTextValueObservationResponseError::Projection { source }) = WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( diff --git a/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs b/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs index 7fe9400ee..2fae4e2a7 100644 --- a/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs +++ b/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs @@ -8,7 +8,7 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use originweave_network::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandKind, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiTextValuePostconditionError, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, WebDriverBiDiWebSocketTextMessage, @@ -102,7 +102,7 @@ fn exact_match_is_the_only_successful_text_postcondition() -> Result<(), Box Result<(), br#"{"type":"success","id":71,"result":{"type":"success","realm":"realm-1","result":{"type":"string","value":"unexpected"}}}"#, )?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); - correlation.register_command(71)?; + correlation.register_command_for(71, WebDriverBiDiCommandKind::TextValueObservation)?; let Err(error) = verify_webdriver_bidi_text_value_postcondition(&response, "expected", &mut correlation) @@ -155,7 +155,7 @@ fn malformed_observation_stays_a_typed_source_error_without_consuming_state() -> Result<(), Box> { let response = receive_server_text(b"not-json")?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); - correlation.register_command(72)?; + correlation.register_command_for(72, WebDriverBiDiCommandKind::TextValueObservation)?; let Err(error) = verify_webdriver_bidi_text_value_postcondition(&response, "expected", &mut correlation) diff --git a/crates/originweave-network/tests/webdriver_bidi_text_value_response_fail_closed.rs b/crates/originweave-network/tests/webdriver_bidi_text_value_response_fail_closed.rs index c51fb2291..85bfd0da1 100644 --- a/crates/originweave-network/tests/webdriver_bidi_text_value_response_fail_closed.rs +++ b/crates/originweave-network/tests/webdriver_bidi_text_value_response_fail_closed.rs @@ -8,7 +8,7 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use originweave_network::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandKind, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiTextValueObservationResponseError, WebDriverBiDiTextValueObservationResult, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, @@ -116,7 +116,7 @@ fn production_instantiation_fails_closed_for_event_protocol_error_and_script_exc )); assert_eq!(correlation.outstanding_count(), 0); - correlation.register_command(73)?; + correlation.register_command_for(73, WebDriverBiDiCommandKind::TextValueObservation)?; let protocol_error = read_text_over_loopback(PROTOCOL_ERROR)?; assert!(matches!( WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( @@ -128,7 +128,7 @@ fn production_instantiation_fails_closed_for_event_protocol_error_and_script_exc )); assert_eq!(correlation.outstanding_count(), 0); - correlation.register_command(74)?; + correlation.register_command_for(74, WebDriverBiDiCommandKind::TextValueObservation)?; let script_exception = read_text_over_loopback(SCRIPT_EXCEPTION)?; assert!(matches!( WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( From 802ec806cdd4560eab48c484f435766ecabda353 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 10:56:53 +0900 Subject: [PATCH 45/64] docs(network): scope typed-text postconditions Make exact response correlation, equality-only success, and page-text non-retention durable in the active-stack evidence contract.\n\nCommit-Message-Assisted-by: Claude (via Claude Code) Signed-off-by: Seongho Bae --- CHANGELOG.md | 1 + .../action-postcondition-evidence.md | 14 ++++++++++++-- tests/test_product_documentation_contract.py | 17 +++++++++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68829fd58..c0c1b9921 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Registry-issued admitted node handles and authority-bound WebDriver BiDi pointer-click construction that revalidate the exact session, context, canonical origin, document epoch, registry provenance, and retained `sharedId` before serializing `input.performActions`; caller-constructed node tuples or arbitrary wire identifiers cannot become typed-input authority, and the command itself grants no policy or Agent authority. - Node-bound WebDriver BiDi text input that revalidates the exact session, browsing context, canonical origin, current document epoch, registry-issued node provenance, and admitted `sharedId` before serializing bounded protocol-safe non-secret `input.performActions`; the command focuses the admitted element before keyboard input, and its diagnostic representation exposes only command metadata and text byte length rather than typed text or the serialized wire payload. - Fixed sandboxed text-value observation for an admitted current node, with product-owned `script.callFunction` source and no generic script surface; transport correlation and observed post-condition comparison remain separate active-stack work. +- Typed text-value post-condition verification that admits only the exact correlated observation response, compares it with the already-authorized expected text, discards page-controlled text, and reports mismatch without treating command acknowledgement as success. - Fail-closed rejection of reviewed Unicode format and bidirectional-override characters in accessibility roles, accessible names, BiDi `sharedId` values, and registry external identifiers, while ordinary spaces in accessible names remain valid. - Credential-safe browser-protocol validation evidence that copies only the already validated protocol family, OriginWeave generation, adapter version, pinned protocol/browser revisions, and exact capability into cloneable audit metadata without recreating the non-cloneable validation prerequisite or granting browser/Agent authority. - Canonical HTTPS and loopback-origin boundary with case-normalized schemes and hosts, default-port normalization, IPv4/IPv6 handling, browser-special numeric-host rejection, and explicit malformed-input errors. diff --git a/docs/traceability/action-postcondition-evidence.md b/docs/traceability/action-postcondition-evidence.md index f0d4ff18d..7c0f19fe3 100644 --- a/docs/traceability/action-postcondition-evidence.md +++ b/docs/traceability/action-postcondition-evidence.md @@ -3,7 +3,7 @@ - **Documentation status:** Active-stack evidence dossier; protected-main truth is called out separately - **Canonical owner:** issue #28 (`Complete the first real Chromium agent vertical slice`) - **Protected-main baseline:** `542ca1e9c0a863595b8b6697790005d2471f5413` -- **Active stack tip at this revision:** PR #269 (`feat/core: observe typed-text postconditions without ambient script authority`) +- **Active stack tip at this revision:** PR #271 (`feat/network: admit and compare typed-text postconditions`) - **Capability maturity:** **PARTIAL** - **Governing decisions:** Accepted ADR 0003 plus Proposed ADR 0106 preserve provenance-native evidence and separation of action execution from verification. @@ -73,6 +73,14 @@ PR #269 adds a fixed sandboxed text-value observation command on top of the node The command only serializes an observation request. It performs no browser I/O, accepts no page/model-supplied script, grants no policy or action authority, and does not prove browser execution or post-condition success. Descendant transport and response slices must correlate the exact response and compare the returned non-secret text with the intended value before any action outcome can become verified. +### PR #271 — correlated typed-text post-condition comparison + +**Capability maturity:** `IMPLEMENTED_ON_ACTIVE_STACK` + +PR #271 admits only a `script.callFunction` response correlated to the exact outstanding text-value observation command family. A successful string result becomes positive evidence only when it exactly equals the already-authorized expected text; a different value returns `PostconditionMismatch`, so protocol acknowledgement and parser success cannot certify browser state. + +The page-controlled text is discarded at the comparison boundary. The public result retains only the command identifier and UTF-8 byte count, while errors expose neither observed nor expected text. This evidence does not prove the preceding action was authorized, transported, or executed through the intended browser process, and it remains active-stack evidence until the full vertical slice is integrated and accepted on protected main. + ## 4. Non-transitive success semantics The intended first-slice chain remains: @@ -112,7 +120,9 @@ The first real Chromium vertical slice remains distributed rather than shipped a - PRs #261–#264 committed-navigation origin/subscription/admission lifecycle; - PR #265 send-time pointer input revalidation against admitted node authority; and - PR #266 node-bound non-secret text input with privacy-safe diagnostics; and -- PR #269 fixed text-value observation construction, still without browser I/O or outcome verification. +- PR #269 fixed text-value observation construction, still without browser I/O or outcome verification; +- PR #270 exact transport of that fixed observation, still without response success; and +- PR #271 correlated response admission and exact text-value comparison with value-free evidence. These pieces do not transfer evidence across heads. A descendant must be revalidated after any parent movement, and protected-main shipment requires fresh integrated acceptance after dependency-ordered merge by an authorized integrator. diff --git a/tests/test_product_documentation_contract.py b/tests/test_product_documentation_contract.py index 42263ded1..77900eec6 100644 --- a/tests/test_product_documentation_contract.py +++ b/tests/test_product_documentation_contract.py @@ -329,6 +329,23 @@ def test_typed_text_postcondition_observation_is_durably_scoped(self) -> None: self.assertIn(phrase, traceability) self.assertIn("Fixed sandboxed text-value observation", changelog) + def test_typed_text_postcondition_result_is_durably_scoped(self) -> None: + """A correlated response must not become success before exact value comparison.""" + + traceability = ( + ROOT / "docs/traceability/action-postcondition-evidence.md" + ).read_text(encoding="utf-8") + changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8") + for phrase in ( + "PR #271", + "PostconditionMismatch", + "page-controlled text is discarded", + "does not prove the preceding action was authorized", + ): + with self.subTest(phrase=phrase): + self.assertIn(phrase, traceability) + self.assertIn("Typed text-value post-condition verification", changelog) + def test_traceability_labels_conversation_derived_future_work(self) -> None: """Conversation decisions must preserve canonical maturity instead of becoming shipped claims.""" traceability = (ROOT / "docs/traceability/README.md").read_text(encoding="utf-8") From 25318878b8f87f770377e8db492de25c415a936b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 11:29:34 +0900 Subject: [PATCH 46/64] test(network): reject text observation replies from another connection --- ...er_bidi_text_value_observation_response.rs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs b/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs index bcbb3d27c..1d999711f 100644 --- a/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs +++ b/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs @@ -59,6 +59,51 @@ fn semantic_observation_proof() -> Result Result<(), Box> { + for payload in [ + OBSERVATION_SUCCESS_RESPONSE, + br#"{"type":"error","id":43,"error":"invalid argument","message":"rejected"}"#, + br#"{"type":"success","id":43,"result":{"type":"exception","realm":"realm-1","exceptionDetails":{"text":"rejected","lineNumber":0,"columnNumber":0,"exception":{"type":"undefined"},"stackTrace":{"callFrames":[]}}}}"#, + ] { + 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)?; + assert!(command.starts_with(br#"{"id":43,"method":"script.callFunction""#)); + Ok(()) + }); + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? + .correlate_session_id(SESSION_ID)? + .into_explicit_connect_target()?; + let connection = WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; + let established = WebDriverBiDiWebSocketHandshakePlan::new( + connection, WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?, + )?.write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?; + let (registry, handle, remote) = admitted_text_field_fixture()?; + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + correlation.register_command_for(44, WebDriverBiDiCommandKind::SessionStatus)?; + let _original = send_webdriver_bidi_text_value_observation( + semantic_observation_proof()?, 43, "context-a", &handle, &remote, ®istry, + established, &mut correlation, WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + Duration::from_millis(500), + )?; + server.join().map_err(|_| io::Error::other("original observation server panicked"))??; + let foreign = receive_server_text(payload)?; + let rejected = WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( + &foreign, "Quarterly review", &mut correlation, + ); + assert!(rejected.is_err(), "a replacement connection completed the original observation"); + assert_eq!(correlation.outstanding_count(), 2); + } + Ok(()) +} + fn admitted_text_field_fixture() -> Result> { let mut registry = BrowserAuthorityRegistry::new(); let browser_session = registry.register_session(SESSION_ID)?; From 17d160303a8daf7c0b401eb495f00b636a5e5225 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 11:30:05 +0900 Subject: [PATCH 47/64] test(network): use existing sibling family in response regression --- .../tests/webdriver_bidi_text_value_observation_response.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs b/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs index 1d999711f..fd3a57e17 100644 --- a/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs +++ b/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs @@ -87,7 +87,7 @@ fn replacement_connection_cannot_complete_text_observation() -> Result<(), Box Date: Mon, 7 Sep 2026 11:30:45 +0900 Subject: [PATCH 48/64] test(network): preserve an unrelated observation request in regression --- .../tests/webdriver_bidi_text_value_observation_response.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs b/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs index fd3a57e17..263e6968b 100644 --- a/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs +++ b/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs @@ -87,7 +87,7 @@ fn replacement_connection_cannot_complete_text_observation() -> Result<(), Box Date: Mon, 7 Sep 2026 11:39:14 +0900 Subject: [PATCH 49/64] fix(network): bind field value replies to their sender connection --- CHANGELOG.md | 2 + ...value_observation_public_boundary_tests.rs | 22 +- ...er_bidi_text_value_observation_response.rs | 22 +- ...webdriver_bidi_text_value_postcondition.rs | 7 +- .../tests/support/text_observation.rs | 235 ++++++++++++++++++ ...bidi_text_value_observation_correlation.rs | 22 +- ...er_bidi_text_value_observation_response.rs | 71 +++--- ...text_value_observation_unicode_response.rs | 30 +-- ...iver_bidi_text_value_postcondition_gate.rs | 41 +-- ...er_bidi_text_value_response_fail_closed.rs | 29 ++- docs/doctoring/browser-agent-protocols.md | 2 + 11 files changed, 377 insertions(+), 106 deletions(-) create mode 100644 crates/originweave-network/tests/support/text_observation.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 39865eac7..65954029c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to OriginWeave are documented in this file. The format follo ## [Unreleased] +- Reject field-value replies received on a replacement connection, even when their request identifier and text match. + - Reject field-observation requests on another browser session and preserve pending requests only when a write may have reached the peer. - Preserve fixed field-observation checks while adopting current input safeguards; constructing a request still does not verify that the field changed. diff --git a/crates/originweave-network/src/webdriver_bidi_text_value_observation_public_boundary_tests.rs b/crates/originweave-network/src/webdriver_bidi_text_value_observation_public_boundary_tests.rs index c566d8975..133958d56 100644 --- a/crates/originweave-network/src/webdriver_bidi_text_value_observation_public_boundary_tests.rs +++ b/crates/originweave-network/src/webdriver_bidi_text_value_observation_public_boundary_tests.rs @@ -9,11 +9,11 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use crate::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandKind, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandKind, WebDriverBiDiConnectionMessageRead, + WebDriverBiDiReceivedTextMessage, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiTextValueObservationResponseError, WebDriverBiDiTextValueObservationResult, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, - WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiWebSocketMessageReader, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -64,7 +64,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<()> { @@ -86,10 +86,10 @@ fn read_text_over_loopback( )? .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!( "validated text frame produced unexpected assembly state: {other:?}" @@ -182,6 +182,12 @@ fn public_text_value_boundary_covers_error_adapters_and_credential_safe_result() assert_eq!(correlation.outstanding_count(), 1); let valid_success = read_text_over_loopback(VALID_SUCCESS)?; + correlation.retire_command_for(70, WebDriverBiDiCommandKind::TextValueObservation)?; + correlation.register_command_for_connection( + 70, + WebDriverBiDiCommandKind::TextValueObservation, + valid_success.connection_generation(), + )?; let result = WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( &valid_success, FINAL_EXPECTED_TEXT, diff --git a/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs b/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs index 01ccde199..1740ba756 100644 --- a/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_text_value_observation_response.rs @@ -7,7 +7,7 @@ use originweave_core::{ use crate::{ WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, WebDriverBiDiCommandKind, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeError, - WebDriverBiDiJsonEnvelopeKind, WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiJsonEnvelopeKind, WebDriverBiDiReceivedTextMessage, }; const MAX_SCRIPT_RESULT_OBJECT_MEMBERS: usize = 64; @@ -54,13 +54,16 @@ impl WebDriverBiDiTextValueObservationResult { /// immediately drops the page-controlled string after computing byte count and equality. This /// boundary does not retry, grant browser or policy authority, retain a realm identifier, or /// claim success when the observed value differs from the expected text. + /// The received message must belong to the exact connection generation registered by the + /// sender. Missing or foreign connection provenance leaves the pending request untouched, + /// including for protocol errors and script exceptions. pub fn parse_correlate_and_compare( - message: &WebDriverBiDiWebSocketTextMessage, + message: &WebDriverBiDiReceivedTextMessage, expected_text: &str, correlation: &mut WebDriverBiDiCommandCorrelation, ) -> Result { validate_expected_text(expected_text)?; - let envelope = WebDriverBiDiJsonEnvelope::parse(message).map_err(|source| { + let envelope = WebDriverBiDiJsonEnvelope::parse(message.message()).map_err(|source| { WebDriverBiDiTextValueObservationResponseError::Envelope { source } })?; @@ -70,9 +73,10 @@ impl WebDriverBiDiTextValueObservationResult { } WebDriverBiDiJsonEnvelopeKind::Error => { let completed = correlation - .correlate_response_for( + .correlate_response_for_connection( &envelope, WebDriverBiDiCommandKind::TextValueObservation, + message.connection_generation(), ) .map_err(|source| { WebDriverBiDiTextValueObservationResponseError::Correlation { source } @@ -84,13 +88,15 @@ impl WebDriverBiDiTextValueObservationResult { ) } WebDriverBiDiJsonEnvelopeKind::Success => { - let projection = project_script_result(message.as_str()).map_err(|source| { - WebDriverBiDiTextValueObservationResponseError::Projection { source } - })?; + let projection = + project_script_result(message.message().as_str()).map_err(|source| { + WebDriverBiDiTextValueObservationResponseError::Projection { source } + })?; let completed = correlation - .correlate_response_for( + .correlate_response_for_connection( &envelope, WebDriverBiDiCommandKind::TextValueObservation, + message.connection_generation(), ) .map_err(|source| { WebDriverBiDiTextValueObservationResponseError::Correlation { source } diff --git a/crates/originweave-network/src/webdriver_bidi_text_value_postcondition.rs b/crates/originweave-network/src/webdriver_bidi_text_value_postcondition.rs index 7952f1015..719889150 100644 --- a/crates/originweave-network/src/webdriver_bidi_text_value_postcondition.rs +++ b/crates/originweave-network/src/webdriver_bidi_text_value_postcondition.rs @@ -1,8 +1,8 @@ use std::{error::Error, fmt}; use crate::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiTextValueObservationResponseError, - WebDriverBiDiTextValueObservationResult, WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiCommandCorrelation, WebDriverBiDiReceivedTextMessage, + WebDriverBiDiTextValueObservationResponseError, WebDriverBiDiTextValueObservationResult, }; /// Credential-minimal proof that one exact correlated text observation matched the authorized @@ -90,11 +90,12 @@ impl Error for WebDriverBiDiTextValuePostconditionError { /// observation consumes its correlated command because the response is complete, but returns a /// typed negative result rather than `Ok`. This prevents command acknowledgment, parser success, or /// correlation success from being mistaken for successful browser state mutation. +/// Only a message received on the sender's exact connection can supply this evidence. /// /// No page-controlled text, expected text, realm identifier, credential, secret, browser authority, /// or policy authority is retained in the returned value or error diagnostics. pub fn verify_webdriver_bidi_text_value_postcondition( - message: &WebDriverBiDiWebSocketTextMessage, + message: &WebDriverBiDiReceivedTextMessage, expected_text: &str, correlation: &mut WebDriverBiDiCommandCorrelation, ) -> Result { diff --git a/crates/originweave-network/tests/support/text_observation.rs b/crates/originweave-network/tests/support/text_observation.rs new file mode 100644 index 000000000..0f009f98c --- /dev/null +++ b/crates/originweave-network/tests/support/text_observation.rs @@ -0,0 +1,235 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::{ + AdmittedNodeHandle, BoundedWebDriverBiDiResponseDocument, BrowserAuthorityRegistry, + BrowserContextDispatchTarget, BrowserContextOriginDispatchTarget, + BrowserContextOriginEpochDispatchTarget, BrowserProtocolAdapterDescriptor, + BrowserProtocolCapability, BrowserProtocolKind, Origin, OriginWeaveProtocolVersion, + ValidatedBrowserProtocolUse, WebDriverBiDiAccessibilityQuery, WebDriverBiDiLocateNodesCommand, + WebDriverBiDiRemoteNodeReference, WebDriverBiDiWebSocketEndpoint, +}; +use originweave_network::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiConnectionMessageRead, + WebDriverBiDiReceivedTextMessage, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageReader, + send_webdriver_bidi_text_value_observation, +}; + +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 ORIGINWEAVE_PROTOCOL_VERSION: OriginWeaveProtocolVersion = + OriginWeaveProtocolVersion::new(0, 1); +const ADAPTER_VERSION: &str = "originweave-bidi-v1"; +const PROTOCOL_REVISION: &str = "webdriver-bidi-wd-2026-06-01"; +const BROWSER_REVISION: &str = "chromium-r1639810"; + +type AdmittedTextFieldFixture = ( + BrowserAuthorityRegistry, + AdmittedNodeHandle, + WebDriverBiDiRemoteNodeReference, +); + +fn semantic_observation_proof() -> Result> { + let descriptor = BrowserProtocolAdapterDescriptor::new( + BrowserProtocolKind::WebDriverBiDi, + ORIGINWEAVE_PROTOCOL_VERSION, + ADAPTER_VERSION, + PROTOCOL_REVISION, + BROWSER_REVISION, + &[BrowserProtocolCapability::SemanticObservation], + )?; + Ok(descriptor.validate_use( + ORIGINWEAVE_PROTOCOL_VERSION, + BrowserProtocolKind::WebDriverBiDi, + ADAPTER_VERSION, + PROTOCOL_REVISION, + BROWSER_REVISION, + BrowserProtocolCapability::SemanticObservation, + )?) +} + +fn admitted_text_field_fixture() -> Result> { + let mut registry = BrowserAuthorityRegistry::new(); + let browser_session = registry.register_session(SESSION_ID)?; + let browsing_context = registry.register_context(browser_session, "context-a")?; + let origin = Origin::parse("https://app.example").map_err(|error| { + io::Error::other(format!("fixture origin rejected unexpectedly: {error:?}")) + })?; + let epoch = registry.bind_context_origin(browser_session, browsing_context, &origin)?; + let target = BrowserContextOriginEpochDispatchTarget::new( + BrowserContextOriginDispatchTarget::new( + BrowserContextDispatchTarget::new(browser_session, browsing_context), + &origin, + ), + epoch, + ); + let query = WebDriverBiDiAccessibilityQuery::new(Some("textbox"), Some("Task title"), 1)?; + let locate = WebDriverBiDiLocateNodesCommand::new(41, "context-a", &query)?; + let document = BoundedWebDriverBiDiResponseDocument::new( + r#"{"type":"success","id":41,"result":{"nodes":[{"type":"node","sharedId":"shared-node-42"}]}}"#, + )?; + let handle = locate + .bind_response_document_nodes( + document, + semantic_observation_proof()?, + &mut registry, + target, + )? + .into_iter() + .next() + .ok_or_else(|| io::Error::other("locateNodes fixture did not bind its node"))?; + let remote = WebDriverBiDiRemoteNodeReference::new("node", Some("shared-node-42"))?; + Ok((registry, handle, 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)?; + usize::try_from(u64::from_be_bytes(extended)).map_err(|_| { + io::Error::new(io::ErrorKind::InvalidData, "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 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 response unexpectedly required 64-bit framing", + )); + } + } + stream.write_all(payload) +} + +pub fn receive_command_responses( + responses: &[&[u8]], + command_id: u64, + correlation: &mut WebDriverBiDiCommandCorrelation, +) -> Result, Box> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let payloads: Vec> = responses.iter().map(|value| value.to_vec()).collect(); + let count = payloads.len(); + 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)?; + let prefix = format!("{{\"id\":{command_id},\"method\":\"script.callFunction\""); + if !command.starts_with(prefix.as_bytes()) { + return Err(io::Error::other("unexpected observation request")); + } + for payload in payloads { + write_text_frame(&mut stream, &payload)?; + } + Ok(()) + }); + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? + .correlate_session_id(SESSION_ID)? + .into_explicit_connect_target()?; + let connection = + WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; + let established = WebDriverBiDiWebSocketHandshakePlan::new( + connection, + WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?, + )? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?; + let (registry, handle, remote) = admitted_text_field_fixture()?; + let mut established = send_webdriver_bidi_text_value_observation( + semantic_observation_proof()?, + command_id, + "context-a", + &handle, + &remote, + ®istry, + established, + correlation, + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + Duration::from_millis(500), + )?; + let mut messages = Vec::new(); + for _ in 0..count { + match WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + WebDriverBiDiConnectionMessageRead::Text { + established: next, + message, + } => { + established = next; + messages.push(message); + } + other => { + return Err(io::Error::other(format!("expected response text: {other:?}")).into()); + } + } + } + server + .join() + .map_err(|_| io::Error::other("observation response server panicked"))??; + Ok(messages) +} diff --git a/crates/originweave-network/tests/webdriver_bidi_text_value_observation_correlation.rs b/crates/originweave-network/tests/webdriver_bidi_text_value_observation_correlation.rs index 856a7f6cb..560838f20 100644 --- a/crates/originweave-network/tests/webdriver_bidi_text_value_observation_correlation.rs +++ b/crates/originweave-network/tests/webdriver_bidi_text_value_observation_correlation.rs @@ -8,11 +8,11 @@ use std::{ use originweave_core::{MAX_WEBDRIVER_BIDI_TYPE_TEXT_BYTES, WebDriverBiDiWebSocketEndpoint}; use originweave_network::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandKind, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandKind, WebDriverBiDiConnectionMessageRead, + WebDriverBiDiReceivedTextMessage, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiTextValueObservationResponseError, WebDriverBiDiTextValueObservationResult, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, - WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiWebSocketMessageReader, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -54,9 +54,7 @@ fn write_text_frame(stream: &mut TcpStream, payload: &[u8]) -> io::Result<()> { stream.write_all(payload) } -fn receive_server_text( - payload: &[u8], -) -> Result> { +fn receive_server_text(payload: &[u8]) -> Result> { let listener = TcpListener::bind(("127.0.0.1", 0))?; let local_addr = listener.local_addr()?; let response = payload.to_vec(); @@ -79,10 +77,10 @@ fn receive_server_text( )? .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 message = match assembler.push_frame(frame)? { - WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + let message = match WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + WebDriverBiDiConnectionMessageRead::Text { message, .. } => message, other => { return Err(io::Error::other(format!( "fixture produced unexpected message assembly state: {other:?}" @@ -97,7 +95,7 @@ fn receive_server_text( } fn require_observation_error( - message: &WebDriverBiDiWebSocketTextMessage, + message: &WebDriverBiDiReceivedTextMessage, correlation: &mut WebDriverBiDiCommandCorrelation, ) -> Result> { match WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( @@ -111,7 +109,7 @@ fn require_observation_error( } fn require_expected_text_error( - message: &WebDriverBiDiWebSocketTextMessage, + message: &WebDriverBiDiReceivedTextMessage, expected_text: &str, correlation: &mut WebDriverBiDiCommandCorrelation, ) -> Result> { diff --git a/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs b/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs index 263e6968b..9ae26b0a2 100644 --- a/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs +++ b/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs @@ -1,3 +1,6 @@ +#[path = "support/text_observation.rs"] +mod text_observation; + use std::{ error::Error, io::{self, Read, Write}, @@ -16,11 +19,11 @@ use originweave_core::{ WebDriverBiDiWebSocketEndpoint, }; use originweave_network::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandKind, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandKind, WebDriverBiDiConnectionMessageRead, + WebDriverBiDiReceivedTextMessage, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiTextValueObservationResponseError, WebDriverBiDiTextValueObservationResult, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler, - WebDriverBiDiWebSocketMessageAssembly, WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageReader, send_webdriver_bidi_text_value_observation, }; @@ -98,7 +101,11 @@ fn replacement_connection_cannot_complete_text_observation() -> Result<(), Box io::Result<()> { stream.write_all(payload) } -fn receive_server_text( - payload: &[u8], -) -> Result> { +fn receive_server_text(payload: &[u8]) -> Result> { let listener = TcpListener::bind(("127.0.0.1", 0))?; let local_addr = listener.local_addr()?; let response = payload.to_vec(); @@ -236,10 +241,10 @@ fn receive_server_text( )? .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 message = match assembler.push_frame(frame)? { - WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + let message = match WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + WebDriverBiDiConnectionMessageRead::Text { message, .. } => message, other => { return Err(io::Error::other(format!( "fixture produced unexpected message assembly state: {other:?}" @@ -312,10 +317,10 @@ fn observed_text_postcondition_consumes_exact_command_without_exposing_page_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, + let text = match WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + WebDriverBiDiConnectionMessageRead::Text { message, .. } => message, other => { return Err(io::Error::other(format!( "text-value observation response produced unexpected assembly state: {other:?}" @@ -344,9 +349,14 @@ fn observed_text_postcondition_consumes_exact_command_without_exposing_page_text #[test] fn response_admission_failures_preserve_or_consume_exact_correlation_state() -> Result<(), Box> { - let invalid = receive_server_text(b"not-json")?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); - correlation.register_command_for(70, WebDriverBiDiCommandKind::TextValueObservation)?; + let mut original_responses = text_observation::receive_command_responses( + &[ + b"not-json", + br#"{"type":"success","id":70,"result":{"type":"success","realm":"realm-1","result":{"type":"string","value":"expected"}}}"#, + ], 70, &mut correlation, + )?; + let invalid = original_responses.remove(0); let Err(envelope_error) = WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( &invalid, "expected", @@ -376,10 +386,12 @@ fn response_admission_failures_preserve_or_consume_exact_correlation_state() )); assert_eq!(correlation.outstanding_count(), 1); - correlation.register_command_for(71, WebDriverBiDiCommandKind::TextValueObservation)?; - let protocol_error = receive_server_text( - br#"{"type":"error","id":71,"error":"unknown error","message":"remote failure"}"#, - )?; + let protocol_error = text_observation::receive_command_responses( + &[br#"{"type":"error","id":71,"error":"unknown error","message":"remote failure"}"#], + 71, + &mut correlation, + )? + .remove(0); assert!(matches!( WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( &protocol_error, @@ -413,10 +425,13 @@ fn response_admission_failures_preserve_or_consume_exact_correlation_state() assert!(correlation_error.source().is_some()); assert_eq!(correlation.outstanding_count(), 1); - correlation.register_command_for(73, WebDriverBiDiCommandKind::TextValueObservation)?; - let malformed_projection = receive_server_text( - br#"{"type":"success","id":73,"result":{"type":"success","result":{"type":"string","value":"expected"}}}"#, + let mut same_connection = text_observation::receive_command_responses( + &[ + br#"{"type":"success","id":73,"result":{"type":"success","result":{"type":"string","value":"expected"}}}"#, + br#"{"type":"success","id":73,"result":{"type":"exception","realm":"realm-1","exceptionDetails":{}}}"#, + ], 73, &mut correlation, )?; + let malformed_projection = same_connection.remove(0); let Err(projection_error) = WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( &malformed_projection, @@ -437,9 +452,7 @@ fn response_admission_failures_preserve_or_consume_exact_correlation_state() assert!(projection_error.source().is_some()); assert_eq!(correlation.outstanding_count(), 2); - let script_exception = receive_server_text( - br#"{"type":"success","id":73,"result":{"type":"exception","realm":"realm-1","exceptionDetails":{}}}"#, - )?; + let script_exception = same_connection.remove(0); assert!(matches!( WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( &script_exception, @@ -450,9 +463,7 @@ fn response_admission_failures_preserve_or_consume_exact_correlation_state() )); assert_eq!(correlation.outstanding_count(), 1); - let valid_success = receive_server_text( - br#"{"type":"success","id":70,"result":{"type":"success","realm":"realm-1","result":{"type":"string","value":"expected"}}}"#, - )?; + let valid_success = original_responses.remove(0); assert!(matches!( WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( &valid_success, diff --git a/crates/originweave-network/tests/webdriver_bidi_text_value_observation_unicode_response.rs b/crates/originweave-network/tests/webdriver_bidi_text_value_observation_unicode_response.rs index 5653cad2b..870595a05 100644 --- a/crates/originweave-network/tests/webdriver_bidi_text_value_observation_unicode_response.rs +++ b/crates/originweave-network/tests/webdriver_bidi_text_value_observation_unicode_response.rs @@ -1,3 +1,6 @@ +#[path = "support/text_observation.rs"] +mod text_observation; + use std::{ error::Error, io::{self, Read, Write}, @@ -8,11 +11,11 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use originweave_network::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandKind, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandKind, WebDriverBiDiConnectionMessageRead, + WebDriverBiDiReceivedTextMessage, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiTextValueObservationResponseError, WebDriverBiDiTextValueObservationResult, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, - WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiWebSocketMessageReader, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -36,9 +39,7 @@ fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { Ok(()) } -fn receive_server_text( - payload: &[u8], -) -> Result> { +fn receive_server_text(payload: &[u8]) -> Result> { if payload.len() > 125 { return Err(io::Error::new( io::ErrorKind::InvalidInput, @@ -70,10 +71,10 @@ fn receive_server_text( )? .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 message = match assembler.push_frame(frame)? { - WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + let message = match WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + WebDriverBiDiConnectionMessageRead::Text { message, .. } => message, other => { return Err(io::Error::other(format!( "fixture produced unexpected message assembly state: {other:?}" @@ -89,11 +90,12 @@ fn receive_server_text( #[test] fn escaped_unicode_is_compared_after_bounded_response_projection() -> Result<(), Box> { - let response = receive_server_text( - br#"{"type":"success","id":81,"result":{"type":"success","realm":"r","result":{"type":"string","value":"\u20ac"}}}"#, - )?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); - correlation.register_command_for(81, WebDriverBiDiCommandKind::TextValueObservation)?; + let response = text_observation::receive_command_responses( + &[ + br#"{"type":"success","id":81,"result":{"type":"success","realm":"r","result":{"type":"string","value":"\u20ac"}}}"#, + ], 81, &mut correlation, + )?.remove(0); let result = WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( &response, diff --git a/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs b/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs index 2fae4e2a7..df0d9765f 100644 --- a/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs +++ b/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs @@ -1,3 +1,6 @@ +#[path = "support/text_observation.rs"] +mod text_observation; + use std::{ error::Error, io::{self, Read, Write}, @@ -8,10 +11,10 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use originweave_network::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandKind, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandKind, WebDriverBiDiConnectionMessageRead, + WebDriverBiDiReceivedTextMessage, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiTextValuePostconditionError, WebDriverBiDiWebSocketClientKey, - WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMessageAssembler, - WebDriverBiDiWebSocketMessageAssembly, WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMessageReader, verify_webdriver_bidi_text_value_postcondition, }; @@ -54,9 +57,7 @@ fn write_text_frame(stream: &mut TcpStream, payload: &[u8]) -> io::Result<()> { stream.write_all(payload) } -fn receive_server_text( - payload: &[u8], -) -> Result> { +fn receive_server_text(payload: &[u8]) -> Result> { let listener = TcpListener::bind(("127.0.0.1", 0))?; let local_addr = listener.local_addr()?; let response = payload.to_vec(); @@ -79,10 +80,10 @@ fn receive_server_text( )? .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!( "fixture produced unexpected message assembly state: {other:?}" @@ -98,11 +99,12 @@ fn receive_server_text( #[test] fn exact_match_is_the_only_successful_text_postcondition() -> Result<(), Box> { - let response = receive_server_text( - br#"{"type":"success","id":70,"result":{"type":"success","realm":"realm-1","result":{"type":"string","value":"expected"}}}"#, - )?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); - correlation.register_command_for(70, WebDriverBiDiCommandKind::TextValueObservation)?; + let response = text_observation::receive_command_responses( + &[ + br#"{"type":"success","id":70,"result":{"type":"success","realm":"realm-1","result":{"type":"string","value":"expected"}}}"#, + ], 70, &mut correlation, + )?.remove(0); let verified = verify_webdriver_bidi_text_value_postcondition(&response, "expected", &mut correlation)?; @@ -116,11 +118,12 @@ fn exact_match_is_the_only_successful_text_postcondition() -> Result<(), Box Result<(), Box> { - let response = receive_server_text( - br#"{"type":"success","id":71,"result":{"type":"success","realm":"realm-1","result":{"type":"string","value":"unexpected"}}}"#, - )?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); - correlation.register_command_for(71, WebDriverBiDiCommandKind::TextValueObservation)?; + let response = text_observation::receive_command_responses( + &[ + br#"{"type":"success","id":71,"result":{"type":"success","realm":"realm-1","result":{"type":"string","value":"unexpected"}}}"#, + ], 71, &mut correlation, + )?.remove(0); let Err(error) = verify_webdriver_bidi_text_value_postcondition(&response, "expected", &mut correlation) @@ -182,7 +185,7 @@ fn unrelated_outstanding_command_cannot_certify_text_postcondition() -> Result<( br#"{"type":"success","id":73,"result":{"type":"success","realm":"realm-1","result":{"type":"string","value":"expected"}}}"#, )?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); - correlation.register_command(73)?; + correlation.register_command_for(73, WebDriverBiDiCommandKind::SessionStatus)?; let Err(error) = verify_webdriver_bidi_text_value_postcondition(&response, "expected", &mut correlation) diff --git a/crates/originweave-network/tests/webdriver_bidi_text_value_response_fail_closed.rs b/crates/originweave-network/tests/webdriver_bidi_text_value_response_fail_closed.rs index 85bfd0da1..e2b7d8537 100644 --- a/crates/originweave-network/tests/webdriver_bidi_text_value_response_fail_closed.rs +++ b/crates/originweave-network/tests/webdriver_bidi_text_value_response_fail_closed.rs @@ -1,3 +1,6 @@ +#[path = "support/text_observation.rs"] +mod text_observation; + use std::{ error::Error, io::{self, Read, Write}, @@ -8,11 +11,11 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use originweave_network::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandKind, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiCommandCorrelation, WebDriverBiDiConnectionMessageRead, + WebDriverBiDiReceivedTextMessage, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiTextValueObservationResponseError, WebDriverBiDiTextValueObservationResult, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, - WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiWebSocketMessageReader, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -61,7 +64,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<()> { @@ -83,10 +86,10 @@ fn read_text_over_loopback( )? .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!( "validated text frame produced unexpected assembly state: {other:?}" @@ -116,8 +119,9 @@ fn production_instantiation_fails_closed_for_event_protocol_error_and_script_exc )); assert_eq!(correlation.outstanding_count(), 0); - correlation.register_command_for(73, WebDriverBiDiCommandKind::TextValueObservation)?; - let protocol_error = read_text_over_loopback(PROTOCOL_ERROR)?; + let protocol_error = + text_observation::receive_command_responses(&[PROTOCOL_ERROR], 73, &mut correlation)? + .remove(0); assert!(matches!( WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( &protocol_error, @@ -128,8 +132,9 @@ fn production_instantiation_fails_closed_for_event_protocol_error_and_script_exc )); assert_eq!(correlation.outstanding_count(), 0); - correlation.register_command_for(74, WebDriverBiDiCommandKind::TextValueObservation)?; - let script_exception = read_text_over_loopback(SCRIPT_EXCEPTION)?; + let script_exception = + text_observation::receive_command_responses(&[SCRIPT_EXCEPTION], 74, &mut correlation)? + .remove(0); assert!(matches!( WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( &script_exception, diff --git a/docs/doctoring/browser-agent-protocols.md b/docs/doctoring/browser-agent-protocols.md index 72cdda1c7..480c0465a 100644 --- a/docs/doctoring/browser-agent-protocols.md +++ b/docs/doctoring/browser-agent-protocols.md @@ -18,6 +18,8 @@ OriginWeave also retains the registered command family until correlation complet The fixed field-observation sender now requires the admitted node's registered session to match the established transport session before writing. It validates the deadline before registration, binds pending requests to the exact connection generation, and uses the existing lifetime command-ID guard. Proven zero-write rejection retires that request; an ambiguous socket write keeps it pending. These transport checks do not prove the field value or a completed user action. +Field-value response admission consumes only sealed messages received on that sender's exact connection. A replacement connection cannot complete the request with a matching identifier, success value, protocol error, or script exception. Equality remains necessary for positive value evidence; response parsing and dispatch alone still do not prove a completed user action. + The same Working Draft defines `ErrorResponse.error` as `ErrorCode`. Its rendered local-end CDDL enumerates 30 values and omits `no such client window`, while §3.5 separately defines `no such client window` and normative client-window algorithms return that error code. OriginWeave therefore admits the finite rendered CDDL vocabulary plus this one separately defined normative error, and still rejects arbitrary error-code text fail closed. This is an explicit interoperability exception for a specification-internal inconsistency, not authority to infer or accept other strings; adding any further code requires fresh primary-source review and regression evidence. Primary sources: World Wide Web Consortium, *WebDriver BiDi* (published Working Draft and current Editor’s Draft). From b0410ae92bd20eaf31d09b7d49390e13cb045999 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 11:48:51 +0900 Subject: [PATCH 50/64] test(network): prove original observation survives replacement reply --- ...value_observation_public_boundary_tests.rs | 23 ++-- ...er_bidi_text_value_observation_response.rs | 104 ++++++++++++------ 2 files changed, 86 insertions(+), 41 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_text_value_observation_public_boundary_tests.rs b/crates/originweave-network/src/webdriver_bidi_text_value_observation_public_boundary_tests.rs index 133958d56..70725b778 100644 --- a/crates/originweave-network/src/webdriver_bidi_text_value_observation_public_boundary_tests.rs +++ b/crates/originweave-network/src/webdriver_bidi_text_value_observation_public_boundary_tests.rs @@ -64,6 +64,7 @@ fn write_unmasked_text_frame(stream: &mut TcpStream, document: &[u8]) -> io::Res fn read_text_over_loopback( document: &'static [u8], + correlation: Option<&mut WebDriverBiDiCommandCorrelation>, ) -> Result> { let listener = TcpListener::bind(("127.0.0.1", 0))?; let local_addr = listener.local_addr()?; @@ -86,6 +87,13 @@ fn read_text_over_loopback( )? .write_opening_request(Duration::from_millis(500))? .read_opening_response(Duration::from_millis(500))?; + if let Some(correlation) = correlation { + correlation.register_command_for_connection( + 70, + WebDriverBiDiCommandKind::TextValueObservation, + established.transport_evidence().connection_generation(), + )?; + } let text = match WebDriverBiDiWebSocketMessageReader::new(established) .read_next(Duration::from_millis(500))? { @@ -110,7 +118,7 @@ fn public_text_value_boundary_covers_error_adapters_and_credential_safe_result() let mut correlation = WebDriverBiDiCommandCorrelation::new(); correlation.register_command_for(70, WebDriverBiDiCommandKind::TextValueObservation)?; - let invalid = read_text_over_loopback(b"not-json")?; + let invalid = read_text_over_loopback(b"not-json", None)?; let envelope_result = WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( &invalid, "expected", @@ -130,7 +138,7 @@ fn public_text_value_boundary_covers_error_adapters_and_credential_safe_result() ); assert_eq!(correlation.outstanding_count(), 1); - let error_unknown = read_text_over_loopback(ERROR_UNKNOWN_COMMAND)?; + let error_unknown = read_text_over_loopback(ERROR_UNKNOWN_COMMAND, None)?; assert!(matches!( WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( &error_unknown, @@ -141,7 +149,7 @@ fn public_text_value_boundary_covers_error_adapters_and_credential_safe_result() )); assert_eq!(correlation.outstanding_count(), 1); - let malformed_projection = read_text_over_loopback(MALFORMED_PROJECTION)?; + let malformed_projection = read_text_over_loopback(MALFORMED_PROJECTION, None)?; let projection_result = WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( &malformed_projection, "expected", @@ -161,7 +169,7 @@ fn public_text_value_boundary_covers_error_adapters_and_credential_safe_result() ); assert_eq!(correlation.outstanding_count(), 1); - let success_unknown = read_text_over_loopback(SUCCESS_UNKNOWN_COMMAND)?; + let success_unknown = read_text_over_loopback(SUCCESS_UNKNOWN_COMMAND, None)?; let correlation_result = WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( &success_unknown, "expected", @@ -181,13 +189,8 @@ fn public_text_value_boundary_covers_error_adapters_and_credential_safe_result() ); assert_eq!(correlation.outstanding_count(), 1); - let valid_success = read_text_over_loopback(VALID_SUCCESS)?; correlation.retire_command_for(70, WebDriverBiDiCommandKind::TextValueObservation)?; - correlation.register_command_for_connection( - 70, - WebDriverBiDiCommandKind::TextValueObservation, - valid_success.connection_generation(), - )?; + let valid_success = read_text_over_loopback(VALID_SUCCESS, Some(&mut correlation))?; let result = WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( &valid_success, FINAL_EXPECTED_TEXT, diff --git a/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs b/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs index 9ae26b0a2..63213c31f 100644 --- a/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs +++ b/crates/originweave-network/tests/webdriver_bidi_text_value_observation_response.rs @@ -4,7 +4,7 @@ mod text_observation; use std::{ error::Error, io::{self, Read, Write}, - net::{TcpListener, TcpStream}, + net::{SocketAddr, TcpListener, TcpStream}, thread, time::Duration, }; @@ -22,9 +22,9 @@ use originweave_network::{ WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandKind, WebDriverBiDiConnectionMessageRead, WebDriverBiDiReceivedTextMessage, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiTextValueObservationResponseError, WebDriverBiDiTextValueObservationResult, - WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageReader, - send_webdriver_bidi_text_value_observation, + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketEstablished, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, + WebDriverBiDiWebSocketMessageReader, send_webdriver_bidi_text_value_observation, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -62,51 +62,93 @@ fn semantic_observation_proof() -> Result 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_text( + established: WebDriverBiDiWebSocketEstablished, +) -> Result> { + match WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + WebDriverBiDiConnectionMessageRead::Text { message, .. } => Ok(message), + _ => Err(io::Error::other("fixture expected a complete text response").into()), + } +} + #[test] fn replacement_connection_cannot_complete_text_observation() -> Result<(), Box> { for payload in [ OBSERVATION_SUCCESS_RESPONSE, br#"{"type":"error","id":43,"error":"invalid argument","message":"rejected"}"#, - br#"{"type":"success","id":43,"result":{"type":"exception","realm":"realm-1","exceptionDetails":{"text":"rejected","lineNumber":0,"columnNumber":0,"exception":{"type":"undefined"},"stackTrace":{"callFrames":[]}}}}"#, + br#"{"type":"success","id":43,"result":{"type":"exception","realm":"realm-1","exceptionDetails":{}}}"#, ] { 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)?; + let (mut first, _) = listener.accept()?; + read_opening_request(&mut first)?; + first.write_all(OPENING_RESPONSE)?; + let command = read_masked_text_frame(&mut first)?; assert!(command.starts_with(br#"{"id":43,"method":"script.callFunction""#)); - Ok(()) + let (mut second, _) = listener.accept()?; + read_opening_request(&mut second)?; + second.write_all(OPENING_RESPONSE)?; + write_text_frame(&mut second, payload)?; + write_text_frame(&mut first, OBSERVATION_SUCCESS_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 (registry, handle, remote) = admitted_text_field_fixture()?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); correlation.register_command_for(44, WebDriverBiDiCommandKind::TextValueObservation)?; - let _original = send_webdriver_bidi_text_value_observation( - semantic_observation_proof()?, 43, "context-a", &handle, &remote, ®istry, - established, &mut correlation, WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + let first = send_webdriver_bidi_text_value_observation( + semantic_observation_proof()?, + 43, + "context-a", + &handle, + &remote, + ®istry, + establish_response_connection(local_addr)?, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), Duration::from_millis(500), )?; - server.join().map_err(|_| io::Error::other("original observation server panicked"))??; - let foreign = receive_server_text(payload)?; - let rejected = WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( - &foreign, "Quarterly review", &mut correlation, + assert_eq!(correlation.outstanding_count(), 2); + let foreign = read_response_text(establish_response_connection(local_addr)?)?; + let rejected = WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare(&foreign, "Quarterly review", &mut correlation); + let original = read_response_text(first)?; + server + .join() + .map_err(|_| io::Error::other("response server panicked"))??; + assert!( + matches!( + rejected, + Err(WebDriverBiDiTextValueObservationResponseError::Correlation { + source: originweave_network::WebDriverBiDiCommandCorrelationError::ResponseConnectionMismatch { + command_id: 43, + }, + }) + ), + "replacement socket must not acknowledge the original request: {rejected:?}" ); - assert!(matches!(rejected, - Err(WebDriverBiDiTextValueObservationResponseError::Correlation { - source: originweave_network::WebDriverBiDiCommandCorrelationError::ResponseConnectionMismatch { command_id: 43 }, - }) - ), "replacement response must fail connection provenance: {rejected:?}"); assert_eq!(correlation.outstanding_count(), 2); + let result = WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare(&original, "Quarterly review", &mut correlation)?; + assert_eq!(result.command_id(), 43); + assert!(result.matches_expected_text()); + assert_eq!(correlation.outstanding_count(), 1); } Ok(()) } From d9a3a403443159878e9233e988dd707c4fda6a90 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 13:07:13 +0900 Subject: [PATCH 51/64] test(network): expose typed-input intent substitution --- ...pe_text_intent_postcondition_provenance.rs | 243 ++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_type_text_intent_postcondition_provenance.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_type_text_intent_postcondition_provenance.rs b/crates/originweave-network/tests/webdriver_bidi_type_text_intent_postcondition_provenance.rs new file mode 100644 index 000000000..def179aae --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_type_text_intent_postcondition_provenance.rs @@ -0,0 +1,243 @@ +#[path = "support/text_observation.rs"] +mod text_observation; + +use std::{ + error::Error, + io::{self, Read, Write}, + net::{TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::{ + AdmittedNodeHandle, BoundedWebDriverBiDiResponseDocument, BrowserAuthorityRegistry, + BrowserContextDispatchTarget, BrowserContextOriginDispatchTarget, + BrowserContextOriginEpochDispatchTarget, BrowserProtocolAdapterDescriptor, + BrowserProtocolCapability, BrowserProtocolKind, Origin, OriginWeaveProtocolVersion, + ValidatedBrowserProtocolUse, WebDriverBiDiAccessibilityQuery, WebDriverBiDiLocateNodesCommand, + WebDriverBiDiRemoteNodeReference, WebDriverBiDiWebSocketEndpoint, +}; +use originweave_network::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiConnectionMessageRead, + WebDriverBiDiTcpConnectionPlan, WebDriverBiDiTypeTextResult, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, + WebDriverBiDiWebSocketMessageReader, send_webdriver_bidi_type_text, + verify_webdriver_bidi_text_value_postcondition, +}; + +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 ORIGINWEAVE_PROTOCOL_VERSION: OriginWeaveProtocolVersion = + OriginWeaveProtocolVersion::new(0, 1); +const ADAPTER_VERSION: &str = "originweave-bidi-v1"; +const PROTOCOL_REVISION: &str = "webdriver-bidi-wd-2026-06-01"; +const BROWSER_REVISION: &str = "chromium-r1639810"; + +type AdmittedTypeTextFixture = ( + BrowserAuthorityRegistry, + AdmittedNodeHandle, + WebDriverBiDiRemoteNodeReference, +); + +fn protocol_proof( + capability: BrowserProtocolCapability, +) -> Result> { + let descriptor = BrowserProtocolAdapterDescriptor::new( + BrowserProtocolKind::WebDriverBiDi, + ORIGINWEAVE_PROTOCOL_VERSION, + ADAPTER_VERSION, + PROTOCOL_REVISION, + BROWSER_REVISION, + &[capability], + )?; + Ok(descriptor.validate_use( + ORIGINWEAVE_PROTOCOL_VERSION, + BrowserProtocolKind::WebDriverBiDi, + ADAPTER_VERSION, + PROTOCOL_REVISION, + BROWSER_REVISION, + capability, + )?) +} + +fn semantic_observation_proof() -> Result> { + protocol_proof(BrowserProtocolCapability::SemanticObservation) +} + +fn typed_input_proof() -> Result> { + protocol_proof(BrowserProtocolCapability::TypedInput) +} + +fn admitted_type_text_fixture() -> Result> { + let mut registry = BrowserAuthorityRegistry::new(); + let browser_session = registry.register_session(SESSION_ID)?; + let browsing_context = registry.register_context(browser_session, "context-a")?; + let origin = Origin::parse("https://app.example").map_err(|error| { + io::Error::other(format!("fixture origin rejected unexpectedly: {error:?}")) + })?; + let epoch = registry.bind_context_origin(browser_session, browsing_context, &origin)?; + let target = BrowserContextOriginEpochDispatchTarget::new( + BrowserContextOriginDispatchTarget::new( + BrowserContextDispatchTarget::new(browser_session, browsing_context), + &origin, + ), + epoch, + ); + let query = WebDriverBiDiAccessibilityQuery::new(Some("textbox"), Some("Task title"), 1)?; + let locate = WebDriverBiDiLocateNodesCommand::new(41, "context-a", &query)?; + let document = BoundedWebDriverBiDiResponseDocument::new( + r#"{"type":"success","id":41,"result":{"nodes":[{"type":"node","sharedId":"shared-node-42"}]}}"#, + )?; + let handle = locate + .bind_response_document_nodes( + document, + semantic_observation_proof()?, + &mut registry, + target, + )? + .into_iter() + .next() + .ok_or_else(|| io::Error::other("locateNodes fixture did not bind its node"))?; + let remote = WebDriverBiDiRemoteNodeReference::new("node", Some("shared-node-42"))?; + Ok((registry, handle, 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)?; + usize::try_from(u64::from_be_bytes(extended)).map_err(|_| { + io::Error::new(io::ErrorKind::InvalidData, "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 write_text_frame(stream: &mut TcpStream, payload: &[u8]) -> io::Result<()> { + stream.write_all(&[0x81, payload.len() as u8])?; + stream.write_all(payload) +} + +#[test] +fn substituted_expected_text_cannot_certify_a_different_authorized_typed_input() +-> 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.starts_with(br#"{"id":42,"method":"input.performActions""#) { + return Err(io::Error::other("unexpected typed-input command")); + } + write_text_frame( + &mut stream, + br#"{"type":"success","id":42,"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 established = WebDriverBiDiWebSocketHandshakePlan::new( + connection, + WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?, + )? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?; + + let (registry, handle, remote) = admitted_type_text_fixture()?; + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + let established = send_webdriver_bidi_type_text( + typed_input_proof()?, + 42, + "context-a", + "authorized-value", + &handle, + &remote, + ®istry, + established, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + Duration::from_millis(500), + )?; + let action_ack = match WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + WebDriverBiDiConnectionMessageRead::Text { message, .. } => message, + other => { + return Err(io::Error::other(format!("expected typed-input ACK: {other:?}")).into()); + } + }; + let action = WebDriverBiDiTypeTextResult::parse_and_correlate(&action_ack, &mut correlation)?; + assert_eq!(action.command_id(), 42); + assert_eq!(correlation.outstanding_count(), 0); + server + .join() + .map_err(|_| io::Error::other("typed-input fixture server panicked"))??; + + let observation = text_observation::receive_command_responses( + &[br#"{"type":"success","id":70,"result":{"type":"success","realm":"realm-1","result":{"type":"string","value":"substituted-value"}}}"#], + 70, + &mut correlation, + )? + .remove(0); + + let substituted = verify_webdriver_bidi_text_value_postcondition( + &observation, + "substituted-value", + &mut correlation, + ); + assert!( + substituted.is_err(), + "a value chosen only at verification time must not certify a different authorized typed-input intent" + ); + Ok(()) +} From af1defe84a9edcd3047584c297d5acd3ec40acc4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 15:20:32 +0900 Subject: [PATCH 52/64] fix(network): bind text postcondition to dispatched intent --- crates/originweave-network/src/lib.rs | 12 +- ...webdriver_bidi_text_value_postcondition.rs | 57 +++-- .../src/webdriver_bidi_type_text_intent.rs | 210 +++++++++++++++ .../tests/support/type_text_intent.rs | 233 +++++++++++++++++ ...iver_bidi_text_value_postcondition_gate.rs | 45 +++- ...pe_text_intent_postcondition_provenance.rs | 239 ++---------------- 6 files changed, 544 insertions(+), 252 deletions(-) create mode 100644 crates/originweave-network/src/webdriver_bidi_type_text_intent.rs create mode 100644 crates/originweave-network/tests/support/type_text_intent.rs diff --git a/crates/originweave-network/src/lib.rs b/crates/originweave-network/src/lib.rs index 73237cdc3..73de6a389 100644 --- a/crates/originweave-network/src/lib.rs +++ b/crates/originweave-network/src/lib.rs @@ -11,9 +11,9 @@ //! complete local-end JSON envelopes, tracks bounded command-response correlation, //! transports narrowly typed pointer-click and node-bound non-secret text-input //! actions and fixed sandboxed text-value observations, admits typed correlated -//! protocol responses and text-value comparisons, requires exact equality before -//! positive text-value post-condition evidence, sends a context-bound -//! committed-navigation subscription and retains +//! protocol responses, binds positive text-value evidence to a sender-minted +//! acknowledged typed-input intent, sends a context-bound committed-navigation +//! subscription and retains //! its typed bounded correlated identifier, binds navigation-event admission to //! that active command/receipt lifecycle with bounded fail-closed navigation replay //! prevention, explicitly unsubscribes that exact @@ -55,6 +55,7 @@ mod webdriver_bidi_session_teardown; mod webdriver_bidi_text_value_observation_response; mod webdriver_bidi_text_value_observation_transport; mod webdriver_bidi_text_value_postcondition; +mod webdriver_bidi_type_text_intent; mod webdriver_bidi_type_text_response; mod webdriver_bidi_type_text_transport; mod webdriver_bidi_websocket_frame; @@ -165,6 +166,11 @@ pub use webdriver_bidi_text_value_postcondition::{ WebDriverBiDiTextValuePostcondition, WebDriverBiDiTextValuePostconditionError, verify_webdriver_bidi_text_value_postcondition, }; +pub use webdriver_bidi_type_text_intent::{ + WebDriverBiDiAcknowledgedTypeTextIntent, WebDriverBiDiTypeTextIntentAcknowledgementError, + WebDriverBiDiTypeTextIntentWitness, acknowledge_webdriver_bidi_type_text_intent, + send_webdriver_bidi_type_text_with_postcondition_intent, +}; pub use webdriver_bidi_type_text_response::{ WebDriverBiDiTypeTextResponseError, WebDriverBiDiTypeTextResult, }; diff --git a/crates/originweave-network/src/webdriver_bidi_text_value_postcondition.rs b/crates/originweave-network/src/webdriver_bidi_text_value_postcondition.rs index 719889150..fc1ff2359 100644 --- a/crates/originweave-network/src/webdriver_bidi_text_value_postcondition.rs +++ b/crates/originweave-network/src/webdriver_bidi_text_value_postcondition.rs @@ -1,19 +1,23 @@ use std::{error::Error, fmt}; use crate::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiReceivedTextMessage, - WebDriverBiDiTextValueObservationResponseError, WebDriverBiDiTextValueObservationResult, + WebDriverBiDiAcknowledgedTypeTextIntent, WebDriverBiDiCommandCorrelation, + WebDriverBiDiReceivedTextMessage, WebDriverBiDiTextValueObservationResponseError, + WebDriverBiDiTextValueObservationResult, }; -/// Credential-minimal proof that one exact correlated text observation matched the authorized -/// expected value. +/// Credential-minimal proof that one exact correlated text observation matched the exact +/// sender-minted and acknowledged typed-input intent that preceded it. /// /// The page-controlled string is discarded by the lower observation boundary before this value is -/// constructed. This type therefore carries only the consumed command identifier and observed byte -/// count. A caller can obtain this value only after exact equality succeeds; a mere command -/// response or successful parser result is not sufficient post-condition evidence. +/// constructed. This type therefore carries only the typed-input command identifier, the consumed +/// observation command identifier, and observed byte count. A caller can obtain this value only +/// after the original one-shot typed-input intent received its exact protocol ACK and the later +/// observation exactly matched that retained intent; a command ACK, parser success, or +/// verification-time caller value is not sufficient post-condition evidence. #[derive(Clone, Copy, Eq, PartialEq)] pub struct WebDriverBiDiTextValuePostcondition { + type_text_command_id: u64, command_id: u64, observed_text_bytes: usize, } @@ -22,6 +26,7 @@ impl fmt::Debug for WebDriverBiDiTextValuePostcondition { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter .debug_struct("WebDriverBiDiTextValuePostcondition") + .field("type_text_command_id", &self.type_text_command_id) .field("command_id", &self.command_id) .field("observed_text_bytes", &self.observed_text_bytes) .finish() @@ -29,6 +34,12 @@ impl fmt::Debug for WebDriverBiDiTextValuePostcondition { } impl WebDriverBiDiTextValuePostcondition { + /// Return the exact local typed-input command identifier whose acknowledged intent was verified. + #[must_use] + pub const fn type_text_command_id(&self) -> u64 { + self.type_text_command_id + } + /// Return the exact local command identifier consumed by the verified observation response. #[must_use] pub const fn command_id(&self) -> u64 { @@ -51,9 +62,11 @@ pub enum WebDriverBiDiTextValuePostconditionError { source: WebDriverBiDiTextValueObservationResponseError, }, /// The response was structurally valid and correlated, but the observed page value differed - /// from the exact already-authorized expected text. + /// from the exact sender-minted typed-input intent. PostconditionMismatch { - /// Exact local command identifier consumed by the negative observation. + /// Exact acknowledged typed-input command whose retained intent was compared. + type_text_command_id: u64, + /// Exact local observation command identifier consumed by the negative observation. command_id: u64, /// UTF-8 byte count of the mismatched page value; the page text itself is not retained. observed_text_bytes: usize, @@ -67,7 +80,7 @@ impl fmt::Display for WebDriverBiDiTextValuePostconditionError { formatter.write_str("WebDriver BiDi text-value postcondition observation failed") } Self::PostconditionMismatch { .. } => formatter.write_str( - "WebDriver BiDi text-value postcondition did not match the authorized expected text", + "WebDriver BiDi text-value postcondition did not match the acknowledged typed-input intent", ), } } @@ -83,25 +96,27 @@ impl Error for WebDriverBiDiTextValuePostconditionError { } /// Admit one bounded correlated text observation and return success only when its page value -/// exactly matches the already-authorized expected text. +/// exactly matches the sender-minted typed-input intent that already received its exact ACK. /// -/// The lower boundary validates expected-text policy, response structure, script result shape, and -/// exact command correlation before this function evaluates the post-condition. A mismatching -/// observation consumes its correlated command because the response is complete, but returns a -/// typed negative result rather than `Ok`. This prevents command acknowledgment, parser success, or -/// correlation success from being mistaken for successful browser state mutation. -/// Only a message received on the sender's exact connection can supply this evidence. +/// The caller must transfer a one-shot [`WebDriverBiDiAcknowledgedTypeTextIntent`]. That value is +/// produced only from the reviewed typed-input sender and its exact connection-bound protocol ACK, +/// so the expected value can no longer be selected at verification time. The lower observation +/// boundary validates response structure, script result shape, and exact observation-command +/// correlation before comparison. A mismatching observation consumes its correlated observation +/// command because the response is complete, but returns a typed negative result rather than `Ok`. /// /// No page-controlled text, expected text, realm identifier, credential, secret, browser authority, -/// or policy authority is retained in the returned value or error diagnostics. +/// or policy authority is retained in the returned value or error diagnostics. The acknowledged +/// intent is consumed exactly once by this call and its private text is dropped afterward. pub fn verify_webdriver_bidi_text_value_postcondition( message: &WebDriverBiDiReceivedTextMessage, - expected_text: &str, + acknowledged_intent: WebDriverBiDiAcknowledgedTypeTextIntent, correlation: &mut WebDriverBiDiCommandCorrelation, ) -> Result { + let type_text_command_id = acknowledged_intent.command_id(); let observation = WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( message, - expected_text, + acknowledged_intent.expected_text(), correlation, ) .map_err(|source| WebDriverBiDiTextValuePostconditionError::Observation { source })?; @@ -109,6 +124,7 @@ pub fn verify_webdriver_bidi_text_value_postcondition( if !observation.matches_expected_text() { return Err( WebDriverBiDiTextValuePostconditionError::PostconditionMismatch { + type_text_command_id, command_id: observation.command_id(), observed_text_bytes: observation.observed_text_bytes(), }, @@ -116,6 +132,7 @@ pub fn verify_webdriver_bidi_text_value_postcondition( } Ok(WebDriverBiDiTextValuePostcondition { + type_text_command_id, command_id: observation.command_id(), observed_text_bytes: observation.observed_text_bytes(), }) diff --git a/crates/originweave-network/src/webdriver_bidi_type_text_intent.rs b/crates/originweave-network/src/webdriver_bidi_type_text_intent.rs new file mode 100644 index 000000000..cf122ea58 --- /dev/null +++ b/crates/originweave-network/src/webdriver_bidi_type_text_intent.rs @@ -0,0 +1,210 @@ +use std::{error::Error, fmt, time::Duration}; + +use originweave_core::{ + AdmittedNodeHandle, BrowserAuthorityRegistry, ValidatedBrowserProtocolUse, + WebDriverBiDiRemoteNodeReference, +}; + +use crate::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiReceivedTextMessage, + WebDriverBiDiTypeTextResponseError, WebDriverBiDiTypeTextResult, + WebDriverBiDiTypeTextSendError, WebDriverBiDiWebSocketEstablished, + WebDriverBiDiWebSocketMaskKey, send_webdriver_bidi_type_text, + webdriver_bidi_connection::WebDriverBiDiConnectionGeneration, + webdriver_bidi_json_envelope::WebDriverBiDiJsonEnvelopeRouting, WebDriverBiDiJsonEnvelope, +}; + +/// Sender-minted one-shot witness for the exact non-secret text intent dispatched by one typed-input command. +/// +/// The witness is created only after the reviewed typed-input sender successfully writes its frame. +/// It binds the exact command id, the private process-local connection generation, and the validated +/// text value. The text is retained only inside this opaque value until its exact protocol ACK is +/// admitted; `Debug`, errors, and public accessors never expose it. The witness is neither `Clone` +/// nor `Copy`, so a caller cannot duplicate one successful dispatch into multiple action intents. +pub struct WebDriverBiDiTypeTextIntentWitness { + command_id: u64, + connection_generation: WebDriverBiDiConnectionGeneration, + expected_text: Box, +} + +impl fmt::Debug for WebDriverBiDiTypeTextIntentWitness { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("WebDriverBiDiTypeTextIntentWitness") + .field("command_id", &self.command_id) + .field("expected_text_bytes", &self.expected_text.len()) + .finish() + } +} + +/// One-shot proof that the exact sender-minted typed-input intent received its correlated protocol ACK. +/// +/// This value keeps the original non-secret text private until post-condition verification. It can +/// only be constructed by [`acknowledge_webdriver_bidi_type_text_intent`] after the ACK matches both +/// the witness command id and the witness's private connection generation. +pub struct WebDriverBiDiAcknowledgedTypeTextIntent { + command_id: u64, + expected_text: Box, +} + +impl fmt::Debug for WebDriverBiDiAcknowledgedTypeTextIntent { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("WebDriverBiDiAcknowledgedTypeTextIntent") + .field("command_id", &self.command_id) + .field("expected_text_bytes", &self.expected_text.len()) + .finish() + } +} + +impl WebDriverBiDiAcknowledgedTypeTextIntent { + /// Return the exact local typed-input command identifier acknowledged for this intent. + #[must_use] + pub const fn command_id(&self) -> u64 { + self.command_id + } + + pub(crate) fn expected_text(&self) -> &str { + &self.expected_text + } +} + +/// Fail-closed failures while binding a typed-input ACK to its sender-minted intent witness. +#[derive(Debug)] +pub enum WebDriverBiDiTypeTextIntentAcknowledgementError { + /// The received message belongs to another verified connection. + ResponseConnectionMismatch, + /// The received response names a different command identifier than the witness. + ResponseCommandMismatch, + /// The existing typed response boundary rejected the ACK. + Response { + /// Exact typed response-admission failure. + source: WebDriverBiDiTypeTextResponseError, + }, +} + +impl fmt::Display for WebDriverBiDiTypeTextIntentAcknowledgementError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(match self { + Self::ResponseConnectionMismatch => { + "WebDriver BiDi text-input ACK arrived on a different connection than its intent" + } + Self::ResponseCommandMismatch => { + "WebDriver BiDi text-input ACK does not match its sender-minted intent" + } + Self::Response { .. } => "WebDriver BiDi text-input intent acknowledgement failed", + }) + } +} + +impl Error for WebDriverBiDiTypeTextIntentAcknowledgementError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::Response { source } => Some(source), + Self::ResponseConnectionMismatch | Self::ResponseCommandMismatch => None, + } + } +} + +/// Dispatch one node-bound text action and mint the only witness that can later authorize its +/// text-value post-condition comparison. +/// +/// The underlying sender performs all existing protocol-use, node/session/document authority, +/// correlation, deadline, and frame-write checks. A witness is returned only after that sender +/// succeeds. It is bound to the exact private connection generation and command id and retains the +/// validated non-secret text only in an opaque non-cloneable value. Command ACK still is not browser +/// success: callers must admit the exact ACK with [`acknowledge_webdriver_bidi_type_text_intent`] +/// and then consume the acknowledged intent in the text-value post-condition boundary. +#[expect( + clippy::too_many_arguments, + reason = "this immediate-use wrapper preserves the typed sender's explicit authority, transport, correlation, masking, and deadline inputs while adding only a one-shot post-condition intent witness" +)] +pub fn send_webdriver_bidi_type_text_with_postcondition_intent( + validated: ValidatedBrowserProtocolUse, + command_id: u64, + browsing_context: &str, + text: &str, + handle: &AdmittedNodeHandle, + node: &WebDriverBiDiRemoteNodeReference, + registry: &BrowserAuthorityRegistry, + established: WebDriverBiDiWebSocketEstablished, + correlation: &mut WebDriverBiDiCommandCorrelation, + masking_key: WebDriverBiDiWebSocketMaskKey, + frame_timeout: Duration, +) -> Result< + ( + WebDriverBiDiWebSocketEstablished, + WebDriverBiDiTypeTextIntentWitness, + ), + WebDriverBiDiTypeTextSendError, +> { + let connection_generation = established.transport_evidence().connection_generation(); + let established = send_webdriver_bidi_type_text( + validated, + command_id, + browsing_context, + text, + handle, + node, + registry, + established, + correlation, + masking_key, + frame_timeout, + )?; + + Ok(( + established, + WebDriverBiDiTypeTextIntentWitness { + command_id, + connection_generation, + expected_text: text.into(), + }, + )) +} + +/// Admit one typed-input protocol ACK and consume the exact sender-minted intent witness. +/// +/// Connection and response-id checks run before typed response correlation can consume pending +/// state. A foreign connection or a response naming another command therefore cannot retire an +/// unrelated action. Successful admission returns a one-shot acknowledged intent whose private text +/// is the original value passed to the reviewed sender, not a value supplied at verification time. +pub fn acknowledge_webdriver_bidi_type_text_intent( + message: &WebDriverBiDiReceivedTextMessage, + witness: WebDriverBiDiTypeTextIntentWitness, + correlation: &mut WebDriverBiDiCommandCorrelation, +) -> Result +{ + if message.connection_generation() != witness.connection_generation { + return Err(WebDriverBiDiTypeTextIntentAcknowledgementError::ResponseConnectionMismatch); + } + + let envelope = WebDriverBiDiJsonEnvelope::parse(message.message()).map_err(|source| { + WebDriverBiDiTypeTextIntentAcknowledgementError::Response { + source: WebDriverBiDiTypeTextResponseError::Envelope { source }, + } + })?; + let response_command_id = match envelope.routing() { + WebDriverBiDiJsonEnvelopeRouting::CommandSuccess { command_id } + | WebDriverBiDiJsonEnvelopeRouting::CommandError { + command_id: Some(command_id), + } => Some(command_id), + WebDriverBiDiJsonEnvelopeRouting::Event + | WebDriverBiDiJsonEnvelopeRouting::CommandError { command_id: None } => None, + }; + if response_command_id.is_some_and(|command_id| command_id != witness.command_id) { + return Err(WebDriverBiDiTypeTextIntentAcknowledgementError::ResponseCommandMismatch); + } + + let result = WebDriverBiDiTypeTextResult::parse_and_correlate(message, correlation).map_err( + |source| WebDriverBiDiTypeTextIntentAcknowledgementError::Response { source }, + )?; + if result.command_id() != witness.command_id { + return Err(WebDriverBiDiTypeTextIntentAcknowledgementError::ResponseCommandMismatch); + } + + Ok(WebDriverBiDiAcknowledgedTypeTextIntent { + command_id: witness.command_id, + expected_text: witness.expected_text, + }) +} diff --git a/crates/originweave-network/tests/support/type_text_intent.rs b/crates/originweave-network/tests/support/type_text_intent.rs new file mode 100644 index 000000000..004aa1680 --- /dev/null +++ b/crates/originweave-network/tests/support/type_text_intent.rs @@ -0,0 +1,233 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::{ + AdmittedNodeHandle, BoundedWebDriverBiDiResponseDocument, BrowserAuthorityRegistry, + BrowserContextDispatchTarget, BrowserContextOriginDispatchTarget, + BrowserContextOriginEpochDispatchTarget, BrowserProtocolAdapterDescriptor, + BrowserProtocolCapability, BrowserProtocolKind, Origin, OriginWeaveProtocolVersion, + ValidatedBrowserProtocolUse, WebDriverBiDiAccessibilityQuery, WebDriverBiDiLocateNodesCommand, + WebDriverBiDiRemoteNodeReference, WebDriverBiDiWebSocketEndpoint, +}; +use originweave_network::{ + WebDriverBiDiAcknowledgedTypeTextIntent, WebDriverBiDiCommandCorrelation, + WebDriverBiDiConnectionMessageRead, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageReader, + acknowledge_webdriver_bidi_type_text_intent, + send_webdriver_bidi_type_text_with_postcondition_intent, +}; + +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 ORIGINWEAVE_PROTOCOL_VERSION: OriginWeaveProtocolVersion = + OriginWeaveProtocolVersion::new(0, 1); +const ADAPTER_VERSION: &str = "originweave-bidi-v1"; +const PROTOCOL_REVISION: &str = "webdriver-bidi-wd-2026-06-01"; +const BROWSER_REVISION: &str = "chromium-r1639810"; + +type AdmittedTypeTextFixture = ( + BrowserAuthorityRegistry, + AdmittedNodeHandle, + WebDriverBiDiRemoteNodeReference, +); + +fn protocol_proof( + capability: BrowserProtocolCapability, +) -> Result> { + let descriptor = BrowserProtocolAdapterDescriptor::new( + BrowserProtocolKind::WebDriverBiDi, + ORIGINWEAVE_PROTOCOL_VERSION, + ADAPTER_VERSION, + PROTOCOL_REVISION, + BROWSER_REVISION, + &[capability], + )?; + Ok(descriptor.validate_use( + ORIGINWEAVE_PROTOCOL_VERSION, + BrowserProtocolKind::WebDriverBiDi, + ADAPTER_VERSION, + PROTOCOL_REVISION, + BROWSER_REVISION, + capability, + )?) +} + +fn semantic_observation_proof() -> Result> { + protocol_proof(BrowserProtocolCapability::SemanticObservation) +} + +fn typed_input_proof() -> Result> { + protocol_proof(BrowserProtocolCapability::TypedInput) +} + +fn admitted_type_text_fixture() -> Result> { + let mut registry = BrowserAuthorityRegistry::new(); + let browser_session = registry.register_session(SESSION_ID)?; + let browsing_context = registry.register_context(browser_session, "context-a")?; + let origin = Origin::parse("https://app.example").map_err(|error| { + io::Error::other(format!("fixture origin rejected unexpectedly: {error:?}")) + })?; + let epoch = registry.bind_context_origin(browser_session, browsing_context, &origin)?; + let target = BrowserContextOriginEpochDispatchTarget::new( + BrowserContextOriginDispatchTarget::new( + BrowserContextDispatchTarget::new(browser_session, browsing_context), + &origin, + ), + epoch, + ); + let query = WebDriverBiDiAccessibilityQuery::new(Some("textbox"), Some("Task title"), 1)?; + let locate = WebDriverBiDiLocateNodesCommand::new(41, "context-a", &query)?; + let document = BoundedWebDriverBiDiResponseDocument::new( + r#"{"type":"success","id":41,"result":{"nodes":[{"type":"node","sharedId":"shared-node-42"}]}}"#, + )?; + let handle = locate + .bind_response_document_nodes( + document, + semantic_observation_proof()?, + &mut registry, + target, + )? + .into_iter() + .next() + .ok_or_else(|| io::Error::other("locateNodes fixture did not bind its node"))?; + let remote = WebDriverBiDiRemoteNodeReference::new("node", Some("shared-node-42"))?; + Ok((registry, handle, 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)?; + usize::try_from(u64::from_be_bytes(extended)).map_err(|_| { + io::Error::new(io::ErrorKind::InvalidData, "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 write_text_frame(stream: &mut TcpStream, payload: &[u8]) -> io::Result<()> { + if payload.len() > 125 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "fixture ACK unexpectedly exceeded one-byte framing", + )); + } + stream.write_all(&[0x81, payload.len() as u8])?; + stream.write_all(payload) +} + +pub fn acknowledged_type_text_intent( + command_id: u64, + text: &str, +) -> Result> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let expected_prefix = + format!(r#"{{"id":{command_id},"method":"input.performActions""#).into_bytes(); + let ack = format!(r#"{{"type":"success","id":{command_id},"result":{{}}}}"#).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.starts_with(&expected_prefix) { + return Err(io::Error::other("unexpected typed-input command")); + } + write_text_frame(&mut stream, &ack) + }); + + 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 (registry, handle, remote) = admitted_type_text_fixture()?; + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + let (established, witness) = send_webdriver_bidi_type_text_with_postcondition_intent( + typed_input_proof()?, + command_id, + "context-a", + text, + &handle, + &remote, + ®istry, + established, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + Duration::from_millis(500), + )?; + let action_ack = match WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + WebDriverBiDiConnectionMessageRead::Text { message, .. } => message, + other => { + return Err(io::Error::other(format!("expected typed-input ACK: {other:?}")).into()); + } + }; + let acknowledged = + acknowledge_webdriver_bidi_type_text_intent(&action_ack, witness, &mut correlation)?; + if correlation.outstanding_count() != 0 { + return Err(io::Error::other("typed-input ACK did not consume its correlation").into()); + } + server + .join() + .map_err(|_| io::Error::other("typed-input fixture server panicked"))??; + Ok(acknowledged) +} diff --git a/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs b/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs index df0d9765f..a6ee81f20 100644 --- a/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs +++ b/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs @@ -1,5 +1,7 @@ #[path = "support/text_observation.rs"] mod text_observation; +#[path = "support/type_text_intent.rs"] +mod type_text_intent; use std::{ error::Error, @@ -99,6 +101,8 @@ fn receive_server_text(payload: &[u8]) -> Result Result<(), Box> { + let acknowledged_intent = + type_text_intent::acknowledged_type_text_intent(42, "expected")?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); let response = text_observation::receive_command_responses( &[ @@ -106,9 +110,13 @@ fn exact_match_is_the_only_successful_text_postcondition() -> Result<(), Box Result<(), Box Result<(), Box> { + let acknowledged_intent = + type_text_intent::acknowledged_type_text_intent(43, "expected")?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); let response = text_observation::receive_command_responses( &[ @@ -125,9 +135,11 @@ fn mismatch_is_typed_failure_after_consuming_its_exact_response() -> Result<(), ], 71, &mut correlation, )?.remove(0); - let Err(error) = - verify_webdriver_bidi_text_value_postcondition(&response, "expected", &mut correlation) - else { + let Err(error) = verify_webdriver_bidi_text_value_postcondition( + &response, + acknowledged_intent, + &mut correlation, + ) else { return Err(io::Error::other( "a mismatched page value must not be returned as successful postcondition evidence", ) @@ -137,6 +149,7 @@ fn mismatch_is_typed_failure_after_consuming_its_exact_response() -> Result<(), assert!(matches!( &error, WebDriverBiDiTextValuePostconditionError::PostconditionMismatch { + type_text_command_id: 43, command_id: 71, observed_text_bytes: 10, } @@ -144,7 +157,7 @@ fn mismatch_is_typed_failure_after_consuming_its_exact_response() -> Result<(), assert_eq!(correlation.outstanding_count(), 0); assert_eq!( error.to_string(), - "WebDriver BiDi text-value postcondition did not match the authorized expected text" + "WebDriver BiDi text-value postcondition did not match the acknowledged typed-input intent" ); assert!(error.source().is_none()); let debug = format!("{error:?}"); @@ -156,13 +169,17 @@ fn mismatch_is_typed_failure_after_consuming_its_exact_response() -> Result<(), #[test] fn malformed_observation_stays_a_typed_source_error_without_consuming_state() -> Result<(), Box> { + let acknowledged_intent = + type_text_intent::acknowledged_type_text_intent(44, "expected")?; let response = receive_server_text(b"not-json")?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); correlation.register_command_for(72, WebDriverBiDiCommandKind::TextValueObservation)?; - let Err(error) = - verify_webdriver_bidi_text_value_postcondition(&response, "expected", &mut correlation) - else { + let Err(error) = verify_webdriver_bidi_text_value_postcondition( + &response, + acknowledged_intent, + &mut correlation, + ) else { return Err(io::Error::other("malformed observation must fail closed").into()); }; @@ -181,15 +198,19 @@ fn malformed_observation_stays_a_typed_source_error_without_consuming_state() #[test] fn unrelated_outstanding_command_cannot_certify_text_postcondition() -> Result<(), Box> { + let acknowledged_intent = + type_text_intent::acknowledged_type_text_intent(45, "expected")?; let response = receive_server_text( br#"{"type":"success","id":73,"result":{"type":"success","realm":"realm-1","result":{"type":"string","value":"expected"}}}"#, )?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); correlation.register_command_for(73, WebDriverBiDiCommandKind::SessionStatus)?; - let Err(error) = - verify_webdriver_bidi_text_value_postcondition(&response, "expected", &mut correlation) - else { + let Err(error) = verify_webdriver_bidi_text_value_postcondition( + &response, + acknowledged_intent, + &mut correlation, + ) else { return Err(io::Error::other( "an unrelated outstanding command id must not certify a text-value postcondition", ) diff --git a/crates/originweave-network/tests/webdriver_bidi_type_text_intent_postcondition_provenance.rs b/crates/originweave-network/tests/webdriver_bidi_type_text_intent_postcondition_provenance.rs index def179aae..dad0f573c 100644 --- a/crates/originweave-network/tests/webdriver_bidi_type_text_intent_postcondition_provenance.rs +++ b/crates/originweave-network/tests/webdriver_bidi_type_text_intent_postcondition_provenance.rs @@ -1,228 +1,22 @@ #[path = "support/text_observation.rs"] mod text_observation; +#[path = "support/type_text_intent.rs"] +mod type_text_intent; -use std::{ - error::Error, - io::{self, Read, Write}, - net::{TcpListener, TcpStream}, - thread, - time::Duration, -}; +use std::{error::Error, io}; -use originweave_core::{ - AdmittedNodeHandle, BoundedWebDriverBiDiResponseDocument, BrowserAuthorityRegistry, - BrowserContextDispatchTarget, BrowserContextOriginDispatchTarget, - BrowserContextOriginEpochDispatchTarget, BrowserProtocolAdapterDescriptor, - BrowserProtocolCapability, BrowserProtocolKind, Origin, OriginWeaveProtocolVersion, - ValidatedBrowserProtocolUse, WebDriverBiDiAccessibilityQuery, WebDriverBiDiLocateNodesCommand, - WebDriverBiDiRemoteNodeReference, WebDriverBiDiWebSocketEndpoint, -}; use originweave_network::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiConnectionMessageRead, - WebDriverBiDiTcpConnectionPlan, WebDriverBiDiTypeTextResult, WebDriverBiDiWebSocketClientKey, - WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, - WebDriverBiDiWebSocketMessageReader, send_webdriver_bidi_type_text, + WebDriverBiDiCommandCorrelation, WebDriverBiDiTextValuePostconditionError, verify_webdriver_bidi_text_value_postcondition, }; -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 ORIGINWEAVE_PROTOCOL_VERSION: OriginWeaveProtocolVersion = - OriginWeaveProtocolVersion::new(0, 1); -const ADAPTER_VERSION: &str = "originweave-bidi-v1"; -const PROTOCOL_REVISION: &str = "webdriver-bidi-wd-2026-06-01"; -const BROWSER_REVISION: &str = "chromium-r1639810"; - -type AdmittedTypeTextFixture = ( - BrowserAuthorityRegistry, - AdmittedNodeHandle, - WebDriverBiDiRemoteNodeReference, -); - -fn protocol_proof( - capability: BrowserProtocolCapability, -) -> Result> { - let descriptor = BrowserProtocolAdapterDescriptor::new( - BrowserProtocolKind::WebDriverBiDi, - ORIGINWEAVE_PROTOCOL_VERSION, - ADAPTER_VERSION, - PROTOCOL_REVISION, - BROWSER_REVISION, - &[capability], - )?; - Ok(descriptor.validate_use( - ORIGINWEAVE_PROTOCOL_VERSION, - BrowserProtocolKind::WebDriverBiDi, - ADAPTER_VERSION, - PROTOCOL_REVISION, - BROWSER_REVISION, - capability, - )?) -} - -fn semantic_observation_proof() -> Result> { - protocol_proof(BrowserProtocolCapability::SemanticObservation) -} - -fn typed_input_proof() -> Result> { - protocol_proof(BrowserProtocolCapability::TypedInput) -} - -fn admitted_type_text_fixture() -> Result> { - let mut registry = BrowserAuthorityRegistry::new(); - let browser_session = registry.register_session(SESSION_ID)?; - let browsing_context = registry.register_context(browser_session, "context-a")?; - let origin = Origin::parse("https://app.example").map_err(|error| { - io::Error::other(format!("fixture origin rejected unexpectedly: {error:?}")) - })?; - let epoch = registry.bind_context_origin(browser_session, browsing_context, &origin)?; - let target = BrowserContextOriginEpochDispatchTarget::new( - BrowserContextOriginDispatchTarget::new( - BrowserContextDispatchTarget::new(browser_session, browsing_context), - &origin, - ), - epoch, - ); - let query = WebDriverBiDiAccessibilityQuery::new(Some("textbox"), Some("Task title"), 1)?; - let locate = WebDriverBiDiLocateNodesCommand::new(41, "context-a", &query)?; - let document = BoundedWebDriverBiDiResponseDocument::new( - r#"{"type":"success","id":41,"result":{"nodes":[{"type":"node","sharedId":"shared-node-42"}]}}"#, - )?; - let handle = locate - .bind_response_document_nodes( - document, - semantic_observation_proof()?, - &mut registry, - target, - )? - .into_iter() - .next() - .ok_or_else(|| io::Error::other("locateNodes fixture did not bind its node"))?; - let remote = WebDriverBiDiRemoteNodeReference::new("node", Some("shared-node-42"))?; - Ok((registry, handle, 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)?; - usize::try_from(u64::from_be_bytes(extended)).map_err(|_| { - io::Error::new(io::ErrorKind::InvalidData, "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 write_text_frame(stream: &mut TcpStream, payload: &[u8]) -> io::Result<()> { - stream.write_all(&[0x81, payload.len() as u8])?; - stream.write_all(payload) -} - #[test] fn substituted_expected_text_cannot_certify_a_different_authorized_typed_input() -> 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.starts_with(br#"{"id":42,"method":"input.performActions""#) { - return Err(io::Error::other("unexpected typed-input command")); - } - write_text_frame( - &mut stream, - br#"{"type":"success","id":42,"result":{}}"#, - ) - }); + let acknowledged_intent = + type_text_intent::acknowledged_type_text_intent(42, "authorized-value")?; - 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 (registry, handle, remote) = admitted_type_text_fixture()?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); - let established = send_webdriver_bidi_type_text( - typed_input_proof()?, - 42, - "context-a", - "authorized-value", - &handle, - &remote, - ®istry, - established, - &mut correlation, - WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), - Duration::from_millis(500), - )?; - let action_ack = match WebDriverBiDiWebSocketMessageReader::new(established) - .read_next(Duration::from_millis(500))? - { - WebDriverBiDiConnectionMessageRead::Text { message, .. } => message, - other => { - return Err(io::Error::other(format!("expected typed-input ACK: {other:?}")).into()); - } - }; - let action = WebDriverBiDiTypeTextResult::parse_and_correlate(&action_ack, &mut correlation)?; - assert_eq!(action.command_id(), 42); - assert_eq!(correlation.outstanding_count(), 0); - server - .join() - .map_err(|_| io::Error::other("typed-input fixture server panicked"))??; - let observation = text_observation::receive_command_responses( &[br#"{"type":"success","id":70,"result":{"type":"success","realm":"realm-1","result":{"type":"string","value":"substituted-value"}}}"#], 70, @@ -232,12 +26,23 @@ fn substituted_expected_text_cannot_certify_a_different_authorized_typed_input() let substituted = verify_webdriver_bidi_text_value_postcondition( &observation, - "substituted-value", + acknowledged_intent, &mut correlation, ); - assert!( - substituted.is_err(), - "a value chosen only at verification time must not certify a different authorized typed-input intent" - ); + let Err(error) = substituted else { + return Err(io::Error::other( + "a value chosen only at verification time must not certify a different authorized typed-input intent", + ) + .into()); + }; + assert!(matches!( + error, + WebDriverBiDiTextValuePostconditionError::PostconditionMismatch { + type_text_command_id: 42, + command_id: 70, + observed_text_bytes: 17, + } + )); + assert_eq!(correlation.outstanding_count(), 0); Ok(()) } From 1e600c446e3314b4ec76412b0172c5f1c3430b94 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 15:38:16 +0900 Subject: [PATCH 53/64] test(network): reject cross-connection postcondition evidence --- ...pe_text_intent_postcondition_provenance.rs | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_type_text_intent_postcondition_provenance.rs b/crates/originweave-network/tests/webdriver_bidi_type_text_intent_postcondition_provenance.rs index dad0f573c..4ead83b2a 100644 --- a/crates/originweave-network/tests/webdriver_bidi_type_text_intent_postcondition_provenance.rs +++ b/crates/originweave-network/tests/webdriver_bidi_type_text_intent_postcondition_provenance.rs @@ -11,8 +11,8 @@ use originweave_network::{ }; #[test] -fn substituted_expected_text_cannot_certify_a_different_authorized_typed_input() --> Result<(), Box> { +fn substituted_expected_text_cannot_certify_a_different_authorized_typed_input( +) -> Result<(), Box> { let acknowledged_intent = type_text_intent::acknowledged_type_text_intent(42, "authorized-value")?; @@ -46,3 +46,37 @@ fn substituted_expected_text_cannot_certify_a_different_authorized_typed_input() assert_eq!(correlation.outstanding_count(), 0); Ok(()) } + +#[test] +fn observation_on_another_connection_cannot_certify_the_acknowledged_typed_input( +) -> Result<(), Box> { + let acknowledged_intent = + type_text_intent::acknowledged_type_text_intent(43, "same-value")?; + + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + let observation = text_observation::receive_command_responses( + &[br#"{"type":"success","id":71,"result":{"type":"success","realm":"realm-1","result":{"type":"string","value":"same-value"}}}"#], + 71, + &mut correlation, + )? + .remove(0); + + if verify_webdriver_bidi_text_value_postcondition( + &observation, + acknowledged_intent, + &mut correlation, + ) + .is_ok() + { + return Err(io::Error::other( + "an observation from another verified connection must not certify an earlier typed-input intent", + ) + .into()); + } + assert_eq!( + correlation.outstanding_count(), + 1, + "foreign post-condition evidence must not consume the pending observation" + ); + Ok(()) +} From e405f9fe78aaf37845b79e82cae842113b2111d8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 02:01:34 +0900 Subject: [PATCH 54/64] fix(network): retain typed-input connection generation --- .../src/webdriver_bidi_type_text_intent.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_type_text_intent.rs b/crates/originweave-network/src/webdriver_bidi_type_text_intent.rs index cf122ea58..344ad8127 100644 --- a/crates/originweave-network/src/webdriver_bidi_type_text_intent.rs +++ b/crates/originweave-network/src/webdriver_bidi_type_text_intent.rs @@ -39,11 +39,14 @@ impl fmt::Debug for WebDriverBiDiTypeTextIntentWitness { /// One-shot proof that the exact sender-minted typed-input intent received its correlated protocol ACK. /// -/// This value keeps the original non-secret text private until post-condition verification. It can -/// only be constructed by [`acknowledge_webdriver_bidi_type_text_intent`] after the ACK matches both -/// the witness command id and the witness's private connection generation. +/// This value keeps the original non-secret text and its private connection generation until +/// post-condition verification. It can only be constructed by +/// [`acknowledge_webdriver_bidi_type_text_intent`] after the ACK matches both the witness command id +/// and the witness's private connection generation. Neither private value is exposed through +/// `Debug`, errors, or public accessors. pub struct WebDriverBiDiAcknowledgedTypeTextIntent { command_id: u64, + connection_generation: WebDriverBiDiConnectionGeneration, expected_text: Box, } @@ -67,6 +70,13 @@ impl WebDriverBiDiAcknowledgedTypeTextIntent { pub(crate) fn expected_text(&self) -> &str { &self.expected_text } + + pub(crate) fn matches_connection_generation( + &self, + connection_generation: WebDriverBiDiConnectionGeneration, + ) -> bool { + self.connection_generation == connection_generation + } } /// Fail-closed failures while binding a typed-input ACK to its sender-minted intent witness. @@ -205,6 +215,7 @@ pub fn acknowledge_webdriver_bidi_type_text_intent( Ok(WebDriverBiDiAcknowledgedTypeTextIntent { command_id: witness.command_id, + connection_generation: witness.connection_generation, expected_text: witness.expected_text, }) } From a73ea7b4dc14d3c352fccb772922ab036ba9652b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 02:01:58 +0900 Subject: [PATCH 55/64] fix(network): reject foreign postcondition connection --- ...webdriver_bidi_text_value_postcondition.rs | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_text_value_postcondition.rs b/crates/originweave-network/src/webdriver_bidi_text_value_postcondition.rs index fc1ff2359..162f1faef 100644 --- a/crates/originweave-network/src/webdriver_bidi_text_value_postcondition.rs +++ b/crates/originweave-network/src/webdriver_bidi_text_value_postcondition.rs @@ -13,8 +13,9 @@ use crate::{ /// constructed. This type therefore carries only the typed-input command identifier, the consumed /// observation command identifier, and observed byte count. A caller can obtain this value only /// after the original one-shot typed-input intent received its exact protocol ACK and the later -/// observation exactly matched that retained intent; a command ACK, parser success, or -/// verification-time caller value is not sufficient post-condition evidence. +/// observation exactly matched that retained intent on the same verified WebDriver BiDi connection; +/// a command ACK, parser success, same textual value on another connection, or verification-time +/// caller value is not sufficient post-condition evidence. #[derive(Clone, Copy, Eq, PartialEq)] pub struct WebDriverBiDiTextValuePostcondition { type_text_command_id: u64, @@ -56,6 +57,9 @@ impl WebDriverBiDiTextValuePostcondition { /// Failure to produce positive text-value post-condition evidence from one correlated response. #[derive(Debug)] pub enum WebDriverBiDiTextValuePostconditionError { + /// The received observation belongs to a different verified connection than the acknowledged + /// typed-input intent. This check runs before observation correlation can consume pending state. + ObservationConnectionMismatch, /// The underlying bounded response admission or correlation failed. Observation { /// Exact typed lower-boundary failure. @@ -76,6 +80,9 @@ pub enum WebDriverBiDiTextValuePostconditionError { impl fmt::Display for WebDriverBiDiTextValuePostconditionError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + Self::ObservationConnectionMismatch => formatter.write_str( + "WebDriver BiDi text-value postcondition observation arrived on a different connection than the acknowledged typed-input intent", + ), Self::Observation { .. } => { formatter.write_str("WebDriver BiDi text-value postcondition observation failed") } @@ -90,7 +97,7 @@ impl Error for WebDriverBiDiTextValuePostconditionError { fn source(&self) -> Option<&(dyn Error + 'static)> { match self { Self::Observation { source } => Some(source), - Self::PostconditionMismatch { .. } => None, + Self::ObservationConnectionMismatch | Self::PostconditionMismatch { .. } => None, } } } @@ -100,19 +107,26 @@ impl Error for WebDriverBiDiTextValuePostconditionError { /// /// The caller must transfer a one-shot [`WebDriverBiDiAcknowledgedTypeTextIntent`]. That value is /// produced only from the reviewed typed-input sender and its exact connection-bound protocol ACK, -/// so the expected value can no longer be selected at verification time. The lower observation -/// boundary validates response structure, script result shape, and exact observation-command +/// so the expected value can no longer be selected at verification time. The received observation +/// must first match the acknowledged intent's private connection generation; that check happens +/// before lower response admission or correlation can consume pending state. The lower observation +/// boundary then validates response structure, script result shape, and exact observation-command /// correlation before comparison. A mismatching observation consumes its correlated observation /// command because the response is complete, but returns a typed negative result rather than `Ok`. /// -/// No page-controlled text, expected text, realm identifier, credential, secret, browser authority, -/// or policy authority is retained in the returned value or error diagnostics. The acknowledged -/// intent is consumed exactly once by this call and its private text is dropped afterward. +/// No page-controlled text, expected text, connection-generation identifier, realm identifier, +/// credential, secret, browser authority, or policy authority is retained in the returned value or +/// error diagnostics. The acknowledged intent is consumed exactly once by this call and its private +/// text and connection generation are dropped afterward. pub fn verify_webdriver_bidi_text_value_postcondition( message: &WebDriverBiDiReceivedTextMessage, acknowledged_intent: WebDriverBiDiAcknowledgedTypeTextIntent, correlation: &mut WebDriverBiDiCommandCorrelation, ) -> Result { + if !acknowledged_intent.matches_connection_generation(message.connection_generation()) { + return Err(WebDriverBiDiTextValuePostconditionError::ObservationConnectionMismatch); + } + let type_text_command_id = acknowledged_intent.command_id(); let observation = WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare( message, From 570f166050fea5c7ba3d9ca66e4de995f5d245ea Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 02:03:41 +0900 Subject: [PATCH 56/64] style(network): apply canonical rustfmt to postcondition tests --- .../webdriver_bidi_text_value_postcondition_gate.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs b/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs index a6ee81f20..ededb89e6 100644 --- a/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs +++ b/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs @@ -101,8 +101,7 @@ fn receive_server_text(payload: &[u8]) -> Result Result<(), Box> { - let acknowledged_intent = - type_text_intent::acknowledged_type_text_intent(42, "expected")?; + let acknowledged_intent = type_text_intent::acknowledged_type_text_intent(42, "expected")?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); let response = text_observation::receive_command_responses( &[ @@ -126,8 +125,7 @@ fn exact_match_is_the_only_successful_text_postcondition() -> Result<(), Box Result<(), Box> { - let acknowledged_intent = - type_text_intent::acknowledged_type_text_intent(43, "expected")?; + let acknowledged_intent = type_text_intent::acknowledged_type_text_intent(43, "expected")?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); let response = text_observation::receive_command_responses( &[ @@ -169,8 +167,7 @@ fn mismatch_is_typed_failure_after_consuming_its_exact_response() -> Result<(), #[test] fn malformed_observation_stays_a_typed_source_error_without_consuming_state() -> Result<(), Box> { - let acknowledged_intent = - type_text_intent::acknowledged_type_text_intent(44, "expected")?; + let acknowledged_intent = type_text_intent::acknowledged_type_text_intent(44, "expected")?; let response = receive_server_text(b"not-json")?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); correlation.register_command_for(72, WebDriverBiDiCommandKind::TextValueObservation)?; @@ -198,8 +195,7 @@ fn malformed_observation_stays_a_typed_source_error_without_consuming_state() #[test] fn unrelated_outstanding_command_cannot_certify_text_postcondition() -> Result<(), Box> { - let acknowledged_intent = - type_text_intent::acknowledged_type_text_intent(45, "expected")?; + let acknowledged_intent = type_text_intent::acknowledged_type_text_intent(45, "expected")?; let response = receive_server_text( br#"{"type":"success","id":73,"result":{"type":"success","realm":"realm-1","result":{"type":"string","value":"expected"}}}"#, )?; From c2c0f4ff476f53ce1b9a313b0c46d8134a9ca15a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 02:04:05 +0900 Subject: [PATCH 57/64] test(network): assert typed foreign-connection rejection --- ...pe_text_intent_postcondition_provenance.rs | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_type_text_intent_postcondition_provenance.rs b/crates/originweave-network/tests/webdriver_bidi_type_text_intent_postcondition_provenance.rs index 4ead83b2a..9dd5a13e0 100644 --- a/crates/originweave-network/tests/webdriver_bidi_type_text_intent_postcondition_provenance.rs +++ b/crates/originweave-network/tests/webdriver_bidi_type_text_intent_postcondition_provenance.rs @@ -11,8 +11,8 @@ use originweave_network::{ }; #[test] -fn substituted_expected_text_cannot_certify_a_different_authorized_typed_input( -) -> Result<(), Box> { +fn substituted_expected_text_cannot_certify_a_different_authorized_typed_input() +-> Result<(), Box> { let acknowledged_intent = type_text_intent::acknowledged_type_text_intent(42, "authorized-value")?; @@ -48,10 +48,9 @@ fn substituted_expected_text_cannot_certify_a_different_authorized_typed_input( } #[test] -fn observation_on_another_connection_cannot_certify_the_acknowledged_typed_input( -) -> Result<(), Box> { - let acknowledged_intent = - type_text_intent::acknowledged_type_text_intent(43, "same-value")?; +fn observation_on_another_connection_cannot_certify_the_acknowledged_typed_input() +-> Result<(), Box> { + let acknowledged_intent = type_text_intent::acknowledged_type_text_intent(43, "same-value")?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); let observation = text_observation::receive_command_responses( @@ -61,22 +60,31 @@ fn observation_on_another_connection_cannot_certify_the_acknowledged_typed_input )? .remove(0); - if verify_webdriver_bidi_text_value_postcondition( + let Err(error) = verify_webdriver_bidi_text_value_postcondition( &observation, acknowledged_intent, &mut correlation, - ) - .is_ok() - { + ) else { return Err(io::Error::other( "an observation from another verified connection must not certify an earlier typed-input intent", ) .into()); - } + }; + assert!(matches!( + &error, + WebDriverBiDiTextValuePostconditionError::ObservationConnectionMismatch + )); + assert_eq!( + error.to_string(), + "WebDriver BiDi text-value postcondition observation arrived on a different connection than the acknowledged typed-input intent" + ); + assert!(error.source().is_none()); assert_eq!( correlation.outstanding_count(), 1, "foreign post-condition evidence must not consume the pending observation" ); + let debug = format!("{error:?}"); + assert!(!debug.contains("same-value")); Ok(()) } From a31971b10042fcb6404a06c18c6c3150794d7f2e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 02:04:51 +0900 Subject: [PATCH 58/64] style(network): apply canonical rustfmt to type-text intent --- .../src/webdriver_bidi_type_text_intent.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_type_text_intent.rs b/crates/originweave-network/src/webdriver_bidi_type_text_intent.rs index 344ad8127..66f393fbe 100644 --- a/crates/originweave-network/src/webdriver_bidi_type_text_intent.rs +++ b/crates/originweave-network/src/webdriver_bidi_type_text_intent.rs @@ -6,12 +6,12 @@ use originweave_core::{ }; use crate::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiReceivedTextMessage, + WebDriverBiDiCommandCorrelation, WebDriverBiDiJsonEnvelope, WebDriverBiDiReceivedTextMessage, WebDriverBiDiTypeTextResponseError, WebDriverBiDiTypeTextResult, WebDriverBiDiTypeTextSendError, WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketMaskKey, send_webdriver_bidi_type_text, webdriver_bidi_connection::WebDriverBiDiConnectionGeneration, - webdriver_bidi_json_envelope::WebDriverBiDiJsonEnvelopeRouting, WebDriverBiDiJsonEnvelope, + webdriver_bidi_json_envelope::WebDriverBiDiJsonEnvelopeRouting, }; /// Sender-minted one-shot witness for the exact non-secret text intent dispatched by one typed-input command. @@ -206,9 +206,8 @@ pub fn acknowledge_webdriver_bidi_type_text_intent( return Err(WebDriverBiDiTypeTextIntentAcknowledgementError::ResponseCommandMismatch); } - let result = WebDriverBiDiTypeTextResult::parse_and_correlate(message, correlation).map_err( - |source| WebDriverBiDiTypeTextIntentAcknowledgementError::Response { source }, - )?; + let result = WebDriverBiDiTypeTextResult::parse_and_correlate(message, correlation) + .map_err(|source| WebDriverBiDiTypeTextIntentAcknowledgementError::Response { source })?; if result.command_id() != witness.command_id { return Err(WebDriverBiDiTypeTextIntentAcknowledgementError::ResponseCommandMismatch); } From 6b67fe479e08aaf4198ece473e4e17053156f4bf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 02:11:07 +0900 Subject: [PATCH 59/64] test(network): preserve connection in postcondition fixtures --- .../tests/support/type_text_intent.rs | 224 ++++++++++++++++-- 1 file changed, 205 insertions(+), 19 deletions(-) diff --git a/crates/originweave-network/tests/support/type_text_intent.rs b/crates/originweave-network/tests/support/type_text_intent.rs index 004aa1680..bde96180d 100644 --- a/crates/originweave-network/tests/support/type_text_intent.rs +++ b/crates/originweave-network/tests/support/type_text_intent.rs @@ -16,10 +16,11 @@ use originweave_core::{ }; use originweave_network::{ WebDriverBiDiAcknowledgedTypeTextIntent, WebDriverBiDiCommandCorrelation, - WebDriverBiDiConnectionMessageRead, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiCommandKind, WebDriverBiDiConnectionMessageRead, + WebDriverBiDiReceivedTextMessage, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageReader, - acknowledge_webdriver_bidi_type_text_intent, + acknowledge_webdriver_bidi_type_text_intent, send_webdriver_bidi_text_value_observation, send_webdriver_bidi_type_text_with_postcondition_intent, }; @@ -38,6 +39,12 @@ type AdmittedTypeTextFixture = ( WebDriverBiDiRemoteNodeReference, ); +type AcknowledgedObservationFixture = ( + WebDriverBiDiAcknowledgedTypeTextIntent, + WebDriverBiDiReceivedTextMessage, + WebDriverBiDiCommandCorrelation, +); + fn protocol_proof( capability: BrowserProtocolCapability, ) -> Result> { @@ -158,13 +165,38 @@ fn write_text_frame(stream: &mut TcpStream, payload: &[u8]) -> io::Result<()> { if payload.len() > 125 { return Err(io::Error::new( io::ErrorKind::InvalidData, - "fixture ACK unexpectedly exceeded one-byte framing", + "fixture response unexpectedly exceeded one-byte framing", )); } stream.write_all(&[0x81, payload.len() as u8])?; stream.write_all(payload) } +fn assert_command_prefix(command: &[u8], expected_prefix: &[u8], name: &str) -> io::Result<()> { + if command.starts_with(expected_prefix) { + Ok(()) + } else { + Err(io::Error::other(format!("unexpected {name} command"))) + } +} + +fn open_established_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))?) +} + pub fn acknowledged_type_text_intent( command_id: u64, text: &str, @@ -179,25 +211,11 @@ pub fn acknowledged_type_text_intent( read_opening_request(&mut stream)?; stream.write_all(OPENING_RESPONSE)?; let command = read_masked_text_frame(&mut stream)?; - if !command.starts_with(&expected_prefix) { - return Err(io::Error::other("unexpected typed-input command")); - } + assert_command_prefix(&command, &expected_prefix, "typed-input")?; write_text_frame(&mut stream, &ack) }); - 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 established = open_established_connection(local_addr)?; let (registry, handle, remote) = admitted_type_text_fixture()?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); let (established, witness) = send_webdriver_bidi_type_text_with_postcondition_intent( @@ -231,3 +249,171 @@ pub fn acknowledged_type_text_intent( .map_err(|_| io::Error::other("typed-input fixture server panicked"))??; Ok(acknowledged) } + +pub fn acknowledged_type_text_intent_and_observation( + type_text_command_id: u64, + text: &str, + observation_command_id: u64, + observation_response: &[u8], +) -> Result> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let type_text_prefix = format!( + r#"{{"id":{type_text_command_id},"method":"input.performActions""# + ) + .into_bytes(); + let observation_prefix = format!( + r#"{{"id":{observation_command_id},"method":"script.callFunction""# + ) + .into_bytes(); + let ack = format!( + r#"{{"type":"success","id":{type_text_command_id},"result":{{}}}}"# + ) + .into_bytes(); + let response = observation_response.to_vec(); + let server = thread::spawn(move || -> io::Result<()> { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + let type_text_command = read_masked_text_frame(&mut stream)?; + assert_command_prefix(&type_text_command, &type_text_prefix, "typed-input")?; + write_text_frame(&mut stream, &ack)?; + let observation_command = read_masked_text_frame(&mut stream)?; + assert_command_prefix(&observation_command, &observation_prefix, "text-observation")?; + write_text_frame(&mut stream, &response) + }); + + let established = open_established_connection(local_addr)?; + let (registry, handle, remote) = admitted_type_text_fixture()?; + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + let (established, witness) = send_webdriver_bidi_type_text_with_postcondition_intent( + typed_input_proof()?, + type_text_command_id, + "context-a", + text, + &handle, + &remote, + ®istry, + established, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + Duration::from_millis(500), + )?; + let (established, action_ack) = match WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + WebDriverBiDiConnectionMessageRead::Text { + established, + message, + } => (established, message), + other => { + return Err(io::Error::other(format!("expected typed-input ACK: {other:?}")).into()); + } + }; + let acknowledged = + acknowledge_webdriver_bidi_type_text_intent(&action_ack, witness, &mut correlation)?; + if correlation.outstanding_count() != 0 { + return Err(io::Error::other("typed-input ACK did not consume its correlation").into()); + } + + let established = send_webdriver_bidi_text_value_observation( + semantic_observation_proof()?, + observation_command_id, + "context-a", + &handle, + &remote, + ®istry, + established, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([5, 6, 7, 8]), + Duration::from_millis(500), + )?; + let observation = match WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + WebDriverBiDiConnectionMessageRead::Text { message, .. } => message, + other => { + return Err(io::Error::other(format!("expected observation response: {other:?}")).into()); + } + }; + server + .join() + .map_err(|_| io::Error::other("combined postcondition fixture server panicked"))??; + Ok((acknowledged, observation, correlation)) +} + +pub fn acknowledged_type_text_intent_and_registered_response( + type_text_command_id: u64, + text: &str, + response_command_id: u64, + response_kind: WebDriverBiDiCommandKind, + response: &[u8], +) -> Result> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let type_text_prefix = format!( + r#"{{"id":{type_text_command_id},"method":"input.performActions""# + ) + .into_bytes(); + let ack = format!( + r#"{{"type":"success","id":{type_text_command_id},"result":{{}}}}"# + ) + .into_bytes(); + let response = response.to_vec(); + let server = thread::spawn(move || -> io::Result<()> { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + let type_text_command = read_masked_text_frame(&mut stream)?; + assert_command_prefix(&type_text_command, &type_text_prefix, "typed-input")?; + write_text_frame(&mut stream, &ack)?; + write_text_frame(&mut stream, &response) + }); + + let established = open_established_connection(local_addr)?; + let (registry, handle, remote) = admitted_type_text_fixture()?; + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + let (established, witness) = send_webdriver_bidi_type_text_with_postcondition_intent( + typed_input_proof()?, + type_text_command_id, + "context-a", + text, + &handle, + &remote, + ®istry, + established, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + Duration::from_millis(500), + )?; + let (established, action_ack) = match WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + WebDriverBiDiConnectionMessageRead::Text { + established, + message, + } => (established, message), + other => { + return Err(io::Error::other(format!("expected typed-input ACK: {other:?}")).into()); + } + }; + let acknowledged = + acknowledge_webdriver_bidi_type_text_intent(&action_ack, witness, &mut correlation)?; + correlation.register_command_for_connection( + response_command_id, + response_kind, + established.transport_evidence().connection_generation(), + )?; + let message = match WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + WebDriverBiDiConnectionMessageRead::Text { message, .. } => message, + other => { + return Err(io::Error::other(format!("expected registered response: {other:?}")).into()); + } + }; + server + .join() + .map_err(|_| io::Error::other("registered-response fixture server panicked"))??; + Ok((acknowledged, message, correlation)) +} From 952d912f1c2042485e5d611176e221ae6556f8d2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 02:11:27 +0900 Subject: [PATCH 60/64] test(network): keep positive postconditions on one connection --- ...iver_bidi_text_value_postcondition_gate.rs | 148 ++++-------------- 1 file changed, 31 insertions(+), 117 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs b/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs index ededb89e6..1950a68cc 100644 --- a/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs +++ b/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs @@ -1,113 +1,22 @@ -#[path = "support/text_observation.rs"] -mod text_observation; #[path = "support/type_text_intent.rs"] mod type_text_intent; -use std::{ - error::Error, - io::{self, Read, Write}, - net::{TcpListener, TcpStream}, - thread, - time::Duration, -}; +use std::{error::Error, io}; -use originweave_core::WebDriverBiDiWebSocketEndpoint; use originweave_network::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandKind, WebDriverBiDiConnectionMessageRead, - WebDriverBiDiReceivedTextMessage, WebDriverBiDiTcpConnectionPlan, - WebDriverBiDiTextValuePostconditionError, WebDriverBiDiWebSocketClientKey, - WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMessageReader, + WebDriverBiDiCommandKind, WebDriverBiDiTextValuePostconditionError, verify_webdriver_bidi_text_value_postcondition, }; -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_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 response unexpectedly required 64-bit framing", - )); - } - } - stream.write_all(payload) -} - -fn receive_server_text(payload: &[u8]) -> Result> { - let listener = TcpListener::bind(("127.0.0.1", 0))?; - let local_addr = listener.local_addr()?; - let response = payload.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_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 text = match WebDriverBiDiWebSocketMessageReader::new(established) - .read_next(Duration::from_millis(500))? - { - WebDriverBiDiConnectionMessageRead::Text { message, .. } => message, - other => { - return Err(io::Error::other(format!( - "fixture produced unexpected message assembly state: {other:?}" - )) - .into()); - } - }; - server - .join() - .map_err(|_| io::Error::other("postcondition fixture server panicked"))??; - Ok(text) -} - #[test] fn exact_match_is_the_only_successful_text_postcondition() -> Result<(), Box> { - let acknowledged_intent = type_text_intent::acknowledged_type_text_intent(42, "expected")?; - let mut correlation = WebDriverBiDiCommandCorrelation::new(); - let response = text_observation::receive_command_responses( - &[ - br#"{"type":"success","id":70,"result":{"type":"success","realm":"realm-1","result":{"type":"string","value":"expected"}}}"#, - ], 70, &mut correlation, - )?.remove(0); + let (acknowledged_intent, response, mut correlation) = + type_text_intent::acknowledged_type_text_intent_and_observation( + 42, + "expected", + 70, + br#"{"type":"success","id":70,"result":{"type":"success","realm":"realm-1","result":{"type":"string","value":"expected"}}}"#, + )?; let verified = verify_webdriver_bidi_text_value_postcondition( &response, @@ -125,13 +34,13 @@ fn exact_match_is_the_only_successful_text_postcondition() -> Result<(), Box Result<(), Box> { - let acknowledged_intent = type_text_intent::acknowledged_type_text_intent(43, "expected")?; - let mut correlation = WebDriverBiDiCommandCorrelation::new(); - let response = text_observation::receive_command_responses( - &[ - br#"{"type":"success","id":71,"result":{"type":"success","realm":"realm-1","result":{"type":"string","value":"unexpected"}}}"#, - ], 71, &mut correlation, - )?.remove(0); + let (acknowledged_intent, response, mut correlation) = + type_text_intent::acknowledged_type_text_intent_and_observation( + 43, + "expected", + 71, + br#"{"type":"success","id":71,"result":{"type":"success","realm":"realm-1","result":{"type":"string","value":"unexpected"}}}"#, + )?; let Err(error) = verify_webdriver_bidi_text_value_postcondition( &response, @@ -167,10 +76,13 @@ fn mismatch_is_typed_failure_after_consuming_its_exact_response() -> Result<(), #[test] fn malformed_observation_stays_a_typed_source_error_without_consuming_state() -> Result<(), Box> { - let acknowledged_intent = type_text_intent::acknowledged_type_text_intent(44, "expected")?; - let response = receive_server_text(b"not-json")?; - let mut correlation = WebDriverBiDiCommandCorrelation::new(); - correlation.register_command_for(72, WebDriverBiDiCommandKind::TextValueObservation)?; + let (acknowledged_intent, response, mut correlation) = + type_text_intent::acknowledged_type_text_intent_and_observation( + 44, + "expected", + 72, + b"not-json", + )?; let Err(error) = verify_webdriver_bidi_text_value_postcondition( &response, @@ -195,12 +107,14 @@ fn malformed_observation_stays_a_typed_source_error_without_consuming_state() #[test] fn unrelated_outstanding_command_cannot_certify_text_postcondition() -> Result<(), Box> { - let acknowledged_intent = type_text_intent::acknowledged_type_text_intent(45, "expected")?; - let response = receive_server_text( - br#"{"type":"success","id":73,"result":{"type":"success","realm":"realm-1","result":{"type":"string","value":"expected"}}}"#, - )?; - let mut correlation = WebDriverBiDiCommandCorrelation::new(); - correlation.register_command_for(73, WebDriverBiDiCommandKind::SessionStatus)?; + let (acknowledged_intent, response, mut correlation) = + type_text_intent::acknowledged_type_text_intent_and_registered_response( + 45, + "expected", + 73, + WebDriverBiDiCommandKind::SessionStatus, + br#"{"type":"success","id":73,"result":{"type":"success","realm":"realm-1","result":{"type":"string","value":"expected"}}}"#, + )?; let Err(error) = verify_webdriver_bidi_text_value_postcondition( &response, From 8c57193033455f7697f51e223150a4247281046d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 02:11:44 +0900 Subject: [PATCH 61/64] test(network): isolate value and connection provenance cases --- ...type_text_intent_postcondition_provenance.rs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_type_text_intent_postcondition_provenance.rs b/crates/originweave-network/tests/webdriver_bidi_type_text_intent_postcondition_provenance.rs index 9dd5a13e0..7d80be369 100644 --- a/crates/originweave-network/tests/webdriver_bidi_type_text_intent_postcondition_provenance.rs +++ b/crates/originweave-network/tests/webdriver_bidi_type_text_intent_postcondition_provenance.rs @@ -13,16 +13,13 @@ use originweave_network::{ #[test] fn substituted_expected_text_cannot_certify_a_different_authorized_typed_input() -> Result<(), Box> { - let acknowledged_intent = - type_text_intent::acknowledged_type_text_intent(42, "authorized-value")?; - - let mut correlation = WebDriverBiDiCommandCorrelation::new(); - let observation = text_observation::receive_command_responses( - &[br#"{"type":"success","id":70,"result":{"type":"success","realm":"realm-1","result":{"type":"string","value":"substituted-value"}}}"#], - 70, - &mut correlation, - )? - .remove(0); + let (acknowledged_intent, observation, mut correlation) = + type_text_intent::acknowledged_type_text_intent_and_observation( + 42, + "authorized-value", + 70, + br#"{"type":"success","id":70,"result":{"type":"success","realm":"realm-1","result":{"type":"string","value":"substituted-value"}}}"#, + )?; let substituted = verify_webdriver_bidi_text_value_postcondition( &observation, From 33731d05d17257397761a368bcee666d38604316 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 02:12:41 +0900 Subject: [PATCH 62/64] test(network): keep combined fixture response in short-frame budget --- .../webdriver_bidi_type_text_intent_postcondition_provenance.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_type_text_intent_postcondition_provenance.rs b/crates/originweave-network/tests/webdriver_bidi_type_text_intent_postcondition_provenance.rs index 7d80be369..f18ef979c 100644 --- a/crates/originweave-network/tests/webdriver_bidi_type_text_intent_postcondition_provenance.rs +++ b/crates/originweave-network/tests/webdriver_bidi_type_text_intent_postcondition_provenance.rs @@ -18,7 +18,7 @@ fn substituted_expected_text_cannot_certify_a_different_authorized_typed_input() 42, "authorized-value", 70, - br#"{"type":"success","id":70,"result":{"type":"success","realm":"realm-1","result":{"type":"string","value":"substituted-value"}}}"#, + br#"{"type":"success","id":70,"result":{"type":"success","realm":"r","result":{"type":"string","value":"substituted-value"}}}"#, )?; let substituted = verify_webdriver_bidi_text_value_postcondition( From 14fb8e7587671faec80ad1c9a4d6076e53f5e69f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 03:18:36 +0900 Subject: [PATCH 63/64] test(network): keep unrelated response on public typed transport --- .../tests/support/type_text_intent.rs | 69 +++++++++++-------- 1 file changed, 39 insertions(+), 30 deletions(-) diff --git a/crates/originweave-network/tests/support/type_text_intent.rs b/crates/originweave-network/tests/support/type_text_intent.rs index bde96180d..7358bf97f 100644 --- a/crates/originweave-network/tests/support/type_text_intent.rs +++ b/crates/originweave-network/tests/support/type_text_intent.rs @@ -16,11 +16,12 @@ use originweave_core::{ }; use originweave_network::{ WebDriverBiDiAcknowledgedTypeTextIntent, WebDriverBiDiCommandCorrelation, - WebDriverBiDiCommandKind, WebDriverBiDiConnectionMessageRead, - WebDriverBiDiReceivedTextMessage, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiCommandKind, WebDriverBiDiConnectionMessageRead, WebDriverBiDiReceivedTextMessage, + WebDriverBiDiSessionStatusCommand, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageReader, - acknowledge_webdriver_bidi_type_text_intent, send_webdriver_bidi_text_value_observation, + acknowledge_webdriver_bidi_type_text_intent, + send_webdriver_bidi_text_value_observation, send_webdriver_bidi_type_text_with_postcondition_intent, }; @@ -258,18 +259,12 @@ pub fn acknowledged_type_text_intent_and_observation( ) -> Result> { let listener = TcpListener::bind(("127.0.0.1", 0))?; let local_addr = listener.local_addr()?; - let type_text_prefix = format!( - r#"{{"id":{type_text_command_id},"method":"input.performActions""# - ) - .into_bytes(); - let observation_prefix = format!( - r#"{{"id":{observation_command_id},"method":"script.callFunction""# - ) - .into_bytes(); - let ack = format!( - r#"{{"type":"success","id":{type_text_command_id},"result":{{}}}}"# - ) - .into_bytes(); + let type_text_prefix = + format!(r#"{{"id":{type_text_command_id},"method":"input.performActions""#).into_bytes(); + let observation_prefix = + format!(r#"{{"id":{observation_command_id},"method":"script.callFunction""#).into_bytes(); + let ack = + format!(r#"{{"type":"success","id":{type_text_command_id},"result":{{}}}}"#).into_bytes(); let response = observation_response.to_vec(); let server = thread::spawn(move || -> io::Result<()> { let (mut stream, _) = listener.accept()?; @@ -279,7 +274,11 @@ pub fn acknowledged_type_text_intent_and_observation( assert_command_prefix(&type_text_command, &type_text_prefix, "typed-input")?; write_text_frame(&mut stream, &ack)?; let observation_command = read_masked_text_frame(&mut stream)?; - assert_command_prefix(&observation_command, &observation_prefix, "text-observation")?; + assert_command_prefix( + &observation_command, + &observation_prefix, + "text-observation", + )?; write_text_frame(&mut stream, &response) }); @@ -333,7 +332,9 @@ pub fn acknowledged_type_text_intent_and_observation( { WebDriverBiDiConnectionMessageRead::Text { message, .. } => message, other => { - return Err(io::Error::other(format!("expected observation response: {other:?}")).into()); + return Err( + io::Error::other(format!("expected observation response: {other:?}")).into(), + ); } }; server @@ -351,14 +352,17 @@ pub fn acknowledged_type_text_intent_and_registered_response( ) -> Result> { let listener = TcpListener::bind(("127.0.0.1", 0))?; let local_addr = listener.local_addr()?; - let type_text_prefix = format!( - r#"{{"id":{type_text_command_id},"method":"input.performActions""# - ) - .into_bytes(); - let ack = format!( - r#"{{"type":"success","id":{type_text_command_id},"result":{{}}}}"# - ) - .into_bytes(); + let type_text_prefix = + format!(r#"{{"id":{type_text_command_id},"method":"input.performActions""#).into_bytes(); + let ack = + format!(r#"{{"type":"success","id":{type_text_command_id},"result":{{}}}}"#).into_bytes(); + if response_kind != WebDriverBiDiCommandKind::SessionStatus { + return Err( + io::Error::other("registered-response fixture only supports session.status").into(), + ); + } + let response_prefix = + format!(r#"{{"id":{response_command_id},"method":"session.status""#).into_bytes(); let response = response.to_vec(); let server = thread::spawn(move || -> io::Result<()> { let (mut stream, _) = listener.accept()?; @@ -367,6 +371,8 @@ pub fn acknowledged_type_text_intent_and_registered_response( let type_text_command = read_masked_text_frame(&mut stream)?; assert_command_prefix(&type_text_command, &type_text_prefix, "typed-input")?; write_text_frame(&mut stream, &ack)?; + let response_command = read_masked_text_frame(&mut stream)?; + assert_command_prefix(&response_command, &response_prefix, "session.status")?; write_text_frame(&mut stream, &response) }); @@ -399,17 +405,20 @@ pub fn acknowledged_type_text_intent_and_registered_response( }; let acknowledged = acknowledge_webdriver_bidi_type_text_intent(&action_ack, witness, &mut correlation)?; - correlation.register_command_for_connection( - response_command_id, - response_kind, - established.transport_evidence().connection_generation(), + let established = WebDriverBiDiSessionStatusCommand::new(response_command_id)?.send( + established, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([5, 6, 7, 8]), + Duration::from_millis(500), )?; let message = match WebDriverBiDiWebSocketMessageReader::new(established) .read_next(Duration::from_millis(500))? { WebDriverBiDiConnectionMessageRead::Text { message, .. } => message, other => { - return Err(io::Error::other(format!("expected registered response: {other:?}")).into()); + return Err( + io::Error::other(format!("expected registered response: {other:?}")).into(), + ); } }; server From 46db0045904f0289738df843d0a2f179c26673d3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 13:04:08 +0900 Subject: [PATCH 64/64] fix(network): complete intent acknowledgement verification Co-Authored-By: OpenAI Codex --- AGENTS.md | 2 + CHANGELOG.md | 2 + .../src/webdriver_bidi_type_text_intent.rs | 5 +- .../tests/support/type_text_intent.rs | 30 ++- ...iver_bidi_text_value_postcondition_gate.rs | 2 +- .../webdriver_bidi_type_text_intent_ack.rs | 178 ++++++++++++++++++ ...pe_text_intent_postcondition_provenance.rs | 2 +- .../action-postcondition-evidence.md | 19 ++ 8 files changed, 228 insertions(+), 12 deletions(-) create mode 100644 crates/originweave-network/tests/webdriver_bidi_type_text_intent_ack.rs diff --git a/AGENTS.md b/AGENTS.md index 6f747c38e..3b4b55fde 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -81,6 +81,8 @@ The organization currently documents a **solo-maintainer** governance condition. ## Testing expectations +For an ACK coverage deficit, trace every pre-consumption guard and the lower correlation result before adding tests. Remove a duplicate post-check only when the same validated envelope and exact-id correlation make its failure impossible. Cover reachable malformed, event, remote-error, wrong-id and foreign-connection paths with real receipts, assert pending-state counts, and compare opaque diagnostics without exposing text. Keep reusable test helpers public only within integration-test crates; never widen production provenance access to make a fixture compile. + Use realistic cases, including: - malformed origins, IPv4/IPv6 loopback, user information, paths, ports, and Unicode/control input; diff --git a/CHANGELOG.md b/CHANGELOG.md index 65954029c..60a6a1904 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to OriginWeave are documented in this file. The format follo ## [Unreleased] +- Strengthened text-entry acknowledgment checks with real-connection regression tests for unrelated replies, malformed responses and private diagnostics; removed a redundant check without changing pending-request protection. + - Reject field-value replies received on a replacement connection, even when their request identifier and text match. - Reject field-observation requests on another browser session and preserve pending requests only when a write may have reached the peer. diff --git a/crates/originweave-network/src/webdriver_bidi_type_text_intent.rs b/crates/originweave-network/src/webdriver_bidi_type_text_intent.rs index 66f393fbe..c7631eb98 100644 --- a/crates/originweave-network/src/webdriver_bidi_type_text_intent.rs +++ b/crates/originweave-network/src/webdriver_bidi_type_text_intent.rs @@ -206,11 +206,8 @@ pub fn acknowledge_webdriver_bidi_type_text_intent( return Err(WebDriverBiDiTypeTextIntentAcknowledgementError::ResponseCommandMismatch); } - let result = WebDriverBiDiTypeTextResult::parse_and_correlate(message, correlation) + WebDriverBiDiTypeTextResult::parse_and_correlate(message, correlation) .map_err(|source| WebDriverBiDiTypeTextIntentAcknowledgementError::Response { source })?; - if result.command_id() != witness.command_id { - return Err(WebDriverBiDiTypeTextIntentAcknowledgementError::ResponseCommandMismatch); - } Ok(WebDriverBiDiAcknowledgedTypeTextIntent { command_id: witness.command_id, diff --git a/crates/originweave-network/tests/support/type_text_intent.rs b/crates/originweave-network/tests/support/type_text_intent.rs index 7358bf97f..4c5b0c322 100644 --- a/crates/originweave-network/tests/support/type_text_intent.rs +++ b/crates/originweave-network/tests/support/type_text_intent.rs @@ -20,8 +20,7 @@ use originweave_network::{ WebDriverBiDiSessionStatusCommand, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageReader, - acknowledge_webdriver_bidi_type_text_intent, - send_webdriver_bidi_text_value_observation, + acknowledge_webdriver_bidi_type_text_intent, send_webdriver_bidi_text_value_observation, send_webdriver_bidi_type_text_with_postcondition_intent, }; @@ -46,6 +45,11 @@ type AcknowledgedObservationFixture = ( WebDriverBiDiCommandCorrelation, ); +type EstablishedPeerFixture = ( + originweave_network::WebDriverBiDiWebSocketEstablished, + thread::JoinHandle>, +); + fn protocol_proof( capability: BrowserProtocolCapability, ) -> Result> { @@ -71,11 +75,11 @@ fn semantic_observation_proof() -> Result Result> { +pub fn typed_input_proof() -> Result> { protocol_proof(BrowserProtocolCapability::TypedInput) } -fn admitted_type_text_fixture() -> Result> { +pub fn admitted_type_text_fixture() -> Result> { let mut registry = BrowserAuthorityRegistry::new(); let browser_session = registry.register_session(SESSION_ID)?; let browsing_context = registry.register_context(browser_session, "context-a")?; @@ -126,7 +130,7 @@ fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { Ok(()) } -fn read_masked_text_frame(stream: &mut TcpStream) -> io::Result> { +pub 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 { @@ -162,7 +166,7 @@ fn read_masked_text_frame(stream: &mut TcpStream) -> io::Result> { Ok(payload) } -fn write_text_frame(stream: &mut TcpStream, payload: &[u8]) -> io::Result<()> { +pub fn write_text_frame(stream: &mut TcpStream, payload: &[u8]) -> io::Result<()> { if payload.len() > 125 { return Err(io::Error::new( io::ErrorKind::InvalidData, @@ -198,6 +202,20 @@ fn open_established_connection( .read_opening_response(Duration::from_millis(500))?) } +pub fn established_with_peer_script( + script: impl FnOnce(&mut TcpStream) -> io::Result<()> + Send + 'static, +) -> Result> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let server = thread::spawn(move || { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + script(&mut stream) + }); + Ok((open_established_connection(local_addr)?, server)) +} + pub fn acknowledged_type_text_intent( command_id: u64, text: &str, diff --git a/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs b/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs index 1950a68cc..1810b023c 100644 --- a/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs +++ b/crates/originweave-network/tests/webdriver_bidi_text_value_postcondition_gate.rs @@ -1,5 +1,5 @@ #[path = "support/type_text_intent.rs"] -mod type_text_intent; +pub mod type_text_intent; use std::{error::Error, io}; diff --git a/crates/originweave-network/tests/webdriver_bidi_type_text_intent_ack.rs b/crates/originweave-network/tests/webdriver_bidi_type_text_intent_ack.rs new file mode 100644 index 000000000..b373dc97c --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_type_text_intent_ack.rs @@ -0,0 +1,178 @@ +#[path = "support/type_text_intent.rs"] +pub mod type_text_intent; + +use originweave_network::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, + WebDriverBiDiConnectionMessageRead, WebDriverBiDiJsonEnvelopeError, + WebDriverBiDiReceivedTextMessage, WebDriverBiDiTypeTextIntentAcknowledgementError as AckError, + WebDriverBiDiTypeTextIntentWitness, WebDriverBiDiTypeTextResponseError as ResponseError, + WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageReader, + acknowledge_webdriver_bidi_type_text_intent, + send_webdriver_bidi_type_text_with_postcondition_intent, +}; +use std::{error::Error, io, io::Read, time::Duration}; + +const PRIVATE_TEXT: &str = "private-action"; +const SUCCESS_ACK: &[u8] = br#"{"type":"success","id":42,"result":{}}"#; +type IntentReply = ( + WebDriverBiDiTypeTextIntentWitness, + WebDriverBiDiReceivedTextMessage, + WebDriverBiDiCommandCorrelation, +); + +fn intent_reply(reply: &[u8]) -> Result> { + let reply = reply.to_vec(); + let (established, peer) = type_text_intent::established_with_peer_script(move |stream| { + let command = type_text_intent::read_masked_text_frame(stream)?; + assert!(command.starts_with(br#"{"id":42,"method":"input.performActions""#)); + type_text_intent::write_text_frame(stream, &reply) + })?; + let (registry, handle, remote) = type_text_intent::admitted_type_text_fixture()?; + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + let (established, witness) = send_webdriver_bidi_type_text_with_postcondition_intent( + type_text_intent::typed_input_proof()?, + 42, + "context-a", + PRIVATE_TEXT, + &handle, + &remote, + ®istry, + established, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + Duration::from_secs(1), + )?; + assert_eq!( + format!("{witness:?}"), + "WebDriverBiDiTypeTextIntentWitness { command_id: 42, expected_text_bytes: 14 }" + ); + let received = + WebDriverBiDiWebSocketMessageReader::new(established).read_next(Duration::from_secs(1)); + peer.join() + .map_err(|_| io::Error::other("ACK peer panicked"))??; + match received? { + WebDriverBiDiConnectionMessageRead::Text { message, .. } => { + Ok((witness, message, correlation)) + } + other => Err(io::Error::other(format!("expected ACK text: {other:?}")).into()), + } +} + +#[test] +fn ack_admission_preserves_correlation_and_opaque_diagnostics() -> Result<(), Box> { + for (reply, expected, pending) in [ + (SUCCESS_ACK, Ok(()), 0), + ( + &b"{"[..], + Err(AckError::Response { + source: ResponseError::Envelope { + source: WebDriverBiDiJsonEnvelopeError::InvalidJson, + }, + }), + 1, + ), + ( + &br#"{"type":"success","id":43,"result":{}}"#[..], + Err(AckError::ResponseCommandMismatch), + 1, + ), + ( + &br#"{"type":"error","id":43,"error":"unknown error","message":"private-action"}"#[..], + Err(AckError::ResponseCommandMismatch), + 1, + ), + ( + &br#"{"type":"error","id":42,"error":"unknown error","message":"private-action"}"#[..], + Err(AckError::Response { + source: ResponseError::RemoteProtocolError { command_id: 42 }, + }), + 0, + ), + ( + &br#"{"type":"error","id":null,"error":"unknown error","message":"private-action"}"#[..], + Err(AckError::Response { + source: ResponseError::Correlation { + source: WebDriverBiDiCommandCorrelationError::UncorrelatableErrorResponse, + }, + }), + 1, + ), + ( + &br#"{"type":"event","method":"log.entryAdded","params":{}}"#[..], + Err(AckError::Response { + source: ResponseError::Correlation { + source: WebDriverBiDiCommandCorrelationError::EventIsNotResponse, + }, + }), + 1, + ), + ] { + let (witness, message, mut correlation) = intent_reply(reply)?; + let result = + acknowledge_webdriver_bidi_type_text_intent(&message, witness, &mut correlation); + if let (Err(actual), Err(expected)) = (&result, &expected) { + assert_eq!(actual.to_string(), expected.to_string()); + assert_eq!(actual.source().is_some(), expected.source().is_some()); + } + let result = result.map(|ack| { + assert_eq!(ack.command_id(), 42); + assert_eq!(format!("{ack:?}"), + "WebDriverBiDiAcknowledgedTypeTextIntent { command_id: 42, expected_text_bytes: 14 }"); + }); + assert_eq!(format!("{result:?}"), format!("{expected:?}")); + assert_eq!(correlation.outstanding_count(), pending); + } + Ok(()) +} + +#[test] +fn foreign_ack_cannot_consume_original_intent_correlation() -> Result<(), Box> { + let (witness, _original, mut correlation) = intent_reply(SUCCESS_ACK)?; + let (_foreign_witness, foreign, foreign_correlation) = intent_reply(SUCCESS_ACK)?; + let Err(error) = + acknowledge_webdriver_bidi_type_text_intent(&foreign, witness, &mut correlation) + else { + return Err(io::Error::other("foreign ACK must fail").into()); + }; + assert_eq!(format!("{error:?}"), "ResponseConnectionMismatch"); + assert_eq!( + error.to_string(), + "WebDriver BiDi text-input ACK arrived on a different connection than its intent" + ); + assert!(error.source().is_none()); + assert_eq!(correlation.outstanding_count(), 1); + assert_eq!(foreign_correlation.outstanding_count(), 1); + Ok(()) +} + +#[test] +fn rejected_sender_cannot_mint_a_witness_or_emit_command_bytes() -> Result<(), Box> { + let (established, peer) = type_text_intent::established_with_peer_script(|stream| { + let mut byte = [0]; + assert_eq!(stream.read(&mut byte)?, 0); + Ok(()) + })?; + let (registry, handle, remote) = type_text_intent::admitted_type_text_fixture()?; + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + let result = send_webdriver_bidi_type_text_with_postcondition_intent( + type_text_intent::typed_input_proof()?, + 42, + "context-a", + "", + &handle, + &remote, + ®istry, + established, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + Duration::from_secs(1), + ); + peer.join() + .map_err(|_| io::Error::other("rejected sender peer panicked"))??; + assert_eq!( + format!("{result:?}"), + "Err(Authority { source: Command(EmptyText) })" + ); + assert_eq!(correlation.outstanding_count(), 0); + Ok(()) +} diff --git a/crates/originweave-network/tests/webdriver_bidi_type_text_intent_postcondition_provenance.rs b/crates/originweave-network/tests/webdriver_bidi_type_text_intent_postcondition_provenance.rs index f18ef979c..20b639900 100644 --- a/crates/originweave-network/tests/webdriver_bidi_type_text_intent_postcondition_provenance.rs +++ b/crates/originweave-network/tests/webdriver_bidi_type_text_intent_postcondition_provenance.rs @@ -1,7 +1,7 @@ #[path = "support/text_observation.rs"] mod text_observation; #[path = "support/type_text_intent.rs"] -mod type_text_intent; +pub mod type_text_intent; use std::{error::Error, io}; diff --git a/docs/traceability/action-postcondition-evidence.md b/docs/traceability/action-postcondition-evidence.md index 36e94c9c2..c899c8705 100644 --- a/docs/traceability/action-postcondition-evidence.md +++ b/docs/traceability/action-postcondition-evidence.md @@ -1,5 +1,24 @@ # Action Post-Condition Evidence Traceability +## Intent acknowledgment verification repair — 2026-09-08 + +Exact #271 predecessor `14fb8e75` failed formatting and complete coverage in +run `34151114820`. The remaining production gaps were confined to the intent +acknowledgment boundary. Connection and command identity are checked before +pending work can be consumed. The lower response owner uses that same validated +envelope's identifier for its result, so a second identifier mismatch after +successful correlation was unreachable and has been removed without weakening +the pre-consumption check. + +Real loopback tests now exercise successful, malformed, event, null-id, remote-error, +wrong-id and foreign-connection replies. Only a matching success or remote error +retires the corresponding pending command; unrelated replies preserve pending work. +Rejected input emits no command and mints no intent witness. Exact diagnostic +comparisons retain only command identity and text length, never the input text or +private connection generation. Shared helpers stay in integration tests; production +provenance visibility is unchanged. Complete current-head verification is separate +from browser outcome evidence, protected-parent acceptance and release readiness. + ## Fixed field observation adopts current input safeguards — 2026-09-07 Ordinary merge `f883fd6f` adopts #268 `ff27220c` while retaining #269